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 vitals card: the clone line and the language meter in one rough box,
   placed before the README so the useful impressions land before the exhibit steers.
   Meter 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 .vitals {
	display: block;
	width: fit-content;
	max-width: 100%;
	box-sizing: border-box;
	margin: 0 auto 1.8rem;
	padding: .4rem 1.2rem .5rem;
}
article .vitals p.clone { margin: .2rem 0; }
article .vitals .langmeter { display: block; margin: .6rem 0 0; }

/* The clone line and branch list sit centered, like gallery cards. */
article p.clone, article #branches { text-align: center; margin-left: auto; margin-right: auto; }


.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)
	}
}
