package webui

import (
	"strconv"
	"strings"

	"kjol/wasmruntime"
	"kjol/wasmruntime/vdom"
)

// Port of FormSignaturePad (jsruntime/uikit/Forms.tsx) — freehand signing.
//
// Two deliberate departures from the TSX.
//
// It draws into an SVG, not a <canvas>. The component's OUTPUT is SVG — the TSX kept
// strokes in memory, painted them onto a canvas for the user, and serialized a separate
// SVG string for the caller. That is two renderers for one drawing, and they can
// disagree. Here the SVG the user is looking at IS the value the caller gets, so it
// cannot be wrong. It also means a stored signature renders on the SERVER: the same
// markup, no client needed to see it.
//
// And the in-flight stroke never touches a signal. A pointer moves sixty times a second,
// and a signal write re-renders the whole page; the live stroke is written straight at
// the element with SetHTML, and only the FINISHED stroke is committed. Without that,
// signing your name would re-render the document a few hundred times.
//
// Create it once, alongside your signals — never inside a render.
type SignaturePad struct {
	svgRef *vdom.Ref

	// strokes are the finished ones: committed, re-rendered, part of the value.
	strokes *vdom.Signal[[]sigStroke]

	// live is the stroke being drawn right now. NOT a signal — see above.
	live     sigStroke
	drawing  bool
	unsubs   []wasmruntime.Unsub
	onChange func(svg string)
	width    float64
	height   float64
}

type sigPoint struct{ X, Y float64 }
type sigStroke []sigPoint

// SignaturePadOptions configures NewSignaturePad.
type SignaturePadOptions struct {
	// Width and Height are the SVG's coordinate space (its viewBox), not its size on
	// screen — the element scales to its container and the strokes scale with it.
	// Default 600x120.
	Width, Height float64

	// OnChange receives the signature as an SVG document, or "" when it is cleared.
	OnChange func(svg string)
}

// NewSignaturePad creates a signature pad.
func NewSignaturePad(o SignaturePadOptions) *SignaturePad {
	if o.Width <= 0 {
		o.Width = 600
	}
	if o.Height <= 0 {
		o.Height = 120
	}
	return &SignaturePad{
		svgRef:   vdom.NewRef(),
		strokes:  vdom.NewSignal([]sigStroke{}),
		onChange: o.OnChange,
		width:    o.Width,
		height:   o.Height,
	}
}

// SignaturePadProps configures a render.
type SignaturePadProps struct {
	Class      string
	Hint       string // shown while empty; default "Sign above"
	ClearText  string // default "Clear"
	Disabled   bool
	HideFooter bool // no hint / Clear button — for showing a signature back, read-only
}

// IsEmpty reports whether anything has been drawn.
func (s *SignaturePad) IsEmpty() bool { return len(s.strokes.Get()) == 0 }

// SVG is the signature as a standalone SVG document — the value to store. It is "" when
// the pad is empty, so an empty pad is an empty string rather than a blank drawing.
func (s *SignaturePad) SVG() string {
	strokes := s.strokes.Get()
	if len(strokes) == 0 {
		return ""
	}
	var b strings.Builder
	b.WriteString(`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 `)
	b.WriteString(sigNum(s.width) + " " + sigNum(s.height))
	b.WriteString(`" width="` + sigNum(s.width) + `" height="` + sigNum(s.height) + `">`)
	b.WriteString(sigPaths(strokes))
	b.WriteString(`</svg>`)
	return b.String()
}

// Clear empties the pad.
func (s *SignaturePad) Clear() {
	s.endDrag()
	s.live = nil
	s.strokes.Set([]sigStroke{})
	if s.onChange != nil {
		s.onChange("")
	}
}

// Dispose removes any listeners left behind by an interrupted drag.
func (s *SignaturePad) Dispose() { s.endDrag() }

// Render draws the pad.
func (s *SignaturePad) Render(p SignaturePadProps) *vdom.VNode {
	svgMods := []vdom.Mod{
		vdom.WithRef(s.svgRef),
		vdom.Attr("viewBox", "0 0 "+sigNum(s.width)+" "+sigNum(s.height)),
		// touch-none: without it, drawing on a phone scrolls the page instead.
		vdom.Attr("class", "block h-auto w-full cursor-crosshair touch-none"),
		vdom.Attr("role", "img"),
		vdom.Raw(sigPaths(s.strokes.Get())),
	}
	if !p.Disabled {
		svgMods = append(svgMods, vdom.OnEvent(vdom.EVENT_POINTERDOWN, s.onDown))
	}

	kids := []*vdom.VNode{vdom.Svg(svgMods...)}

	if !p.HideFooter {
		hint := ""
		if s.IsEmpty() {
			hint = pick(p.Hint, "Sign above")
		}
		clear := []vdom.Mod{
			vdom.Attr("type", "button"),
			vdom.Attr("class", "text-ss text-ink-muted hover:text-ink disabled:opacity-50"),
			vdom.Text(pick(p.ClearText, "Clear")),
		}
		if p.Disabled || s.IsEmpty() {
			clear = append(clear, vdom.Attr("disabled", "disabled"))
		} else {
			clear = append(clear, vdom.On(vdom.EVENT_CLICK, s.Clear))
		}
		kids = append(kids, vdom.Div(
			vdom.Attr("class", "flex items-center justify-between border-t border-line bg-surface-muted px-2 py-1"),
			vdom.Span(vdom.Attr("class", "text-ss italic text-ink-faint"), vdom.Text(hint)),
			vdom.Button(clear...),
		))
	}

	mods := []vdom.Mod{vdom.Attr("class", cx("overflow-hidden rounded-default border border-line-strong bg-surface", p.Class))}
	for _, k := range kids {
		mods = append(mods, k)
	}
	return vdom.Div(mods...)
}

// ---- drawing ------------------------------------------------------------

func (s *SignaturePad) onDown(e vdom.Event) {
	e.PreventDefault()
	s.endDrag() // a previous drag that never got its pointerup (alt-tab, say)

	s.drawing = true
	s.live = sigStroke{s.point(e)}

	// The listeners go on the DOCUMENT, not the element. Drag off the edge of the pad
	// and the stroke should follow the cursor and finish when you let go — with element
	// listeners the pointer simply escapes and the stroke is left half-drawn.
	s.unsubs = append(s.unsubs,
		wasmruntime.OnDocument(vdom.EVENT_POINTERMOVE, false, s.onMove),
		wasmruntime.OnDocument(vdom.EVENT_POINTERUP, false, s.onUp),
		wasmruntime.OnDocument(vdom.EVENT_POINTERCANCEL, false, s.onUp),
	)
}

func (s *SignaturePad) onMove(e vdom.Event) {
	if !s.drawing {
		return
	}
	s.live = append(s.live, s.point(e))

	// Imperative. This runs on every pointer move; a signal here would re-render the
	// entire application between one pixel of ink and the next.
	wasmruntime.SetHTML(s.svgRef, sigPaths(append(append([]sigStroke{}, s.strokes.Get()...), s.live)))
}

func (s *SignaturePad) onUp(vdom.Event) {
	if !s.drawing {
		return
	}
	s.endDrag()

	// A stroke of one point is a click, not a mark. Dropping it keeps a stray tap from
	// counting as a signature.
	if len(s.live) >= 2 {
		s.strokes.Set(append(append([]sigStroke{}, s.strokes.Get()...), s.live))
		if s.onChange != nil {
			s.onChange(s.SVG())
		}
	}
	s.live = nil
}

func (s *SignaturePad) endDrag() {
	s.drawing = false
	for _, u := range s.unsubs {
		u()
	}
	s.unsubs = nil
}

// point maps a pointer's viewport coordinates into the SVG's coordinate space. The
// element is fluid and the viewBox is fixed, so the two differ by whatever the browser
// scaled the SVG to — measuring is the only way to know.
func (s *SignaturePad) point(e vdom.Event) sigPoint {
	r := wasmruntime.Measure(s.svgRef)
	if r.Width == 0 || r.Height == 0 {
		return sigPoint{}
	}
	return sigPoint{
		X: (float64(e.ClientX()) - r.X) * (s.width / r.Width),
		Y: (float64(e.ClientY()) - r.Y) * (s.height / r.Height),
	}
}

// ---- serialization ------------------------------------------------------

// sigPaths renders strokes as SVG <path> elements, smoothed.
//
// Straight lines between raw pointer samples look like a seismograph, not handwriting.
// Each segment is a quadratic curve THROUGH the sampled point and ending at the midpoint
// of the next one, which is the standard trick for turning a polyline into something
// that reads as a pen stroke.
func sigPaths(strokes []sigStroke) string {
	var b strings.Builder
	for _, st := range strokes {
		if len(st) < 2 {
			continue
		}
		b.WriteString(`<path d="M`)
		b.WriteString(sigNum(st[0].X) + "," + sigNum(st[0].Y))
		if len(st) == 2 {
			b.WriteString(" L" + sigNum(st[1].X) + "," + sigNum(st[1].Y))
		} else {
			for i := 1; i < len(st)-1; i++ {
				mx := (st[i].X + st[i+1].X) / 2
				my := (st[i].Y + st[i+1].Y) / 2
				b.WriteString(" Q" + sigNum(st[i].X) + "," + sigNum(st[i].Y) + "," + sigNum(mx) + "," + sigNum(my))
			}
			last := st[len(st)-1]
			b.WriteString(" L" + sigNum(last.X) + "," + sigNum(last.Y))
		}
		b.WriteString(`" fill="none" stroke="#1a1a2e" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>`)
	}
	return b.String()
}

// sigNum formats a coordinate to one decimal, without a trailing ".0" — the markup is
// the value the caller stores, and there is no reason to store 600.0 as six bytes.
func sigNum(f float64) string {
	return strconv.FormatFloat(f, 'f', -1, 64)
}
