package ui

import (
	"fmt"
	"image"
	"log"

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

type AlignMode int

const (
	AlignModeCentre AlignMode = 0
	AlignModeLeft   AlignMode = 1
)

// A non-interactive element is not a widget; it merely displays some pixels and
// may optionally have a tooltip for display within bounds.
//
// For non-animated non-interactive elements, just give them a single frame.
type noninteractive struct {
	locator string
	frames  animation
	rect    image.Rectangle

	// Some non-interactives, e.g., overlays, are an image + text to be shown
	label *label

	clickImpl // Alright, alright, it turns out the bridge mission briefing is clickable
	hoverImpl
}

// Paint some text to screen
type label struct {
	align AlignMode
	rect  image.Rectangle
	text  string
	font  *assetstore.Font
}

// This particular animation has entry and exit sequences, which are invoked
// when entering and leaving hover, respectively. Example: bridge doors
type animationHover struct {
	noninteractive           // Use the frames in here for the "enter hover" animation
	exitFrames     animation // and here the "exit hover" animation

	atTick  int // Tracks progress through the frames
	opening bool
	closing bool
}

func (d *Driver) buildNoninteractive(p *menus.Properties) (*noninteractive, error) {
	// FIXME: SpriteID takes precedence over SHARE if present, but is that
	// always right?
	spriteId := p.BaseSpriteID()
	if spriteId < 0 {
		return nil, fmt.Errorf("No base sprite for %v", p.Locator)
	}

	sprite, err := d.menu.Sprite(p.ObjectIdx, spriteId)
	if err != nil {
		return nil, err
	}

	ni := &noninteractive{
		locator: p.Locator,
		frames:  animation{sprite.Image},
		rect:    sprite.Rect.Add(p.Point()),
	}

	return ni, nil
}

func (d *Driver) buildStatic(p *menus.Properties) (*noninteractive, *Widget, error) {
	ni, err := d.buildNoninteractive(p)
	if err != nil {
		return nil, nil, err
	}

	ni.hoverImpl.text = p.Text

	widget := &Widget{
		Locator:       ni.locator,
		Active:        p.Active,
		ownClickables: []clickable{ni}, // FIXME: credits background needs to be clickable
		ownHoverables: []hoverable{ni},
		ownPaintables: []paintable{ni},
	}

	return ni, widget, nil
}

func (d *Driver) buildHypertext(p *menus.Properties) (*noninteractive, *Widget, error) {
	ni, err := d.buildNoninteractive(p)
	if err != nil {
		return nil, nil, err
	}

	// FIXME: check if this is still needed on the bridge -> briefing transition
	widget := &Widget{
		Locator:       ni.locator,
		Active:        p.Active,
		ownClickables: []clickable{ni},
		ownHoverables: []hoverable{ni},
	}

	return ni, widget, nil
}

// An overlay is a static image + some text that needs to be rendered
func (d *Driver) buildOverlay(p *menus.Properties) (*noninteractive, *Widget, error) {
	ni, err := d.buildNoninteractive(p)
	if err != nil {
		return nil, nil, err
	}

	widget := &Widget{
		Locator:       ni.locator,
		Active:        p.Active,
		ownPaintables: []paintable{ni},
	}

	if p.Text != "" {
		// FIXME: is this always right? Seems to make sense for Main.mnu
		fnt := d.menu.Font(p.FontType/10 - 1)

		ni.label = &label{
			font: fnt,
			rect: ni.rect, // We will be centered by default
			text: p.Text,
		}
	} else {
		log.Printf("Overlay without text detected in %v", p.Locator)
	}

	return ni, widget, nil
}

// An animation is a non-interactive element that displays something in a loop
func (d *Driver) buildAnimationSample(p *menus.Properties) (*noninteractive, *Widget, error) {
	sprite, err := d.menu.Sprite(p.ObjectIdx, p.SpriteId[0])
	if err != nil {
		return nil, nil, err
	}

	frames, err := d.menu.Images(p.ObjectIdx, p.SpriteId[0], p.DrawType)
	if err != nil {
		return nil, nil, err
	}

	ani := &noninteractive{
		locator:   p.Locator,
		frames:    animation(frames),
		hoverImpl: hoverImpl{text: p.Text},
		rect:      sprite.Rect.Add(p.Point()),
	}

	widget := &Widget{
		Active:        p.Active,
		ownHoverables: []hoverable{ani},
		ownPaintables: []paintable{ani},
	}

	return ani, widget, nil
}

func (d *Driver) buildAnimationHover(p *menus.Properties) (*animationHover, *Widget, error) {
	sprite, err := d.menu.Sprite(p.ObjectIdx, p.SpriteId[0])
	if err != nil {
		return nil, nil, err
	}

	enterFrames, err := d.menu.Images(p.ObjectIdx, p.SpriteId[0], p.DrawType)
	if err != nil {
		return nil, nil, err
	}

	exitFrames, err := d.menu.Images(p.ObjectIdx, p.SpriteId[0]+p.DrawType, p.DrawType)
	if err != nil {
		return nil, nil, err
	}

	ani := &animationHover{
		noninteractive: noninteractive{
			locator:   p.Locator,
			frames:    animation(enterFrames),
			hoverImpl: hoverImpl{text: p.Text},
			rect:      sprite.Rect.Add(p.Point()),
		},

		exitFrames: animation(exitFrames),
	}

	widget := &Widget{
		Active:        p.Active,
		ownHoverables: []hoverable{ani},
		ownPaintables: []paintable{ani},
	}

	return ani, widget, nil
}

func (n *noninteractive) id() string {
	return n.locator
}

func (n *noninteractive) bounds() image.Rectangle {
	return n.rect
}

func (n *noninteractive) regions(tick int) []region {
	out := oneRegion(n.bounds().Min, n.frames.image(tick))

	// Text for a noninteractive is not registered separately
	if n.label != nil {
		out = append(out, n.label.regions(tick)...)
	}

	return out
}

func (a *animationHover) regions(tick int) []region {
	if a.opening || a.closing {
		var anim animation
		if a.opening {
			anim = a.frames
		} else {
			anim = a.exitFrames
		}

		out := oneRegion(a.bounds().Min, anim[a.atTick])

		if a.atTick < len(anim)-1 {
			a.atTick += 1
		} else if !a.hoverState() {
			a.closing = false
		}

		return out
	}

	// Nothing doing, show a closed door
	return oneRegion(a.bounds().Min, a.frames.image(0))
}

func (a *animationHover) setHoverState(value bool) {
	a.atTick = 0
	a.opening = value
	a.closing = !value

	a.hoverImpl.setHoverState(value)
}

// Top-left of where to start drawing the text. We want it to appear to be in
// the centre of the rect.
//
// TODO: additional modes (left-aligned, especially)
func (l *label) pos() image.Point {
	pos := l.rect.Min

	textRect := l.font.CalculateBounds(l.text)

	// Centre the text horizontally
	if l.align == AlignModeCentre {
		xSlack := l.rect.Dx() - textRect.Dx()
		if xSlack > 0 {
			pos.X += xSlack / 2
		}
	} else {
		// FIXME: we're giving it 8pts of left to not look horrible
		pos.X += 8
	}

	// Centre the text vertically
	ySlack := l.rect.Dy() - textRect.Dy()
	if ySlack > 0 {
		pos.Y += ySlack / 2
	}

	return pos
}

func (l *label) regions(tick int) []region {
	var out []region

	pt := l.pos()

	for _, r := range l.text {
		glyph, err := l.font.Glyph(r)
		if err != nil {
			log.Printf("FIXME: ignoring misssing glyph %v", r)
			continue
		}

		out = append(out, oneRegion(pt, glyph.Image)...)
		pt.X += glyph.Rect.Dx()
	}

	return out
}
