package pncdsl

import (
	"image/color"

	"github.com/hajimehoshi/ebiten/v2"
	"github.com/hajimehoshi/ebiten/v2/vector"
)

// engine is the ebiten.Game adapter. It owns the per-frame Update/Draw flow
// and delegates everything stateful to *Game.
type engine struct {
	g *Game
}

func (e *engine) Layout(outsideWidth, outsideHeight int) (int, int) {
	return e.g.Width, e.g.Height
}

func (e *engine) Update() error {
	g := e.g
	dt := 1.0 / 60.0
	g.input.poll()
	g.UI.tick(dt)
	g.transition.update(dt)

	if g.transition.active && g.transition.out {
		// during fade-out, freeze input
		return nil
	}

	if g.UI.endCard != "" {
		return nil
	}

	if g.scriptRunner != nil {
		ctx := g.scriptCtx
		ctx.DT = dt
		// keep Scene reference fresh in case the script changed it
		if g.currentScene != "" {
			s := g.SceneManager.MustGet(g.currentScene)
			ctx.Scene = &s
		}
		s := g.scriptRunner.Tick(ctx)
		if s != StatusRunning {
			g.scriptRunner = nil
			g.scriptCtx = nil
		}
		g.tickCharacters(dt)
		return nil
	}

	// dialog click handling
	if g.dialog != nil {
		if g.input.LeftClicked() {
			g.input.ConsumeLeft()
			ctx := g.makeCtx()
			picked := g.dialog.handleClick(ctx, g.input.Point())
			if picked != nil {
				// record once-tracking
				if picked.Once {
					g.State.NoteTalked(g.dialog.node.Name + ":" + picked.Text)
				}
				// run the choice's actions as a synthetic script
				if len(picked.Actions) > 0 {
					g.queueAction(Seq(picked.Actions...), "choice")
				}
			}
		}
		g.tickCharacters(dt)
		return nil
	}

	// free interaction
	e.handleFreeInput()
	g.tickCharacters(dt)
	return nil
}

func (e *engine) handleFreeInput() {
	g := e.g
	g.UI.rebuildVerbButtons()
	mp := g.input.Point()

	// hover label resolution
	g.UI.hoverLabel = ""
	if mp.Y < uiPanelY-4 {
		if h := e.hotspotAt(mp); h != nil {
			if h.Label != "" {
				g.UI.hoverLabel = h.Label
			} else {
				g.UI.hoverLabel = h.Name
			}
		}
	} else {
		// hovering UI: show item description when over an inv slot
		if hit := g.UI.hitTest(mp); len(hit) > 4 && hit[:4] == "inv:" {
			name := hit[4:]
			if it, ok := g.ItemManager.Get(name); ok && it.Description != "" {
				g.UI.hoverLabel = it.Description
			} else {
				g.UI.hoverLabel = name
			}
		}
	}

	if g.input.RightClicked() {
		g.input.ConsumeRight()
		// right click: deselect item / reset verb
		if g.Inventory.Selected() != "" {
			g.Inventory.Select("")
		} else {
			g.selectedVerb = "look"
		}
	}

	if !g.input.LeftClicked() {
		return
	}

	// UI hits first
	if hit := g.UI.hitTest(mp); hit != "" {
		g.input.ConsumeLeft()
		switch hit[:4] {
		case "verb":
			g.selectedVerb = hit[5:]
		case "inv:":
			name := hit[4:]
			if g.selectedVerb == "look" {
				if it, ok := g.ItemManager.Get(name); ok && it.Description != "" {
					g.queueAction(Say("player", it.Description), "inv-look")
					return
				}
			}
			if g.Inventory.Selected() == name {
				g.Inventory.Select("")
			} else {
				g.Inventory.Select(name)
			}
		}
		return
	}

	// game-world click
	g.input.ConsumeLeft()
	h := e.hotspotAt(mp)
	if h == nil {
		return
	}
	sel := g.Inventory.Selected()
	if sel != "" {
		// use-with: first check hotspot's OnUseWith, then item's
		if h.OnUseWith != nil {
			if a, ok := h.OnUseWith[sel]; ok && a != nil {
				g.queueAction(a, "useWith hotspot")
				g.Inventory.Select("")
				return
			}
		}
		if it, ok := g.ItemManager.Get(sel); ok && it.OnUseWith != nil {
			if a, ok := it.OnUseWith[h.Name]; ok && a != nil {
				g.queueAction(a, "useWith item")
				g.Inventory.Select("")
				return
			}
		}
		g.UI.FlashLine("Nem ehhez.")
		g.Inventory.Select("")
		return
	}
	a := h.handler(g.selectedVerb)
	if a == nil {
		if v, ok := g.VerbManager.Get(g.selectedVerb); ok && v.Default != nil {
			a = v.Default
		}
	}
	if a == nil {
		g.UI.FlashLine("Semmi említésre méltó.")
		return
	}
	g.queueAction(a, "hotspot "+g.selectedVerb+" "+h.Name)
}

func (e *engine) hotspotAt(p Point) *Hotspot {
	g := e.g
	if g.currentScene == "" {
		return nil
	}
	s := g.SceneManager.MustGet(g.currentScene)
	for i := range s.Hotspots {
		h := &s.Hotspots[i]
		if h.Area != nil && h.Area.Contains(p) {
			return h
		}
	}
	return nil
}

func (e *engine) Draw(screen *ebiten.Image) {
	g := e.g
	// scene background
	if g.currentScene != "" {
		s := g.SceneManager.MustGet(g.currentScene)
		img := g.loaded.image(g.AssetManager, s.Background)
		op := &ebiten.DrawImageOptions{}
		bw, bh := img.Bounds().Dx(), img.Bounds().Dy()
		if bw > 0 && bh > 0 {
			op.GeoM.Scale(float64(g.Width)/float64(bw), float64(g.Height)/float64(bh))
		}
		screen.DrawImage(img, op)

		// hotspot debug outlines
		if DebugLog {
			for _, h := range s.Hotspots {
				if r, ok := h.Area.(Rectangle); ok {
					vector.StrokeRect(screen, float32(r.X), float32(r.Y), float32(r.W), float32(r.H), 1, color.RGBA{255, 255, 0, 200}, false)
				}
			}
		}
	} else {
		screen.Fill(color.RGBA{0, 0, 0, 255})
	}

	// characters (y-sorted)
	for _, c := range sortedChars(g) {
		drawCharacter(screen, g, c)
	}

	// speech bubble
	g.UI.speech.draw(screen, g)

	// dialog box (if active)
	if g.dialog != nil {
		g.dialog.draw(screen, g)
	} else {
		g.UI.drawHUD(screen)
	}

	// transition overlay
	g.transition.draw(screen, g.Width, g.Height)

	// end card
	if g.UI.endCard != "" {
		vector.DrawFilledRect(screen, 0, 0, float32(g.Width), float32(g.Height), color.RGBA{0, 0, 0, 230}, false)
		w := textWidth(g.UI.endCard)
		drawText(screen, g.UI.endCard, (g.Width-w)/2, g.Height/2-8, color.White)
	}

	// cursor on top
	drawCursor(screen, g)
}

func sortedChars(g *Game) []*runtimeChar {
	out := make([]*runtimeChar, 0, len(g.chars))
	for _, c := range g.chars {
		out = append(out, c)
	}
	// insertion sort by Y (small N)
	for i := 1; i < len(out); i++ {
		for j := i; j > 0 && out[j].pos.Y < out[j-1].pos.Y; j-- {
			out[j], out[j-1] = out[j-1], out[j]
		}
	}
	return out
}

func drawCharacter(dst *ebiten.Image, g *Game, c *runtimeChar) {
	w := c.def.W
	h := c.def.H
	if w == 0 {
		w = 24
	}
	if h == 0 {
		h = 56
	}
	// Try the real sprite first. Only fall through to the stylized
	// placeholder when no actual art is on disk.
	if c.def.Sprite != "" && !g.loaded.isPlaceholder(c.def.Sprite) {
		img := g.loaded.image(g.AssetManager, c.def.Sprite)
		sw, sh := img.Bounds().Dx(), img.Bounds().Dy()
		if sw > 0 && sh > 0 {
			op := &ebiten.DrawImageOptions{}
			op.GeoM.Scale(w/float64(sw), h/float64(sh))
			op.GeoM.Translate(c.pos.X-w/2, c.pos.Y-h)
			dst.DrawImage(img, op)
			return
		}
	}
	// Touch the asset cache so isPlaceholder is populated for later frames.
	if c.def.Sprite != "" {
		_ = g.loaded.image(g.AssetManager, c.def.Sprite)
	}
	x := c.pos.X - w/2
	y := c.pos.Y - h
	bodyCol := color.RGBA{200, 200, 200, 255}
	if rgba, ok := c.def.SpeechColor.(color.RGBA); ok {
		bodyCol = rgba
	}
	if w > h {
		drawQuadrupedPlaceholder(dst, x, y, w, h, bodyCol)
	} else {
		drawHumanoidPlaceholder(dst, x, y, w, h, bodyCol)
	}
}

func drawHumanoidPlaceholder(dst *ebiten.Image, x, y, w, h float64, body color.RGBA) {
	pants := color.RGBA{40, 50, 90, 255}
	skin := color.RGBA{245, 200, 155, 255}
	outline := color.RGBA{20, 20, 30, 255}

	headR := h * 0.14
	headCY := y + headR + 1

	// pants / legs — two narrow strips
	pantsTop := y + h*0.62
	pantsH := h - (pantsTop - y) - 1
	legW := w * 0.36
	vector.DrawFilledRect(dst, float32(x+w*0.08), float32(pantsTop), float32(legW), float32(pantsH), pants, false)
	vector.DrawFilledRect(dst, float32(x+w*0.56), float32(pantsTop), float32(legW), float32(pantsH), pants, false)

	// torso — full-width rect, shirt color
	torsoTop := headCY + headR
	torsoH := pantsTop - torsoTop
	vector.DrawFilledRect(dst, float32(x), float32(torsoTop), float32(w), float32(torsoH), body, false)
	vector.StrokeRect(dst, float32(x), float32(torsoTop), float32(w), float32(torsoH), 1, outline, false)

	// head — circle in skin tone
	vector.DrawFilledCircle(dst, float32(x+w/2), float32(headCY), float32(headR), skin, true)

	// feet — short dark blobs at the very bottom
	footH := 2.0
	vector.DrawFilledRect(dst, float32(x+w*0.05), float32(y+h-footH), float32(legW+2), float32(footH), outline, false)
	vector.DrawFilledRect(dst, float32(x+w*0.55), float32(y+h-footH), float32(legW+2), float32(footH), outline, false)
}

func drawQuadrupedPlaceholder(dst *ebiten.Image, x, y, w, h float64, body color.RGBA) {
	outline := color.RGBA{20, 20, 30, 255}
	dark := color.RGBA{
		R: body.R / 2, G: body.G / 2, B: body.B / 2, A: 255,
	}

	// body
	bodyY := y + h*0.30
	bodyH := h * 0.65
	vector.DrawFilledRect(dst, float32(x), float32(bodyY), float32(w*0.78), float32(bodyH), body, false)

	// head — circle on the right
	headR := h * 0.30
	headCX := x + w - headR
	headCY := y + h*0.50
	vector.DrawFilledCircle(dst, float32(headCX), float32(headCY), float32(headR), body, true)

	// ears — two triangles approximated as small rects
	earW := w * 0.07
	earH := h * 0.30
	vector.DrawFilledRect(dst, float32(headCX-headR*0.7), float32(y), float32(earW), float32(earH), body, false)
	vector.DrawFilledRect(dst, float32(headCX+headR*0.4), float32(y), float32(earW), float32(earH), body, false)

	// eye — dark dot
	vector.DrawFilledCircle(dst, float32(headCX+headR*0.25), float32(headCY-1), 1.2, outline, true)

	// tail — small dark line stub on the left
	vector.DrawFilledRect(dst, float32(x-w*0.04), float32(bodyY+1), float32(w*0.08), 2, dark, false)

	// legs — two short bars at the bottom
	legW := w * 0.07
	vector.DrawFilledRect(dst, float32(x+w*0.10), float32(y+h-3), float32(legW), 3, dark, false)
	vector.DrawFilledRect(dst, float32(x+w*0.55), float32(y+h-3), float32(legW), 3, dark, false)
}
