<script setup lang="ts">
/**
 * A design system's tokens, drawn rather than listed (#2431).
 *
 * WHAT MAKES THIS WORK FOR A SYSTEM YOU AREN'T RUNNING
 * Every value is resolved on an offscreen probe carrying only this system's
 * declarations (`resolveDeclared`), never read from the page. So a token like
 * `color-mix(in srgb, var(--accent) 15%, transparent)` shows THIS system's
 * accent, not the accent of the app you happen to be looking at. Previewing
 * another project's palette from here is the point; a preview that quietly
 * borrows the host app's values would be worse than no preview, because it
 * would look right.
 *
 * SPECIMENS ARE CHOSEN BY VALUE SHAPE, NEVER BY NAME
 * A colour is drawn as a swatch, a length as a rule of that length, a font
 * stack as text set in it. Nothing here matches `--fs-space-*` or any other
 * naming convention, because the convention is the install's (rule #115) — a
 * system that calls its spacing `--gap-N` gets the same treatment.
 *
 * A token with no value for the chosen mode is shown as undecided rather than
 * skipped. A named role awaiting a decision is information; a gap in a grid
 * is not.
 */
import { computed, ref, watch } from "vue";

import type { ResolvedToken } from "@/api/designSystems";
import { BASE_MODE, modesPresent, resolveDeclared, valueForMode } from "@/utils/designValues";

const props = defineProps<{ tokens: ResolvedToken[] }>();

const modes = computed(() => modesPresent(props.tokens));
const mode = ref(BASE_MODE);

/** Values as the browser would compute them, for the chosen mode. */
const rendered = ref<Map<string, string>>(new Map());

function recompute() {
  const declared = new Map<string, string>();
  for (const token of props.tokens) {
    const value = valueForMode(token.value_by_mode, mode.value);
    if (value) declared.set(token.name, value);
  }
  rendered.value = resolveDeclared(declared);
}

watch(
  [() => props.tokens, mode],
  () => {
    // Keep the selection only while it still exists — switching systems can
    // drop a mode, and a stale one would silently render as base.
    if (!modes.value.includes(mode.value)) mode.value = modes.value[0] ?? BASE_MODE;
    recompute();
  },
  { immediate: true, deep: false },
);

type Shape = "colour" | "surface" | "length" | "font" | "plain";

const COLOUR = /^(#|rgba?\(|hsla?\(|color-mix\(|light-dark\()/;
const LENGTH = /^-?\d*\.?\d+(px|rem|em|ch|vh|vw)$/;
const GRADIENT = /gradient\(/;
/** Two or more space-separated parts ending in a colour — i.e. a shadow. */
const SHADOW = /^[^,]*\d\s+.*(#|rgba?\(|color-mix\()/;
/** A stack of family names: commas, no functions, no digits. */
const FONT_STACK = /^[^(){}\d]+,[^(){}\d]+$/;

function shapeOf(value: string): Shape {
  const v = value.trim();
  if (!v) return "plain";
  if (COLOUR.test(v)) return "colour";
  if (GRADIENT.test(v) || SHADOW.test(v)) return "surface";
  if (LENGTH.test(v)) return "length";
  if (FONT_STACK.test(v)) return "font";
  return "plain";
}

interface Specimen {
  name: string;
  declared: string;
  rendered: string;
  shape: Shape;
  purpose: string | null;
  /** True when `var()` substitution changed the value — worth showing on hover. */
  substituted: boolean;
}

const groups = computed(() => {
  const out = new Map<string, Specimen[]>();
  for (const token of props.tokens) {
    const declared = valueForMode(token.value_by_mode, mode.value);
    const value = rendered.value.get(token.name) ?? "";
    const bucket = out.get(token.group_name ?? "ungrouped") ?? [];
    bucket.push({
      name: token.name,
      declared,
      rendered: value,
      shape: shapeOf(value),
      purpose: token.purpose,
      substituted: Boolean(declared) && value !== declared,
    });
    out.set(token.group_name ?? "ungrouped", bucket);
  }
  return [...out.entries()];
});

/**
 * Lengths are drawn to scale up to a ceiling, so a 40px heading and a 4px gap
 * are visibly different — but a stray `100vw` can't stretch the row.
 */
function ruleWidth(value: string): string {
  return `min(${value}, 12rem)`;
}
</script>

<template>
  <div class="tp">
    <div v-if="modes.length > 1" class="tp-modes">
      <button
        v-for="m in modes"
        :key="m"
        class="tp-mode"
        :class="{ active: m === mode }"
        @click="mode = m"
      >{{ m }}</button>
      <span class="tp-modes-note">
        The system's own modes — independent of the theme this app is in.
      </span>
    </div>

    <div v-for="[group, specimens] in groups" :key="group" class="tp-group">
      <h3 class="tp-group-heading">{{ group }}</h3>
      <ul class="tp-grid">
        <li v-for="s in specimens" :key="s.name" class="tp-item">
          <div
            class="tp-specimen"
            :class="`is-${s.shape}`"
            :title="s.substituted ? `${s.declared} → ${s.rendered}` : s.declared"
          >
            <span
              v-if="s.shape === 'colour'"
              class="tp-swatch"
              :style="{ '--tp-fill': s.rendered }"
            />
            <span
              v-else-if="s.shape === 'surface'"
              class="tp-surface"
              :style="s.rendered.includes('gradient(')
                ? { background: s.rendered }
                : { boxShadow: s.rendered }"
            />
            <span v-else-if="s.shape === 'length'" class="tp-rule-wrap">
              <span class="tp-rule" :style="{ width: ruleWidth(s.rendered) }" />
              <span class="tp-rule-label">{{ s.rendered }}</span>
            </span>
            <span
              v-else-if="s.shape === 'font'"
              class="tp-font"
              :style="{ fontFamily: s.rendered }"
            >Ag</span>
            <span v-else-if="!s.declared" class="tp-undecided">to be decided</span>
            <span v-else class="tp-plain">{{ s.rendered }}</span>
          </div>

          <code class="tp-name">{{ s.name }}</code>
          <span class="tp-value" :title="s.declared">{{ s.declared || "—" }}</span>
          <span v-if="s.purpose" class="tp-purpose" :title="s.purpose">{{ s.purpose }}</span>
        </li>
      </ul>
    </div>
  </div>
</template>

<style scoped>
.tp-modes {
  display: flex;
  align-items: center;
  gap: var(--fs-space-2);
  flex-wrap: wrap;
  margin-bottom: var(--fs-space-4);
}

.tp-mode {
  padding: 0.2rem 0.6rem;
  font: inherit;
  font-size: var(--fs-size-body-sm);
  color: var(--fs-text-secondary);
  background: transparent;
  border: 1px solid var(--fs-border-color);
  border-radius: var(--fs-radius-sm);
  cursor: pointer;
}
.tp-mode:hover { color: var(--fs-text-primary); }
.tp-mode.active {
  color: var(--fs-accent);
  border-color: var(--fs-accent);
  background: var(--fs-accent-faint);
}

.tp-modes-note {
  font-size: var(--fs-size-tiny);
  color: var(--fs-text-tertiary);
}

.tp-group { margin-bottom: var(--fs-space-6); }

.tp-group-heading {
  text-transform: uppercase;
  letter-spacing: var(--fs-tracking-tiny);
  font-size: var(--fs-size-tiny);
  color: var(--fs-text-tertiary);
  margin: 0 0 var(--fs-space-3);
  padding-bottom: var(--fs-space-2);
  border-bottom: var(--fs-border);
}

.tp-grid {
  list-style: none;
  padding: 0;
  margin: 0;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(13rem, 1fr));
  gap: var(--fs-space-4) var(--fs-space-3);
}

.tp-item {
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 0.15rem;
}

/* A fixed-height stage so a 40px rule and a 2px one still line up in a grid.
 *
 * The stage itself is plain. An earlier version put the checkerboard here, so
 * every specimen — including opaque colours and plain text — sat inside a
 * frame of checks, and the pattern read as the loudest thing on the page. The
 * checks belong to the ONE case that needs them: a colour that might be
 * translucent. */
.tp-specimen {
  height: 2.5rem;
  display: flex;
  align-items: center;
  border-radius: var(--fs-radius-sm);
  padding: var(--fs-space-1);
  overflow: hidden;
  background: var(--fs-surface-raised);
}

/* Text-bearing specimens get no box at all — a border around a value is a
   frame around nothing, which is most of what made the grid feel busy. */
.tp-specimen.is-plain,
.tp-specimen.is-length,
.tp-specimen.is-font {
  background: none;
  padding: 0 var(--fs-space-1);
}

/* Checks UNDER the colour, not around it: an opaque value hides them
   completely, and a 15% tint shows exactly as much of them as it should.
   Layering the fill as a gradient is what lets one element do both. */
.tp-swatch {
  /* Declared here, overridden inline per swatch. Two reasons it is a real
     default rather than a formality: a token that resolves to nothing renders
     as bare checks instead of an invalid gradient, and a custom property that
     exists ONLY as an inline style is invisible to the CI token check — which
     reads it as an unresolvable reference, correctly, since nothing in any
     stylesheet declares it. */
  --tp-fill: transparent;
  width: 100%;
  height: 100%;
  border-radius: calc(var(--fs-radius-sm) - 2px);
  background-image:
    linear-gradient(var(--tp-fill), var(--tp-fill)),
    repeating-conic-gradient(
      var(--fs-border-color) 0% 25%,
      var(--fs-surface-raised) 0% 50%
    );
  background-size: auto, 10px 10px;
}

.tp-surface {
  width: 100%;
  height: 100%;
  border-radius: calc(var(--fs-radius-sm) - 2px);
  background: var(--fs-surface-raised);
}

.tp-rule-wrap {
  width: 100%;
  display: flex;
  align-items: center;
  gap: var(--fs-space-2);
  min-width: 0;
}

.tp-rule {
  height: 0.4rem;
  min-width: 1px;
  flex: none;
  background: var(--fs-accent);
  border-radius: 999px;
}

.tp-rule-label {
  font-family: var(--fs-font-mono);
  font-size: var(--fs-size-tiny);
  color: var(--fs-text-tertiary);
  white-space: nowrap;
}

.tp-font {
  font-size: 1.4rem;
  color: var(--fs-text-primary);
  line-height: 1;
}

.tp-plain {
  font-family: var(--fs-font-mono);
  font-size: var(--fs-size-code);
  color: var(--fs-text-secondary);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.tp-undecided {
  font-size: var(--fs-size-tiny);
  color: var(--fs-text-tertiary);
  font-style: italic;
}

/* One line each, with the full text on hover.
 *
 * These wrapped freely at first, so a card was two lines tall or five depending
 * on how long its `color-mix()` happened to be, and the grid lost any rhythm —
 * which is most of what "messy" was. A derived value is not something anyone
 * reads character by character in a gallery; it is something you check the
 * shape of and open if it matters. */
.tp-name,
.tp-value,
.tp-purpose {
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

.tp-name {
  font-size: var(--fs-size-body-sm);
  color: var(--fs-text-primary);
}

.tp-value {
  font-family: var(--fs-font-mono);
  font-size: var(--fs-size-tiny);
  color: var(--fs-text-secondary);
}

.tp-purpose {
  font-size: var(--fs-size-tiny);
  color: var(--fs-text-tertiary);
}
</style>
