package main

import (
	"fmt"
	"sort"
)

// ---------------------------------------------------------------------------
// advise: the smallest hardware change that makes the scene fit fully
//
// Nothing here is guessed. A catalogue of candidate topologies is generated,
// each one is ordered by how many physical pixels it adds, and the fit is
// RE-RUN against each candidate in that order. The first candidate that fits
// the whole scene at full size wins.
// ---------------------------------------------------------------------------

// monitorCatalogue is the set of panels advise is willing to suggest adding.
var monitorCatalogue = [][2]int{
	{1366, 768},
	{1600, 900},
	{1920, 1080},
	{1920, 1200},
	{2560, 1440},
	{3440, 1440},
	{3840, 2160},
}

const (
	heightStep = 60
	heightMax  = 1080
	widthStep  = 80
	widthMax   = 1920
)

// AdviseOption is one candidate hardware change.
type AdviseOption struct {
	Kind        string   `json:"kind"`
	Monitor     string   `json:"monitor,omitempty"`
	DeltaPixels int      `json:"delta_pixels,omitempty"`
	AddWidth    int      `json:"add_width,omitempty"`
	AddHeight   int      `json:"add_height,omitempty"`
	CostPixels  int64    `json:"cost_pixels"`
	Description string   `json:"description"`
	Topology    Topology `json:"topology"`
	FitPerMille int64    `json:"fit_per_mille"`
	FullFit     bool     `json:"full_fit"`
}

// AdviseSummary is the compact "where you are now" block.
type AdviseSummary struct {
	Feasible     bool   `json:"feasible"`
	Infeasible   string `json:"infeasible_reason,omitempty"`
	FullFit      bool   `json:"full_fit"`
	FitPerMille  int64  `json:"fit_per_mille"`
	CountFull    int    `json:"panes_full"`
	CountShrunk  int    `json:"panes_shrunk"`
	CountStacked int    `json:"panes_stacked"`
	CountDropped int    `json:"panes_dropped"`
}

// AdviseResult is the answer of the advise subcommand.
type AdviseResult struct {
	Scene            string         `json:"scene"`
	SceneSource      string         `json:"scene_source,omitempty"`
	Topology         string         `json:"topology"`
	Current          AdviseSummary  `json:"current"`
	AlreadyFits      bool           `json:"already_fits"`
	Recommended      *AdviseOption  `json:"recommended,omitempty"`
	Alternatives     []AdviseOption `json:"alternatives"`
	CandidatesTested int            `json:"candidates_tested"`
	SearchSpace      string         `json:"search_space"`
	NoSolution       bool           `json:"no_solution"`
	Note             string         `json:"note,omitempty"`
}

// fitsFully reports whether every pane can be placed at its ideal size with no
// stacking and no drops. It runs the same packer the optimiser runs, on the
// all-panes-separate configuration at shrink level 100, which is exactly the
// condition FitResult.FullFit describes.
func fitsFully(sc Scene, top Topology) bool {
	mons := top.logical()
	groups := stackGroups(sc)
	splits := make([]int, len(groups))
	for i := range groups {
		splits[i] = len(groups[i].members)
	}
	units := buildUnits(sc, groups, splits)
	active := make([]bool, len(units))
	for i := range active {
		active[i] = true
	}
	for _, u := range units {
		if !fitsSomewhere(u, mons) {
			return false
		}
	}
	level, _, ok := tryPack(units, active, mons)
	return ok && level == 100
}

// candidates builds every hardware change advise is prepared to consider,
// ordered cheapest first. Cost is the number of physical pixels added.
func candidates(base Topology) []AdviseOption {
	var out []AdviseOption
	for i, m := range base.Monitors {
		for d := heightStep; d <= heightMax; d += heightStep {
			t := base.clone()
			t.Monitors[i].Height += d
			t.Monitors[i].WorkArea.Height += d
			t.Name = fmt.Sprintf("%s + %dpx height on %s", base.Name, d, m.ID)
			out = append(out, AdviseOption{
				Kind: "grow-height", Monitor: m.ID, DeltaPixels: d,
				CostPixels: int64(d) * int64(m.Width),
				Description: fmt.Sprintf("%d more px of height on monitor %q (%dx%d -> %dx%d)",
					d, m.ID, m.Width, m.Height, m.Width, m.Height+d),
				Topology: t,
			})
		}
		for d := widthStep; d <= widthMax; d += widthStep {
			t := base.clone()
			t.Monitors[i].Width += d
			t.Monitors[i].WorkArea.Width += d
			t.Name = fmt.Sprintf("%s + %dpx width on %s", base.Name, d, m.ID)
			out = append(out, AdviseOption{
				Kind: "grow-width", Monitor: m.ID, DeltaPixels: d,
				CostPixels: int64(d) * int64(m.Height),
				Description: fmt.Sprintf("%d more px of width on monitor %q (%dx%d -> %dx%d)",
					d, m.ID, m.Width, m.Height, m.Width+d, m.Height),
				Topology: t,
			})
		}
	}
	for _, wh := range monitorCatalogue {
		w, h := wh[0], wh[1]
		t := base.clone()
		t.Monitors = append(t.Monitors, Monitor{
			ID: fmt.Sprintf("added-%dx%d", w, h), Width: w, Height: h, Scale: 100,
			WorkArea: &WorkArea{X: 0, Y: 0, Width: w, Height: h},
		})
		t.Name = fmt.Sprintf("%s + one %dx%d monitor", base.Name, w, h)
		out = append(out, AdviseOption{
			Kind: "add-monitor", AddWidth: w, AddHeight: h,
			CostPixels:  int64(w) * int64(h),
			Description: fmt.Sprintf("one more %dx%d monitor at 100%% scale", w, h),
			Topology:    t,
		})
	}
	rank := map[string]int{"grow-height": 0, "grow-width": 1, "add-monitor": 2}
	sort.SliceStable(out, func(a, b int) bool {
		x, y := out[a], out[b]
		if x.CostPixels != y.CostPixels {
			return x.CostPixels < y.CostPixels
		}
		if rank[x.Kind] != rank[y.Kind] {
			return rank[x.Kind] < rank[y.Kind]
		}
		if x.Monitor != y.Monitor {
			return x.Monitor < y.Monitor
		}
		if x.DeltaPixels != y.DeltaPixels {
			return x.DeltaPixels < y.DeltaPixels
		}
		return x.Description < y.Description
	})
	return out
}

func adviseScene(sc Scene, top Topology) AdviseResult {
	cur := fitScene(sc, top)
	res := AdviseResult{
		Scene:    sc.Name,
		Topology: top.Name,
		Current: AdviseSummary{
			Feasible: cur.Feasible, Infeasible: cur.Infeasible, FullFit: cur.FullFit,
			FitPerMille: cur.FitPerMille, CountFull: cur.CountFull,
			CountShrunk: cur.CountShrunk, CountStacked: cur.CountStacked,
			CountDropped: cur.CountDropped,
		},
		Alternatives: []AdviseOption{},
		SearchSpace: fmt.Sprintf(
			"height growth %d..%dpx step %d and width growth %d..%dpx step %d on each of %d monitors, plus one added panel from a %d-model catalogue",
			heightStep, heightMax, heightStep, widthStep, widthMax, widthStep, len(top.Monitors), len(monitorCatalogue)),
	}
	if cur.FullFit {
		res.AlreadyFits = true
		res.Note = "the scene already fits at full size on this hardware; no change is needed"
		return res
	}

	cands := candidates(top)
	res.CandidatesTested = len(cands)
	var hits []AdviseOption
	for _, c := range cands {
		if !fitsFully(sc, c.Topology) {
			continue
		}
		f := fitScene(sc, c.Topology)
		if !f.FullFit {
			// Belt and braces: never recommend something the full optimiser
			// disagrees with.
			continue
		}
		c.FitPerMille = f.FitPerMille
		c.FullFit = true
		hits = append(hits, c)
		if len(hits) >= 4 {
			break
		}
	}
	if len(hits) == 0 {
		res.NoSolution = true
		res.Note = "no single change in the searched catalogue is enough; the scene needs more than one added panel or a larger one than " +
			fmt.Sprintf("%dx%d", monitorCatalogue[len(monitorCatalogue)-1][0], monitorCatalogue[len(monitorCatalogue)-1][1])
		return res
	}
	r := hits[0]
	res.Recommended = &r
	if len(hits) > 1 {
		res.Alternatives = hits[1:]
	}
	return res
}
