package main

import (
	"fmt"
	"strings"
)

// ---------------------------------------------------------------------------
// SVG rendering
//
// The diagram is drawn in DESKTOP PIXEL COORDINATES and scaled to a viewing
// size purely by the SVG viewBox, so no coordinate in this file is ever
// multiplied by a floating point scale factor: the rectangles written into the
// file are literally the rectangles the solver computed.
// ---------------------------------------------------------------------------

// svgTargetWidth is the on-screen width in CSS pixels the diagram asks for.
const svgTargetWidth = 1400

var zonePalette = []string{
	"#3b6ea5", "#2f8f6f", "#a8632c", "#7a4f9e", "#b03a5b",
	"#3f7d8c", "#8a7b2f", "#5566b5", "#2f7f4f", "#9c4b8f",
}

func escapeXML(s string) string {
	var b strings.Builder
	for _, r := range s {
		switch r {
		case '&':
			b.WriteString("&amp;")
		case '<':
			b.WriteString("&lt;")
		case '>':
			b.WriteString("&gt;")
		case '"':
			b.WriteString("&quot;")
		case '\'':
			b.WriteString("&apos;")
		default:
			if r < 0x20 && r != '\t' && r != '\n' {
				continue // control characters are not legal XML
			}
			b.WriteRune(r)
		}
	}
	return b.String()
}

// RenderSVG draws a solved layout as a scale diagram of the whole desktop.
func RenderSVG(res *SolveResult, topo *Topology, title string) []byte {
	desk := topo.DesktopBounds()
	// Margin around the desktop, proportional to it so the labels have room.
	margin := maxInt(desk.W/40, 40)
	view := Rect{
		X: desk.X - margin,
		Y: desk.Y - margin,
		W: desk.W + 2*margin,
		H: desk.H + 2*margin + margin, // extra strip at the bottom for the caption
	}
	outW := svgTargetWidth
	outH := view.H * outW / view.W
	if outH < 1 {
		outH = 1
	}

	stroke := maxInt(view.W/700, 2)
	font := maxInt(view.W/95, 12)
	small := maxInt(font*3/4, 10)

	var b strings.Builder
	fmt.Fprintf(&b, `<?xml version="1.0" encoding="UTF-8"?>`+"\n")
	fmt.Fprintf(&b, `<svg xmlns="http://www.w3.org/2000/svg" width="%d" height="%d" viewBox="%d %d %d %d" role="img" aria-label="%s">`+"\n",
		outW, outH, view.X, view.Y, view.W, view.H, escapeXML(title))
	fmt.Fprintf(&b, "  <title>%s</title>\n", escapeXML(title))
	fmt.Fprintf(&b, `  <rect x="%d" y="%d" width="%d" height="%d" fill="#12151a"/>`+"\n",
		view.X, view.Y, view.W, view.H)

	byMon := map[string][]Placement{}
	for _, p := range res.Placements {
		byMon[p.Monitor] = append(byMon[p.Monitor], p)
	}

	zoneIdx := 0
	for _, mp := range res.Monitors {
		// Monitor bounds.
		fmt.Fprintf(&b, "  <g id=\"monitor-%s\">\n", escapeXML(sanitizeID(mp.ID)))
		fmt.Fprintf(&b, `    <rect x="%d" y="%d" width="%d" height="%d" fill="#1b1f27" stroke="#5a6472" stroke-width="%d"/>`+"\n",
			mp.Bounds.X, mp.Bounds.Y, mp.Bounds.W, mp.Bounds.H, stroke)
		// Work area.
		if mp.Work != mp.Bounds {
			fmt.Fprintf(&b, `    <rect x="%d" y="%d" width="%d" height="%d" fill="none" stroke="#7d8796" stroke-width="%d" stroke-dasharray="%d %d"/>`+"\n",
				mp.Work.X, mp.Work.Y, mp.Work.W, mp.Work.H, stroke, stroke*4, stroke*3)
		}
		label := mp.ID
		if mp.Label != "" {
			label += " - " + mp.Label
		}
		tag := fmt.Sprintf("%s  [rank %d%s]  %dx%d px  %s  work %dx%d",
			label, mp.Rank, primarySuffix(mp.Primary), mp.Bounds.W, mp.Bounds.H, mp.Scale, mp.Work.W, mp.Work.H)
		fmt.Fprintf(&b, `    <text x="%d" y="%d" font-family="monospace" font-size="%d" fill="#c9d1d9">%s</text>`+"\n",
			mp.Bounds.X, mp.Bounds.Y-font/2, font, escapeXML(tag))

		for _, p := range byMon[mp.ID] {
			color := zonePalette[zoneIdx%len(zonePalette)]
			zoneIdx++
			fmt.Fprintf(&b, `    <rect x="%d" y="%d" width="%d" height="%d" fill="%s" fill-opacity="0.30" stroke="%s" stroke-width="%d"/>`+"\n",
				p.Frame.X, p.Frame.Y, maxInt(p.Frame.W, 1), maxInt(p.Frame.H, 1), color, color, stroke)
			tx := p.Frame.X + font/2
			ty := p.Frame.Y + font + font/2
			fmt.Fprintf(&b, `    <text x="%d" y="%d" font-family="monospace" font-size="%d" fill="#f0f3f6">%s</text>`+"\n",
				tx, ty, font, escapeXML(p.Zone))
			fmt.Fprintf(&b, `    <text x="%d" y="%d" font-family="monospace" font-size="%d" fill="#aab4c0">%s</text>`+"\n",
				tx, ty+small+small/2, small, escapeXML(p.Frame.String()))
			for i, app := range p.Apps {
				fmt.Fprintf(&b, `    <text x="%d" y="%d" font-family="monospace" font-size="%d" fill="#dfe6ee">%s</text>`+"\n",
					tx, ty+(small+small/2)*(i+2), small, escapeXML("- "+app))
			}
			if len(p.Apps) == 0 {
				fmt.Fprintf(&b, `    <text x="%d" y="%d" font-family="monospace" font-size="%d" fill="#8b95a1">%s</text>`+"\n",
					tx, ty+(small+small/2)*2, small, "(no window)")
			}
		}
		b.WriteString("  </g>\n")
	}

	caption := fmt.Sprintf("%s - layout %q on topology %q - %d monitors, %d zones, drawn to scale (1 SVG unit = 1 device pixel)",
		appName, res.LayoutName, res.TopologyName, len(res.Monitors), len(res.Placements))
	fmt.Fprintf(&b, `  <text x="%d" y="%d" font-family="monospace" font-size="%d" fill="#8b95a1">%s</text>`+"\n",
		desk.X, desk.Bottom()+margin*3/4, font, escapeXML(caption))
	b.WriteString("</svg>\n")
	return []byte(b.String())
}

func primarySuffix(p bool) string {
	if p {
		return ", primary"
	}
	return ""
}

// sanitizeID makes a monitor id usable as an XML id attribute.
func sanitizeID(s string) string {
	var b strings.Builder
	for _, r := range s {
		switch {
		case r >= 'a' && r <= 'z', r >= 'A' && r <= 'Z', r >= '0' && r <= '9', r == '-', r == '_':
			b.WriteRune(r)
		default:
			b.WriteByte('_')
		}
	}
	if b.Len() == 0 {
		return "m"
	}
	return b.String()
}
