package webui

import (
	"math"
	"strconv"
	"strings"
)

// Pure geometry + formatting for chart.go — the Go twins of the helpers in
// jsruntime/uikit/Chart.tsx, producing the same SVG path/rect/text markup.

// num formats a coordinate compactly (2 decimals, trailing zeros trimmed) for a path
// string — 44.57 not 44.571428571428.
func num(f float64) string {
	if math.IsNaN(f) || math.IsInf(f, 0) {
		return "0"
	}
	s := strconv.FormatFloat(f, 'f', 2, 64)
	if strings.ContainsRune(s, '.') {
		s = strings.TrimRight(s, "0")
		s = strings.TrimRight(s, ".")
	}
	return s
}

var svgEscaper = strings.NewReplacer("&", "&amp;", "<", "&lt;", ">", "&gt;", `"`, "&quot;")

// svgEsc escapes text/attribute content going into the Raw SVG string — labels and
// colours can be untrusted (a series name from data, a "<").
func svgEsc(s string) string { return svgEscaper.Replace(s) }

// groupNum is the default value format: up to 2 decimals, thousands grouped.
func groupNum(v float64) string {
	neg := v < 0
	s := strconv.FormatFloat(math.Abs(v), 'f', 2, 64)
	s = strings.TrimRight(strings.TrimRight(s, "0"), ".")
	intPart, frac := s, ""
	if i := strings.IndexByte(s, '.'); i >= 0 {
		intPart, frac = s[:i], s[i:]
	}
	if n := len(intPart); n > 3 {
		var b strings.Builder
		pre := n % 3
		if pre > 0 {
			b.WriteString(intPart[:pre])
			b.WriteByte(',')
		}
		for i := pre; i < n; i += 3 {
			b.WriteString(intPart[i : i+3])
			if i+3 < n {
				b.WriteByte(',')
			}
		}
		intPart = b.String()
	}
	out := intPart + frac
	if neg && out != "0" {
		out = "-" + out
	}
	return out
}

func roundTo(v float64, n int) float64 {
	p := math.Pow(10, float64(n))
	return math.Round(v*p) / p
}

// niceScale: rounded min/max plus recognisable tick values (0, 20, 40 …).
func niceScale(mn, mx float64, maxTicks int) (float64, float64, []float64) {
	if math.IsInf(mn, 0) || math.IsInf(mx, 0) || math.IsNaN(mn) || math.IsNaN(mx) || mn == mx {
		v := mx
		if math.IsInf(v, 0) || math.IsNaN(v) {
			v = 0
		}
		mn = math.Min(0, v)
		if v == mn {
			mx = mn + 1
		} else {
			mx = math.Max(0, v)
		}
	}
	niceNum := func(rng float64, round bool) float64 {
		if rng <= 0 {
			rng = 1
		}
		exp := math.Floor(math.Log10(rng))
		frac := rng / math.Pow(10, exp)
		var nf float64
		if round {
			switch {
			case frac < 1.5:
				nf = 1
			case frac < 3:
				nf = 2
			case frac < 7:
				nf = 5
			default:
				nf = 10
			}
		} else {
			switch {
			case frac <= 1:
				nf = 1
			case frac <= 2:
				nf = 2
			case frac <= 5:
				nf = 5
			default:
				nf = 10
			}
		}
		return nf * math.Pow(10, exp)
	}
	step := niceNum((mx-mn)/math.Max(1, float64(maxTicks-1)), true)
	niceMin := math.Floor(mn/step) * step
	niceMax := math.Ceil(mx/step) * step
	decimals := int(math.Max(0, -math.Floor(math.Log10(step))))
	var ticks []float64
	for v := niceMin; v <= niceMax+step*0.5; v += step {
		ticks = append(ticks, roundTo(v, decimals+2))
	}
	return niceMin, niceMax, ticks
}

// roundRectPath: a rectangle with the two corners on `side` rounded (the data-end), the
// rest square.
func roundRectPath(x, y, w, h, r float64, side string) string {
	rr := math.Max(0, math.Min(math.Min(r, w/2), h/2))
	var tl, tr, br, bl float64
	switch side {
	case "top":
		tl, tr = rr, rr
	case "bottom":
		bl, br = rr, rr
	case "left":
		tl, bl = rr, rr
	case "right":
		tr, br = rr, rr
	}
	return "M" + num(x+tl) + "," + num(y) +
		" L" + num(x+w-tr) + "," + num(y) + " Q" + num(x+w) + "," + num(y) + " " + num(x+w) + "," + num(y+tr) +
		" L" + num(x+w) + "," + num(y+h-br) + " Q" + num(x+w) + "," + num(y+h) + " " + num(x+w-br) + "," + num(y+h) +
		" L" + num(x+bl) + "," + num(y+h) + " Q" + num(x) + "," + num(y+h) + " " + num(x) + "," + num(y+h-bl) +
		" L" + num(x) + "," + num(y+tl) + " Q" + num(x) + "," + num(y) + " " + num(x+tl) + "," + num(y) + " Z"
}

// bar3D: a bar extruded up-and-right by (dx, dy) — a darkened right face, a lightened top
// face, then the front. Flat overlays instead of colour maths on a CSS variable.
func bar3D(x, y, w, h float64, color string, op, dx, dy float64) string {
	top := "M" + num(x) + "," + num(y) + " L" + num(x+dx) + "," + num(y-dy) + " L" + num(x+w+dx) + "," + num(y-dy) + " L" + num(x+w) + "," + num(y) + " Z"
	right := "M" + num(x+w) + "," + num(y) + " L" + num(x+w+dx) + "," + num(y-dy) + " L" + num(x+w+dx) + "," + num(y+h-dy) + " L" + num(x+w) + "," + num(y+h) + " Z"
	return `<path d="` + right + `" fill="` + color + `" fill-opacity="` + num(op) + `"/><path d="` + right + `" fill="#000" fill-opacity="` + num(0.24*op) + `"/>` +
		`<path d="` + top + `" fill="` + color + `" fill-opacity="` + num(op) + `"/><path d="` + top + `" fill="#fff" fill-opacity="` + num(0.2*op) + `"/>` +
		`<rect x="` + num(x) + `" y="` + num(y) + `" width="` + num(w) + `" height="` + num(h) + `" fill="` + color + `" fill-opacity="` + num(op) + `"/>`
}

func linePathD(pts [][2]float64) string {
	if len(pts) == 0 {
		return ""
	}
	var b strings.Builder
	for i, p := range pts {
		if i == 0 {
			b.WriteByte('M')
		} else {
			b.WriteString(" L")
		}
		b.WriteString(num(p[0]) + "," + num(p[1]))
	}
	return b.String()
}

func monoSign(x float64) float64 {
	if x < 0 {
		return -1
	}
	return 1
}

// monoTangent is the interior tangent for monotone-cubic interpolation (d3's
// curveMonotoneX): the lesser of the two neighbouring secant slopes, and zero at a local
// extremum. Capping the tangent is what stops a segment bulging past its endpoints, so an
// area fill can't dip below a value-0 point into negative space the way a Catmull-Rom
// overshoot does.
func monoTangent(x0, y0, x1, y1, x2, y2 float64) float64 {
	h0, h1 := x1-x0, x2-x1
	var s0, s1 float64
	if h0 != 0 {
		s0 = (y1 - y0) / h0
	}
	if h1 != 0 {
		s1 = (y2 - y1) / h1
	}
	p := (s0*h1 + s1*h0) / (h0 + h1)
	m := (monoSign(s0) + monoSign(s1)) * math.Min(math.Min(math.Abs(s0), math.Abs(s1)), 0.5*math.Abs(p))
	if math.IsNaN(m) || math.IsInf(m, 0) {
		return 0
	}
	return m
}

// monoEndTangent is the endpoint tangent (d3 slope2): a parabola-end estimate constrained
// by the adjacent interior tangent t, so the boundary segments don't overshoot either.
func monoEndTangent(x0, y0, x1, y1, t float64) float64 {
	h := x1 - x0
	if h == 0 {
		return t
	}
	return (3*(y1-y0)/h - t) / 2
}

// smoothPathD: monotone cubic through every point, emitted as cubic beziers. A monotone
// interpolant never overshoots its data, so the smoothed curve stays within the value
// range of each pair of adjacent points.
func smoothPathD(pts [][2]float64) string {
	n := len(pts)
	if n < 3 {
		return linePathD(pts)
	}
	m := make([]float64, n)
	for i := 1; i < n-1; i++ {
		m[i] = monoTangent(pts[i-1][0], pts[i-1][1], pts[i][0], pts[i][1], pts[i+1][0], pts[i+1][1])
	}
	m[0] = monoEndTangent(pts[0][0], pts[0][1], pts[1][0], pts[1][1], m[1])
	m[n-1] = monoEndTangent(pts[n-2][0], pts[n-2][1], pts[n-1][0], pts[n-1][1], m[n-2])
	var b strings.Builder
	b.WriteString("M" + num(pts[0][0]) + "," + num(pts[0][1]))
	for i := 0; i < n-1; i++ {
		dx := (pts[i+1][0] - pts[i][0]) / 3
		c1x, c1y := pts[i][0]+dx, pts[i][1]+dx*m[i]
		c2x, c2y := pts[i+1][0]-dx, pts[i+1][1]-dx*m[i+1]
		b.WriteString(" C" + num(c1x) + "," + num(c1y) + " " + num(c2x) + "," + num(c2y) + " " + num(pts[i+1][0]) + "," + num(pts[i+1][1]))
	}
	return b.String()
}

// tiltPoint: a point on a circle tilted about its horizontal axis by k (k=1 upright).
func tiltPoint(cx, cy, r, deg, k float64) (float64, float64) {
	a := (deg - 90) * math.Pi / 180
	return cx + r*math.Cos(a), cy + k*r*math.Sin(a)
}

// slicePathD: one pie/donut slice a0→a1 degrees, tilted by k. Elliptical arcs make the
// tilt exact.
func slicePathD(cx, cy, rOut, rIn, a0, a1, k float64) string {
	large := "0"
	if a1-a0 > 180 {
		large = "1"
	}
	ox0, oy0 := tiltPoint(cx, cy, rOut, a0, k)
	ox1, oy1 := tiltPoint(cx, cy, rOut, a1, k)
	ryO := k * rOut
	if rIn <= 0 {
		return "M" + num(cx) + "," + num(cy) + " L" + num(ox0) + "," + num(oy0) +
			" A" + num(rOut) + "," + num(ryO) + " 0 " + large + " 1 " + num(ox1) + "," + num(oy1) + " Z"
	}
	ix1, iy1 := tiltPoint(cx, cy, rIn, a1, k)
	ix0, iy0 := tiltPoint(cx, cy, rIn, a0, k)
	ryI := k * rIn
	return "M" + num(ox0) + "," + num(oy0) +
		" A" + num(rOut) + "," + num(ryO) + " 0 " + large + " 1 " + num(ox1) + "," + num(oy1) +
		" L" + num(ix1) + "," + num(iy1) +
		" A" + num(rIn) + "," + num(ryI) + " 0 " + large + " 0 " + num(ix0) + "," + num(iy0) + " Z"
}

// ── small numeric helpers ─────────────────────────────────────────────────────────

func maxf(a, b float64) float64 {
	if a > b {
		return a
	}
	return b
}
func maxi(a, b int) int {
	if a > b {
		return a
	}
	return b
}
func clampf(v, lo, hi float64) float64 { return math.Min(hi, math.Max(lo, v)) }
func clampi(v, lo, hi int) int {
	if v < lo {
		return lo
	}
	if v > hi {
		return hi
	}
	return v
}
func maxLen(ss []string) int {
	m := 0
	for _, s := range ss {
		if len(s) > m {
			m = len(s)
		}
	}
	return m
}
func fmtAll(p ChartProps, ticks []float64) []string {
	out := make([]string, len(ticks))
	for i, t := range ticks {
		out[i] = chartFmt(p, t)
	}
	return out
}
