package ui

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

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

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

func init() {
	// These menu types don't need driving, so we can ignore them
	registerBuilder(menus.TypeMenu, nil)     // Menus are just containers
	registerBuilder(menus.TypeDragMenu, nil) // Menus are just containers

	// FIXME: these need implementing

	// Needed for Keyboard.mnu (main -> options -> keyboard)
	registerBuilder(menus.TypeLineKbd, registerDebug("Unimplemented LineKbd", nil))
	registerBuilder(menus.TypeDialogue, registerDebug("Unimplemented Dialogue", nil))

	// Needed for Briefing.mnu
	registerBuilder(menus.TypeLineBriefing, registerDebug("Unimplemented LineBriefing", nil))

	// Needed for ChaEquip.mnu
	registerBuilder(menus.TypeThumb, registerDebug("Unimplemented Thumb", nil))

	// Needed for MainGameChaos.mnu
	registerBuilder(menus.TypeStatusBar, registerDebug("Unimplemented StatusBar", nil))

	// Needed for Multiplayer_Choose.mnu
	registerBuilder(menus.TypeComboBoxItem, registerDebug("Unimplemented ComboBoxItem", nil))
	registerBuilder(menus.TypeDropdownButton, registerDebug("Unimplemented DropdownButton", nil))

	// Needed for Multiplayer_Configure.mnu
	registerBuilder(menus.TypeEditBox, registerDebug("Unimplemented EditBox", nil))

	// Needed for Multiplayer_Connect.mnu
	registerBuilder(menus.TypeRadioButton, registerDebug("Unimplemented RadioButton", nil))
}

const (
	OriginalX = 640.0
	OriginalY = 480.0
)

var (
	// Widgets register their builder here
	widgetBuilders = map[menus.MenuType]builderFunc{}
)

// Used to add widgets to a driver
type builderFunc func(d *Driver, r *menus.Record) error

func registerDebug(reason string, onward builderFunc) builderFunc {
	return func(d *Driver, r *menus.Record) error {
		log.Printf("%v: %#+v", reason, r)
		if onward == nil {
			return registerStatic(d, r)
		} else {
			return onward(d, r)
		}

		return nil
	}
}

func registerBuilder(t menus.MenuType, f builderFunc) {
	if _, ok := widgetBuilders[t]; ok {
		panic(fmt.Sprintf("A builder for menu type %v already exists", t))
	}

	widgetBuilders[t] = f
}

// 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.
type Driver struct {
	Name string

	menu *assetstore.Menu

	// UI elements we need to drive
	clickables []clickable
	freezables []freezable
	hoverables []hoverable
	mouseables []mouseable
	paintables []paintable
	valueables []valueable

	// 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(menu *assetstore.Menu) (*Driver, error) {
	driver := &Driver{
		Name: menu.Name,
		menu: menu,
	}

	for _, record := range menu.Records() {
		if err := driver.addRecord(record); err != nil {
			return nil, err
		}
	}

	return driver, nil
}

func (d *Driver) Value(id string, into *string) error {
	for _, valueable := range d.valueables {
		if valueable.id() == id {
			*into = valueable.value()
			return nil
		}
	}

	return fmt.Errorf("Couldn't find valueable widget %q", id)
}

func (d *Driver) SetValue(id, value string) error {
	for _, valueable := range d.valueables {
		if valueable.id() == id {
			valueable.setValue(value)
			return nil
		}
	}

	return fmt.Errorf("Couldn't find valueable widget %q", id)
}

func (d *Driver) ValueBool(id string, into *bool) error {
	var vStr string
	if err := d.Value(id, &vStr); err != nil {
		return err
	}

	*into = vStr == "1"
	return nil
}

func (d *Driver) SetValueBool(id string, value bool) error {
	vStr := "0"
	if value {
		vStr = "1"
	}

	return d.SetValue(id, vStr)
}

func (d *Driver) SetFreeze(id string, value bool) error {
	for _, freezable := range d.freezables {
		if freezable.id() == id {
			freezable.setFreezeState(value)
			return nil
		}
	}

	return fmt.Errorf("Couldn't find clickable widget %q", id)
}

func (d *Driver) OnClick(id string, f func()) error {
	for _, clickable := range d.clickables {
		if clickable.id() == id {
			clickable.onClick(f)
			return nil
		}
	}

	return fmt.Errorf("Couldn't find clickable widget %q", id)
}

// FIXME: HURK. Surely I'm missing something? steps is value:offset
func (d *Driver) ConfigureSlider(id string, steps map[int]int) error {
	for _, clickable := range d.clickables {
		if slider, ok := clickable.(*slider); ok && slider.id() == id {
			slider.steps = steps

			return nil
		}
	}

	return fmt.Errorf("Couldn't find slider %q", id)
}

func (d *Driver) ValueInt(id string, into *int) error {
	var vStr string
	if err := d.Value(id, &vStr); err != nil {
		return err
	}

	value, err := strconv.Atoi(vStr)
	if err != nil {
		return err
	}

	*into = value

	return nil
}

func (d *Driver) SetValueInt(id string, value int) error {
	vStr := strconv.Itoa(value)

	return d.SetValue(id, vStr)
}

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.hoverables {
		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.clickables {
		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.mouseables {
		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.paintables {
		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) addRecord(record *menus.Record) error {
	//log.Printf("Adding record: %#+v", record)

	handler, ok := widgetBuilders[record.Type]
	if !ok {
		return fmt.Errorf("UI driver encountered unknown menu record: %#+v", record)
	}

	if handler != nil {
		if err := handler(d, record); err != nil {
			return err
		}
	}

	// Recursively add all children of this record
	for _, record := range record.Children {
		if err := d.addRecord(record); err != nil {
			return err
		}
	}

	return nil
}

func (d *Driver) hoverStartEvent(h hoverable, inBounds bool) {
	if inBounds && !h.hoverState() {
		//log.Printf("hoverable false -> true")
		h.setHoverState(true)
	}
}

func (d *Driver) hoverEndEvent(h hoverable, inBounds bool) {
	if !inBounds && h.hoverState() {
		//log.Printf("hoverable true -> false")
		h.setHoverState(false)
	}
}

func (d *Driver) mouseDownEvent(c clickable, inBounds, wasDown, isDown bool) {
	if inBounds && !wasDown && isDown {
		//log.Printf("mouse down false -> true")
		c.setMouseDownState(true)
	}
}

func (d *Driver) mouseClickEvent(c clickable, inBounds, wasDown, isDown bool) {
	if inBounds && wasDown && !isDown {
		//log.Printf("mouse click")
		c.registerMouseClick()
	}
}

func (d *Driver) mouseUpEvent(c clickable, inBounds, wasDown, isDown bool) {
	if inBounds {
		if wasDown && !isDown {
			//log.Printf("mouse down true -> false")
			c.setMouseDownState(false)
		}
	} else {
		if wasDown {
			//log.Printf("mouse down true -> false")
			c.setMouseDownState(false)
		}
	}
}
