src/appearance.ts

import { z } from "zod";
import { applyPalette, clearPalette, customThemeIdSchema, customThemesSchema, type CustomTheme, type ThemePreview } from "./themes";

export const modes = [
  { id: "system", label: "System" },
  { id: "light", label: "Light" },
  { id: "dark", label: "Dark" },
] as const;

// Each theme has a dark and a light palette. Themes tint the window frame and
// sidebar; the working panes stay close to neutral in both.
export const themes = [
  { id: "black", label: "Black", lightLabel: "White", dark: "#000", light: "#f2f2f4" },
  { id: "graphite", label: "Graphite", lightLabel: "Graphite", dark: "#1c1c1e", light: "#e4e4e8" },
  { id: "midnight", label: "Midnight", lightLabel: "Midnight", dark: "#0b1020", light: "#e1e7f3" },
  { id: "forest", label: "Forest", lightLabel: "Forest", dark: "#0b1410", light: "#dfeae3" },
] as const;

// Light mode uses deeper accents so links and selection keep their contrast on white.
export const accents = [
  { id: "blue", label: "Blue", dark: "#6b9cff", light: "#2563eb" },
  { id: "purple", label: "Purple", dark: "#a78bfa", light: "#7c3aed" },
  { id: "pink", label: "Pink", dark: "#f472b6", light: "#db2777" },
  { id: "orange", label: "Orange", dark: "#fb923c", light: "#ea580c" },
  { id: "green", label: "Green", dark: "#4ade80", light: "#16a34a" },
  { id: "graphite", label: "Graphite", dark: "#a0a0a6", light: "#636366" },
] as const;

// Subtle tints the window from the wallpaper (Windows Mica) and stays smooth;
// Glass blurs whatever is behind the window (Windows Acrylic) and costs more
// GPU while the window moves. macOS has one vibrancy material for both.
export const transparencies = [
  { id: "off", label: "Off" },
  { id: "subtle", label: "Subtle" },
  { id: "glass", label: "Glass" },
] as const;

// Side by side keeps the list beside the open email; Full width opens it in place of the list.
export const readingLayouts = [
  { id: "split", label: "Side by side" },
  { id: "full", label: "Full width" },
] as const;

export type Mode = (typeof modes)[number]["id"];
export type Scheme = "light" | "dark";
export type ThemeId = (typeof themes)[number]["id"] | CustomTheme["id"];
export type AccentId = (typeof accents)[number]["id"];
export type Transparency = (typeof transparencies)[number]["id"];
export type ReadingLayout = (typeof readingLayouts)[number]["id"];
export type WindowMaterial = "none" | "mica" | "acrylic";
export type Appearance = {
  mode: Mode;
  theme: ThemeId;
  accent: AccentId;
  transparency: Transparency;
  reading: ReadingLayout;
  customThemes: CustomTheme[];
};

export const defaultAppearance: Appearance = { mode: "dark", theme: "black", accent: "blue", transparency: "glass", reading: "split", customThemes: [] };
const storageKey = "emc2.appearance.v1";

/** Whether the desktop window has an OS material behind it to show through. */
export function supportsGlass(): boolean {
  return !!window.desktop?.materials;
}

/** Transparency choices for this platform; macOS has no separate Subtle material. */
export function transparencyChoices(platform = window.desktop?.platform) {
  return transparencies.filter((choice) => platform !== "darwin" || choice.id !== "subtle");
}

/** The Windows material behind the page for a transparency choice. */
export function windowMaterial(transparency: Transparency): WindowMaterial {
  return transparency === "glass" ? "acrylic" : transparency === "subtle" ? "mica" : "none";
}

/** A token color as #rrggbb, for the system caption buttons; null for anything else. */
export function hexColor(value: string): string | null {
  const color = value.trim().toLowerCase();
  if (/^#[\da-f]{6}$/.test(color)) return color;
  return /^#[\da-f]{3}$/.test(color) ? `#${[...color.slice(1)].map((digit) => digit + digit).join("")}` : null;
}

/** The scheme to draw: System follows the operating system's preference. */
export function resolveScheme(mode: Mode, prefersLight: boolean): Scheme {
  return mode === "system" ? (prefersLight ? "light" : "dark") : mode;
}

const oneOf = <T extends { id: string }>(list: readonly T[], value: unknown): value is T["id"] =>
  list.some((item) => item.id === value);

export function readAppearance(): Appearance {
  try {
    const stored: unknown = JSON.parse(localStorage.getItem(storageKey) || "null");
    const parsed = z.record(z.string(), z.unknown()).safeParse(stored);
    if (!parsed.success) return { ...defaultAppearance };
    const input = parsed.data;
    const customThemes = customThemesSchema.safeParse(input.customThemes ?? []);
    const library = customThemes.success ? customThemes.data : [];
    const customId = customThemeIdSchema.safeParse(input.theme);
    // Earlier builds stored a boolean `glass` switch.
    const legacy = typeof input.glass === "boolean" ? (input.glass ? "glass" : "off") : undefined;
    let transparency = oneOf(transparencies, input.transparency) ? input.transparency : legacy ?? defaultAppearance.transparency;
    if (transparency === "subtle" && window.desktop?.platform === "darwin") transparency = "glass";
    return {
      mode: oneOf(modes, input.mode) ? input.mode : defaultAppearance.mode,
      theme: oneOf(themes, input.theme) ? input.theme
        : customId.success && library.some((theme) => theme.id === customId.data) ? customId.data : defaultAppearance.theme,
      accent: oneOf(accents, input.accent) ? input.accent : defaultAppearance.accent,
      transparency,
      reading: oneOf(readingLayouts, input.reading) ? input.reading : defaultAppearance.reading,
      customThemes: library,
    };
  } catch {
    return { ...defaultAppearance };
  }
}

export function saveAppearance(value: Appearance): boolean {
  try {
    const { mode, theme, accent, transparency, reading } = value;
    const library = customThemesSchema.parse(value.customThemes);
    if (!oneOf(themes, theme) && !library.some((custom) => custom.id === theme)) return false;
    localStorage.setItem(storageKey, JSON.stringify({ mode, theme, accent, transparency, reading, customThemes: library }));
    return true;
  } catch {
    return false;
  }
}

/** Applies a preset to the document root and the window; called on start and on every change. */
export function applyAppearance(value: Appearance, scheme: Scheme = resolveScheme(value.mode, false), root = document.documentElement, preview: ThemePreview | null = null) {
  const activeScheme = preview?.scheme ?? scheme;
  root.dataset.scheme = activeScheme;
  root.dataset.theme = value.theme;
  clearPalette(root);
  root.style.setProperty("--accent", accents.find((accent) => accent.id === value.accent)![activeScheme]);
  const palette = preview?.palette ?? value.customThemes.find((theme) => theme.id === value.theme)?.palettes[activeScheme];
  if (palette) applyPalette(palette, root);
  const glass = supportsGlass() && value.transparency !== "off";
  root.toggleAttribute("data-glass", glass);
  // The native theme decides the material's tint, native menus and what System reports.
  window.desktop?.setScheme?.(preview?.scheme ?? value.mode);
  window.desktop?.setMaterial?.(glass ? windowMaterial(value.transparency) : "none");
}