package ui

import (
	"image"
	"image/color"

	"gioui.org/f32"
	"gioui.org/layout"
	"gioui.org/op"
	"gioui.org/op/clip"
	"gioui.org/op/paint"
	"gioui.org/text"
	"gioui.org/unit"
	"gioui.org/widget/material"
	"golang.org/x/image/math/fixed"
)

// Renderer consumes a slice of elements and draws them.
type Renderer struct {
	theme Theme
	shp   *text.Shaper
}

// New creates a new Renderer.
func New(th Theme, shp *text.Shaper) *Renderer {
	return &Renderer{theme: th, shp: shp}
}

// Draw iterates elements and draws each in slice order (back-to-front).
func (r *Renderer) Draw(gtx layout.Context, elems []Element) {
	for _, e := range elems {
		if !e.Visible() {
			continue
		}
		switch v := e.(type) {
		case Label:
			r.drawLabel(gtx, v)
		case ListView:
			r.drawListView(gtx, v)
		case StatusBar:
			r.drawStatusBar(gtx, v)
		case BottomBar:
			r.drawBottomBar(gtx, v)
		case Button:
			r.drawButton(gtx, v)
		default:
			// unknown element type, skip
		}
	}
}

func (r *Renderer) drawLabel(gtx layout.Context, l Label) {
	reg := l.Region()
	th := material.NewTheme()
	th.Shaper = r.shp
	size := l.FontSize
	if size == 0 {
		size = r.theme.FontSize
	}

	// Position label at region origin
	gtx.Constraints.Min.X = int(reg.X)
	gtx.Constraints.Min.Y = int(reg.Y)
	material.Label(th, size, l.Text).Layout(gtx)
}

func (r *Renderer) drawListView(gtx layout.Context, lv ListView) {
	reg := lv.Region()
	th := material.NewTheme()
	th.Shaper = r.shp

	// Position list view at region origin
	gtx.Constraints.Min.X = int(reg.X)
	gtx.Constraints.Min.Y = int(reg.Y)
	for _, item := range lv.Items {
		material.Label(th, r.theme.FontSize, item.Text).Layout(gtx)
	}
}

func (r *Renderer) drawStatusBar(gtx layout.Context, sb StatusBar) {
	reg := sb.Region()
	th := material.NewTheme()
	th.Shaper = r.shp

	// Clip to status bar region
	c := clip.Rect{
		Min: image.Point{X: int(reg.X), Y: int(reg.Y)},
		Max: image.Point{X: int(reg.X + reg.W), Y: int(reg.Y + reg.H)},
	}.Push(gtx.Ops)

	// Line 1: Filename (truncated with ellipsis if needed)
	filenameLineY := reg.Y
	filenameLineH := unit.Dp(24)

	// Truncate filename only if it doesn't fit
	displayFilename := sb.Filename
	availableWidth := int(reg.W - unit.Dp(8) - unit.Dp(40)) // left margin + right icons in DP

	// Layout ellipsis once and get its width
	th.Shaper.LayoutString(text.Parameters{PxPerEm: fixed.I(gtx.Sp(r.theme.FontSize))}, "...")
	var ellipsisWidth int
	for {
		g, ok := th.Shaper.NextGlyph()
		if !ok {
			break
		}
		ellipsisWidth = int((g.X + g.Advance) >> 6)
	}
	// Measure how wide "..." is, then find how many chars fit in availableWidth - ellipsisWidth
	spaceForText := availableWidth - ellipsisWidth

	// Layout filename, measure widths, and draw in a single pass
	r.drawTruncatedText(gtx, th.Shaper, displayFilename, r.theme.FontSize, reg.X+unit.Dp(8), filenameLineY, availableWidth, spaceForText, color.NRGBA{R: 0, G: 0, B: 0, A: 255})

	// Line 2: Icons
	iconsLineY := filenameLineY + filenameLineH

	// Left side: Cut, Copy, Paste icons
	leftX := reg.X + unit.Dp(8)
	iconY := iconsLineY

	if sb.CutCopy {
		r.drawText(gtx, th.Shaper, "✂", r.theme.FontSize, leftX, iconY, color.NRGBA{R: 0, G: 0, B: 0, A: 255})
		leftX += unit.Dp(36)
	}
	if sb.Copy {
		r.drawText(gtx, th.Shaper, "⎘", r.theme.FontSize, leftX, iconY, color.NRGBA{R: 0, G: 0, B: 0, A: 255})
		leftX += unit.Dp(36)
	}
	if sb.Paste {
		r.drawText(gtx, th.Shaper, "⎘", r.theme.FontSize, leftX, iconY, color.NRGBA{R: 0, G: 0, B: 0, A: 255})
		leftX += unit.Dp(36)
	}

	// Right side: Conflict, Search icons
	rightX := reg.X + reg.W - unit.Dp(8)
	if sb.Search {
		rightX -= unit.Dp(36)
		r.drawText(gtx, th.Shaper, "🔍", r.theme.FontSize, rightX, iconY, color.NRGBA{R: 0, G: 0, B: 0, A: 255})
	}
	if sb.ConflictIcon {
		rightX -= unit.Dp(36)
		r.drawText(gtx, th.Shaper, "⚠", r.theme.FontSize, rightX, iconY, color.NRGBA{R: 0, G: 0, B: 0, A: 255})
	}

	c.Pop()
}

func (r *Renderer) drawBottomBar(gtx layout.Context, bb BottomBar) {
	reg := bb.Region()
	th := material.NewTheme()
	th.Shaper = r.shp

	// Left: Cursor position
	cursorX := reg.X + unit.Dp(8)
	call1 := op.Offset(image.Point{X: int(cursorX), Y: int(reg.Y)}).Push(gtx.Ops)
	material.Label(th, r.theme.FontSize, bb.CursorPos).Layout(gtx)
	call1.Pop()

	// Center: Byte position
	byteX := reg.X + reg.W/2 - unit.Dp(60) // approximate center
	call2 := op.Offset(image.Point{X: int(byteX), Y: int(reg.Y)}).Push(gtx.Ops)
	material.Label(th, r.theme.FontSize, bb.BytePos).Layout(gtx)
	call2.Pop()

	// Right: Word wrap button
	wordWrapX := reg.X + reg.W - unit.Dp(80)
	call3 := op.Offset(image.Point{X: int(wordWrapX), Y: int(reg.Y)}).Push(gtx.Ops)
	material.Label(th, r.theme.FontSize, "W:"+boolToString(bb.WordWrap)).Layout(gtx)
	call3.Pop()
}

func (r *Renderer) drawButton(gtx layout.Context, btn Button) {
	reg := btn.Region()
	th := material.NewTheme()
	th.Shaper = r.shp

	// Position button at region origin
	gtx.Constraints.Min.X = int(reg.X)
	gtx.Constraints.Min.Y = int(reg.Y)
	material.Label(th, r.theme.FontSize, btn.Text).Layout(gtx)
}

func (r *Renderer) drawIconButton(gtx layout.Context, th *material.Theme, icon string, x, y unit.Dp) {
	// Position icon button at (x, y)
	gtx.Constraints.Min.X = int(x)
	gtx.Constraints.Min.Y = int(y)
	material.Label(th, r.theme.FontSize, icon).Layout(gtx)
}

func boolToString(b bool) string {
	if b {
		return "On"
	}
	return "Off"
}

// drawTruncatedText layouts text, measures widths, truncates if needed, and draws in a single pass.
func (r *Renderer) drawTruncatedText(gtx layout.Context, shp *text.Shaper, str string, size unit.Sp, x, y unit.Dp, availableWidth, spaceForText int, col color.NRGBA) {
	// Layout text
	shp.LayoutString(text.Parameters{PxPerEm: fixed.I(gtx.Sp(size))}, str)

	// Measure widths and find truncation point
	var widths []int
	var cumWidth fixed.Int26_6
	fullStr := str
	for {
		g, ok := shp.NextGlyph()
		if !ok {
			break
		}
		cumWidth += g.Advance
		w := int(cumWidth >> 6)
		widths = append(widths, w)
	}

	// Truncate if needed
	if len(widths) > 0 && widths[len(widths)-1] > availableWidth {
		// Find truncation point (from longest to shortest)
		for i := len(widths)-1; i >= 0; i-- {
			if widths[i] <= spaceForText {
				fullStr = str[:i] + "..."
				break
			}
		}
	}

	// Re-layout truncated text and draw glyphs directly
	shp.LayoutString(text.Parameters{PxPerEm: fixed.I(gtx.Sp(size))}, fullStr)
	r.drawLineText(gtx, shp, x, y, col)
}

// drawText draws text using the same approach as Gio's textView:
// layout text, iterate glyphs, buffer into lines, and draw using shaper.Shape().
func (r *Renderer) drawText(gtx layout.Context, shp *text.Shaper, str string, size unit.Sp, x, y unit.Dp, col color.NRGBA) {
	// Layout text
	shp.LayoutString(text.Parameters{PxPerEm: fixed.I(gtx.Sp(size))}, str)
	r.drawLineText(gtx, shp, x, y, col)
}

// drawLineText draws already-laid-out glyphs using shaper.Shape() and shaper.Bitmaps().
func (r *Renderer) drawLineText(gtx layout.Context, shp *text.Shaper, x, y unit.Dp, col color.NRGBA) {
	// Match Gio's textView: record into a macro for clipping
	m := op.Record(gtx.Ops)
	var glyphs [32]text.Glyph
	line := glyphs[:0]
	for g, ok := shp.NextGlyph(); ok; g, ok = shp.NextGlyph() {
		line = append(line, g)
		if g.Flags&text.FlagLineBreak != 0 || cap(line)-len(line) == 0 {
			r.drawLine(gtx, shp, line, x, y, col)
			line = line[:0]
		}
	}
	if len(line) > 0 {
		r.drawLine(gtx, shp, line, x, y, col)
	}
	call := m.Stop()
	call.Add(gtx.Ops)
}

// drawLine draws a line of glyphs using shaper.Shape() and shaper.Bitmaps().
func (r *Renderer) drawLine(gtx layout.Context, shp *text.Shaper, line []text.Glyph, x, y unit.Dp, col color.NRGBA) {
	if len(line) == 0 {
		return
	}
	first := line[0]
	// shaper.Shape(line) returns a path where glyph positions are relative to
	// the first glyph (first.X == 0 in the returned path). So we offset by
	// (x, y) and let the shaper handle the rest.
	// first.Y is the baseline; we subtract it so the top of the text is at y.
	offX := float32(gtx.Dp(x))
	offY := float32(gtx.Dp(y)) - float32(first.Y)
	t := op.Affine(f32.Affine2D{}.Offset(f32.Pt(offX, offY))).Push(gtx.Ops)

	// Draw vector glyphs
	path := shp.Shape(line)
	outline := clip.Outline{Path: path}.Op().Push(gtx.Ops)
	paint.ColorOp{Color: col}.Add(gtx.Ops)
	paint.PaintOp{}.Add(gtx.Ops)
	outline.Pop()

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

	t.Pop()
}

// charWidths returns the cumulative width after each rune in str.
func charWidths(gtx layout.Context, shp *text.Shaper, str string, size unit.Sp) []int {
	// Match Gio's textView: PxPerEm = font size in device pixels
	shp.LayoutString(text.Parameters{PxPerEm: fixed.I(gtx.Sp(size))}, str)
	var widths []int
	var cumWidth fixed.Int26_6 = 0
	for {
		g, ok := shp.NextGlyph()
		if !ok {
			break
		}
		cumWidth += g.Advance
		widths = append(widths, int(cumWidth>>6))
	}
	return widths
}
