package ui

import (
	"flag"
	"fmt"
	"log"
	"os"
	"runtime/debug"
	"runtime/pprof"

	"github.com/hajimehoshi/ebiten"
	"github.com/hajimehoshi/ebiten/ebitenutil"
	"github.com/hajimehoshi/ebiten/inpututil"
)

type Game interface {
	Update(screenX, screenY int) error
	Draw(*ebiten.Image) error
}

type CustomCursor interface {
	// The cursor draw operation
	Cursor() (*ebiten.Image, *ebiten.DrawImageOptions, error)
}

var (
	screenScale = flag.Float64("screen-scale", 1.0, "Scale the window by this factor")
	cpuprofile  = flag.String("cpuprofile", "", "write cpu profile to `file`")
)

// TODO: move all scaling into Window, so drivers only need to cope with one
// coordinate space. This will allow us to draw custom mouse cursors in the
// window, rather than in the driver.
type Window struct {
	Title             string
	KeyUpHandlers     map[ebiten.Key]func()
	MouseWheelHandler func(float64, float64)
	MouseClickHandler func()

	WhileKeyDownHandlers map[ebiten.Key]func()

	// Allow the "game" to be switched out at any time
	game Game

	debug    bool
	firstRun bool

	xRes int
	yRes int
}

// 0,0 is the *top left* of the window
//
// ebiten assumes a single window, so only call this once...
func NewWindow(game Game, title string, xRes int, yRes int) (*Window, error) {
	ebiten.SetRunnableInBackground(true)

	return &Window{
		Title:    title,
		debug:    true,
		firstRun: true,
		game:     game,
		xRes:     xRes,
		yRes:     yRes,

		WhileKeyDownHandlers: make(map[ebiten.Key]func()),

		KeyUpHandlers: make(map[ebiten.Key]func()),
	}, nil
}

// TODO: multiple handlers for the same key?
func (w *Window) OnKeyUp(key ebiten.Key, f func()) {
	w.KeyUpHandlers[key] = f
}

func (w *Window) WhileKeyDown(key ebiten.Key, f func()) {
	w.WhileKeyDownHandlers[key] = f
}

func (w *Window) OnMouseWheel(f func(x, y float64)) {
	w.MouseWheelHandler = f
}

func (w *Window) OnMouseClick(f func()) {
	w.MouseClickHandler = f
}

func (w *Window) Layout(_, _ int) (int, int) {
	return w.xRes, w.yRes
}

func (w *Window) drawCursor(screen *ebiten.Image) error {
	cIface, ok := w.game.(CustomCursor)
	if !ok {
		return nil
	}

	cursor, op, err := cIface.Cursor()
	if err != nil {
		return err
	}

	// Hide the system cursor if we have a custom one
	if cursor == nil {
		ebiten.SetCursorMode(ebiten.CursorModeVisible)
		return nil
	}

	ebiten.SetCursorMode(ebiten.CursorModeHidden)

	return screen.DrawImage(cursor, op)
}

func (w *Window) Update(screen *ebiten.Image) (outErr error) {
	// Ebiten does not like it if we panic inside its main loop
	defer func() {
		if panicErr := recover(); panicErr != nil {
			if w.debug {
				debug.PrintStack()
			}

			outErr = fmt.Errorf("Panic: %v", panicErr)
		}
	}()

	if err := w.game.Update(screen.Size()); err != nil {
		return err
	}

	// Process keys.
	// FIXME: : should this happen before or after update?
	// TODO: efficient set operations

	for key, cb := range w.KeyUpHandlers {
		if inpututil.IsKeyJustReleased(key) {
			cb()
		}
	}

	for key, cb := range w.WhileKeyDownHandlers {
		if ebiten.IsKeyPressed(key) {
			cb()
		}
	}

	if w.MouseWheelHandler != nil {
		x, y := ebiten.Wheel()
		if x != 0 || y != 0 {
			w.MouseWheelHandler(x, y)
		}
	}

	if w.MouseClickHandler != nil {
		if inpututil.IsMouseButtonJustPressed(ebiten.MouseButtonLeft) {
			w.MouseClickHandler()
		}
	}

	if ebiten.IsDrawingSkipped() {
		return nil
	}

	if err := w.game.Draw(screen); err != nil {
		return err
	}

	if w.debug {
		// Draw FPS, etc, to the screen
		msg := fmt.Sprintf("tps=%0.2f fps=%0.2f", ebiten.CurrentTPS(), ebiten.CurrentFPS())
		ebitenutil.DebugPrint(screen, msg)
	}

	// Draw the cursor last
	return w.drawCursor(screen)
}

// TODO: a stop or other cancellation mechanism
//
// Note that this must be called on the main OS thread
func (w *Window) Run() error {
	if *cpuprofile != "" {
		f, err := os.Create(*cpuprofile)
		if err != nil {
			log.Fatal("could not create CPU profile: ", err)
		}
		defer f.Close() // error handling omitted for example
		if err := pprof.StartCPUProfile(f); err != nil {
			log.Fatal("could not start CPU profile: ", err)
		}
		defer pprof.StopCPUProfile()
	}

	ebiten.SetWindowSize(int(float64(w.xRes)*(*screenScale)), int(float64(w.yRes)*(*screenScale)))
	ebiten.SetWindowTitle(w.Title)
	return ebiten.RunGame(w)
}
