// Copyright 2015 Hajime Hoshi
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package main

import (
	"bytes"
	"flag"
	"fmt"
	"image"
	_ "image/jpeg"
	"log"
	"math"
	"math/rand/v2"
	"regexp"
	"strconv"
	"strings"

	"github.com/ebitengine/debugui"

	"github.com/hajimehoshi/ebiten/v2"
	"github.com/hajimehoshi/ebiten/v2/examples/resources/images"
	"github.com/hajimehoshi/ebiten/v2/inpututil"
)

var (
	flagFullscreen          = flag.Bool("fullscreen", false, "fullscreen")
	flagResizable           = flag.Bool("resizable", false, "make the window resizable")
	flagWindowPosition      = flag.String("windowposition", "", "window position (e.g., 100,200)")
	flagTransparent         = flag.Bool("transparent", false, "screen transparent")
	flagFloating            = flag.Bool("floating", false, "make the window floating")
	flagPassthrough         = flag.Bool("passthrough", false, "make the window mouse passthrough")
	flagMaximize            = flag.Bool("maximize", false, "maximize the window")
	flagVsync               = flag.Bool("vsync", true, "enable vsync")
	flagInitFocused         = flag.Bool("initfocused", true, "whether the window is focused on start")
	flagMinWindowSize       = flag.String("minwindowsize", "", "minimum window size (e.g., 100x200)")
	flagMaxWindowSize       = flag.String("maxwindowsize", "", "maximum window size (e.g., 1920x1080)")
	flagGraphicsLibrary     = flag.String("graphicslibrary", "", "graphics library (e.g. opengl)")
	flagRunnableOnUnfocused = flag.Bool("runnableonunfocused", true, "whether the app is runnable even on unfocused")
	flagColorSpace          = flag.String("colorspace", "", "color space ('', 'srgb', or 'display-p3')")
	flagColorMode           = flag.String("colormode", "", "window color mode ('', 'light', or 'dark')")
)

func init() {
	flag.Parse()
}

const (
	initScreenWidth  = 640
	initScreenHeight = 640
	initScreenScale  = 1
)

var (
	gophersImage *ebiten.Image
)

func createRandomIconImage() image.Image {
	const size = 32

	rf := float64(rand.IntN(0x100))
	gf := float64(rand.IntN(0x100))
	bf := float64(rand.IntN(0x100))
	img := ebiten.NewImage(size, size)
	pix := make([]byte, 4*size*size)
	for j := range size {
		for i := range size {
			af := float64(i+j) / float64(2*size)
			if af > 0 {
				pix[4*(j*size+i)] = byte(rf * af)
				pix[4*(j*size+i)+1] = byte(gf * af)
				pix[4*(j*size+i)+2] = byte(bf * af)
				pix[4*(j*size+i)+3] = byte(af * 0xff)
			}
		}
	}
	img.WritePixels(pix)

	return img
}

type game struct {
	debugUI debugui.DebugUI

	count        int
	screenWidth  float64
	screenHeight float64
	positionX    int
	positionY    int

	autoRestore    bool
	autoAdjustment bool
	tps            int
}

func (g *game) Layout(outsideWidth, outsideHeight int) (int, int) {
	// As game implements the interface LayoutFer, Layout is never called and LayoutF is called instead.
	// However, game has to implement Layout to satisfy the interface Game.
	panic("windowsize: Layout must not be called")
}

func (g *game) LayoutF(outsideWidth, outsideHeight float64) (float64, float64) {
	if g.autoAdjustment {
		g.screenWidth, g.screenHeight = outsideWidth, outsideHeight
		return outsideWidth, outsideHeight
	}
	// Ignore the outside size. This means that the offscreen is not adjusted with the outside world.
	return g.screenWidth, g.screenHeight
}

func windowResigingModeString(m ebiten.WindowResizingModeType) string {
	switch m {
	case ebiten.WindowResizingModeDisabled:
		return "Disabled"
	case ebiten.WindowResizingModeOnlyFullscreenEnabled:
		return "Fullscreen Only"
	case ebiten.WindowResizingModeEnabled:
		return "Enabled"
	default:
		panic("not reached")
	}
}

func cursorModeString(m ebiten.CursorModeType) string {
	switch m {
	case ebiten.CursorModeVisible:
		return "Visible"
	case ebiten.CursorModeHidden:
		return "Hidden"
	case ebiten.CursorModeCaptured:
		return "Captured"
	default:
		panic("not reached")
	}
}

func colorModeString(c ebiten.ColorMode) string {
	switch c {
	case ebiten.ColorModeUnknown:
		return "Unknown"
	case ebiten.ColorModeLight:
		return "Light"
	case ebiten.ColorModeDark:
		return "Dark"
	default:
		panic("not reached")
	}
}

func (g *game) Update() error {
	g.tps = ebiten.TPS()
	g.positionX, g.positionY = ebiten.WindowPosition()

	// ebiten.WindowSize can return (0, 0) on browsers or mobiles.
	screenScale := 1.0
	if ww, wh := ebiten.WindowSize(); ww > 0 && wh > 0 {
		screenScale = min(float64(ww)/g.screenWidth, float64(wh)/g.screenHeight)
	}

	// Call SetWindowSize and SetWindowPosition only when necessary.
	// When a window is maximized, SetWindowSize and SetWindowPosition should not be called.
	// Otherwise, the restored window size and position are not correct.
	var (
		toUpdateWindowSize     bool
		toUpdateWindowPosition bool
	)

	if _, err := g.debugUI.Update(func(ctx *debugui.Context) error {
		ctx.Window("Window Size", image.Rect(10, 10, 330, 490), func(layout debugui.ContainerLayout) {
			ctx.Header("Instructions", false, func() {
				ctx.SetGridLayout([]int{-1, -1}, nil)
				ctx.Text("[Arrow keys]")
				ctx.Text("Move the window")
				ctx.Text("[Shift + Arrow keys]")
				ctx.Text("Change the window size")
				ctx.Text("[E]")
				ctx.Text("Restore the window (only when the window is maximized or minimized)")
			})
			ctx.Header("Settings (Window, Desktop Only)", true, func() {
				ctx.SetGridLayout([]int{-2, -1}, nil)

				ctx.Text("Resizing Mode")
				resizingMode := ebiten.WindowResizingMode()
				ctx.Button(windowResigingModeString(resizingMode)).On(func() {
					switch resizingMode {
					case ebiten.WindowResizingModeDisabled:
						ebiten.SetWindowResizingMode(ebiten.WindowResizingModeOnlyFullscreenEnabled)
					case ebiten.WindowResizingModeOnlyFullscreenEnabled:
						ebiten.SetWindowResizingMode(ebiten.WindowResizingModeEnabled)
					case ebiten.WindowResizingModeEnabled:
						ebiten.SetWindowResizingMode(ebiten.WindowResizingModeDisabled)
					default:
						panic("not reached")
					}
				})

				if ebiten.WindowResizingMode() == ebiten.WindowResizingModeEnabled {
					ctx.Text("Maxmize")
					ctx.Button("Maximize").On(func() {
						ebiten.MaximizeWindow()
					})
				}
				ctx.Text("Minimize")
				ctx.Button("Minimize").On(func() {
					ebiten.MinimizeWindow()
				})
				ctx.Text("Auto Restore")
				ctx.Checkbox(&g.autoRestore, "")
				if !g.autoAdjustment {
					ctx.Text("Screen Scale")
					ctx.Button(fmt.Sprintf("%0.2f", screenScale)).On(func() {
						switch {
						case screenScale < 1:
							screenScale = 1
						case screenScale < 1.5:
							screenScale = 1.5
						case screenScale < 2:
							screenScale = 2
						default:
							screenScale = 0.75
						}
						toUpdateWindowSize = true
					})
				}

				ctx.Text("Decorated")
				decorated := ebiten.IsWindowDecorated()
				ctx.Checkbox(&decorated, "").On(func() {
					ebiten.SetWindowDecorated(decorated)
				})
				ctx.Text("Floating")
				floating := ebiten.IsWindowFloating()
				ctx.Checkbox(&floating, "").On(func() {
					ebiten.SetWindowFloating(floating)
				})

				ctx.Text("Update Icon")
				ctx.Button("Update").On(func() {
					ebiten.SetWindowIcon([]image.Image{createRandomIconImage()})
				})
				ctx.Text("Reset Icon")
				ctx.Button("Reset").On(func() {
					ebiten.SetWindowIcon(nil)
				})

				ctx.Text("Window Color Mode")
				mode := ebiten.WindowColorMode()
				ctx.Button(colorModeString(mode)).On(func() {
					switch mode {
					case ebiten.ColorModeLight:
						ebiten.SetWindowColorMode(ebiten.ColorModeDark)
					case ebiten.ColorModeDark:
						ebiten.SetWindowColorMode(ebiten.ColorModeUnknown)
					case ebiten.ColorModeUnknown:
						ebiten.SetWindowColorMode(ebiten.ColorModeLight)
					}
				})
			})
			ctx.Header("Settings (Rendering)", true, func() {
				ctx.SetGridLayout([]int{-2, -1}, nil)

				ctx.Text("Auto Size Adjustment")
				ctx.Checkbox(&g.autoAdjustment, "")
				ctx.Text("Fullscreen")
				fullscreen := ebiten.IsFullscreen()
				ctx.Checkbox(&fullscreen, "").On(func() {
					ebiten.SetFullscreen(fullscreen)
				})
				ctx.Text("Runnable on Unfocused")
				runnableOnUnfocused := ebiten.IsRunnableOnUnfocused()
				ctx.Checkbox(&runnableOnUnfocused, "").On(func() {
					ebiten.SetRunnableOnUnfocused(runnableOnUnfocused)
				})
				ctx.Text("Vsync")
				vsyncEnabled := ebiten.IsVsyncEnabled()
				ctx.Checkbox(&vsyncEnabled, "").On(func() {
					ebiten.SetVsyncEnabled(vsyncEnabled)
				})
				ctx.Text("TPS Mode")
				tpsStr := "Sync w/ FPS"
				if t := ebiten.TPS(); t != ebiten.SyncWithFPS {
					tpsStr = fmt.Sprintf("%d", t)
				}
				ctx.Button(tpsStr).On(func() {
					switch g.tps {
					case ebiten.SyncWithFPS:
						g.tps = 30
					case 30:
						g.tps = 60
					case 60:
						g.tps = 120
					case 120:
						g.tps = ebiten.SyncWithFPS
					default:
						panic("not reached")
					}
				})
				ctx.Text("Clear Screen Every Frame")
				screenCleared := ebiten.IsScreenClearedEveryFrame()
				ctx.Checkbox(&screenCleared, "").On(func() {
					ebiten.SetScreenClearedEveryFrame(screenCleared)
				})
			})
			ctx.Header("Settings (Mouse Cursor)", true, func() {
				ctx.SetGridLayout([]int{-2, -1}, nil)

				ctx.Text("Mode [C]")
				cursorMode := ebiten.CursorMode()
				updateCursorMode := func() {
					switch cursorMode {
					case ebiten.CursorModeVisible:
						ebiten.SetCursorMode(ebiten.CursorModeHidden)
					case ebiten.CursorModeHidden:
						ebiten.SetCursorMode(ebiten.CursorModeCaptured)
					case ebiten.CursorModeCaptured:
						ebiten.SetCursorMode(ebiten.CursorModeVisible)
					}
				}
				ctx.Button(cursorModeString(cursorMode)).On(updateCursorMode)
				if inpututil.IsKeyJustPressed(ebiten.KeyC) {
					updateCursorMode()
				}

				ctx.Text("Passthrough (desktop only) [P]")
				mousePassthrough := ebiten.IsWindowMousePassthrough()
				updateMousePassthrough := func() {
					ebiten.SetWindowMousePassthrough(mousePassthrough)
				}
				ctx.Checkbox(&mousePassthrough, "").On(updateMousePassthrough)
				if inpututil.IsKeyJustPressed(ebiten.KeyP) {
					mousePassthrough = !mousePassthrough
					updateMousePassthrough()
				}
			})
			ctx.Header("Info", true, func() {
				ctx.SetGridLayout([]int{-3, -2}, nil)

				ctx.Text("Window Position")
				wx, wy := ebiten.WindowPosition()
				ctx.Text(fmt.Sprintf("(%d, %d)", wx, wy))

				ctx.Text("Window Size")
				ww, wh := ebiten.WindowSize()
				ctx.Text(fmt.Sprintf("(%d, %d)", ww, wh))

				minw, minh, maxw, maxh := ebiten.WindowSizeLimits()
				ctx.Text("Minimum Window Size")
				ctx.Text(fmt.Sprintf("(%d, %d)", minw, minh))
				ctx.Text("Maximum Window Size")
				ctx.Text(fmt.Sprintf("(%d, %d)", maxw, maxh))

				ctx.Text("Cursor")
				cx, cy := ebiten.CursorPosition()
				ctx.Text(fmt.Sprintf("(%d, %d)", cx, cy))

				ctx.Text("Cursor (float)")
				cxf, cyf := ebiten.CursorPositionF()
				ctx.Text(fmt.Sprintf("(%0.2f, %0.2f)", cxf, cyf))

				ctx.Text("Device Scale Factor")
				ctx.Text(fmt.Sprintf("%0.2f", ebiten.Monitor().DeviceScaleFactor()))

				w, h := ebiten.Monitor().Size()
				ctx.Text("Screen Size in Fullscreen")
				ctx.Text(fmt.Sprintf("(%d, %d)", w, h))

				ctx.Text("Focused?")
				ctx.Text(fmt.Sprintf("%t", ebiten.IsFocused()))

				ctx.Text("TPS")
				ctx.Text(fmt.Sprintf("%0.2f", ebiten.ActualTPS()))

				ctx.Text("FPS")
				ctx.Text(fmt.Sprintf("%0.2f", ebiten.ActualFPS()))

				ctx.Text("Color Mode")
				ctx.Text(colorModeString(ebiten.SystemColorMode()))
			})
			ctx.Header("Info (Debug)", true, func() {
				ctx.SetGridLayout([]int{-2, -1}, nil)

				var debug ebiten.DebugInfo
				ebiten.ReadDebugInfo(&debug)

				ctx.Text("Graphics Lib")
				ctx.Text(debug.GraphicsLibrary.String())

				ctx.Text("GPU Memory Usage")
				ctx.Text(fmt.Sprintf("%d", debug.TotalGPUImageMemoryUsageInBytes))
			})
		})
		return nil
	}); err != nil {
		return err
	}

	const d = 16
	if ebiten.IsKeyPressed(ebiten.KeyShift) {
		if inpututil.IsKeyJustPressed(ebiten.KeyArrowDown) {
			g.screenHeight += d
			toUpdateWindowSize = true
		}
		if inpututil.IsKeyJustPressed(ebiten.KeyArrowUp) {
			if 16 < g.screenHeight && d < g.screenHeight {
				g.screenHeight -= d
				toUpdateWindowSize = true
			}
		}
		if inpututil.IsKeyJustPressed(ebiten.KeyArrowLeft) {
			if 16 < g.screenWidth && d < g.screenWidth {
				g.screenWidth -= d
				toUpdateWindowSize = true
			}
		}
		if inpututil.IsKeyJustPressed(ebiten.KeyArrowRight) {
			g.screenWidth += d
			toUpdateWindowSize = true
		}
	} else {
		if inpututil.IsKeyJustPressed(ebiten.KeyArrowUp) {
			g.positionY -= d
			toUpdateWindowPosition = true
		}
		if inpututil.IsKeyJustPressed(ebiten.KeyArrowDown) {
			g.positionY += d
			toUpdateWindowPosition = true
		}
		if inpututil.IsKeyJustPressed(ebiten.KeyArrowLeft) {
			g.positionX -= d
			toUpdateWindowPosition = true
		}
		if inpututil.IsKeyJustPressed(ebiten.KeyArrowRight) {
			g.positionX += d
			toUpdateWindowPosition = true
		}
	}

	var restore bool
	if ebiten.IsWindowMaximized() || ebiten.IsWindowMinimized() {
		if g.autoRestore {
			restore = g.count%ebiten.TPS() == 0
		} else {
			restore = inpututil.IsKeyJustPressed(ebiten.KeyE)
		}
	}

	if toUpdateWindowSize {
		ebiten.SetWindowSize(int(g.screenWidth*screenScale), int(g.screenHeight*screenScale))
	}

	ebiten.SetTPS(g.tps)
	if toUpdateWindowPosition {
		ebiten.SetWindowPosition(g.positionX, g.positionY)
	}
	if restore {
		ebiten.RestoreWindow()
	}

	g.count++
	return nil
}

func (g *game) Draw(screen *ebiten.Image) {
	w, h := gophersImage.Bounds().Dx(), gophersImage.Bounds().Dy()
	w2, h2 := screen.Bounds().Dx(), screen.Bounds().Dy()
	op := &ebiten.DrawImageOptions{}
	op.GeoM.Translate(float64(-w+w2)/2, float64(-h+h2)/2)
	dx := math.Cos(2*math.Pi*float64(g.count)/360) * 20
	dy := math.Sin(2*math.Pi*float64(g.count)/360) * 20
	op.GeoM.Translate(dx, dy)
	screen.DrawImage(gophersImage, op)

	g.debugUI.Draw(screen)
}

func parseWindowPosition() (int, int, bool) {
	if *flagWindowPosition == "" {
		return 0, 0, false
	}
	tokens := strings.Split(*flagWindowPosition, ",")
	if len(tokens) != 2 {
		return 0, 0, false
	}
	x, err := strconv.Atoi(tokens[0])
	if err != nil {
		return 0, 0, false
	}
	y, err := strconv.Atoi(tokens[1])
	if err != nil {
		return 0, 0, false
	}
	return x, y, true
}

func main() {
	// Decode an image from the image file's byte slice.
	img, _, err := image.Decode(bytes.NewReader(images.Gophers_jpg))
	if err != nil {
		log.Fatal(err)
	}
	gophersImage = ebiten.NewImageFromImage(img)

	ebiten.SetWindowIcon([]image.Image{createRandomIconImage()})

	if x, y, ok := parseWindowPosition(); ok {
		ebiten.SetWindowPosition(x, y)
	}

	g := &game{
		screenWidth:  initScreenWidth,
		screenHeight: initScreenHeight,
	}

	if *flagFullscreen {
		ebiten.SetFullscreen(true)
	}
	if *flagResizable {
		ebiten.SetWindowResizingMode(ebiten.WindowResizingModeEnabled)
	}
	if *flagFloating {
		ebiten.SetWindowFloating(true)
	}
	if *flagPassthrough {
		ebiten.SetWindowMousePassthrough(true)
	}
	if *flagMaximize {
		ebiten.SetWindowResizingMode(ebiten.WindowResizingModeEnabled)
		ebiten.MaximizeWindow()
	}
	if !*flagVsync {
		ebiten.SetVsyncEnabled(false)
	}
	if !*flagRunnableOnUnfocused {
		ebiten.SetRunnableOnUnfocused(false)
	}

	minw, minh, maxw, maxh := -1, -1, -1, -1
	reSize := regexp.MustCompile(`^(\d+)x(\d+)$`)
	if m := reSize.FindStringSubmatch(*flagMinWindowSize); m != nil {
		minw, _ = strconv.Atoi(m[1])
		minh, _ = strconv.Atoi(m[2])
	}
	if m := reSize.FindStringSubmatch(*flagMaxWindowSize); m != nil {
		maxw, _ = strconv.Atoi(m[1])
		maxh, _ = strconv.Atoi(m[2])
	}
	if minw >= 0 || minh >= 0 || maxw >= 0 || maxh >= 0 {
		ebiten.SetWindowSizeLimits(minw, minh, maxw, maxh)
		ebiten.SetWindowResizingMode(ebiten.WindowResizingModeEnabled)
	}

	op := &ebiten.RunGameOptions{}
	switch *flagGraphicsLibrary {
	case "":
		op.GraphicsLibrary = ebiten.GraphicsLibraryAuto
	case "opengl":
		op.GraphicsLibrary = ebiten.GraphicsLibraryOpenGL
	case "directx":
		op.GraphicsLibrary = ebiten.GraphicsLibraryDirectX
	case "metal":
		op.GraphicsLibrary = ebiten.GraphicsLibraryMetal
	default:
		log.Fatalf("unexpected graphics library: %s", *flagGraphicsLibrary)
	}
	switch *flagColorSpace {
	case "":
		op.ColorSpace = ebiten.ColorSpaceDefault
	case "srgb":
		op.ColorSpace = ebiten.ColorSpaceSRGB
	case "display-p3":
		op.ColorSpace = ebiten.ColorSpaceDisplayP3
	}
	switch *flagColorMode {
	case "light":
		ebiten.SetWindowColorMode(ebiten.ColorModeLight)
	case "dark":
		ebiten.SetWindowColorMode(ebiten.ColorModeDark)
	default:
		ebiten.SetWindowColorMode(ebiten.ColorModeUnknown)
	}
	op.InitUnfocused = !*flagInitFocused
	op.ScreenTransparent = *flagTransparent
	op.X11ClassName = "Window-Size"
	op.X11InstanceName = "window-size"

	const title = "Window Size (Ebitengine Demo)"
	ww := int(float64(g.screenWidth) * initScreenScale)
	wh := int(float64(g.screenHeight) * initScreenScale)
	ebiten.SetWindowSize(ww, wh)
	ebiten.SetWindowTitle(title)
	if err := ebiten.RunGameWithOptions(g, op); err != nil {
		log.Fatal(err)
	}
}
