package ui

import (
	"fmt"
	"image"
	"runtime/debug"

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

	"code.ur.gs/lupine/ordoor/internal/assetstore"
)

const (
	OriginalX = 640.0
	OriginalY = 480.0
)

// Driver acts as an interface between the main loop and the widgets specified
// in a menu.
//
// Menu assets assume a 640x480 screen; Driver is responsible for scaling to the
// actual screen size when drawing.
//
// TODO: move scaling responsibilities to Window?
type Driver struct {
	Name string

	assets *assetstore.AssetStore
	menu   *assetstore.Menu

	// UI elements we need to drive. Note that widgets are hierarchical - these
	// are just the toplevel. Dialogues are separated out. We only want to show
	// one dialogue at a time, and if a dialogue is active, the main widgets are
	// unusable (i.e., dialogues are modal)
	dialogues []*Widget
	widgets   []*Widget

	activeDialogue *Widget

	cursor assetstore.CursorName

	// The cursor in two different coordinate spaces: original, and screen-scaled
	cursorOrig   image.Point
	cursorScaled image.Point

	// These two matrices are used for scaling between the two
	orig2native ebiten.GeoM
	native2orig ebiten.GeoM

	ticks   int // Used in animation effects
	tooltip string
}

func NewDriver(assets *assetstore.AssetStore, menu *assetstore.Menu) (*Driver, error) {
	driver := &Driver{
		Name: menu.Name,

		assets: assets,
		menu:   menu,
	}

	for _, group := range menu.Groups() {
		if err := driver.registerGroup(group); err != nil {
			return nil, err
		}
	}

	return driver, nil
}

func (d *Driver) Update(screenX, screenY int) error {
	if d == nil {
		debug.PrintStack()
		return fmt.Errorf("Tried to update a nil ui.Driver")
	}

	// This will be updated while processing hovers
	d.tooltip = ""
	d.ticks += 1

	// Update translation matrices
	d.orig2native.Reset()
	d.orig2native.Scale(float64(screenX)/OriginalX, float64(screenY)/OriginalY)

	d.native2orig = d.orig2native
	d.native2orig.Invert()

	// Update original and scaled mouse coordinates
	mouseX, mouseY := ebiten.CursorPosition()
	d.cursorScaled = image.Pt(mouseX, mouseY)

	mnX, mnY := d.native2orig.Apply(float64(mouseX), float64(mouseY))
	d.cursorOrig = image.Pt(int(mnX), int(mnY))

	// Dispatch notifications to our widgets
	for _, hoverable := range d.activeHoverables() {
		inBounds := d.cursorOrig.In(hoverable.bounds())

		d.hoverStartEvent(hoverable, inBounds)
		d.hoverEndEvent(hoverable, inBounds)

		if hoverable.hoverState() && hoverable.tooltip() != "" {
			d.tooltip = hoverable.tooltip()
		}
	}

	mouseIsDown := ebiten.IsMouseButtonPressed(ebiten.MouseButtonLeft)
	for _, clickable := range d.activeClickables() {
		inBounds := d.cursorOrig.In(clickable.bounds())
		mouseWasDown := clickable.mouseDownState()

		d.mouseDownEvent(clickable, inBounds, mouseWasDown, mouseIsDown)
		d.mouseClickEvent(clickable, inBounds, mouseWasDown, mouseIsDown)
		d.mouseUpEvent(clickable, inBounds, mouseWasDown, mouseIsDown)
	}

	for _, mouseable := range d.activeMouseables() {
		mouseable.registerMousePosition(d.cursorOrig)
	}

	return nil
}

func (d *Driver) Draw(screen *ebiten.Image) error {
	if d == nil {
		debug.PrintStack()
		return fmt.Errorf("Tried to draw a nil ui.Driver")
	}

	var do ebiten.DrawImageOptions

	for _, paint := range d.activePaintables() {
		for _, region := range paint.regions(d.ticks) {
			x, y := d.orig2native.Apply(float64(region.offset.X), float64(region.offset.Y))

			do.GeoM = d.orig2native
			do.GeoM.Translate(x, y)

			if err := screen.DrawImage(region.image, &do); err != nil {
				return err
			}
		}
	}

	if d.tooltip != "" {
		x, y := d.cursorScaled.X+16, d.cursorScaled.Y-16
		ebitenutil.DebugPrintAt(screen, d.tooltip, x, y)
	}

	return nil
}

func (d *Driver) Cursor() (*ebiten.Image, *ebiten.DrawImageOptions, error) {
	cursor, err := d.assets.Cursor(d.cursor)
	if err != nil {
		return nil, nil, err
	}

	op := &ebiten.DrawImageOptions{}
	op.GeoM.Translate(float64(d.cursorOrig.X), float64(d.cursorOrig.Y))
	op.GeoM.Concat(d.orig2native)
	op.GeoM.Translate(float64(-cursor.Hotspot.X), float64(-cursor.Hotspot.Y))

	return cursor.Image, op, nil
}

func (d *Driver) allClickables() []clickable {
	var out []clickable

	for _, widget := range d.widgets {
		out = append(out, widget.allClickables()...)
	}

	for _, widget := range d.dialogues {
		out = append(out, widget.allClickables()...)
	}

	return out
}

func (d *Driver) allFreezables() []freezable {
	var out []freezable
	for _, widget := range d.widgets {
		out = append(out, widget.allFreezables()...)
	}

	for _, widget := range d.dialogues {
		out = append(out, widget.allFreezables()...)
	}

	return out
}

func (d *Driver) allValueables() []valueable {
	var out []valueable

	for _, widget := range d.widgets {
		out = append(out, widget.allValueables()...)
	}

	for _, widget := range d.dialogues {
		out = append(out, widget.allValueables()...)
	}

	return out
}

func (d *Driver) activeClickables() []clickable {
	if d.activeDialogue != nil {
		return d.activeDialogue.activeClickables()
	}

	var out []clickable
	for _, widget := range d.widgets {
		out = append(out, widget.activeClickables()...)
	}

	return out
}

func (d *Driver) activeHoverables() []hoverable {
	if d.activeDialogue != nil {
		return d.activeDialogue.activeHoverables()
	}

	var out []hoverable
	for _, widget := range d.widgets {
		out = append(out, widget.activeHoverables()...)
	}

	return out
}

func (d *Driver) activeMouseables() []mouseable {
	if d.activeDialogue != nil {
		return d.activeDialogue.activeMouseables()
	}

	var out []mouseable
	for _, widget := range d.widgets {
		out = append(out, widget.activeMouseables()...)
	}

	return out
}

func (d *Driver) activePaintables() []paintable {
	var out []paintable

	for _, widget := range d.widgets {
		out = append(out, widget.activePaintables()...)
	}

	if d.activeDialogue != nil {
		out = append(out, d.activeDialogue.activePaintables()...)
	}

	return out
}

func (d *Driver) findWidget(locator string) *Widget {
	toplevels := append(d.widgets, d.dialogues...)
	for _, widget := range toplevels {
		if w := widget.findWidget(locator); w != nil {
			return w
		}
	}

	return nil
}
