package wasmruntime

import (
	"strconv"
	"strings"
)

// The host API: the channel through which the browser tells WebAssembly things Go
// cannot work out on its own — how big an element is, where the pointer is, how
// tall the viewport is — plus the imperative escape hatches (write a style, focus
// an input, download a file) that a pure re-render cannot express.
//
// Every function here exists in two builds: the real one (host_wasm.go, js+wasm)
// and a no-op that returns zero values (host_native.go). That is what lets the
// neutral kjol/webui components — which must also compile on the server for SSR —
// call them unconditionally. On the server a measurement is simply the zero Rect,
// a listener is never installed, and a timer never fires; components render their
// pre-measurement state (a panel with `visibility: hidden`), which is exactly what
// SSR should ship.
//
// Two rules for callers:
//
//  1. Measure only after the DOM exists. A ref is empty until the reconciler has
//     committed; use AfterRender to run measurement code once the current render
//     is on screen.
//  2. Do NOT position through signals. Signal.Set re-renders the whole tree; doing
//     that on every scroll frame is pathological. Write positions with SetStyle,
//     which mutates the DOM node directly and leaves the vdom alone.

// Rect is an element's box in viewport coordinates — the result of
// getBoundingClientRect. Viewport coordinates compose directly with
// `position: fixed`, so no scroll compensation is needed (or wanted) anywhere.
type Rect struct{ X, Y, Width, Height float64 }

func (r Rect) Top() float64     { return r.Y }
func (r Rect) Left() float64    { return r.X }
func (r Rect) Right() float64   { return r.X + r.Width }
func (r Rect) Bottom() float64  { return r.Y + r.Height }
func (r Rect) CenterX() float64 { return r.X + r.Width/2 }
func (r Rect) CenterY() float64 { return r.Y + r.Height/2 }

// Empty reports whether the rect carries no useful geometry — an unmounted ref, or
// an element that has not been laid out yet. Positioning code must treat this as
// "cannot measure yet" rather than as a box at the origin, or the panel paints at
// 0,0 for a frame.
func (r Rect) Empty() bool { return r.Width == 0 && r.Height == 0 }

// Size is a width/height pair — the viewport, or an element's size.
type Size struct{ Width, Height float64 }

// Unsub removes a listener installed by OnWindow / OnDocument / ObserveResize.
// Always call it when the thing that installed it goes away; a floating panel that
// leaks a window scroll listener per open will crawl.
type Unsub func()

// ScrollBlock values for ScrollIntoView.
const (
	ScrollBlockStart   = "start"
	ScrollBlockCenter  = "center"
	ScrollBlockEnd     = "end"
	ScrollBlockNearest = "nearest"
)

// parseCSSLength resolves a CSS length to pixels. rootFontSize is the computed
// font-size of :root (itself a px string), which is what a rem is measured
// against. Unitless and unparseable values yield 0.
func parseCSSLength(value, rootFontSize string) float64 {
	value = strings.TrimSpace(value)
	if value == "" {
		return 0
	}
	switch {
	case strings.HasSuffix(value, "px"):
		return parseFloat(strings.TrimSuffix(value, "px"))
	case strings.HasSuffix(value, "rem"):
		root := parseFloat(strings.TrimSuffix(strings.TrimSpace(rootFontSize), "px"))
		if root == 0 {
			root = 16 // the browser default, if :root's font-size is unreadable
		}
		return parseFloat(strings.TrimSuffix(value, "rem")) * root
	default:
		return parseFloat(value)
	}
}

func parseFloat(s string) float64 {
	f, err := strconv.ParseFloat(strings.TrimSpace(s), 64)
	if err != nil {
		return 0
	}
	return f
}
