package main

import (
	"fmt"
	"html/template"
	"log"
	"math"
	"net/http"
	"strings"

	"vitrine/display"
)

// This file is socle's page chrome: the gallery-sketchbook skin.
// Every page is an exhibit sheet: hatched flanks at the margins, a crumb trail,
// an arc of round buttons over the content, and the gallery floor at the bottom.
// All texture comes from the sketch assets (sketch.go); there is no JavaScript.

// pageData is everything the socle page template needs.
type pageData struct {
	Title    string
	SiteName string
	Crumbs   []display.Crumb
	Arc      []arcButton
	ArcDrop  int  // the deepest button drop, so the arc region reserves room for it (transforms take no layout space)
	Dawn     bool // true: the full dawn arc (repo home); false: the compact corner arc
	Content  template.HTML
}

// arcButton is one round button on the arc.  Buttons are for moves; prose links stay links.
type arcButton struct {
	Label   string // one short word, drawn inside the circle
	Title   string // the fuller hint, for the title attribute
	Href    string
	Variant string // "a", "b", or "c": which circle drawing the button wears
	DY      int    // the button's drop below the arc apex, in px; computed, static per page
}

// arcLayout drops each button onto a circle arc: each sinks below the arc's apex by the
// circle's sagitta at its x position.  Computed server-side so positions are static per
// page type (buttons must not move around).  apexAt is the apex's x position in button
// indices (i.e. 1.0 means "centered on the second button"): the dawn arc puts the apex at
// the row's center for a symmetric sunrise, and the corner arc puts it just past the
// rightmost button, so every button sits on a visibly rising line -- even a pair of them
// (a symmetric pair straddles the apex at equal height and reads as flat; we learned).
// Drawing variants cycle a/b/c so neighbors never wear the same circle.
func arcLayout(radius, spacing, apexAt float64, btns []arcButton) []arcButton {
	for i := range btns {
		x := (apexAt - float64(i)) * spacing
		btns[i].DY = int(radius - math.Sqrt(radius*radius-x*x) + 0.5)
		btns[i].Variant = string(rune('a' + i%3))
	}
	return btns
}

// The spacing passed to arcLayout is the distance between button centers, so it must
// equal the button diameter plus the flex gap in the stylesheet (nav.arc rules).

// dawnArc is the repo home's full arc: the sun of moves rising over the exhibit.
// 88px buttons + 2.4rem gap; apex at the row's center.
func dawnArc(btns []arcButton) []arcButton {
	return arcLayout(300, 126, float64(len(btns)-1)/2, btns)
}

// cornerArc is the compact contextual arc every other page gets: a rising line toward
// the corner, apex half a spacing past the rightmost button.  58px buttons + 1.1rem gap.
func cornerArc(btns []arcButton) []arcButton {
	return arcLayout(380, 76, float64(len(btns)-1)+0.5, btns)
}

var soclePageTmpl = template.Must(template.New("socle").Parse(`<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{.Title}}</title>
<link rel="stylesheet" href="/-/style.css">
<link rel="icon" type="image/svg+xml" href="/-/sketch/favicon.svg">
</head>
<body>
<div class="flank left"></div><div class="flank right"></div>
<div class="sheet">
<nav class="crumbs">
{{range $i, $c := .Crumbs}}{{if $i}} / {{end}}{{if $c.Href}}<a href="{{$c.Href}}">{{$c.Name}}</a>{{else}}{{$c.Name}}{{end}}{{end}}
</nav>
{{if .Arc}}<nav class="arc{{if not .Dawn}} corner{{end}}" aria-label="page moves" style="--arc-drop:{{.ArcDrop}}px">
{{range .Arc}}<a class="v{{.Variant}}" href="{{.Href}}"{{if .Title}} title="{{.Title}}"{{end}} style="--dy:{{.DY}}px">{{.Label}}</a>
{{end}}</nav>
{{end}}<main><article>{{.Content}}</article></main>
</div>
<footer class="ground">
<div class="floor"></div>
<p class="colophon">{{.SiteName}} · made with socle</p>
</footer>
</body>
</html>
`))

// serveSketch hands out one generated sketch asset.  The bytes are a pure function of
// the theme seed, so they are safe to cache long; a redeploy or reload with a new seed
// changes nothing here (same URLs, new bytes), hence long-but-not-immutable.
func serveSketch(w http.ResponseWriter, snap *snapshot, name string) {
	b, ok := snap.face.sketch[name]
	if !ok {
		http.Error(w, "no such sketch asset", http.StatusNotFound)
		return
	}
	w.Header().Set("Content-Type", "image/svg+xml")
	w.Header().Set("Cache-Control", "public, max-age=604800")
	w.Write(b)
}

// socleCSS is the scheme-independent part of the sketchbook stylesheet, appended after
// display.StyleCSS in /-/style.css.  Rules that reference sketch assets live in
// sketchCSS, which is emitted once per color scheme.
const socleCSS = `
/* ---- socle: the gallery sketchbook ---- */

/* socle lays out its own page: a full-width column so the floor can span the viewport,
   with the sheet (the exhibit column) centered inside it. */
body {
	max-width: none;
	margin: 0;
	padding: 0;
	display: flex;
	flex-direction: column;
	min-height: 100vh;
}
.sheet {
	width: 100%;
	max-width: 64rem;
	margin: 0 auto;
	padding: 1rem 1.25rem 0;
	box-sizing: border-box;
	flex: 1;
}

/* The hatched flanks: texture, not content.  Fixed, behind everything, unclickable,
   and gone on narrow viewports where they would crowd the sheet. */
.flank {
	position: fixed;
	top: 0;
	bottom: 0;
	width: 240px;
	pointer-events: none;
	z-index: -1;
	background-repeat: repeat-y;
	background-size: 240px 720px;
}
.flank.left { left: 0; }
.flank.right { right: 0; transform: scaleX(-1); }
/* Below this, a flank's inner (already faded) end would still reach under the sheet. */
@media (max-width: 1550px) { .flank { display: none; } }

/* The arc: round buttons over the exhibit.  Offsets along the arc are computed
   server-side into --dy; hover swaps the circle drawing, never the position.
   The page emits its deepest drop as --arc-drop, and the bottom padding reserves
   that room here (transforms take no layout space); narrow screens flatten the
   arc by overriding both, further down. */
nav.arc {
	display: flex;
	justify-content: center;
	align-items: flex-start;
	gap: 2.4rem;
	border: none;
	margin: 0;
	padding: 2.4rem 0 calc(.8rem + var(--arc-drop, 0px));
	background: none;
}
nav.arc a {
	display: flex;
	align-items: center;
	justify-content: center;
	width: 88px;
	height: 88px;
	border-radius: 50%;
	text-decoration: none;
	color: var(--ink);
	font-size: .8rem;
	letter-spacing: .02em;
	background-position: center;
	background-size: 100% 100%;
	background-repeat: no-repeat;
	transform: translateY(var(--dy, 0));
}
nav.arc a:hover { color: var(--accent); }
nav.arc a:focus-visible {
	outline: 3px solid var(--accent);
	outline-offset: 3px;
}
nav.arc.corner {
	justify-content: flex-end;
	gap: 1.1rem;
	padding: 1.1rem 0 calc(.8rem + var(--arc-drop, 0px));
}
nav.arc.corner a {
	width: 58px;
	height: 58px;
	font-size: .72rem;
}

/* The entrance: repos as exhibit stands, surveyed rather than parsed.
   Each stand is name / pedestal / placard / one-line "now showing" caption. */
article ul.entrance {
	list-style: none;
	margin: 2.5rem 0 1rem;
	padding: 0;
	display: grid;
	grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr));
	gap: 2.8rem 1.5rem;
}
.stand { text-align: center; min-width: 0; }
.stand .name { font-size: 1.35rem; margin: 0; }
.stand .name a { text-decoration: none; }
.stand .name a:hover { text-decoration: underline; }
.stand .pedestal {
	height: 84px;
	background-position: top center;
	background-size: 140px 84px;
	background-repeat: no-repeat;
}
.stand .placard {
	font-size: .8rem;
	letter-spacing: .12em;
	color: var(--ink-soft);
	margin: .7rem 0 .15rem;
}
.stand .showing {
	font-size: .78rem;
	font-style: italic;
	color: var(--ink-soft);
	margin: 0;
	white-space: nowrap;
	overflow: hidden;
	text-overflow: ellipsis;
}

/* The tagline: the repo description, read as the exhibit's subtitle. */
article .tagline {
	text-align: center;
	font-style: italic;
	color: var(--ink-soft);
	margin: 0 0 .75rem;
}

/* The clone box and branch list sit centered under the plaque, like gallery cards. */
article p.clone, article #branches { text-align: center; margin-left: auto; margin-right: auto; }
article p.clone.boxed { display: block; width: fit-content; }

/* The language meter: composition by bytes at the shown commit, in its rough box
   between the plaque and the clone line.  Colors are linguist's conventions, used as
   swatches only; names and percentages stay ink.  The faint inset ring keeps pale
   swatches from dissolving into the paper (or the dark ground). */
article .langmeter {
	display: block;
	width: fit-content;
	max-width: 100%;
	box-sizing: border-box;
	margin: 0 auto 1.4rem;
}
.langmeter .langbar {
	display: flex;
	gap: 3px;
	width: 28rem;
	max-width: 100%;
	height: 12px;
}
.langmeter .langbar .seg {
	flex-basis: 0;
	min-width: 2px;
	box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--ink) 25%, transparent);
}
.langmeter .langchips {
	margin: .6rem 0 0;
	font-size: .78rem;
	letter-spacing: .08em;
	color: var(--ink-soft);
	text-align: center;
}
.langmeter .chip { white-space: nowrap; }
.langmeter .chip + .chip { margin-left: .9rem; }
/* The swatch is shared: the meter's legend and the entrance placards both wear it. */
.swatch {
	display: inline-block;
	width: 10px;
	height: 10px;
	margin-right: .35em;
	border-radius: 2px;
	box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--ink) 25%, transparent);
}
.langmeter .seg.other, .langmeter .swatch.other { background: var(--ink-soft); }

/* The plaque: museum-label typography for the metadata line under an exhibit. */
.plaque {
	text-align: center;
	font-size: .8rem;
	letter-spacing: .16em;
	color: var(--ink-soft);
	margin: .9rem auto 2.2rem;
	overflow-wrap: anywhere;
}
article .plaque { margin-left: auto; }
article .plaque code { background: none; padding: 0; letter-spacing: .06em; }

/* Exhibit interiors.  A file view's whole body is one pre (pre.file): there the frame
   is the box, so that pre drops its well.  Code blocks inside a framed README keep
   theirs; they are exhibits' content, not the exhibit itself. */
.exhibit { margin-top: .5rem; }
article .exhibit { margin-left: 0; }
.exhibit > :first-child { margin-top: 0; }
.exhibit > :last-child { margin-bottom: 0; }
article .exhibit > pre.file {
	margin-left: 0;
	background: none;
	border: none;
	padding: 0;
}

/* The ground: the gallery floor at the end of every page.  The floor's gather
   starts high and sparse, so a short page's run-up reads as approach, not absence. */
footer.ground { margin-top: 2.5rem; }
footer.ground .floor {
	height: 230px;
	background-position: bottom center;
	background-size: 1800px 230px;
	background-repeat: no-repeat;
}
footer.ground .colophon {
	text-align: center;
	font-size: .78rem;
	letter-spacing: .16em;
	color: var(--ink-soft);
	margin: .5rem 0 1.1rem;
}

/* The ledger: provenance rows on the log page.  Subjects are prose and wrap
   freely; the attribution line stays soft and small.  Links wear ink, not accent:
   a hundred accent rows would shout; hover answers the "is it live" question. */
article .provenance {
	margin: -.4rem 0 1.6rem;
	font-size: .78rem;
	letter-spacing: .16em;
	color: var(--ink-soft);
}
article ul.ledger { list-style: none; padding: 0; }
ul.ledger li { padding: .5rem 0 .8rem; }
ul.ledger .subject { margin: 0; max-width: 44rem; }
ul.ledger .subject a { color: var(--ink); text-decoration: none; }
ul.ledger .subject a:hover { color: var(--accent); text-decoration: underline; }
ul.ledger .attrib {
	margin: .3rem 0 0;
	font-size: .78rem;
	color: var(--ink-soft);
}
a.sha {
	font-family: monospace;
	color: var(--ink);
	text-decoration: none;
	display: inline-block;
	padding: 0 .35rem;
	margin-right: .35rem;
}
a.sha:hover { color: var(--accent); }

/* The catalogue: listings keep their tabular bones; rows separate with soft
   hand-rules (set in sketchCSS) and directory links carry the drawer-tab mark. */
article table.tree { border-collapse: collapse; }
article table.tree td, article table.tree th { border: none; padding: .3rem .6rem; }
article table.tree tbody tr:hover { background-color: var(--well); }
article td.dir > a {
	background-repeat: no-repeat;
	background-position: left center;
	background-size: 24px 18px;
	padding-left: 30px;
}
article th { text-align: left; background: none; }
/* Table links skip the underline: in a column of links it is pure noise;
   the accent color and the row hover carry the affordance.  Hover restores it. */
article table.tree a { text-decoration: none; }
article table.tree a:hover { text-decoration: underline; }
article td.name { white-space: nowrap; }
article td.size { text-align: right; color: var(--ink-soft); white-space: nowrap; }
article td.date { color: var(--ink-soft); white-space: nowrap; }
article td.soft { color: var(--ink-soft); }
article td.hash a { font-family: monospace; }
article p.clone code { user-select: all; }
article img.file { max-width: 100%; }
article pre.file { tab-size: 4; }
/* Unified diffs color by text, not background: safe in both schemes, and the
   hues are the palette's existing success/danger pair. */
article pre.diff { tab-size: 4; }
article pre.diff .d-add { color: var(--success); }
article pre.diff .d-del { color: var(--danger); }
article pre.diff .d-hunk { color: var(--accent); }
article pre.diff .d-file { font-weight: bold; }
/* Headings and inline code may carry paths, shas, and clone commands: long tokens
   break rather than force the page wide.  (Wells scroll; this is for prose flow.) */
article h1, article code { overflow-wrap: anywhere; }

/* Phone widths: the arc flattens to a tappable row, insets yield to content,
   and the exhibit mat thins.  Buttons stay still within this layout; the
   breakpoint is the only place they are allowed to differ. */
@media (max-width: 560px) {
	/* The sun still rises on phones: buttons shrink and the precomputed drops scale
	   down with them (same ratio as the size change), but the curve stays.
	   Width check: 4 dawn buttons at 64px + 3 gaps ~= 296px, fits 390 without wrap. */
	nav.arc { gap: .8rem; padding: 1.2rem 0 calc(.6rem + var(--arc-drop, 0px) * .72); }
	nav.arc a { transform: translateY(calc(var(--dy, 0px) * .72)); width: 64px; height: 64px; font-size: .72rem; }
	nav.arc.corner { padding: .8rem 0 calc(.4rem + var(--arc-drop, 0px) * .72); }
	nav.arc.corner a { width: 48px; height: 48px; }
	article > * { margin-left: 0; }
	article pre { margin-left: 0; }
	.exhibit { padding: .9rem 1rem; }
}
`

// sketchCSS emits the rules that reference sketch assets, for one color scheme.
// Written once so light and dark cannot drift apart; the caller wraps the dark copy
// in its media query.
func sketchCSS(scheme string) string {
	u := func(name string) string { return "url(/-/sketch/" + name + "-" + scheme + ".svg)" }
	return fmt.Sprintf(`
.flank { background-image: %s; }
footer.ground .floor { background-image: %s; }
.stand .pedestal { background-image: %s; }
/* Hand-ruled lines replace solid borders on the nav and headings. */
nav.crumbs, article h1, article h2 {
	border-bottom: none;
	background-image: %s;
	background-position: bottom left;
	background-repeat: repeat-x;
	background-size: auto 10px;
	padding-bottom: .55rem;
}
/* Each ledger row sits under its own light hand-rule; the chip outline around a
   sha is the small sibling of the frames, so it reads as a button, not a well. */
ul.ledger li {
	background-image: %s;
	background-position: bottom left;
	background-repeat: repeat-x;
	background-size: auto 9px;
}
/* Catalogue rows separate with the rule's soft sibling; the tab mark rides the link. */
article table.tree tbody tr {
	background-image: %s;
	background-position: bottom left;
	background-repeat: repeat-x;
	background-size: auto 8px;
}
article td.dir > a { background-image: %s; }
a.sha {
	border: 7px solid transparent;
	border-image: %s 8 / 7px round;
}
/* Rough-ruled frames, as border-images.  The tile is sliced at 16 and the border is
   near that size, so the stroke renders at the weight it was drawn. */
.exhibit {
	border: 13px solid transparent;
	border-image: %s 16 / 13px round;
	padding: 1.4rem 2rem;
}
.boxed {
	border: 10px solid transparent;
	border-image: %s 16 / 10px round;
	padding: .3rem .9rem;
	display: inline-block;
}
/* Each button wears one of three circle drawings; hover moves to the next drawing,
   so the ink appears to shift under a living hand.  Active presses to the third. */
nav.arc a.va { background-image: %s; }
nav.arc a.vb { background-image: %s; }
nav.arc a.vc { background-image: %s; }
nav.arc a.va:hover { background-image: %s; }
nav.arc a.vb:hover { background-image: %s; }
nav.arc a.vc:hover { background-image: %s; }
nav.arc a.va:active { background-image: %s; }
nav.arc a.vb:active { background-image: %s; }
nav.arc a.vc:active { background-image: %s; }
`,
		u("flank"), u("ground"), u("plinth"), u("rule"), u("rule"), u("rulesoft"), u("tab"), u("chip"), u("frame"), u("frame"),
		u("circle-a"), u("circle-b"), u("circle-c"),
		u("circle-b"), u("circle-c"), u("circle-a"),
		u("circle-c"), u("circle-a"), u("circle-b"))
}

// buildStyle assembles the full /-/style.css body: vitrine's base sheet, socle's
// sketchbook sheet, the per-scheme sketch rules, and the host theme's :root override
// last (at equal specificity the later declaration wins).
func buildStyle(theme display.Theme) []byte {
	var sb strings.Builder
	sb.Write(display.StyleCSS)
	sb.WriteString(socleCSS)
	sb.WriteString(sketchCSS("light"))
	sb.WriteString("@media (prefers-color-scheme: dark) {")
	sb.WriteString(sketchCSS("dark"))
	sb.WriteString("}\n")
	sb.Write(theme.CSS)
	return []byte(sb.String())
}

// renderPage computes the arc's reserved height and writes the full page.
func (s *server) renderPage(w http.ResponseWriter, pd pageData) {
	for _, b := range pd.Arc {
		pd.ArcDrop = max(pd.ArcDrop, b.DY)
	}
	w.Header().Set("Content-Type", "text/html; charset=utf-8")
	if err := soclePageTmpl.Execute(w, pd); err != nil {
		// Headers are gone already; leave a trace in the body and the log.
		log.Print("socle: ", err)
		fmt.Fprintf(w, "\n<!-- template error: %v -->", err)
	}
}
