package ui

import (
	"bytes"
	"embed"
	"image"
	"image/color"
	_ "image/png"
	"time"

	"gioui.org/f32"
	"gioui.org/gesture"
	"gioui.org/io/input"
	"gioui.org/io/pointer"
	"gioui.org/layout"
	"gioui.org/op"
	"gioui.org/op/clip"
	"gioui.org/op/paint"
	"gioui.org/text"
	"gioui.org/unit"
	"golang.org/x/image/math/fixed"
)

// maxInt32 is a large value used as MaxWidth for single-line text layout.
const maxInt32 = 1<<31 - 1

//go:embed icons/*.png
var iconFS embed.FS

// ScaleProvider provides access to the current scale factor.
type ScaleProvider interface {
	Scale() float32
}

// clickReg pairs a gesture.Click with its handler.
type clickReg struct {
	click   *gesture.Click
	handler func(any)
}

// scrollReg pairs a gesture.Scroll with its handler.
type scrollReg struct {
	scroll  *gesture.Scroll
	handler func(any)
}

// Renderer consumes a slice of elements and draws them.
type Renderer struct {
	theme             Theme
	shp               *text.Shaper
	scale             ScaleProvider
	icons             map[string]image.Image
	clicks            map[string]clickReg
	scrolls           map[string]scrollReg
	displayLineCount  int    // number of display lines from last drawWrappedText
	lastLineY         Dp     // last line baseline offset from text origin, in Dp
}

// New creates a new Renderer.
func New(th Theme, shp *text.Shaper, scale ScaleProvider) *Renderer {
	r := &Renderer{
		theme:   th,
		shp:     shp,
		scale:   scale,
		icons:   make(map[string]image.Image),
		clicks:  make(map[string]clickReg),
		scrolls: make(map[string]scrollReg),
	}
	r.loadIcons()
	return r
}

// loadIcons loads PNG icons from the embedded filesystem.
func (r *Renderer) loadIcons() {
	for _, name := range []string{"cut", "copy", "paste"} {
		data, err := iconFS.ReadFile("icons/" + name + ".png")
		if err != nil {
			continue
		}
		img, _, err := image.Decode(bytes.NewReader(data))
		if err != nil {
			continue
		}
		r.icons[name] = img
	}
}

// icon returns a loaded icon image by name, or nil if not found.
func (r *Renderer) icon(name string) image.Image {
	return r.icons[name]
}

// toPx converts Dp to physical pixels using State's scale.
func (r *Renderer) toPx(dp Dp) Px {
	return ToPx(dp, r.scale.Scale())
}

// toDp converts physical pixels to Dp using State's scale.
func (r *Renderer) toDp(px Px) Dp {
	return ToDp(px, r.scale.Scale())
}

// Draw iterates elements and draws each in slice order (back-to-front).
func (r *Renderer) Draw(gtx layout.Context, elems []Element) {
	// Gio sets constraints to layout.Exact(windowSize), so Min==Max. Use (0,0) as Min.
	winW := gtx.Constraints.Max.X
	winH := gtx.Constraints.Max.Y
	clipRect := clip.Rect{
		Min: image.Point{X: 0, Y: 0},
		Max: image.Point{X: winW, Y: winH},
	}.Push(gtx.Ops)

	for _, e := range elems {
		if !e.Visible() {
			continue
		}
		r.drawElement(gtx, e)
	}

	clipRect.Pop()
}

// registerInteraction registers a gesture for an element.
// Supports Tap and Scroll.
func (r *Renderer) registerInteraction(id string, interaction Interaction, gtx layout.Context, region Region) {
	switch interaction.Gesture {
	case Tap:
		reg, ok := r.clicks[id]
		if !ok {
			reg = clickReg{click: &gesture.Click{}}
			r.clicks[id] = reg
		}
		// click.Add() is called inside drawText, within the text clip
		reg.handler = interaction.Handler
		r.clicks[id] = reg
	case Scroll:
		reg, ok := r.scrolls[id]
		if !ok {
			reg = scrollReg{scroll: &gesture.Scroll{}}
			r.scrolls[id] = reg
		}
		// Clip scroll gesture to element region
		scrollClip := clip.Rect{
			Min: image.Point{X: int(r.toPx(region.X)), Y: int(r.toPx(region.Y))},
			Max: image.Point{X: int(r.toPx(region.X + region.W)), Y: int(r.toPx(region.Y + region.H))},
		}.Push(gtx.Ops)
		reg.scroll.Add(gtx.Ops)
		scrollClip.Pop()
		reg.handler = interaction.Handler
		r.scrolls[id] = reg
	}
}

// CheckGestures checks all registered gestures and returns any events.
func (r *Renderer) CheckGestures(q input.Source, m unit.Metric) []InputEvent {
	var events []InputEvent
	for _, reg := range r.clicks {
		evt, ok := reg.click.Update(q)
		if ok && evt.Kind == gesture.KindClick {
			events = append(events, InputEvent{
				Handler: reg.handler,
				Data:    evt,
			})
		}
	}
	for _, reg := range r.scrolls {
		// gesture.Scroll.Update returns scroll delta in pixels.
		// ScrollY range: Min = -scrollOffset (remaining above), Max = large (content height unknown yet).
		// With Min==Max==0, clampSplit consumes zero scroll.
		delta := reg.scroll.Update(m, q, time.Now(), gesture.Vertical,
			pointer.ScrollRange{}, pointer.ScrollRange{Min: -(1<<30), Max: 1<<30})
		if delta != 0 {
			events = append(events, InputEvent{
				Handler: reg.handler,
				Data:    delta,
			})
		}
	}
	return events
}

// DisplayLineCount returns the number of display lines from the last
// drawWrappedText call. Used by the main loop to report back to logic.
func (r *Renderer) DisplayLineCount() int {
	return r.displayLineCount
}

// LastLineY returns the last line baseline offset from the text origin, in Dp.
// Used by logic to compute max scroll without off-by-one errors.
func (r *Renderer) LastLineY() Dp {
	return r.lastLineY
}

func (r *Renderer) drawElement(gtx layout.Context, e Element) {
	// Register interactions before drawing
	if interactive, ok := e.(Interactive); ok {
		for _, interaction := range interactive.Interactions() {
			r.registerInteraction(interactive.ID(), interaction, gtx, e.Region())
		}
	}

	reg := e.Region()

	if container, ok := e.(Container); ok {
		// Clip to container bounds, draw background, then offset children
		clipRect := clip.Rect{
			Min: image.Point{X: int(r.toPx(reg.X)), Y: int(r.toPx(reg.Y))},
			Max: image.Point{X: int(r.toPx(reg.X + reg.W)), Y: int(r.toPx(reg.Y + reg.H))},
		}.Push(gtx.Ops)
		e.Draw(gtx, r) // draw container background
		offset := op.Offset(image.Pt(int(r.toPx(reg.X)), int(r.toPx(reg.Y)))).Push(gtx.Ops)
		for _, child := range container.children {
			r.drawElement(gtx, child)
		}
		offset.Pop()
		clipRect.Pop()
	} else {
		// Leaf element: no self-clip (region may be zero-sized; clip is handled by parent container)
		e.Draw(gtx, r)
	}
}

func (r *Renderer) drawBg(gtx layout.Context, reg Region, col Color) {
	bgClip := clip.Rect{
		Min: image.Point{X: int(r.toPx(reg.X)), Y: int(r.toPx(reg.Y))},
		Max: image.Point{X: int(r.toPx(reg.X + reg.W)), Y: int(r.toPx(reg.Y + reg.H))},
	}.Push(gtx.Ops)
	paint.ColorOp{Color: color.NRGBA{R: col.R, G: col.G, B: col.B, A: col.A}}.Add(gtx.Ops)
	paint.PaintOp{}.Add(gtx.Ops)
	bgClip.Pop()
}

func (r *Renderer) drawText(gtx layout.Context, str string, size unit.Sp, reg Region, align TextAlign, col Color, id string) {
	if str == "" {
		return
	}
	params := text.Parameters{
		PxPerEm:  fixed.I(gtx.Sp(size)),
		MinWidth: 0,
		MaxWidth: maxInt32,
		MaxLines: 1,
	}

	// Measure text width via glyph iteration (consumes iterator)
	r.shp.LayoutString(params, str)
	var totalAdvance fixed.Int26_6
	for g, ok := r.shp.NextGlyph(); ok; g, ok = r.shp.NextGlyph() {
		totalAdvance += g.Advance
	}
	textW := Dp(float32(totalAdvance>>6) / r.scale.Scale())

	// Compute aligned X position
	var drawX Dp
	switch align {
	case AlignStart:
		drawX = reg.X
	case AlignCenter:
		drawX = reg.X + (reg.W - textW) / 2
	case AlignEnd:
		drawX = reg.X + reg.W - textW
	}

	// Clip to just the text area, register click inside the clip
	textClip := clip.Rect{
		Min: image.Point{X: int(r.toPx(drawX)), Y: int(r.toPx(reg.Y))},
		Max: image.Point{X: int(r.toPx(drawX + textW)), Y: int(r.toPx(reg.Y + reg.H))},
	}.Push(gtx.Ops)

	if cr, ok := r.clicks[id]; ok && cr.click != nil {
		cr.click.Add(gtx.Ops)
	}

	// Layout again (iterator consumed) and draw
	r.shp.LayoutString(params, str)
	r.drawLineText(gtx, drawX, reg.Y, col)

	textClip.Pop()
}

// drawLineText iterates laid-out glyphs and draws each line using op.Record for clipping safety.
func (r *Renderer) drawLineText(gtx layout.Context, x, y Dp, col Color) {
	m := op.Record(gtx.Ops)
	var glyphs [32]text.Glyph
	line := glyphs[:0]
	for g, ok := r.shp.NextGlyph(); ok; g, ok = r.shp.NextGlyph() {
		line = append(line, g)
		if g.Flags&text.FlagLineBreak != 0 || cap(line)-len(line) == 0 {
			r.drawLine(gtx, line, x, y, col)
			line = line[:0]
		}
	}
	if len(line) > 0 {
		r.drawLine(gtx, line, x, y, col)
	}
	call := m.Stop()
	call.Add(gtx.Ops)
}

// drawLine draws a single line of glyphs at the given position.
// Matches Gio's paintGlyph: offset by (x + first.X, y + first.Y).
func (r *Renderer) drawLine(gtx layout.Context, line []text.Glyph, x, y Dp, col Color) {
	if len(line) == 0 {
		return
	}
	first := line[0]
	// Offset: desired document position + first glyph's relative position.
	// first.X is in fixed.Int26_6 (divide by 64 for pixels), first.Y is in pixels.
	offX := float32(gtx.Dp(unit.Dp(x))) + float32(first.X)/64.0
	offY := float32(gtx.Dp(unit.Dp(y))) + float32(first.Y)
	t := op.Affine(f32.Affine2D{}.Offset(f32.Pt(offX, offY))).Push(gtx.Ops)

	// Draw vector glyphs
	path := r.shp.Shape(line)
	outline := clip.Outline{Path: path}.Op().Push(gtx.Ops)
	paint.ColorOp{Color: color.NRGBA{R: col.R, G: col.G, B: col.B, A: col.A}}.Add(gtx.Ops)
	paint.PaintOp{}.Add(gtx.Ops)
	outline.Pop()

	// Draw bitmap glyphs (emoji, etc.)
	if call := r.shp.Bitmaps(line); call != (op.CallOp{}) {
		call.Add(gtx.Ops)
	}

	t.Pop()
}

// drawWrappedText shapes text once with word wrap and draws display lines inline.
// One LayoutString call — no double-shaping. The shaper handles word boundary
// detection via WrapHeuristically. Long words overflow the wrap width.
// Line spacing is fixed: LineHeight = fontSize × LineHeightScale, independent
// of glyph metrics. The shaper's first.Y accounts for line spacing.
func (r *Renderer) drawWrappedText(gtx layout.Context, str string, reg Region, wrapWidth Dp, scrollOffset Dp) {
	if str == "" {
		return
	}

	// Fixed line height based on font size, not glyph metrics
	lineHeightSp := unit.Sp(float32(r.theme.FontSize) * LineHeightScale)
	params := text.Parameters{
		PxPerEm:         fixed.I(gtx.Sp(r.theme.FontSize)),
		MinWidth:        0,
		MaxWidth:        int(r.toPx(wrapWidth)),
		MaxLines:        0,               // unlimited — wrap at MaxWidth
		LineHeight:      fixed.I(gtx.Sp(lineHeightSp)),
		LineHeightScale: 1.0,             // use LineHeight directly, don't scale
		WrapPolicy:      text.WrapHeuristically,
	}
	r.shp.LayoutString(params, str)

	// Clip to TextField region so text doesn't spill into status/bottom bars
	textClip := clip.Rect{
		Min: image.Point{X: int(r.toPx(reg.X)), Y: int(r.toPx(reg.Y))},
		Max: image.Point{X: int(r.toPx(reg.X + reg.W)), Y: int(r.toPx(reg.Y + reg.H))},
	}.Push(gtx.Ops)

	// Y position: region top minus scroll offset.
	// The shaper's first.Y handles line spacing — each line's first.Y is
	// ascent + lineHeight × lineIndex. drawLine adds first.Y to y,
	// so passing the same y for all lines gives correct baseline spacing.
	y := reg.Y - scrollOffset
	col := Color{R: 0, G: 0, B: 0, A: 255} // black text

	m := op.Record(gtx.Ops)
	var glyphs [32]text.Glyph
	line := glyphs[:0]
	lineCount := 0
	var lastGlyphY float32 // shaper Y value of last glyph drawn
	for g, ok := r.shp.NextGlyph(); ok; g, ok = r.shp.NextGlyph() {
		line = append(line, g)
		if g.Flags&text.FlagLineBreak != 0 || cap(line)-len(line) == 0 {
			r.drawLine(gtx, line, reg.X, y, col)
			lastGlyphY = float32(line[len(line)-1].Y)
			line = line[:0]
			if g.Flags&text.FlagLineBreak != 0 {
				lineCount++
			}
		}
	}
	if len(line) > 0 {
		r.drawLine(gtx, line, reg.X, y, col)
		lastGlyphY = float32(line[len(line)-1].Y)
		lineCount++
	}
	call := m.Stop()
	call.Add(gtx.Ops)

	textClip.Pop()
	r.displayLineCount = lineCount
	r.lastLineY = Dp(lastGlyphY / r.scale.Scale()) // pixels → Dp
}

func (r *Renderer) drawPng(gtx layout.Context, img image.Image, reg Region, width, height Dp) {
	if img == nil {
		return
	}
	// Auto-size: if width or height is 0, use the region dimensions
	w := width
	h := height
	if w == 0 && h == 0 {
		w, h = reg.W, reg.H
	} else if w == 0 {
		w = h
	} else if h == 0 {
		h = w
	}
	xPx := int(r.toPx(reg.X))
	yPx := int(r.toPx(reg.Y))
	wPx := int(r.toPx(w))
	hPx := int(r.toPx(h))
	origW := img.Bounds().Dx()
	origH := img.Bounds().Dy()
	if origW == 0 || origH == 0 {
		return
	}
	sx := float32(wPx) / float32(origW)
	sy := float32(hPx) / float32(origH)
	// Position, then scale so the image fills the target size
	offset := op.Offset(image.Pt(xPx, yPx)).Push(gtx.Ops)
	scale := op.Affine(f32.Affine2D{}.Scale(f32.Pt(0, 0), f32.Pt(sx, sy))).Push(gtx.Ops)
	paint.NewImageOp(img).Add(gtx.Ops)
	paint.PaintOp{}.Add(gtx.Ops)
	scale.Pop()
	offset.Pop()
}
