package main

import (
	"errors"
	"fmt"
	"sort"
	"strings"
)

// ---------------------------------------------------------------------------
// Rectangles. Every coordinate is an integer device pixel. No geometry value
// in this program is ever stored or computed as a floating point number.
// ---------------------------------------------------------------------------

// Rect is a half-open pixel rectangle: it covers columns [X, X+W) and rows
// [Y, Y+H). Two rects that share an edge coordinate do not overlap.
type Rect struct {
	X int `json:"x"`
	Y int `json:"y"`
	W int `json:"w"`
	H int `json:"h"`
}

func (r Rect) Right() int  { return r.X + r.W }
func (r Rect) Bottom() int { return r.Y + r.H }

// Area is int64 because a 4-monitor 8K wall overflows a 32-bit int.
func (r Rect) Area() int64 {
	if r.W <= 0 || r.H <= 0 {
		return 0
	}
	return int64(r.W) * int64(r.H)
}

func (r Rect) Empty() bool { return r.W <= 0 || r.H <= 0 }

func (r Rect) String() string {
	return fmt.Sprintf("%dx%d+%d+%d", r.W, r.H, r.X, r.Y)
}

// intersect returns the overlapping region of a and b. The result is Empty
// when they do not overlap.
func intersect(a, b Rect) Rect {
	x0 := maxInt(a.X, b.X)
	y0 := maxInt(a.Y, b.Y)
	x1 := minInt(a.Right(), b.Right())
	y1 := minInt(a.Bottom(), b.Bottom())
	if x1 <= x0 || y1 <= y0 {
		return Rect{X: x0, Y: y0}
	}
	return Rect{X: x0, Y: y0, W: x1 - x0, H: y1 - y0}
}

// containsRect reports whether inner lies entirely inside outer.
func containsRect(outer, inner Rect) bool {
	return inner.X >= outer.X && inner.Y >= outer.Y &&
		inner.Right() <= outer.Right() && inner.Bottom() <= outer.Bottom()
}

// insetRect shrinks r by the four given edge amounts. The result can be empty;
// callers decide whether that is an error.
func insetRect(r Rect, top, right, bottom, left int) Rect {
	return Rect{
		X: r.X + left,
		Y: r.Y + top,
		W: r.W - left - right,
		H: r.H - top - bottom,
	}
}

func insetAll(r Rect, n int) Rect { return insetRect(r, n, n, n, n) }

func minInt(a, b int) int {
	if a < b {
		return a
	}
	return b
}

func maxInt(a, b int) int {
	if a > b {
		return a
	}
	return b
}

// ---------------------------------------------------------------------------
// Largest-remainder apportionment
// ---------------------------------------------------------------------------

// largestRemainder splits total pixels across the given weights so that the
// parts sum to EXACTLY total, with no rounding drift accumulating along the
// row or column.
//
// Each part gets the integer floor of its exact share; the pixels left over
// (strictly fewer than len(weights)) are handed out one each to the parts with
// the largest fractional remainder, ties broken by ascending index. This is the
// Hare/Hamilton largest-remainder method, and it is completely deterministic:
// the same weights and total always produce the same parts.
func largestRemainder(total int, weights []int) ([]int, error) {
	n := len(weights)
	if n == 0 {
		return nil, errors.New("no weights to distribute across")
	}
	if total < 0 {
		return nil, fmt.Errorf("cannot distribute a negative extent (%d px)", total)
	}
	var sum int64
	for i, w := range weights {
		if w < 1 {
			return nil, fmt.Errorf("weight #%d is %d; weights must be >= 1", i+1, w)
		}
		sum += int64(w)
	}

	type rem struct {
		idx int
		r   int64
	}
	parts := make([]int, n)
	rems := make([]rem, n)
	assigned := 0
	for i, w := range weights {
		exact := int64(total) * int64(w)
		parts[i] = int(exact / sum)
		rems[i] = rem{idx: i, r: exact % sum}
		assigned += parts[i]
	}
	leftover := total - assigned // 0 <= leftover < n, always
	sort.SliceStable(rems, func(i, j int) bool {
		if rems[i].r != rems[j].r {
			return rems[i].r > rems[j].r
		}
		return rems[i].idx < rems[j].idx
	})
	for k := 0; k < leftover; k++ {
		parts[rems[k].idx]++
	}
	return parts, nil
}

// ---------------------------------------------------------------------------
// App / title matching
// ---------------------------------------------------------------------------

// Matcher is one compiled app-id or window-title test.
type Matcher struct {
	Kind  string `json:"kind"`  // exact | prefix | glob
	Value string `json:"value"` // literal or glob pattern
}

// parseMatcher compiles a match spec. Recognised forms:
//
//	exact:VALUE   the whole string must equal VALUE
//	prefix:VALUE  the string must start with VALUE
//	glob:PATTERN  wildcard match, * = any run, ? = one character
//	VALUE         no prefix at all is treated as exact
//
// Matching is case sensitive.
func parseMatcher(spec string) (Matcher, error) {
	if spec == "" {
		return Matcher{}, errors.New("empty match spec")
	}
	switch {
	case strings.HasPrefix(spec, "exact:"):
		v := spec[len("exact:"):]
		if v == "" {
			return Matcher{}, errors.New(`"exact:" needs a value after the colon`)
		}
		return Matcher{Kind: "exact", Value: v}, nil
	case strings.HasPrefix(spec, "prefix:"):
		v := spec[len("prefix:"):]
		if v == "" {
			return Matcher{}, errors.New(`"prefix:" needs a value after the colon`)
		}
		return Matcher{Kind: "prefix", Value: v}, nil
	case strings.HasPrefix(spec, "glob:"):
		v := spec[len("glob:"):]
		if v == "" {
			return Matcher{}, errors.New(`"glob:" needs a pattern after the colon`)
		}
		return Matcher{Kind: "glob", Value: v}, nil
	default:
		return Matcher{Kind: "exact", Value: spec}, nil
	}
}

func (m Matcher) String() string { return m.Kind + ":" + m.Value }

// Match applies the compiled matcher to s.
func (m Matcher) Match(s string) bool {
	switch m.Kind {
	case "exact":
		return s == m.Value
	case "prefix":
		return strings.HasPrefix(s, m.Value)
	case "glob":
		return globMatch(m.Value, s)
	}
	return false
}

// globMatch implements '*' (any run of characters, possibly empty) and '?'
// (exactly one character) with linear-time backtracking. Nothing else is
// special; '/' and '.' are ordinary characters, unlike filepath.Match.
func globMatch(pattern, s string) bool {
	p, si := 0, 0
	star, mark := -1, 0
	for si < len(s) {
		if p < len(pattern) && (pattern[p] == '?' || pattern[p] == s[si]) {
			p++
			si++
			continue
		}
		if p < len(pattern) && pattern[p] == '*' {
			star = p
			p++
			mark = si
			continue
		}
		if star >= 0 {
			p = star + 1
			mark++
			si = mark
			continue
		}
		return false
	}
	for p < len(pattern) && pattern[p] == '*' {
		p++
	}
	return p == len(pattern)
}
