package main

import (
	"encoding/json"
	"fmt"
	"os"
	"sort"
	"strings"
)

// ---------------------------------------------------------------------------
// Display topology
//
// A topology is a description of a physical desk: which monitors exist, where
// their pixels live in the virtual desktop coordinate space, how they are
// scaled, and how much of each screen a taskbar or menu bar has taken away.
//
// WorkspaceForge never asks the operating system for this. It is read from a
// JSON file you write or generate yourself.
// ---------------------------------------------------------------------------

// Insets are the edges of a monitor that system chrome (menu bar, taskbar,
// dock, panel) has claimed. The work area is the bounds minus the insets.
type Insets struct {
	Top    int `json:"top,omitempty"`
	Right  int `json:"right,omitempty"`
	Bottom int `json:"bottom,omitempty"`
	Left   int `json:"left,omitempty"`
}

// monitorFile is the on-disk shape of one monitor.
type monitorFile struct {
	ID      string  `json:"id"`
	Label   string  `json:"label,omitempty"`
	Primary bool    `json:"primary,omitempty"`
	Bounds  Rect    `json:"bounds"`
	Scale   float64 `json:"scale,omitempty"`
	Insets  Insets  `json:"insets,omitempty"`
}

type topologyFile struct {
	Name     string        `json:"name"`
	Monitors []monitorFile `json:"monitors"`
}

// Monitor is a validated monitor. ScalePermille is the scale factor times
// 1000, held as an integer so that every later computation stays integral:
// a scale of 1.5 is stored as 1500 and never touches a float again.
type Monitor struct {
	ID            string `json:"id"`
	Label         string `json:"label,omitempty"`
	Primary       bool   `json:"primary"`
	Bounds        Rect   `json:"bounds"`
	Work          Rect   `json:"work_area"`
	Insets        Insets `json:"insets"`
	ScalePermille int    `json:"scale_permille"`
}

// ScaleString renders the scale factor for humans, e.g. "1.5x".
func (m Monitor) ScaleString() string {
	whole := m.ScalePermille / 1000
	frac := m.ScalePermille % 1000
	if frac == 0 {
		return fmt.Sprintf("%dx", whole)
	}
	s := fmt.Sprintf("%d.%03d", whole, frac)
	s = strings.TrimRight(s, "0")
	return s + "x"
}

// LogicalW is the work-area width expressed in logical (scale-independent)
// pixels, rounded to nearest with integer arithmetic only.
func (m Monitor) LogicalW() int { return toLogical(m.Work.W, m.ScalePermille) }
func (m Monitor) LogicalH() int { return toLogical(m.Work.H, m.ScalePermille) }

// toLogical converts device pixels to logical pixels, rounding to nearest.
func toLogical(device, permille int) int {
	if permille <= 0 {
		return device
	}
	return (device*1000 + permille/2) / permille
}

// toDevice converts logical pixels to device pixels, rounding UP so that a
// stated minimum size is never quietly shaved by a fraction of a pixel.
func toDevice(logical, permille int) int {
	if permille <= 0 {
		return logical
	}
	return (logical*permille + 999) / 1000
}

// Topology is a validated set of monitors.
type Topology struct {
	Name     string    `json:"name"`
	Source   string    `json:"source,omitempty"`
	Monitors []Monitor `json:"monitors"`
	// PrimaryDerived is true when no monitor declared itself primary and
	// WorkspaceForge picked one by the documented ranking.
	PrimaryDerived bool `json:"primary_derived"`
}

// Get returns the monitor with the given id.
func (t *Topology) Get(id string) (*Monitor, bool) {
	for i := range t.Monitors {
		if t.Monitors[i].ID == id {
			return &t.Monitors[i], true
		}
	}
	return nil, false
}

// LoadTopology reads and fully validates a topology file.
func LoadTopology(path string) (*Topology, error) {
	f, err := os.Open(path)
	if err != nil {
		return nil, fmt.Errorf("cannot read topology %s: %w", path, err)
	}
	defer f.Close()

	dec := json.NewDecoder(f)
	dec.DisallowUnknownFields()
	var tf topologyFile
	if err := dec.Decode(&tf); err != nil {
		return nil, fmt.Errorf("topology %s is not valid JSON: %w", path, err)
	}
	t, err := buildTopology(tf)
	if err != nil {
		return nil, fmt.Errorf("topology %s: %w", path, err)
	}
	t.Source = path
	return t, nil
}

// ParseTopology validates an in-memory topology document. It exists so the
// tests can exercise validation without touching the filesystem.
func ParseTopology(data []byte) (*Topology, error) {
	dec := json.NewDecoder(strings.NewReader(string(data)))
	dec.DisallowUnknownFields()
	var tf topologyFile
	if err := dec.Decode(&tf); err != nil {
		return nil, fmt.Errorf("not valid JSON: %w", err)
	}
	return buildTopology(tf)
}

func buildTopology(tf topologyFile) (*Topology, error) {
	if len(tf.Monitors) == 0 {
		return nil, fmt.Errorf("no monitors declared; a topology needs at least one")
	}
	t := &Topology{Name: tf.Name}
	if t.Name == "" {
		t.Name = "(unnamed)"
	}
	seen := map[string]bool{}
	primaries := 0
	for i, mf := range tf.Monitors {
		id := strings.TrimSpace(mf.ID)
		if id == "" {
			return nil, fmt.Errorf("monitor #%d has no id", i+1)
		}
		if seen[id] {
			return nil, fmt.Errorf("monitor id %q appears twice; ids must be unique", id)
		}
		seen[id] = true
		if mf.Bounds.W <= 0 || mf.Bounds.H <= 0 {
			return nil, fmt.Errorf("monitor %q has bounds %dx%d; width and height must be positive",
				id, mf.Bounds.W, mf.Bounds.H)
		}
		in := mf.Insets
		if in.Top < 0 || in.Right < 0 || in.Bottom < 0 || in.Left < 0 {
			return nil, fmt.Errorf("monitor %q has a negative inset", id)
		}
		work := insetRect(mf.Bounds, in.Top, in.Right, in.Bottom, in.Left)
		if work.Empty() {
			return nil, fmt.Errorf("monitor %q: insets (t=%d r=%d b=%d l=%d) leave a %dx%d work area; nothing can be placed on it",
				id, in.Top, in.Right, in.Bottom, in.Left, work.W, work.H)
		}
		scale := mf.Scale
		if scale == 0 {
			scale = 1
		}
		if scale < 0.25 || scale > 8 {
			return nil, fmt.Errorf("monitor %q has scale %g; expected between 0.25 and 8", id, scale)
		}
		permille := int(scale*1000 + 0.5)
		if mf.Primary {
			primaries++
		}
		t.Monitors = append(t.Monitors, Monitor{
			ID:            id,
			Label:         mf.Label,
			Primary:       mf.Primary,
			Bounds:        mf.Bounds,
			Work:          work,
			Insets:        in,
			ScalePermille: permille,
		})
	}
	if primaries > 1 {
		return nil, fmt.Errorf("%d monitors are marked primary; exactly one may be", primaries)
	}
	if err := checkOverlap(t); err != nil {
		return nil, err
	}
	if err := checkConnected(t); err != nil {
		return nil, err
	}
	if primaries == 0 {
		order := RankOrder(t)
		t.Monitors[order[0]].Primary = true
		t.PrimaryDerived = true
	}
	return t, nil
}

// checkOverlap rejects topologies whose monitors claim the same pixels. Real
// display servers never do this; a file that does is a mistake, and silently
// solving it would put two windows on top of each other.
func checkOverlap(t *Topology) error {
	for i := 0; i < len(t.Monitors); i++ {
		for j := i + 1; j < len(t.Monitors); j++ {
			ov := intersect(t.Monitors[i].Bounds, t.Monitors[j].Bounds)
			if !ov.Empty() {
				return fmt.Errorf("monitors %q %s and %q %s overlap over %s; monitor bounds must be disjoint",
					t.Monitors[i].ID, t.Monitors[i].Bounds,
					t.Monitors[j].ID, t.Monitors[j].Bounds, ov)
			}
		}
	}
	return nil
}

// abut reports whether a and b share an edge segment of positive length.
// Touching at a single corner does not count: a window straddling that point
// would have nowhere to go.
func abut(a, b Rect) bool {
	if a.Right() == b.X || b.Right() == a.X {
		return minInt(a.Bottom(), b.Bottom())-maxInt(a.Y, b.Y) > 0
	}
	if a.Bottom() == b.Y || b.Bottom() == a.Y {
		return minInt(a.Right(), b.Right())-maxInt(a.X, b.X) > 0
	}
	return false
}

// checkConnected rejects topologies whose monitors do not form one contiguous
// desktop. Monitors must abut exactly, edge to edge; a stray gap of even one
// pixel means a dragged window falls into a hole that does not exist on any
// screen, so it is reported instead of solved.
func checkConnected(t *Topology) error {
	n := len(t.Monitors)
	if n < 2 {
		return nil
	}
	group := make([]int, n)
	for i := range group {
		group[i] = -1
	}
	g := 0
	for i := 0; i < n; i++ {
		if group[i] != -1 {
			continue
		}
		queue := []int{i}
		group[i] = g
		for len(queue) > 0 {
			cur := queue[0]
			queue = queue[1:]
			for j := 0; j < n; j++ {
				if group[j] == -1 && abut(t.Monitors[cur].Bounds, t.Monitors[j].Bounds) {
					group[j] = g
					queue = append(queue, j)
				}
			}
		}
		g++
	}
	if g == 1 {
		return nil
	}
	groups := make([][]string, g)
	for i, gi := range group {
		groups[gi] = append(groups[gi], t.Monitors[i].ID)
	}
	var parts []string
	for _, ids := range groups {
		parts = append(parts, "{"+strings.Join(ids, ", ")+"}")
	}
	return fmt.Errorf("monitors form %d disjoint groups %s; every monitor must share an edge with the rest of the desktop (check for a gap or a corner-only touch between bounds)",
		g, strings.Join(parts, " and "))
}

// RankOrder returns monitor indices in WorkspaceForge's canonical order:
//
//  1. the primary monitor first;
//  2. then by descending work-area area (the biggest screen is next);
//  3. then by ascending x, then ascending y (left to right, top to bottom);
//  4. then by ascending id, so the order is total and never depends on the
//     order monitors happen to appear in the file.
//
// Re-fit pairs monitors by this rank, so the ordering is part of the
// documented contract, not an implementation detail.
func RankOrder(t *Topology) []int {
	idx := make([]int, len(t.Monitors))
	for i := range idx {
		idx[i] = i
	}
	m := t.Monitors
	sort.SliceStable(idx, func(a, b int) bool {
		i, j := idx[a], idx[b]
		if m[i].Primary != m[j].Primary {
			return m[i].Primary
		}
		ai, aj := m[i].Work.Area(), m[j].Work.Area()
		if ai != aj {
			return ai > aj
		}
		if m[i].Bounds.X != m[j].Bounds.X {
			return m[i].Bounds.X < m[j].Bounds.X
		}
		if m[i].Bounds.Y != m[j].Bounds.Y {
			return m[i].Bounds.Y < m[j].Bounds.Y
		}
		return m[i].ID < m[j].ID
	})
	return idx
}

// RankedMonitors returns the monitors in canonical rank order.
func RankedMonitors(t *Topology) []Monitor {
	out := make([]Monitor, 0, len(t.Monitors))
	for _, i := range RankOrder(t) {
		out = append(out, t.Monitors[i])
	}
	return out
}

// DesktopBounds is the bounding box of every monitor in the topology.
func (t *Topology) DesktopBounds() Rect {
	if len(t.Monitors) == 0 {
		return Rect{}
	}
	b := t.Monitors[0].Bounds
	x0, y0, x1, y1 := b.X, b.Y, b.Right(), b.Bottom()
	for _, m := range t.Monitors[1:] {
		x0 = minInt(x0, m.Bounds.X)
		y0 = minInt(y0, m.Bounds.Y)
		x1 = maxInt(x1, m.Bounds.Right())
		y1 = maxInt(y1, m.Bounds.Bottom())
	}
	return Rect{X: x0, Y: y0, W: x1 - x0, H: y1 - y0}
}
