package main

import (
	"fmt"
	"sort"
)

// ---------------------------------------------------------------------------
// Re-fit
//
// You saved a layout on three 1440p screens. Today you are on a laptop. Re-fit
// maps the saved layout onto the topology you actually have, deterministically.
//
// THE ALGORITHM, IN FULL
//
//  1. Rank the monitors of BOTH topologies with the same canonical order:
//     primary first, then descending work-area area, then ascending x, then
//     ascending y, then ascending id. (See RankOrder.)
//
//  2. Pair rank i of the source with rank i of the destination. The source
//     monitor's split tree is re-solved against the destination monitor's work
//     area, so every zone keeps its proportions and its neighbours but gets
//     destination-sized rectangles.
//
//  3. Source monitors whose rank is beyond the last destination rank have
//     disappeared. Each one is FOLDED into the fallback monitor, which is
//     always destination rank 0 (the primary). The fallback monitor's tree
//     becomes a new split: along the long axis of its work area (columns if it
//     is wider than tall, rows otherwise), with the fallback's original tree
//     first and each folded tree after it, in source rank order, every child
//     weighted 1. So a laptop taking two folded screens ends up with three
//     equal columns: its own layout, then the two folded ones.
//
//  4. Destination monitors beyond the last source rank get no layout. They are
//     reported as empty rather than filled with something invented.
//
//  5. Everything is then solved normally, and every window is compared against
//     its source rectangle and classified.
//
// Nothing here consults window contents, history or heuristics. The same two
// topologies always produce the same migration.
// ---------------------------------------------------------------------------

// MonitorMapping is one row of the source-to-destination monitor table.
type MonitorMapping struct {
	SourceRank int    `json:"source_rank"`
	Source     string `json:"source,omitempty"`
	DestRank   int    `json:"dest_rank"`
	Dest       string `json:"dest,omitempty"`
	Action     string `json:"action"` // paired | folded | empty
	Detail     string `json:"detail"`
}

// WindowMove is what happened to one window.
type WindowMove struct {
	WindowID    string `json:"window_id"`
	App         string `json:"app"`
	Title       string `json:"title,omitempty"`
	Zone        string `json:"zone,omitempty"`
	FromMonitor string `json:"from_monitor,omitempty"`
	FromRect    *Rect  `json:"from,omitempty"`
	ToMonitor   string `json:"to_monitor,omitempty"`
	ToRect      *Rect  `json:"to,omitempty"`
	Folded      bool   `json:"folded"`
	Status      string `json:"status"` // unchanged | moved | resized | moved+resized | folded-away | unplaced
	Detail      string `json:"detail"`
}

// ZoneMove is what happened to one zone.
type ZoneMove struct {
	Zone        string `json:"zone"`
	FromMonitor string `json:"from_monitor,omitempty"`
	FromRect    *Rect  `json:"from,omitempty"`
	ToMonitor   string `json:"to_monitor,omitempty"`
	ToRect      *Rect  `json:"to,omitempty"`
	Folded      bool   `json:"folded"`
	Status      string `json:"status"`
}

// RefitResult is the whole migration report.
type RefitResult struct {
	LayoutName string           `json:"layout"`
	From       string           `json:"from_topology"`
	To         string           `json:"to_topology"`
	Fallback   string           `json:"fallback_monitor,omitempty"`
	Mapping    []MonitorMapping `json:"monitor_mapping"`
	Zones      []ZoneMove       `json:"zones"`
	Windows    []WindowMove     `json:"windows"`
	Source     *SolveResult     `json:"-"`
	Target     *SolveResult     `json:"-"`
	Derived    *Layout          `json:"-"`
	Issues     []Issue          `json:"issues,omitempty"`
	Summary    RefitSummary     `json:"summary"`
}

// RefitSummary counts the outcomes.
type RefitSummary struct {
	MonitorsFrom int `json:"monitors_from"`
	MonitorsTo   int `json:"monitors_to"`
	Folded       int `json:"monitors_folded"`
	EmptyDest    int `json:"monitors_left_empty"`
	Unchanged    int `json:"windows_unchanged"`
	Moved        int `json:"windows_moved"`
	Resized      int `json:"windows_resized"`
	FoldedWins   int `json:"windows_on_folded_monitors"`
	Unplaced     int `json:"windows_unplaced"`
}

// Refit maps lay, authored for from, onto to.
func Refit(lay *Layout, from, to *Topology) *RefitResult {
	r := &RefitResult{
		LayoutName: lay.Name,
		From:       from.Name,
		To:         to.Name,
	}
	src := Solve(lay, from)
	r.Source = src

	fromRank := RankOrder(from)
	toRank := RankOrder(to)
	r.Summary.MonitorsFrom = len(fromRank)
	r.Summary.MonitorsTo = len(toRank)

	derived := &Layout{
		Name:     lay.Name,
		Topology: to.Name,
		Rules:    lay.Rules,
		Windows:  lay.Windows,
	}
	fallbackID := to.Monitors[toRank[0]].ID
	r.Fallback = fallbackID

	// Step 2: pair rank for rank.
	folded := map[string][]string{} // dest monitor -> source monitors folded in
	foldedFrom := map[string]string{}
	type pending struct {
		dest string
		lm   *LayoutMonitor
	}
	var pairs []pending
	for i, fi := range fromRank {
		srcMon := from.Monitors[fi]
		lm := lay.MonitorLayout(srcMon.ID)
		if i < len(toRank) {
			destMon := to.Monitors[toRank[i]]
			action := "paired"
			detail := fmt.Sprintf("rank %d -> rank %d, work area %s -> %s", i, i, srcMon.Work, destMon.Work)
			if lm == nil {
				action = "empty"
				detail = fmt.Sprintf("source monitor %q has no layout block; %q is left empty", srcMon.ID, destMon.ID)
			}
			r.Mapping = append(r.Mapping, MonitorMapping{
				SourceRank: i, Source: srcMon.ID, DestRank: i, Dest: destMon.ID,
				Action: action, Detail: detail,
			})
			if lm != nil {
				pairs = append(pairs, pending{dest: destMon.ID, lm: lm})
			}
			continue
		}
		// Step 3: fold.
		r.Summary.Folded++
		folded[fallbackID] = append(folded[fallbackID], srcMon.ID)
		foldedFrom[srcMon.ID] = fallbackID
		detail := fmt.Sprintf("source rank %d has no counterpart (destination has %d monitors); folded into fallback %q",
			i, len(toRank), fallbackID)
		if lm == nil {
			detail = fmt.Sprintf("source rank %d has no counterpart and no layout block; nothing to fold", i)
		}
		r.Mapping = append(r.Mapping, MonitorMapping{
			SourceRank: i, Source: srcMon.ID, DestRank: -1, Dest: fallbackID,
			Action: "folded", Detail: detail,
		})
		if lm != nil {
			pairs = append(pairs, pending{dest: fallbackID, lm: lm})
		}
	}

	// Step 4: destination monitors with no source counterpart.
	for i := len(fromRank); i < len(toRank); i++ {
		destMon := to.Monitors[toRank[i]]
		r.Summary.EmptyDest++
		r.Mapping = append(r.Mapping, MonitorMapping{
			SourceRank: -1, DestRank: i, Dest: destMon.ID,
			Action: "empty",
			Detail: fmt.Sprintf("destination rank %d has no source monitor (source has %d); left empty", i, len(fromRank)),
		})
	}

	// Build the derived layout: merge every pending tree onto its destination.
	byDest := map[string]*LayoutMonitor{}
	var destOrder []string
	for _, p := range pairs {
		existing, ok := byDest[p.dest]
		if !ok {
			nm := &LayoutMonitor{
				Monitor: p.dest,
				Gap:     p.lm.Gap,
				Padding: p.lm.Padding,
				Root:    p.lm.Root.clone(),
			}
			byDest[p.dest] = nm
			destOrder = append(destOrder, p.dest)
			continue
		}
		// Fold: wrap in a split along the long axis of the destination work area.
		destMon, _ := to.Get(p.dest)
		axis := splitColumns
		if destMon != nil && destMon.Work.H > destMon.Work.W {
			axis = splitRows
		}
		if !existing.Root.foldWrap {
			one := 1
			first := existing.Root
			first.Weight = &one
			existing.Root = &Node{Split: axis, Children: []*Node{first}, foldWrap: true}
		}
		add := p.lm.Root.clone()
		w := 1
		add.Weight = &w
		existing.Root.Children = append(existing.Root.Children, add)
	}
	for _, id := range destOrder {
		derived.Monitors = append(derived.Monitors, byDest[id])
	}
	// Re-derive zone bookkeeping for the new layout.
	rebuildZones(derived)
	r.Derived = derived

	tgt := Solve(derived, to)
	for i := range tgt.Monitors {
		if ids, ok := folded[tgt.Monitors[i].ID]; ok {
			tgt.Monitors[i].FoldedIn = ids
		}
	}
	r.Target = tgt
	r.Issues = append(r.Issues, tgt.Issues...)

	// Step 5: diff.
	zoneNames := append([]string(nil), lay.Zones()...)
	sort.Strings(zoneNames)
	for _, z := range zoneNames {
		zm := ZoneMove{Zone: z}
		sp, sok := src.Zone(z)
		tp, tok := tgt.Zone(z)
		if sok {
			f := sp.Frame
			zm.FromMonitor, zm.FromRect = sp.Monitor, &f
			if _, was := foldedFrom[sp.Monitor]; was {
				zm.Folded = true
			}
		}
		if tok {
			f := tp.Frame
			zm.ToMonitor, zm.ToRect = tp.Monitor, &f
		}
		switch {
		case !tok:
			zm.Status = "unplaced"
		case !sok:
			zm.Status = "new"
		default:
			zm.Status = classify(sp.Frame, tp.Frame, sp.Monitor, tp.Monitor)
		}
		r.Zones = append(r.Zones, zm)
	}

	for _, a := range tgt.Assignments {
		wm := WindowMove{
			WindowID: a.Window.ID,
			App:      a.Window.App,
			Title:    a.Window.Title,
			Zone:     a.Zone,
		}
		if a.Zone == "" {
			wm.Status = "unplaced"
			wm.Detail = "no rule matches this window"
			r.Summary.Unplaced++
			r.Windows = append(r.Windows, wm)
			continue
		}
		sp, sok := src.Zone(a.Zone)
		tp, tok := tgt.Zone(a.Zone)
		if sok {
			f := sp.Frame
			wm.FromMonitor, wm.FromRect = sp.Monitor, &f
			if _, was := foldedFrom[sp.Monitor]; was {
				wm.Folded = true
				r.Summary.FoldedWins++
			}
		}
		if tok {
			f := tp.Frame
			wm.ToMonitor, wm.ToRect = tp.Monitor, &f
		}
		switch {
		case !tok:
			wm.Status = "unplaced"
			wm.Detail = fmt.Sprintf("zone %q could not be solved on the destination", a.Zone)
			r.Summary.Unplaced++
		case !sok:
			wm.Status = "new"
			wm.Detail = fmt.Sprintf("zone %q did not exist in the source plan", a.Zone)
		default:
			wm.Status = classify(sp.Frame, tp.Frame, sp.Monitor, tp.Monitor)
			wm.Detail = describeMove(sp, tp)
			switch wm.Status {
			case "unchanged":
				r.Summary.Unchanged++
			default:
				if sp.Frame.W != tp.Frame.W || sp.Frame.H != tp.Frame.H {
					r.Summary.Resized++
				}
				if sp.Frame.X != tp.Frame.X || sp.Frame.Y != tp.Frame.Y || sp.Monitor != tp.Monitor {
					r.Summary.Moved++
				}
			}
		}
		if wm.Folded {
			wm.Detail = "monitor " + wm.FromMonitor + " disappeared; folded into " + fallbackID + "; " + wm.Detail
		}
		r.Windows = append(r.Windows, wm)
	}
	return r
}

func classify(a, b Rect, ma, mb string) string {
	moved := a.X != b.X || a.Y != b.Y || ma != mb
	resized := a.W != b.W || a.H != b.H
	switch {
	case moved && resized:
		return "moved+resized"
	case moved:
		return "moved"
	case resized:
		return "resized"
	default:
		return "unchanged"
	}
}

func describeMove(from, to Placement) string {
	if from.Monitor != to.Monitor {
		return fmt.Sprintf("%s on %s -> %s on %s", from.Frame, from.Monitor, to.Frame, to.Monitor)
	}
	if from.Frame == to.Frame {
		return "unchanged at " + from.Frame.String()
	}
	return fmt.Sprintf("%s -> %s", from.Frame, to.Frame)
}

// rebuildZones recomputes the zone list of a layout assembled in memory.
func rebuildZones(l *Layout) {
	l.zones = nil
	var walk func(n *Node)
	walk = func(n *Node) {
		if n == nil {
			return
		}
		if n.isLeaf() {
			l.zones = append(l.zones, n.Zone)
			return
		}
		for _, c := range n.Children {
			walk(c)
		}
	}
	for _, lm := range l.Monitors {
		walk(lm.Root)
	}
}
