/**
 * Analytic glyph samplers for Orby.
 *
 * Coordinates are normalized to a square from -1 to 1. Every sampler is pure,
 * allocation-light, and returns coverage in the 0..1 range. The relatively
 * heavy strokes are intentional: the semantic silhouette survives a 16px grid
 * before a field, dither, or pixel-switch operator is applied.
 *
 * Common context values:
 *   size | resolution  raster width used to select an anti-aliasing feather
 *   reducedMotion      freeze decorative motion at a representative phase
 *   timelinePhase      normalized 0..1 position in the recipe's macro cycle
 *   progress | value   normalized progress for progress/generating/transfer
 *   audioLevel | level normalized listening/speaking energy
 *   direction          "up", "down", "left", "right", or "sync"
 *   customSampler      (x, y, t, context) => coverage
 */

const TAU = Math.PI * 2;
const HALF_PI = Math.PI * 0.5;
const EMPTY_CONTEXT = Object.freeze({});

const clamp = (value, min, max) => Math.min(max, Math.max(min, value));
const saturate = (value) => clamp(value, 0, 1);
const finite = (value, fallback = 0) =>
  Number.isFinite(Number(value)) ? Number(value) : fallback;
const fract = (value) => value - Math.floor(value);
const positiveAngle = (value) => ((value % TAU) + TAU) % TAU;

function smoothstep(edge0, edge1, value) {
  if (edge0 === edge1) return value < edge0 ? 0 : 1;
  const unit = saturate((value - edge0) / (edge1 - edge0));
  return unit * unit * (3 - 2 * unit);
}

function motionTime(t, context) {
  if (!context?.reducedMotion) return finite(t);
  return finite(context.reducedPhase, 0.23) * TAU;
}

function featherFor(context) {
  const explicit = finite(context?.feather, -1);
  if (explicit >= 0) return clamp(explicit, 0.006, 0.18);
  const size = clamp(
    finite(context?.resolution ?? context?.size, 24),
    8,
    1024,
  );
  // Keep the analytic edge below half a raster cell. The previous feather was
  // wide enough to turn small rings and adjacent strokes into soft blobs once
  // the 1-bit gate was applied.
  return clamp(0.86 / size, 0.008, 0.085);
}

function coverage(distance, context, featherScale = 1) {
  const feather = featherFor(context) * featherScale;
  return 1 - smoothstep(-feather, feather, distance);
}

function sdCircle(x, y, radius) {
  return Math.hypot(x, y) - radius;
}

function sdRoundBox(x, y, halfWidth, halfHeight, radius) {
  const corner = Math.min(radius, halfWidth, halfHeight);
  const qx = Math.abs(x) - halfWidth + corner;
  const qy = Math.abs(y) - halfHeight + corner;
  return (
    Math.hypot(Math.max(qx, 0), Math.max(qy, 0)) +
    Math.min(Math.max(qx, qy), 0) -
    corner
  );
}

function sdSegment(x, y, ax, ay, bx, by, radius) {
  const vx = bx - ax;
  const vy = by - ay;
  const denominator = vx * vx + vy * vy || 1;
  const amount = saturate(((x - ax) * vx + (y - ay) * vy) / denominator);
  return Math.hypot(x - (ax + vx * amount), y - (ay + vy * amount)) - radius;
}

function sdArc(x, y, radius, halfWidth, start, span) {
  const safeSpan = clamp(span, 0, TAU);
  if (safeSpan >= TAU - 1e-5) {
    return Math.abs(Math.hypot(x, y) - radius) - halfWidth;
  }

  const relative = positiveAngle(Math.atan2(y, x) - start);
  if (relative <= safeSpan) {
    return Math.abs(Math.hypot(x, y) - radius) - halfWidth;
  }

  const end = start + safeSpan;
  const startDistance =
    Math.hypot(x - Math.cos(start) * radius, y - Math.sin(start) * radius) -
    halfWidth;
  const endDistance =
    Math.hypot(x - Math.cos(end) * radius, y - Math.sin(end) * radius) -
    halfWidth;
  return Math.min(startDistance, endDistance);
}

function sdPolygon(x, y, points) {
  let inside = false;
  let minimumSquared = Infinity;
  let previous = points.length - 1;

  for (let current = 0; current < points.length; current += 1) {
    const ax = points[previous][0];
    const ay = points[previous][1];
    const bx = points[current][0];
    const by = points[current][1];
    const vx = bx - ax;
    const vy = by - ay;
    const denominator = vx * vx + vy * vy || 1;
    const amount = saturate(((x - ax) * vx + (y - ay) * vy) / denominator);
    const dx = x - (ax + vx * amount);
    const dy = y - (ay + vy * amount);
    minimumSquared = Math.min(minimumSquared, dx * dx + dy * dy);

    if (
      (ay > y) !== (by > y) &&
      x < ((bx - ax) * (y - ay)) / (by - ay || Number.EPSILON) + ax
    ) {
      inside = !inside;
    }
    previous = current;
  }

  return Math.sqrt(minimumSquared) * (inside ? -1 : 1);
}

function hash(index, seed = 0) {
  return fract(Math.sin(index * 127.1 + seed * 311.7) * 43758.5453123);
}

function stableUnit(index, seed = 0) {
  let value = (Math.trunc(seed) ^ Math.imul(index + 1, 0x9e3779b1)) >>> 0;
  value = Math.imul(value ^ (value >>> 16), 0x21f0aaad);
  value = Math.imul(value ^ (value >>> 15), 0x735a2d97);
  return ((value ^ (value >>> 15)) >>> 0) / 4294967296;
}

/**
 * A seed-stable set of four irregular arc neighborhoods. The gaps are as
 * important as the marks: proximity and good continuation let the eye close
 * the circle without turning the boundary into a spinner or a literal rim.
 */
export function gestaltArcSupport(
  angle,
  context = EMPTY_CONTEXT,
  openness = 0.5,
) {
  const seed = finite(context?.seed, 0);
  const open = clamp(finite(openness, 0.5), 0, 1);
  const phase = stableUnit(701, seed) * TAU;
  const halfWidthScale = 1.14 - open * 0.34;
  let support = 0;

  for (let index = 0; index < 4; index += 1) {
    const center =
      phase +
      (index * TAU) / 4 +
      (stableUnit(733 + index * 29, seed) - 0.5) * 0.54;
    const halfWidth =
      (0.32 + stableUnit(811 + index * 37, seed) * 0.17) * halfWidthScale;
    const delta = Math.abs(positiveAngle(angle - center + Math.PI) - Math.PI);
    const arc = 1 - smoothstep(halfWidth * 0.7, halfWidth, delta);
    support = Math.max(support, arc);
  }

  const stableGrain = 0.92 + saturate(finite(context?.random, 0.5)) * 0.08;
  return saturate(support * stableGrain);
}

const TRIANGLE = Object.freeze([
  Object.freeze([0, -0.72]),
  Object.freeze([0.69, 0.56]),
  Object.freeze([-0.69, 0.56]),
]);

const DIAMOND = Object.freeze([
  Object.freeze([0, -0.64]),
  Object.freeze([0.62, 0]),
  Object.freeze([0, 0.64]),
  Object.freeze([-0.62, 0]),
]);

const HOURGLASS_TOP_SAND = Object.freeze([
  Object.freeze([-0.25, -0.34]),
  Object.freeze([0.25, -0.34]),
  Object.freeze([0, -0.055]),
]);

const HOURGLASS_BOTTOM_SAND = Object.freeze([
  Object.freeze([0, 0.055]),
  Object.freeze([0.25, 0.34]),
  Object.freeze([-0.25, 0.34]),
]);

const STILL_WORKING_CYCLE_SECONDS = 3.2;

const SPEAKER = Object.freeze([
  Object.freeze([-0.57, -0.2]),
  Object.freeze([-0.32, -0.2]),
  Object.freeze([0.02, -0.49]),
  Object.freeze([0.02, 0.49]),
  Object.freeze([-0.32, 0.2]),
  Object.freeze([-0.57, 0.2]),
]);

const STAR = Object.freeze(
  Array.from({ length: 10 }, (_, index) => {
    const angle = -HALF_PI + (index * Math.PI) / 5;
    const radius = index % 2 === 0 ? 0.67 : 0.29;
    return Object.freeze([Math.cos(angle) * radius, Math.sin(angle) * radius]);
  }),
);

const TRANSFER_ROTATION = Object.freeze({
  down: 0,
  up: Math.PI,
  right: -HALF_PI,
  left: HALF_PI,
});

function sampleIdle(x, y, t = 0, context = EMPTY_CONTEXT) {
  const phase = context?.reducedMotion
    ? saturate(finite(context.reducedPhase, 0.18))
    : Number.isFinite(Number(context?.timelinePhase))
      ? fract(Number(context.timelinePhase))
      : fract(finite(t) / 14.4);
  const breath = Math.sin(phase * TAU * 2 - HALF_PI);
  const radius = 0.67 + breath * 0.022;
  const radial = Math.hypot(x, y);
  const normalizedX = x / radius;
  const normalizedY = y / radius;
  const normalZ = Math.sqrt(
    Math.max(0, 1 - normalizedX * normalizedX - normalizedY * normalizedY),
  );

  const orbit = phase * TAU - 0.72;
  let lightX = Math.cos(orbit);
  let lightY = -0.28;
  let lightZ = Math.sin(orbit) * 0.9;
  const lightLength = Math.hypot(lightX, lightY, lightZ) || 1;
  lightX /= lightLength;
  lightY /= lightLength;
  lightZ /= lightLength;
  const illumination =
    normalizedX * lightX + normalizedY * lightY + normalZ * lightZ;
  // A moon terminator is a lighting gradient, not a silhouette edge. Blend a
  // broad penumbra with diffuse light so the phase remains crisp at the rim
  // while its interior shades through several pixel-density bands.
  const rasterFeather = featherFor(context);
  const directLight = Math.pow(Math.max(0, illumination), 1.25);
  const penumbra = smoothstep(
    -0.24 - rasterFeather,
    0.26 + rasterFeather,
    illumination,
  );
  const lit = saturate(directLight * 0.82 + penumbra * 0.28);
  const sphereShade = 0.68 + normalZ * 0.32;
  const disk = coverage(sdCircle(x, y, radius), context);
  const surface = disk * lit * sphereShade * (1 + breath * 0.035);

  if (context?.orbBoundary === "gestalt") {
    const resolution = clamp(
      finite(context?.resolution ?? context?.size, 68),
      8,
      1024,
    );
    const cell = 2 / resolution;
    const boundaryBand = clamp(cell * 3.1, 0.06, 0.2);
    const arcSupport = gestaltArcSupport(
      Math.atan2(y, x),
      context,
      context?.gestaltOpenness,
    );
    const edgeBand =
      smoothstep(
        radius - boundaryBand * 1.16,
        radius - boundaryBand * 0.42,
        radial,
      ) * disk;
    const surfaceTone = saturate(
      lit * sphereShade * (1 + breath * 0.035),
    );
    const cueNeed = 1 - smoothstep(0.42, 0.74, surfaceTone);
    const impliedLimb =
      edgeBand *
      arcSupport *
      (0.34 + lit * 0.38 + sphereShade * 0.08) *
      cueNeed;
    return saturate(Math.max(surface, impliedLimb));
  }

  const rimWidth = 0.038 + (breath + 1) * 0.002;
  const rim = coverage(Math.abs(radial - radius) - rimWidth, context);
  return saturate(Math.max(rim * 0.86, surface));
}

function sampleListening(x, y, t = 0, context = EMPTY_CONTEXT) {
  const time = motionTime(t, context);
  const level = saturate(
    finite(context.audioLevel ?? context.level ?? context.energy, 0.28),
  );
  const microphone = sdRoundBox(x, y + 0.09, 0.155, 0.34, 0.15);
  const cradle = sdArc(x, y + 0.04, 0.39, 0.068, 0.05, Math.PI - 0.1);
  const stem = sdSegment(x, y, 0, 0.43, 0, 0.64, 0.062);
  const foot = sdSegment(x, y, -0.2, 0.64, 0.2, 0.64, 0.062);
  let result = coverage(
    Math.min(microphone, cradle, stem, foot),
    context,
  );

  const pulse = fract(time * 0.34);
  const rippleRadius = 0.47 + pulse * 0.25;
  const ripple = coverage(
    sdArc(x, y + 0.03, rippleRadius, 0.038 + level * 0.025, -0.7, 1.4),
    context,
  );
  const oppositeRipple = coverage(
    sdArc(
      x,
      y + 0.03,
      rippleRadius,
      0.038 + level * 0.025,
      Math.PI - 0.7,
      1.4,
    ),
    context,
  );
  result = Math.max(result, (ripple + oppositeRipple) * 0.5 * (0.35 + level));
  return saturate(result);
}

function sampleThinking(x, y, t = 0, context = EMPTY_CONTEXT) {
  const time = motionTime(t, context);
  const angle = time * 0.68 - HALF_PI;
  const orbitRadius = 0.51;
  let shape = sdArc(
    x,
    y,
    orbitRadius,
    0.052,
    angle + 0.36,
    TAU - 0.98,
  );
  for (let index = 0; index < 3; index += 1) {
    const nodeAngle = angle + (index * TAU) / 3;
    const node = sdCircle(
      x - Math.cos(nodeAngle) * orbitRadius,
      y - Math.sin(nodeAngle) * orbitRadius,
      index === 0 ? 0.115 : 0.09,
    );
    shape = Math.min(shape, node);
  }
  const core = sdCircle(x, y, 0.115);
  return coverage(Math.min(shape, core), context);
}

function sampleThinkingDeep(x, y, t = 0, context = EMPTY_CONTEXT) {
  const time = motionTime(t, context);
  const outerAngle = time * 0.24 - HALF_PI;
  const middleAngle = -time * 0.37 + 0.3;
  const innerAngle = time * 0.52 + 1.1;
  const outer = sdArc(x, y, 0.62, 0.064, outerAngle, TAU - 1.02);
  const middle = sdArc(x, y, 0.4, 0.061, middleAngle, TAU - 1.16);
  const inner = sdArc(x, y, 0.21, 0.058, innerAngle, TAU - 1.32);
  const outerBeadAngle = outerAngle + TAU - 1.02;
  const middleBeadAngle = middleAngle + TAU - 1.16;
  const outerBead = sdCircle(
    x - Math.cos(outerBeadAngle) * 0.62,
    y - Math.sin(outerBeadAngle) * 0.62,
    0.092,
  );
  const middleBead = sdCircle(
    x - Math.cos(middleBeadAngle) * 0.4,
    y - Math.sin(middleBeadAngle) * 0.4,
    0.078,
  );
  const core = sdCircle(x, y, 0.07 + Math.sin(time * 0.8) * 0.012);
  return coverage(
    Math.min(outer, middle, inner, outerBead, middleBead, core),
    context,
  );
}

function sampleStillWorking(x, y, t = 0, context = EMPTY_CONTEXT) {
  const reducedMotion = Boolean(context?.reducedMotion);
  const elapsed = Math.max(
    0,
    finite(context?.age ?? context?.elapsed, finite(t)),
  );
  const cyclePhase = fract(elapsed / STILL_WORKING_CYCLE_SECONDS);
  const phase = reducedMotion
    ? saturate(finite(context?.reducedPhase, 0.46))
    : cyclePhase >= 1 - 1e-9
      ? 0
      : cyclePhase;

  // Sand drains while the frame is upright, then the complete hourglass makes
  // a deliberate half-turn. The end pose is visually identical to the start,
  // so the short cycle closes without a snap.
  const drain = smoothstep(0.03, 0.48, phase);
  const turn = smoothstep(0.5, 0.76, phase);
  const rotation = reducedMotion ? 0 : turn * Math.PI;
  const cosine = Math.cos(rotation);
  const sine = Math.sin(rotation);
  const localX = x * cosine + y * sine;
  const localY = -x * sine + y * cosine;

  const topRail = sdSegment(
    localX,
    localY,
    -0.32,
    -0.43,
    0.32,
    -0.43,
    0.06,
  );
  const bottomRail = sdSegment(
    localX,
    localY,
    -0.32,
    0.43,
    0.32,
    0.43,
    0.06,
  );
  const upperLeft = sdSegment(
    localX,
    localY,
    -0.29,
    -0.37,
    -0.075,
    0,
    0.055,
  );
  const lowerLeft = sdSegment(
    localX,
    localY,
    -0.075,
    0,
    -0.29,
    0.37,
    0.055,
  );
  const upperRight = sdSegment(
    localX,
    localY,
    0.29,
    -0.37,
    0.075,
    0,
    0.055,
  );
  const lowerRight = sdSegment(
    localX,
    localY,
    0.075,
    0,
    0.29,
    0.37,
    0.055,
  );
  const frame = coverage(
    Math.min(
      topRail,
      bottomRail,
      upperLeft,
      lowerLeft,
      upperRight,
      lowerRight,
    ),
    context,
  );

  const topSurface = -0.34 + drain * 0.285;
  const bottomSurface = 0.34 - drain * 0.285;
  const topSand = coverage(
    Math.max(
      sdPolygon(localX, localY, HOURGLASS_TOP_SAND),
      topSurface - localY,
    ),
    context,
    0.82,
  );
  const bottomSand = coverage(
    Math.max(
      sdPolygon(localX, localY, HOURGLASS_BOTTOM_SAND),
      bottomSurface - localY,
    ),
    context,
    0.82,
  );
  const falling = Math.sin(drain * Math.PI);
  const stream =
    coverage(
      sdSegment(localX, localY, 0, -0.08, 0, 0.17, 0.026),
      context,
    ) * falling;
  const fallingGrain =
    coverage(
      sdCircle(localX, localY - (-0.03 + drain * 0.25), 0.036),
      context,
    ) * falling;

  // Two restrained flip guides rotate with the object. They clarify the turn
  // without competing with the hourglass at thumbnail scale.
  const guideStart = -0.44;
  const guideSpan = 0.78;
  const guideA = sdArc(localX, localY, 0.67, 0.036, guideStart, guideSpan);
  const guideB = sdArc(
    localX,
    localY,
    0.67,
    0.036,
    guideStart + Math.PI,
    guideSpan,
  );
  const guideEndA = guideStart + guideSpan;
  const guideEndB = guideEndA + Math.PI;
  const guideDots = Math.min(
    sdCircle(
      localX - Math.cos(guideEndA) * 0.67,
      localY - Math.sin(guideEndA) * 0.67,
      0.065,
    ),
    sdCircle(
      localX - Math.cos(guideEndB) * 0.67,
      localY - Math.sin(guideEndB) * 0.67,
      0.065,
    ),
  );
  const guides = coverage(Math.min(guideA, guideB, guideDots), context) * 0.68;

  return saturate(
    Math.max(frame, topSand, bottomSand, stream, fallingGrain, guides),
  );
}

function sampleLoading(x, y, t = 0, context = EMPTY_CONTEXT) {
  const time = motionTime(t, context);
  const segmentCount = clamp(
    Math.round(finite(context.segments, 8)),
    6,
    12,
  );
  const head = positiveAngle(time * finite(context.rotationSpeed, 1.35));
  let result = 0;

  for (let index = 0; index < segmentCount; index += 1) {
    const start = (index / segmentCount) * TAU - HALF_PI;
    const span = (TAU / segmentCount) * 0.56;
    const segment = coverage(sdArc(x, y, 0.53, 0.09, start, span), context);
    const behind = positiveAngle(head - (start + span * 0.5));
    const tail = Math.exp(-behind * 0.68);
    result = Math.max(result, segment * (0.56 + tail * 0.44));
  }
  return saturate(result);
}

function sampleProgress(x, y, t = 0, context = EMPTY_CONTEXT) {
  const progress = saturate(finite(context.progress ?? context.value, 0.62));
  const track = coverage(
    Math.abs(Math.hypot(x, y) - 0.54) - 0.075,
    context,
  );
  const amount = progress <= 0 ? 0 : Math.max(0.012, TAU * progress);
  const fill =
    progress <= 0
      ? 0
      : coverage(
          sdArc(x, y, 0.54, 0.105, -HALF_PI, amount),
          context,
        );
  const capAngle = -HALF_PI + amount;
  const cap =
    progress <= 0
      ? 0
      : coverage(
          sdCircle(
            x - Math.cos(capAngle) * 0.54,
            y - Math.sin(capAngle) * 0.54,
            0.105,
          ),
          context,
        );
  // The inactive track must remain legible after a 1-bit gate at 16–18 px.
  let result = Math.max(track * 0.4, fill, cap);
  if (progress >= 0.995) {
    result = Math.max(
      result,
      coverage(
        Math.min(
          sdSegment(x, y, -0.27, 0.01, -0.07, 0.22, 0.085),
          sdSegment(x, y, -0.07, 0.22, 0.33, -0.25, 0.085),
        ),
        context,
      ),
    );
  }
  return saturate(result);
}

function sampleGenerating(x, y, t = 0, context = EMPTY_CONTEXT) {
  const time = motionTime(t, context);
  const progress = saturate(
    finite(context.progress ?? context.value, 0.48),
  );
  let result = coverage(
    Math.max(sdPolygon(x, y, DIAMOND), -sdPolygon(x, y, DIAMOND) - 0.105),
    context,
  );

  const cross = Math.min(
    sdSegment(x, y, -0.25, 0, 0.25, 0, 0.058),
    sdSegment(x, y, 0, -0.25, 0, 0.25, 0.058),
  );
  result = Math.max(result, coverage(cross, context));

  const targetGlyph = context.targetGlyph;
  if (
    typeof targetGlyph === "string" &&
    resolveGlyphId(targetGlyph) !== "generating"
  ) {
    const targetCoverage =
      typeof context.glyphSampler === "function"
        ? context.glyphSampler(targetGlyph, x, y, t, context)
        : getGlyph(targetGlyph)(x, y, t, context);
    result = Math.max(result * (1 - progress * 0.7), targetCoverage * progress);
  }

  const seed = finite(context.seed, 0);
  for (let index = 0; index < 5; index += 1) {
    const phase = fract(time * (0.11 + index * 0.009) + hash(index, seed));
    const angle = hash(index + 19, seed) * TAU + time * 0.19;
    const radius = 0.78 - phase * 0.57;
    const px = Math.cos(angle) * radius;
    const py = Math.sin(angle) * radius;
    const particle = coverage(
      sdCircle(x - px, y - py, 0.045 + phase * 0.025),
      context,
    );
    result = Math.max(result, particle * (0.35 + progress * 0.65));
  }
  return saturate(result);
}

function sampleSearching(x, y, t = 0, context = EMPTY_CONTEXT) {
  const time = motionTime(t, context);
  const lensX = x + 0.1;
  const lensY = y + 0.11;
  const lens = Math.abs(Math.hypot(lensX, lensY) - 0.4) - 0.085;
  const handle = sdSegment(x, y, 0.21, 0.2, 0.59, 0.58, 0.095);
  const sweepAngle = time * 1.12 - HALF_PI;
  const sweep = sdSegment(
    lensX,
    lensY,
    0,
    0,
    Math.cos(sweepAngle) * 0.34,
    Math.sin(sweepAngle) * 0.34,
    0.047,
  );
  let result = coverage(Math.min(lens, handle), context);
  result = Math.max(result, coverage(sweep, context) * 0.72);

  const hitCount = clamp(Math.round(finite(context.hits, 2)), 0, 8);
  for (let index = 0; index < hitCount; index += 1) {
    const angle = hash(index + 7, finite(context.seed, 0)) * TAU;
    const radius = 0.12 + hash(index + 13, finite(context.seed, 0)) * 0.18;
    const hit = coverage(
      sdCircle(
        lensX - Math.cos(angle) * radius,
        lensY - Math.sin(angle) * radius,
        0.045,
      ),
      context,
    );
    result = Math.max(result, hit);
  }
  return saturate(result);
}

function sampleToolUse(x, y, t = 0, context = EMPTY_CONTEXT) {
  const time = motionTime(t, context);
  const steps = Math.max(1, Math.round(finite(context.steps, 12)));
  const angle = context.reducedMotion
    ? finite(context.reducedPhase, 0.125) * TAU
    : Math.floor(time * 1.8 * steps) / steps;
  const cosine = Math.cos(angle);
  const sine = Math.sin(angle);
  const rx = x * cosine + y * sine;
  const ry = -x * sine + y * cosine;
  const radius = Math.hypot(rx, ry);
  const sector = Math.cos(Math.atan2(ry, rx) * 8);
  const tooth = smoothstep(0.18, 0.55, sector) * 0.105;
  const outer = radius - (0.5 + tooth);
  const inner = radius - 0.275;
  const gear = Math.max(outer, -inner);
  const hub = Math.abs(radius - 0.125) - 0.043;
  const spokes = Math.min(
    sdRoundBox(rx, ry, 0.38, 0.044, 0.025),
    sdRoundBox(rx, ry, 0.044, 0.38, 0.025),
  );
  const clippedSpokes = Math.max(spokes, 0.165 - radius, radius - 0.41);
  return coverage(Math.min(gear, hub, clippedSpokes), context);
}

function sampleSpeaking(x, y, t = 0, context = EMPTY_CONTEXT) {
  const time = motionTime(t, context);
  const fallbackEnvelope =
    0.34 +
    Math.max(0, Math.sin(time * 5.1)) * 0.32 +
    Math.max(0, Math.sin(time * 7.7 + 1.3)) * 0.2;
  const level = saturate(
    finite(context.audioLevel ?? context.level, fallbackEnvelope),
  );
  let result = coverage(sdPolygon(x, y, SPEAKER), context);
  const first = coverage(
    sdArc(x - 0.01, y, 0.36, 0.065, -0.72, 1.44),
    context,
  );
  const second = coverage(
    sdArc(x - 0.01, y, 0.59, 0.065, -0.68, 1.36),
    context,
  );
  result = Math.max(result, first * (0.42 + level * 0.58));
  result = Math.max(result, second * smoothstep(0.18, 0.78, level));
  return saturate(result);
}

function sampleAwaitingInput(x, y, t = 0, context = EMPTY_CONTEXT) {
  const phase = context?.reducedMotion
    ? saturate(finite(context.reducedPhase, 0.44))
    : Number.isFinite(Number(context?.timelinePhase))
      ? fract(Number(context.timelinePhase))
      : fract(finite(t) / 10.8);
  const elapsed = phase * 10.8;
  const firstBeat =
    1 - smoothstep(0.18, 0.5, Math.abs(elapsed - 0.64));
  const secondBeat =
    (1 - smoothstep(0.16, 0.46, Math.abs(elapsed - 1.42))) * 0.76;
  const acknowledgement = saturate(
    finite(context?.acknowledgement ?? context?.acknowledged, 0),
  );
  const invitation = Math.max(firstBeat, secondBeat) * (1 - acknowledgement);
  const resolution = clamp(
    finite(context?.resolution ?? context?.size, 68),
    8,
    1024,
  );
  const smallGrid = saturate((28 - resolution) / 12);
  const radius = Math.hypot(x, y);
  const angle = Math.atan2(y, x);
  const orbit = phase * TAU;
  const ringRadius = 0.65 + invitation * 0.014 - acknowledgement * 0.008;
  const ring = coverage(
    Math.abs(radius - ringRadius) -
      (0.027 + smallGrid * 0.01 + invitation * 0.005),
    context,
  );
  // A low-cost harmonic mask makes seven irregular points wander and twinkle
  // around the focus ring without turning the state into a loading spinner.
  const fireflyRadius =
    0.755 + x * y * 0.035;
  const fireflyBand =
    1 - smoothstep(0.008, 0.034, Math.abs(radius - fireflyRadius));
  let fireflyLobes = Math.max(
    0,
    Math.cos(
      angle * 7 + Math.sin(orbit) * 0.42 + x * 1.1 - y * 0.65,
    ),
  );
  fireflyLobes *= fireflyLobes;
  fireflyLobes *= fireflyLobes;
  fireflyLobes *= fireflyLobes;
  fireflyLobes *= fireflyLobes;
  const fireflyTwinkle =
    0.82 + (x - y) * 0.06;
  const fireflies = saturate(
    fireflyBand *
      fireflyLobes *
      (fireflyTwinkle + invitation * 0.24 + acknowledgement * 0.12),
  );
  const questionStroke = 0.055 + smallGrid * 0.008;
  const question = Math.min(
    sdArc(x + 0.04, y + 0.18, 0.24, questionStroke, -2.72, 3.58),
    sdSegment(x, y, 0.117, 0.002, 0.005, 0.145, questionStroke - 0.001),
    sdSegment(x, y, 0.005, 0.145, 0.005, 0.235, questionStroke - 0.001),
    sdCircle(
      x - 0.005,
      y - 0.405,
      0.066 + smallGrid * 0.014 + invitation * 0.01,
    ),
  );
  const prompt = coverage(question, context);
  return saturate(
    Math.max(
      prompt,
      ring *
        (0.52 +
          smallGrid * 0.26 +
          invitation * 0.28 +
          acknowledgement * 0.14),
      fireflies,
    ),
  );
}

function sampleSuccess(x, y, t = 0, context = EMPTY_CONTEXT) {
  const age = Math.max(0, finite(context.age ?? context.elapsed, t));
  const tail = sdSegment(x, y, -0.48, 0.02, -0.13, 0.34, 0.105);
  const head = sdSegment(x, y, -0.13, 0.34, 0.49, -0.38, 0.105);
  let result = coverage(Math.min(tail, head), context);
  if (!context.reducedMotion && age < 0.72) {
    const haloRadius = 0.5 + smoothstep(0, 0.72, age) * 0.28;
    const halo = coverage(
      Math.abs(Math.hypot(x, y) - haloRadius) - 0.04,
      context,
    );
    result = Math.max(result, halo * (1 - smoothstep(0.2, 0.72, age)));
  }
  return saturate(result);
}

function sampleWarning(x, y, t = 0, context = EMPTY_CONTEXT) {
  const triangleDistance = sdPolygon(x, y, TRIANGLE);
  const outline = Math.abs(triangleDistance) - 0.075;
  const stem = sdRoundBox(x, y + 0.08, 0.077, 0.245, 0.055);
  const dot = sdCircle(x, y - 0.36, 0.087);
  return coverage(Math.min(outline, stem, dot), context);
}

function sampleError(x, y, t = 0, context = EMPTY_CONTEXT) {
  const age = Math.max(0, finite(context.age ?? context.elapsed, t));
  const shake =
    context.reducedMotion || age > 0.44
      ? 0
      : Math.sin(age * 54) * 0.065 * (1 - age / 0.44);
  const first = sdSegment(
    x - shake,
    y,
    -0.43,
    -0.43,
    0.43,
    0.43,
    0.105,
  );
  const second = sdSegment(
    x - shake,
    y,
    0.43,
    -0.43,
    -0.43,
    0.43,
    0.105,
  );
  return coverage(Math.min(first, second), context);
}

function samplePaused(x, y, t = 0, context = EMPTY_CONTEXT) {
  const left = sdRoundBox(x + 0.22, y, 0.105, 0.5, 0.055);
  const right = sdRoundBox(x - 0.22, y, 0.105, 0.5, 0.055);
  return coverage(Math.min(left, right), context);
}

function sampleCancelled(x, y, t = 0, context = EMPTY_CONTEXT) {
  const time = motionTime(t, context);
  const settle = context.reducedMotion
    ? 1
    : smoothstep(0, 0.36, Math.max(0, finite(context.age, time)));
  const extent = 0.39 + (1 - settle) * 0.12;
  const stop = sdRoundBox(x, y, extent, extent, 0.075);
  const inset = sdRoundBox(x, y, extent - 0.13, extent - 0.13, 0.04);
  const ring = Math.max(stop, -inset);
  return coverage(Math.min(ring, sdCircle(x, y, 0.095)), context);
}

function sampleOffline(x, y, t = 0, context = EMPTY_CONTEXT) {
  const time = motionTime(t, context);
  const upper = sdArc(x, y, 0.54, 0.075, -2.92, 2.28);
  const lower = sdArc(x, y, 0.54, 0.075, 0.22, 2.28);
  const slash = sdSegment(x, y, -0.49, -0.49, 0.49, 0.49, 0.09);
  let result = coverage(Math.min(upper, lower, slash), context);
  const retry = saturate(
    finite(context.retry ?? context.signalStrength, 0.25),
  );
  const packetPhase = fract(time * (0.19 + retry * 0.16));
  const packetAngle = -2.92 + packetPhase * 2.28;
  const packet = coverage(
    sdCircle(
      x - Math.cos(packetAngle) * 0.54,
      y - Math.sin(packetAngle) * 0.54,
      0.07,
    ),
    context,
  );
  result = Math.max(result, packet * (0.28 + retry * 0.72));
  return saturate(result);
}

function sampleTransfer(x, y, t = 0, context = EMPTY_CONTEXT) {
  const direction = String(context.direction ?? "down").toLowerCase();
  const progress = saturate(
    finite(context.progress ?? context.value, 0.54),
  );
  if (direction === "sync") {
    const time = motionTime(t, context);
    const angle = time * 0.85;
    const first = sdArc(x, y, 0.5, 0.08, angle, 2.35);
    const second = sdArc(x, y, 0.5, 0.08, angle + Math.PI, 2.35);
    const firstTipX = Math.cos(angle + 2.35) * 0.5;
    const firstTipY = Math.sin(angle + 2.35) * 0.5;
    const secondTipX = Math.cos(angle + Math.PI + 2.35) * 0.5;
    const secondTipY = Math.sin(angle + Math.PI + 2.35) * 0.5;
    const arrowA = sdSegment(
      x,
      y,
      firstTipX,
      firstTipY,
      firstTipX - 0.17,
      firstTipY - 0.03,
      0.07,
    );
    const arrowB = sdSegment(
      x,
      y,
      secondTipX,
      secondTipY,
      secondTipX + 0.17,
      secondTipY + 0.03,
      0.07,
    );
    return coverage(Math.min(first, second, arrowA, arrowB), context);
  }

  const rotation = TRANSFER_ROTATION[direction] ?? TRANSFER_ROTATION.down;
  const cosine = Math.cos(rotation);
  const sine = Math.sin(rotation);
  const rx = x * cosine - y * sine;
  const ry = x * sine + y * cosine;
  const shaft = sdRoundBox(rx, ry + 0.13, 0.1, 0.43, 0.05);
  const leftHead = sdSegment(rx, ry, 0, 0.48, -0.32, 0.14, 0.09);
  const rightHead = sdSegment(rx, ry, 0, 0.48, 0.32, 0.14, 0.09);
  const fillLine = -0.58 + progress * 1.16;
  const fillMask = ry - fillLine;
  const arrow = Math.min(shaft, leftHead, rightHead);
  const trackCoverage = coverage(arrow, context) * 0.58;
  const fillCoverage = coverage(Math.max(arrow, fillMask), context);
  return saturate(Math.max(trackCoverage, fillCoverage));
}

function sampleHandoff(x, y, t = 0, context = EMPTY_CONTEXT) {
  const time = motionTime(t, context);
  const accepted = Boolean(context.accepted ?? context.handoffAccepted);
  const leftNode = Math.abs(Math.hypot(x + 0.52, y) - 0.16) - 0.057;
  const rightNode = Math.abs(Math.hypot(x - 0.52, y) - 0.16) - 0.057;
  const path = sdSegment(x, y, -0.3, 0, 0.29, 0, 0.057);
  const arrowA = sdSegment(x, y, 0.29, 0, 0.1, -0.15, 0.06);
  const arrowB = sdSegment(x, y, 0.29, 0, 0.1, 0.15, 0.06);
  let result = coverage(
    Math.min(leftNode, rightNode, path, arrowA, arrowB),
    context,
  );

  const phase = accepted ? 1 : fract(time * 0.34);
  const packetX = -0.29 + phase * 0.58;
  const packetY = 0;
  const packet = coverage(
    sdCircle(x - packetX, y - packetY, accepted ? 0.09 : 0.06),
    context,
  );
  result = Math.max(result, packet);
  return saturate(result);
}

function sampleCelebration(x, y, t = 0, context = EMPTY_CONTEXT) {
  const age = Math.max(0, finite(context.age ?? context.elapsed, t));
  let result = coverage(sdPolygon(x, y, STAR), context);
  if (context.reducedMotion) return result;

  const burst = 1 - smoothstep(0.42, 0.84, age);
  const seed = finite(context.seed, 0);
  for (let index = 0; index < 7; index += 1) {
    const angle = hash(index + 31, seed) * TAU;
    const speed = 0.47 + hash(index + 47, seed) * 0.36;
    const radius = Math.min(age, 0.84) * speed + 0.45;
    const particleX = Math.cos(angle) * radius;
    const particleY = Math.sin(angle) * radius;
    const particle =
      index % 2 === 0
        ? sdCircle(x - particleX, y - particleY, 0.04)
        : sdRoundBox(x - particleX, y - particleY, 0.038, 0.075, 0.015);
    result = Math.max(result, coverage(particle, context) * burst);
  }
  return saturate(result);
}

function sampleCustom(x, y, t = 0, context = EMPTY_CONTEXT) {
  const customSampler =
    context?.customSampler ?? context?.glyphSampler ?? context?.sampler;
  if (typeof customSampler === "function" && customSampler !== sampleCustom) {
    const sampled = Number(customSampler(x, y, t, context));
    if (Number.isFinite(sampled)) return saturate(sampled);
  }

  const diamond = Math.abs(sdPolygon(x, y, DIAMOND)) - 0.085;
  const core = sdCircle(x, y, 0.12);
  return coverage(Math.min(diamond, core), context);
}

/**
 * Canonical, un-namespaced glyph IDs. Sprite IDs are accepted by sampleGlyph
 * through GLYPH_ALIASES so recipes can stay semantically namespaced.
 */
export const GLYPHS = Object.freeze({
  idle: sampleIdle,
  listening: sampleListening,
  thinking: sampleThinking,
  "thinking-deep": sampleThinkingDeep,
  "still-working": sampleStillWorking,
  loading: sampleLoading,
  progress: sampleProgress,
  generating: sampleGenerating,
  searching: sampleSearching,
  "tool-use": sampleToolUse,
  speaking: sampleSpeaking,
  "awaiting-input": sampleAwaitingInput,
  success: sampleSuccess,
  warning: sampleWarning,
  error: sampleError,
  paused: samplePaused,
  cancelled: sampleCancelled,
  offline: sampleOffline,
  transfer: sampleTransfer,
  handoff: sampleHandoff,
  celebration: sampleCelebration,
  custom: sampleCustom,
});

export const GLYPH_IDS = Object.freeze(Object.keys(GLYPHS));

export const GLYPH_ALIASES = Object.freeze({
  ready: "idle",
  listen: "listening",
  reasoning: "thinking",
  "deep-thinking": "thinking-deep",
  working: "still-working",
  spinner: "loading",
  determinate: "progress",
  generate: "generating",
  search: "searching",
  tool: "tool-use",
  voice: "speaking",
  prompt: "awaiting-input",
  check: "success",
  alert: "warning",
  failure: "error",
  pause: "paused",
  cancel: "cancelled",
  disconnected: "offline",
  sync: "transfer",
  star: "celebration",
  "ai.idle": "idle",
  "ai.listening": "listening",
  "ai.thinking": "thinking",
  "ai.thinking-deep": "thinking-deep",
  "ai.still-working": "still-working",
  "ai.loading": "loading",
  "ai.progress": "progress",
  "ai.generating": "generating",
  "ai.searching": "searching",
  "ai.tool-use": "tool-use",
  "ai.speaking": "speaking",
  "ai.awaiting-input": "awaiting-input",
  "status.success": "success",
  "status.warning": "warning",
  "status.error": "error",
  "status.paused": "paused",
  "status.cancelled": "cancelled",
  "status.offline": "offline",
  "transfer.active": "transfer",
  "workflow.handoff": "handoff",
  "status.celebration": "celebration",
});

export function resolveGlyphId(id) {
  const normalized = String(id ?? "custom").trim().toLowerCase();
  if (Object.prototype.hasOwnProperty.call(GLYPHS, normalized)) {
    return normalized;
  }
  return GLYPH_ALIASES[normalized] ?? "custom";
}

export function getGlyph(id) {
  return GLYPHS[resolveGlyphId(id)];
}

export function listGlyphs() {
  return GLYPH_IDS;
}

export function sampleGlyph(
  id,
  x,
  y,
  t = 0,
  context = EMPTY_CONTEXT,
) {
  const safeContext =
    context && typeof context === "object" ? context : EMPTY_CONTEXT;
  const sampler = getGlyph(id);
  const sampled = sampler(
    finite(x),
    finite(y),
    finite(t),
    safeContext,
  );
  return saturate(Number.isFinite(sampled) ? sampled : 0);
}

export default GLYPHS;
