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"
)

// StateReader provides access to scale and window dimensions from State.
// Implemented by editor.State to avoid import cycles.
type StateReader interface {
	Scale() float32
	ScreenWidth() Dp
	ScreenHeight() Dp
}

// Renderer consumes a slice of elements and draws them.
type Renderer struct {
	theme Theme
	shp   *text.Shaper
	state StateReader // read-only access to State for scale/window size
}

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

// SetState sets the State reference for the Renderer.
// The Renderer reads scale and window size from State directly.
func (r *Renderer) SetState(state StateReader) {
	r.state = state
}

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

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

// Draw iterates elements and draws each in slice order (back-to-front).
// All scale and window size values come from State, never from gtx.
func (r *Renderer) Draw(gtx layout.Context, elems []Element) {
	// Use window dimensions from State — convert DP to pixels using State.Scale
	state := r.state
	windowWPx := int(r.toPx(state.ScreenWidth()))
	windowHPx := int(r.toPx(state.ScreenHeight()))
	initialConstraints := WindowConstraints{
		Min: image.Point{X: 0, Y: 0},
		Max: image.Point{X: windowWPx, Y: windowHPx},
	}

	for _, e := range elems {
		if !e.Visible() {
			continue
		}
		switch v := e.(type) {
		case Label:
			r.drawLabel(gtx, v, initialConstraints)
		case ListView:
			r.drawListView(gtx, v, initialConstraints)
		case StatusBar:
			r.drawStatusBar(gtx, v, initialConstraints)
		case BottomBar:
			r.drawBottomBar(gtx, v, initialConstraints)
		case Button:
			r.drawButton(gtx, v, initialConstraints)
		default:
			// unknown element type, skip
		}
	}
}

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

	// Convert region to pixels for Gio interop
	gtx.Constraints.Min.X = int(r.toPx(reg.X))
	gtx.Constraints.Min.Y = int(r.toPx(reg.Y))
	material.Label(th, size, l.Text).Layout(gtx)
}

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

	// Convert region to pixels for Gio interop
	gtx.Constraints.Min.X = int(r.toPx(reg.X))
	gtx.Constraints.Min.Y = int(r.toPx(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, _ WindowConstraints) {
	reg := sb.Region()
	th := material.NewTheme()
	th.Shaper = r.shp

	// Draw light gray background
	bgColor := color.NRGBA{R: 230, G: 230, B: 230, A: 255}
	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: bgColor}.Add(gtx.Ops)
	paint.PaintOp{}.Add(gtx.Ops)
	bgClip.Pop()

	// Clip to status bar region for text (convert Dp to pixels)
	c := 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)

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

	// Truncate filename only if it doesn't fit
	displayFilename := sb.Filename
	availableWidth := int(r.toPx(reg.W - Dp(8) - 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)),
		MinWidth: 0,
		MaxWidth: 1000,
		MaxLines: 1,
	}, "...")
	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+Dp(8), filenameLineY, availableWidth, spaceForText, color.NRGBA{R: 0, G: 0, B: 0, A: 255})

	// Line 2: Icons — positioned 20DP below filename baseline.
	iconsLineY := filenameLineY + Dp(20)

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

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

	// Right side: Conflict (conditional), Search (always visible)
	rightX := reg.X + reg.W - Dp(8)
	if sb.ConflictIcon {
		rightX -= Dp(36)
		r.drawText(gtx, th.Shaper, "⚠", r.theme.FontSize, rightX, iconY, color.NRGBA{R: 0, G: 0, B: 0, A: 255})
	}
	rightX -= 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, constraints WindowConstraints) {
	reg := bb.Region()

	// Draw light gray background
	bgColor := color.NRGBA{R: 230, G: 230, B: 230, A: 255}
	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: bgColor}.Add(gtx.Ops)
	paint.PaintOp{}.Add(gtx.Ops)
	bgClip.Pop()

	// Use captured constraints for positioning — they are in pixels and
	// represent the actual window bounds. Never use gtx.Constraints here
	// because clips modify them.
	windowW := constraints.Max.X
	windowH := constraints.Max.Y

	// Convert region to pixels for positioning
	regXPx := r.toPx(reg.X)
	regWPx := r.toPx(reg.W)

	// Clamp right edge to window width
	rightXPx := regXPx + regWPx
	if rightXPx > Px(windowW) {
		rightXPx = Px(windowW)
	}
	barW := rightXPx - regXPx

	// Position BottomBar at bottom of visible window (in pixels)
	bottomBarHeightPx := r.toPx(Dp(24))
	marginPx := r.toPx(Dp(10))
	drawYPx := Px(windowH) - bottomBarHeightPx - marginPx

	// Convert draw position back to Dp for text rendering
	drawY := r.toDp(drawYPx)

	// Left: Cursor position
	cursorXPx := regXPx + r.toPx(Dp(8))
	cursorXDp := r.toDp(cursorXPx)
	r.drawText(gtx, r.shp, bb.CursorPos, r.theme.FontSize, cursorXDp, drawY, color.NRGBA{R: 0, G: 0, B: 0, A: 255})

	// Center: Byte position
	byteXPx := regXPx + barW/2 - r.toPx(Dp(60))
	byteXDp := r.toDp(byteXPx)
	r.drawText(gtx, r.shp, bb.BytePos, r.theme.FontSize, byteXDp, drawY, color.NRGBA{R: 0, G: 0, B: 0, A: 255})

	// Right: Word wrap button
	wordWrapXPx := regXPx + barW - r.toPx(Dp(80))
	wordWrapXDp := r.toDp(wordWrapXPx)
	r.drawText(gtx, r.shp, "Wrap:"+boolToString(bb.WordWrap), r.theme.FontSize, wordWrapXDp, drawY, color.NRGBA{R: 0, G: 0, B: 0, A: 255})
}

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

	// Convert region to pixels for Gio interop
	gtx.Constraints.Min.X = int(r.toPx(reg.X))
	gtx.Constraints.Min.Y = int(r.toPx(reg.Y))
	material.Label(th, r.theme.FontSize, btn.Text).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 Dp, availableWidth, spaceForText int, col color.NRGBA) {
	// Layout text with width constraints so the shaper doesn't wrap every character.
	shp.LayoutString(text.Parameters{
		PxPerEm:  fixed.I(gtx.Sp(size)),
		MinWidth: 0,
		MaxWidth: availableWidth,
		MaxLines: 1,
	}, 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)),
		MinWidth: 0,
		MaxWidth: availableWidth,
		MaxLines: 1,
	}, 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 Dp, col color.NRGBA) {
	// Layout text with width constraints so the shaper doesn't wrap every character.
	shp.LayoutString(text.Parameters{
		PxPerEm:  fixed.I(gtx.Sp(size)),
		MinWidth: 0,
		MaxWidth: 1000,
		MaxLines: 1,
	}, 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 Dp, col color.NRGBA) {
	// Convert Dp to pixels for glyph positioning
	xPx := r.toPx(x)
	yPx := r.toPx(y)

	// 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, xPx, yPx, col)
			line = line[:0]
		}
	}
	if len(line) > 0 {
		r.drawLine(gtx, shp, line, xPx, yPx, col)
	}
	call := m.Stop()
	call.Add(gtx.Ops)
}

// drawLine draws a line of glyphs using shaper.Shape() and shaper.Bitmaps().
// Matches Gio's paintGlyph exactly: offset by (x + first.X, y + first.Y).
func (r *Renderer) drawLine(gtx layout.Context, shp *text.Shaper, line []text.Glyph, x, y Px, 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. Offset by (x + first.X, y + first.Y) to place the line
	// at the desired document position. Matches Gio's paintGlyph:
	//   lineOff = (glyph.X, glyph.Y) - viewport.Min
	//   op.Affine(f32.Affine2D{}.Offset(lineOff))
	offX := float32(x) + float32(first.X)/64.0
	offY := float32(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()
}

// WindowConstraints tracks the initial window constraints captured at the start of Draw.
// All elements are positioned relative to these bounds to ensure consistency.
type WindowConstraints struct {
	Min image.Point
	Max image.Point
}
