/**
 * Printing methods and palettes core utilities.
 *
 * - Defines typed structures for printing methods and their colors
 * - Tracks the globally active printing method
 * - Tracks a global color harmonization scope (object / canvas / document)
 *   through the Zustand global store (AsukaStore)
 * - Provides color similarity helpers for plain colors (hex/rgb)
 *   and URL-based patterns using approximate average colors.
 */

import { useAsukaStore } from '../store/asukaStore';

export type ColorScope = 'object' | 'canvas' | 'document';

export interface PrintingColor {
  id: string;
  label: string;
  /**
   * Primary value:
   *  - hex / rgb (for example "#ff0000", "rgb(255,0,0)")
   *  - or an image URL for pattern-based colors
   */
  value: string;
}

export interface PrintingMethod {
  id: string; // ex: 'dtg', 'screen', 'flex', ...
  label: string;
  description?: string;
  colors: PrintingColor[];
}

// --- No implicit fallback printing methods ------------------------------
// The canonical source of truth is the Zustand global store.
// If nothing is configured, the designer must behave as fully unconfigured:
// no default printing methods, no default palette, and therefore the Colors
// module can fall back to a plain color picker.

// --- Global state: active printing method and scope (via Zustand) --------

type PrintingListener = () => void;

let activePrintingMethodId: string | null = null;
const printingListeners = new Set<PrintingListener>();

let scopeSubscriptionInitialized = false;

function ensureScopeSubscription(): void {
  if (scopeSubscriptionInitialized) return;
  scopeSubscriptionInitialized = true;

  // This store isn't using `subscribeWithSelector`, so do a minimal compare.
  let prevScope = (useAsukaStore.getState() as any).colorScope;
  useAsukaStore.subscribe((s) => {
    const nextScope = (s as any).colorScope;
    if (nextScope === prevScope) return;
    prevScope = nextScope;
    printingListeners.forEach((listener) => {
      try {
        listener();
      } catch {
        // ignore UI errors
      }
    });
  });
}

export function getPrintingMethods(): PrintingMethod[] {
  const fromStore = (useAsukaStore.getState() as any).printingMethods;
  if (Array.isArray(fromStore) && fromStore.length) return fromStore as PrintingMethod[];
  return [];
}

export function getPrintingMethodById(id: string | null | undefined): PrintingMethod | null {
  if (!id) return null;
  return getPrintingMethods().find((m) => m.id === id) ?? null;
}

export function getActivePrintingMethodId(): string | null {
  return activePrintingMethodId;
}

export function getActivePrintingMethod(): PrintingMethod | null {
  return getPrintingMethodById(activePrintingMethodId);
}

/**
 * Set the active printing method and notify listeners.
 */
export function setActivePrintingMethodId(id: string | null): void {
  if (activePrintingMethodId === id) return;
  activePrintingMethodId = id;
  printingListeners.forEach((listener) => {
    try {
      listener();
    } catch {
      // Ignore UI-side errors.
    }
  });
}

/**
 * Global scope used when propagating color harmonization.
 * Stored in the shared Zustand store (AsukaStore).
 */
export function getColorScope(): ColorScope {
  return ((useAsukaStore.getState() as any).colorScope ?? 'object') as ColorScope;
}

export function setColorScope(scope: ColorScope): void {
  (useAsukaStore.getState() as any).setColorScope?.(scope);
}

/**
 * Allow the UI (modules, toolbar, and similar surfaces) to react to printing method,
 * scope, and future configuration changes.
 */
export function subscribePrintingConfig(listener: PrintingListener): () => void {
  ensureScopeSubscription();
  printingListeners.add(listener);
  return () => {
    printingListeners.delete(listener);
  };
}

/**
 * Return the active method palette, a merged palette of all configured methods,
 * or an empty array when nothing is configured.
 */
export function getPaletteForActiveMethodOrFallback(): PrintingColor[] {
  const active = getActivePrintingMethod();
  if (active) {
    return active.colors;
  }

  // Simple fallback: concatenate all palettes while removing duplicate `value` entries.
  const all = getPrintingMethods().flatMap((m) => m.colors);
  const seen = new Set<string>();
  const unique: PrintingColor[] = [];
  for (const c of all) {
    if (seen.has(c.value)) continue;
    seen.add(c.value);
    unique.push(c);
  }
  return unique;
}

// --- Color utilities: parsing and similarity ------------------------------

export interface RgbColor {
  r: number;
  g: number;
  b: number;
}

/**
 * Parse a hex color (#RRGGBB or #RGB) or a basic rgb()/rgba() string into { r, g, b }.
 */
export function parseColorToRgb(input: string): RgbColor | null {
  if (!input) return null;
  const raw = String(input).trim();
  const value = raw.toLowerCase();

  if (value === 'none' || value === 'transparent' || value === 'currentcolor') return null;

  if (value.startsWith('#')) {
    const hex = value.slice(1);
    if (hex.length === 3) {
      const r = parseInt(hex[0] + hex[0], 16);
      const g = parseInt(hex[1] + hex[1], 16);
      const b = parseInt(hex[2] + hex[2], 16);
      if (Number.isNaN(r) || Number.isNaN(g) || Number.isNaN(b)) return null;
      return { r, g, b };
    }
    if (hex.length === 6) {
      const r = parseInt(hex.slice(0, 2), 16);
      const g = parseInt(hex.slice(2, 4), 16);
      const b = parseInt(hex.slice(4, 6), 16);
      if (Number.isNaN(r) || Number.isNaN(g) || Number.isNaN(b)) return null;
      return { r, g, b };
    }
    return null;
  }

  const fnMatch = value.match(/^rgba?\((.*)\)$/);
  if (fnMatch) {
    const inside = fnMatch[1].trim();
    const [rgbPart] = inside.split('/').map((s) => s.trim());
    const parts = rgbPart.includes(',')
      ? rgbPart.split(',').map((p) => p.trim()).filter(Boolean)
      : rgbPart.split(/\s+/).map((p) => p.trim()).filter(Boolean);

    if (parts.length < 3) return null;

    const parseChannel = (s: string): number | null => {
      if (!s) return null;
      if (s.endsWith('%')) {
        const pct = parseFloat(s.slice(0, -1));
        if (Number.isNaN(pct)) return null;
        return Math.max(0, Math.min(255, (pct / 100) * 255));
      }
      const n = parseFloat(s);
      if (Number.isNaN(n)) return null;
      return Math.max(0, Math.min(255, n));
    };

    const r = parseChannel(parts[0]);
    const g = parseChannel(parts[1]);
    const b = parseChannel(parts[2]);
    if (r == null || g == null || b == null) return null;
    return { r, g, b };
  }

  // Named colors fallback (browser only)
  if (typeof document !== 'undefined') {
    try {
      const el = document.createElement('span');
      el.style.color = raw;
      document.body?.appendChild?.(el);
      const computed = getComputedStyle(el).color;
      el.remove();
      const parsed = parseColorToRgb(computed);
      if (parsed) return parsed;
    } catch {
      // ignore
    }
  }

  return null;
}

function squaredDistance(a: RgbColor, b: RgbColor): number {
  const dr = a.r - b.r;
  const dg = a.g - b.g;
  const db = a.b - b.b;
  return dr * dr + dg * dg + db * db;
}

// --- Color space helpers ----------------------------------------------------
// We keep these small and dependency-free.

type LabColor = { l: number; a: number; b: number };

function srgbToLinear(u: number): number {
  const x = u / 255;
  return x <= 0.04045 ? x / 12.92 : Math.pow((x + 0.055) / 1.055, 2.4);
}

function rgbToXyz(rgb: RgbColor): { x: number; y: number; z: number } {
  const r = srgbToLinear(rgb.r);
  const g = srgbToLinear(rgb.g);
  const b = srgbToLinear(rgb.b);
  // D65
  const x = r * 0.4124 + g * 0.3576 + b * 0.1805;
  const y = r * 0.2126 + g * 0.7152 + b * 0.0722;
  const z = r * 0.0193 + g * 0.1192 + b * 0.9505;
  return { x, y, z };
}

function xyzToLab(xyz: { x: number; y: number; z: number }): LabColor {
  // Reference white D65
  const Xn = 0.95047;
  const Yn = 1.0;
  const Zn = 1.08883;

  const fx = xyz.x / Xn;
  const fy = xyz.y / Yn;
  const fz = xyz.z / Zn;

  const f = (t: number) => (t > 0.008856 ? Math.cbrt(t) : (7.787 * t) + 16 / 116);
  const xr = f(fx);
  const yr = f(fy);
  const zr = f(fz);

  const l = (116 * yr) - 16;
  const a = 500 * (xr - yr);
  const b = 200 * (yr - zr);
  return { l, a, b };
}

function rgbToLab(rgb: RgbColor): LabColor {
  return xyzToLab(rgbToXyz(rgb));
}

function deltaE76(a: LabColor, b: LabColor): number {
  const dl = a.l - b.l;
  const da = a.a - b.a;
  const db = a.b - b.b;
  return Math.sqrt(dl * dl + da * da + db * db);
}

function rgbToHsl(rgb: RgbColor): { h: number; s: number; l: number } {
  const r = rgb.r / 255;
  const g = rgb.g / 255;
  const b = rgb.b / 255;

  const max = Math.max(r, g, b);
  const min = Math.min(r, g, b);
  const d = max - min;

  let h = 0;
  let s = 0;
  const l = (max + min) / 2;

  if (d !== 0) {
    s = d / (1 - Math.abs(2 * l - 1));
    switch (max) {
      case r:
        h = ((g - b) / d) % 6;
        break;
      case g:
        h = (b - r) / d + 2;
        break;
      default:
        h = (r - g) / d + 4;
        break;
    }
    h *= 60;
    if (h < 0) h += 360;
  }

  // Clamp just in case of tiny floating errors
  if (!Number.isFinite(s)) s = 0;
  return { h, s: Math.max(0, Math.min(1, s)), l: Math.max(0, Math.min(1, l)) };
}

type ColorFamily = 'neutral' | 'red' | 'orange' | 'yellow' | 'green' | 'blue' | 'purple';

function colorFamilyFromHsl(hsl: { h: number; s: number; l: number }): ColorFamily {
  // Low saturation -> neutral/gray/black/white
  if (hsl.s < 0.12) return 'neutral';

  const h = hsl.h;
  if (h >= 340 || h < 20) return 'red';
  if (h < 45) return 'orange';
  if (h < 65) return 'yellow';
  if (h < 170) return 'green';
  if (h < 260) return 'blue';
  return 'purple';
}

function hueDistance(a: number, b: number): number {
  const d = Math.abs(a - b) % 360;
  return d > 180 ? 360 - d : d;
}

function isValidUrl(value: string): boolean {
  try {

    new URL(value);
    return true;
  } catch {
    return false;
  }
}

/**
 * Synchronous version of getSimilarColor for simple cases (hex/rgb only).
 * It remains useful for lightweight paths, while the asynchronous
 * advanced version also supports URL-based patterns.
 */
export function getSimilarColor(
  value: string,
  palette: PrintingColor[],
): PrintingColor | null {
  if (!palette || palette.length === 0) {
    return null;
  }

  const ref = parseColorToRgb(value);
  if (!ref) {
    return null;
  }

  let best: PrintingColor | null = null;
  let bestDist = Number.POSITIVE_INFINITY;
  for (const color of palette) {
    const rgb = parseColorToRgb(color.value);
    if (!rgb) continue;
    const d2 = squaredDistance(ref, rgb);
    if (d2 < bestDist) {
      bestDist = d2;
      best = color;
    }
  }
  return best;
}

// --- Advanced similarity: pattern URL handling ------------------------

/**
 * In-memory cache of average colors to avoid recomputing the same URLs repeatedly.
 */
const averageColorCache = new Map<string, RgbColor>();

async function computeAverageColorFromImage(url: string): Promise<RgbColor | null> {
  if (typeof document === 'undefined') {
    // En SSR / tests, on ne peut pas utiliser Canvas.
    return null;
  }

  if (averageColorCache.has(url)) {
    return averageColorCache.get(url)!;
  }

  // Chargement de l'image
  const img = await new Promise<HTMLImageElement | null>((resolve) => {
    const image = new Image();
    image.crossOrigin = 'anonymous';
    image.onload = () => resolve(image);
    image.onerror = () => resolve(null);
    image.src = url;
  });
  if (!img) {
    return null;
  }

  // Create a small off-screen canvas to compute an approximate average color
  const canvas = document.createElement('canvas');
  const ctx = canvas.getContext('2d');
  if (!ctx) {
    return null;
  }

  const width = 32;
  const height = 32;
  canvas.width = width;
  canvas.height = height;

  try {
    ctx.drawImage(img, 0, 0, width, height);
  } catch {
    return null;
  }

  let data: ImageData;
  try {
    data = ctx.getImageData(0, 0, width, height);
  } catch {
    return null;
  }

  const { data: pixels } = data;
  let rSum = 0;
  let gSum = 0;
  let bSum = 0;
  let count = 0;

  for (let i = 0; i < pixels.length; i += 4) {
    const r = pixels[i];
    const g = pixels[i + 1];
    const b = pixels[i + 2];
    const a = pixels[i + 3];
    if (a === 0) continue; // ignore fully transparent pixels
    rSum += r;
    gSum += g;
    bSum += b;
    count += 1;
  }

  if (count === 0) {
    return null;
  }

  const avg: RgbColor = {
    r: Math.round(rSum / count),
    g: Math.round(gSum / count),
    b: Math.round(bSum / count),
  };

  averageColorCache.set(url, avg);
  return avg;
}

/**
 * Return the reference color converted to RGB:
 *  - hex / rgb -> parseColorToRgb
 *  - URL -> moyenne de l'image
 */
async function toRgbAdvanced(value: string): Promise<RgbColor | null> {
  if (!value) return null;
  if (isValidUrl(value)) {
    return computeAverageColorFromImage(value);
  }
  return parseColorToRgb(value);
}

/**
 * Advanced asynchronous version of `getSimilarColor`:
 *
 * - Supports hex / rgb values and URLs used as patterns
 * - Uses approximate average colors for URLs through Canvas
 * - Caches results to avoid reloading the same images repeatedly
 *
 * @returns The closest color in the palette, or null when nothing usable is available.
 */
export async function getSimilarColorAsync(
  value: string,
  palette: PrintingColor[],
): Promise<PrintingColor | null> {
  // Unconditional marker (debug build)
  if (!palette || palette.length === 0) {
    return null;
  }

  const refRgb = await toRgbAdvanced(value);
  if (!refRgb) {
    return null;
  }

  const refLab = rgbToLab(refRgb);
  const refHsl = rgbToHsl(refRgb);
  const refHue = refHsl.h;
  const refFamily = colorFamilyFromHsl(refHsl);

  type Candidate = {
    color: PrintingColor;
    rgb: RgbColor;
    lab: LabColor;
    hsl: { h: number; s: number; l: number };
    family: ColorFamily;
    hue: number;
    hueDiff: number;
    de: number;
  };

  const candidates: Candidate[] = [];
  for (const color of palette) {
    const rgb = await toRgbAdvanced(color.value);
    if (!rgb) continue;
    const lab = rgbToLab(rgb);
    const hsl = rgbToHsl(rgb);
    const hue = hsl.h;
    const hueDiff = hueDistance(refHue, hue);
    const de = deltaE76(refLab, lab);
    const family = colorFamilyFromHsl(hsl);
    candidates.push({ color, rgb, lab, hsl, family, hue, hueDiff, de });
  }

  if (candidates.length === 0) return null;

  // Palette mapping rules:
  // - If the reference is a neutral (low saturation), prefer neutral candidates (avoid picking white for dark grays).
  // - Otherwise, prefer candidates from the same "color family" (avoid yellow-green mapping to orange/gold if greens exist).
  const preferredSet = (() => {
    if (refFamily === 'neutral') {
      const neutrals = candidates.filter((c) => c.family === 'neutral');
      if (neutrals.length > 0) return neutrals;
    }

    const sameFamily = candidates.filter((c) => c.family === refFamily);
    if (sameFamily.length > 0) return sameFamily;

    return candidates;
  })();

  // Pick smallest DeltaE within preferred set (tie-breaker: hueDiff).
  let best: Candidate | null = null;
  for (const c of preferredSet) {
    if (!best || c.de < best.de || (c.de === best.de && c.hueDiff < best.hueDiff)) {
      best = c;
    }
  }

  // Debug: show top few.
  const dbgLimit = Number((globalThis as any).__ASUKA_COLOR_DEBUG_LIMIT__ ?? 12);
  const show = [...candidates]
    .sort((a, b) => (a.family === refFamily ? -1 : 1) - (b.family === refFamily ? -1 : 1) || (a.de - b.de) || (a.hueDiff - b.hueDiff))
    .slice(0, Math.max(0, dbgLimit));
    void show;
  return best?.color ?? null;
}
