package main

import (
	"bytes"
	"encoding/xml"
	"fmt"
	"sort"
	"strings"
)

// renderSVG draws the computed placement to scale. Monitors are drawn in
// PHYSICAL device pixels in their virtual-desktop positions; each pane's
// logical rectangle is converted back to physical pixels through its
// monitor's scale factor, so a 200% HiDPI panel correctly shows half as many
// logical pixels in the same physical space. Every coordinate written into the
// file is an integer.
func renderSVG(fr FitResult, top Topology) []byte {
	mons := top.logical()
	byID := map[string]logicalMon{}
	for _, m := range mons {
		byID[m.ID] = m
	}

	minX, minY := 1<<30, 1<<30
	maxX, maxY := -(1 << 30), -(1 << 30)
	for _, m := range mons {
		if m.PhysX < minX {
			minX = m.PhysX
		}
		if m.PhysY < minY {
			minY = m.PhysY
		}
		if m.PhysX+m.PhysW > maxX {
			maxX = m.PhysX + m.PhysW
		}
		if m.PhysY+m.PhysH > maxY {
			maxY = m.PhysY + m.PhysH
		}
	}
	if maxX <= minX {
		minX, maxX = 0, 1920
	}
	if maxY <= minY {
		minY, maxY = 0, 1080
	}

	var dropped []PaneOutcome
	for _, p := range fr.Panes {
		if p.Outcome == "dropped" {
			dropped = append(dropped, p)
		}
	}
	sort.SliceStable(dropped, func(a, b int) bool { return dropped[a].Priority > dropped[b].Priority })

	const (
		pad     = 80
		headerH = 240
		lineH   = 58
	)
	footerH := 130 + lineH*len(dropped)
	deskW := maxX - minX
	deskH := maxY - minY
	vbW := deskW + 2*pad
	vbH := headerH + deskH + footerH

	outW := vbW
	if outW > 1600 {
		outW = 1600
	}
	outH := vbH * outW / vbW

	// desktop -> viewBox translation
	tx := pad - minX
	ty := headerH - minY

	var b bytes.Buffer
	fmt.Fprintf(&b, `<svg xmlns="http://www.w3.org/2000/svg" width="%d" height="%d" viewBox="0 0 %d %d" role="img">`+"\n",
		outW, outH, vbW, vbH)
	fmt.Fprintf(&b, `<title>%s</title>`+"\n", esc("DeskScene layout: "+fr.Scene+" on "+fr.Topology))
	fmt.Fprintf(&b, `<rect x="0" y="0" width="%d" height="%d" fill="#11151b"/>`+"\n", vbW, vbH)

	// Header
	fmt.Fprintf(&b, `<text x="%d" y="%d" font-family="Helvetica,Arial,sans-serif" font-size="76" font-weight="bold" fill="#e6edf3">%s</text>`+"\n",
		pad, 100, esc(fr.Scene))
	sub := fmt.Sprintf("%s   |   fit %d/1000   |   %d full, %d shrunk, %d stacked, %d dropped   |   shrink level %d%%",
		fr.Topology, fr.FitPerMille, fr.CountFull, fr.CountShrunk, fr.CountStacked, fr.CountDropped, fr.ShrinkLevel)
	fmt.Fprintf(&b, `<text x="%d" y="%d" font-family="Helvetica,Arial,sans-serif" font-size="44" fill="#9fb0c0">%s</text>`+"\n",
		pad, 170, esc(sub))

	// Monitors
	for _, m := range mons {
		fmt.Fprintf(&b, `<rect x="%d" y="%d" width="%d" height="%d" fill="#171d26" stroke="#3c4855" stroke-width="6"/>`+"\n",
			m.PhysX+tx, m.PhysY+ty, m.PhysW, m.PhysH)
		if !m.FullMon {
			fmt.Fprintf(&b, `<rect x="%d" y="%d" width="%d" height="%d" fill="none" stroke="#2a3542" stroke-width="4" stroke-dasharray="18 12"/>`+"\n",
				m.OriginX+tx, m.OriginY+ty, m.PhysWAW, m.PhysWAH)
		}
		lbl := fmt.Sprintf("%s  %dx%d @ %d%%  (%dx%d logical work area)", m.ID, m.PhysW, m.PhysH, m.Scale, m.W, m.H)
		fmt.Fprintf(&b, `<text x="%d" y="%d" font-family="Helvetica,Arial,sans-serif" font-size="40" fill="#7f8fa0">%s</text>`+"\n",
			m.PhysX+tx+10, m.PhysY+ty-18, esc(lbl))
	}

	// Panes, front-tab only for stacks.
	drawn := make([]PaneOutcome, 0, len(fr.Panes))
	for _, p := range fr.Panes {
		if p.Rect == nil {
			continue
		}
		if p.Outcome == "stacked" && !p.StackFront {
			continue
		}
		drawn = append(drawn, p)
	}
	sort.SliceStable(drawn, func(a, b int) bool { return drawn[a].ID < drawn[b].ID })

	for _, p := range drawn {
		m, ok := byID[p.Monitor]
		if !ok {
			continue
		}
		x := m.OriginX + tx + p.Rect.X*m.Scale/100
		y := m.OriginY + ty + p.Rect.Y*m.Scale/100
		w := p.Rect.W * m.Scale / 100
		h := p.Rect.H * m.Scale / 100
		fill, stroke := outcomeColours(p.Outcome)
		fmt.Fprintf(&b, `<g><title>%s</title>`, esc(p.Label+" - "+p.Reason))
		fmt.Fprintf(&b, `<rect x="%d" y="%d" width="%d" height="%d" fill="%s" stroke="%s" stroke-width="5"/>`,
			x+6, y+6, maxInt(w-12, 1), maxInt(h-12, 1), fill, stroke)
		fmt.Fprintf(&b, `<text x="%d" y="%d" font-family="Helvetica,Arial,sans-serif" font-size="42" font-weight="bold" fill="#f2f6fa">%s</text>`,
			x+34, y+72, esc(p.Label))
		second := fmt.Sprintf("p%d  %dx%d  %s", p.Priority, p.Rect.W, p.Rect.H, p.Outcome)
		if p.Outcome == "shrunk" {
			second = fmt.Sprintf("p%d  %dx%d  shrunk to %d%%", p.Priority, p.Rect.W, p.Rect.H, p.ScalePercent)
		}
		if p.Outcome == "stacked" {
			second = fmt.Sprintf("p%d  %dx%d  stack of %d", p.Priority, p.Rect.W, p.Rect.H, len(p.StackedWith)+1)
		}
		fmt.Fprintf(&b, `<text x="%d" y="%d" font-family="Helvetica,Arial,sans-serif" font-size="36" fill="#cfdae5">%s</text>`,
			x+34, y+126, esc(second))
		if p.Outcome == "stacked" && len(p.StackedWith) > 0 {
			fmt.Fprintf(&b, `<text x="%d" y="%d" font-family="Helvetica,Arial,sans-serif" font-size="32" fill="#9fb8d0">%s</text>`,
				x+34, y+178, esc("tabs: "+strings.Join(p.StackedWith, ", ")))
		}
		fmt.Fprint(&b, `</g>`+"\n")
	}

	// Footer: legend and dropped panes.
	fy := headerH + deskH + 90
	legend := []struct {
		label string
		fill  string
	}{
		{"full", "#1f5c3a"},
		{"shrunk", "#6b5117"},
		{"stacked", "#1d3f6b"},
	}
	lx := pad
	for _, l := range legend {
		fmt.Fprintf(&b, `<rect x="%d" y="%d" width="44" height="44" fill="%s" stroke="#8fa3b6" stroke-width="4"/>`+"\n", lx, fy-36, l.fill)
		fmt.Fprintf(&b, `<text x="%d" y="%d" font-family="Helvetica,Arial,sans-serif" font-size="40" fill="#c9d6e2">%s</text>`+"\n",
			lx+60, fy, esc(l.label))
		lx += 260
	}
	for i, p := range dropped {
		fmt.Fprintf(&b, `<text x="%d" y="%d" font-family="Helvetica,Arial,sans-serif" font-size="38" fill="#e0757a">%s</text>`+"\n",
			pad, fy+lineH*(i+1), esc(fmt.Sprintf("dropped: %s (priority %d, min %dx%d)", p.Label, p.Priority, p.MinWidth, p.MinHeight)))
	}
	fmt.Fprint(&b, "</svg>\n")
	return b.Bytes()
}

func outcomeColours(outcome string) (string, string) {
	switch outcome {
	case "full":
		return "#1f5c3a", "#46c07f"
	case "shrunk":
		return "#6b5117", "#e0a72c"
	case "stacked":
		return "#1d3f6b", "#4f9de0"
	default:
		return "#3a2126", "#e0757a"
	}
}

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

func esc(s string) string {
	var b bytes.Buffer
	_ = xml.EscapeText(&b, []byte(s))
	return b.String()
}
