import { parseCssColor, type ParsedColor } from "./colorValue";
import { COMMON_LOCAL_FONT_FAMILIES } from "./fontCatalog";
import type { DomEditSelection } from "./domEditing";
import type { GsapAnimation } from "@hyperframes/parsers/gsap-parser";
import type { TimelineElement } from "../../player";
import { roundToCenti } from "../../utils/rounding";

export type {
  BackgroundRemovalProgress,
  BackgroundRemovalResult,
  PropertyPanelProps,
} from "./propertyPanelTypes";

export function stripQueryAndHash(value: string): string {
  const queryIndex = value.indexOf("?");
  const hashIndex = value.indexOf("#");
  if (queryIndex < 0) return hashIndex < 0 ? value : value.slice(0, hashIndex);
  if (hashIndex < 0) return value.slice(0, queryIndex);
  return value.slice(0, Math.min(queryIndex, hashIndex));
}

export function isSelectedElementHidden(
  elements: readonly TimelineElement[],
  selectedElementId: string | null,
): boolean {
  if (!selectedElementId) return false;
  return (
    elements.find((element) => (element.key ?? element.id) === selectedElementId)?.hidden === true
  );
}

/* ------------------------------------------------------------------ */
/*  Font types & constants (shared by font and section modules)        */
/* ------------------------------------------------------------------ */

export const GENERIC_FONT_FAMILIES = new Set([
  "inherit",
  "initial",
  "revert",
  "revert-layer",
  "serif",
  "sans-serif",
  "monospace",
  "cursive",
  "fantasy",
  "system-ui",
  "ui-sans-serif",
  "ui-serif",
  "ui-monospace",
  "ui-rounded",
  "emoji",
  "math",
  "fangsong",
]);

export const DEFAULT_FONT_FAMILIES = [
  ...COMMON_LOCAL_FONT_FAMILIES,
  "Inter",
  "system-ui",
  "sans-serif",
  "serif",
  "monospace",
];

export interface LocalFontData {
  family: string;
  fullName?: string;
  postscriptName?: string;
  style?: string;
  blob?: () => Promise<Blob>;
}

export type FontSource = "Current" | "Document" | "Imported" | "Local" | "Google" | "System";

export interface FontOption {
  family: string;
  source: FontSource;
}

declare global {
  interface Window {
    queryLocalFonts?: () => Promise<LocalFontData[]>;
  }
}

export function sanitizeFontFilePart(value: string): string {
  return value
    .replace(/[^\w .-]+/g, " ")
    .replace(/\s+/g, " ")
    .trim();
}

export function localFontSortScore(font: LocalFontData): number {
  const style = font.style?.toLowerCase() ?? "";
  const fullName = font.fullName?.toLowerCase() ?? "";
  if (style === "regular" || fullName.endsWith(" regular")) return 0;
  if (style === "normal" || fullName.endsWith(" normal")) return 1;
  if (style === "medium" || fullName.endsWith(" medium")) return 2;
  return 3;
}

export function uniqueFontFamilies(values: string[]): string[] {
  const seen = new Set<string>();
  return values.reduce<string[]>((result, value) => {
    const family = value.trim();
    if (!family) return result;
    const key = family.toLowerCase();
    if (seen.has(key)) return result;
    seen.add(key);
    result.push(family);
    return result;
  }, []);
}

export function uniqueFontOptions(values: FontOption[]): FontOption[] {
  const seen = new Set<string>();
  return values.reduce<FontOption[]>((result, value) => {
    const family = value.family.trim();
    if (!family) return result;
    const key = family.toLowerCase();
    if (seen.has(key)) return result;
    seen.add(key);
    result.push({ family, source: value.source });
    return result;
  }, []);
}

export function sortFontOptions(options: FontOption[]): FontOption[] {
  return [...options].sort((a, b) => {
    const rankDelta = fontSourceRank(a.source) - fontSourceRank(b.source);
    if (rankDelta !== 0) return rankDelta;
    const commonA = COMMON_LOCAL_FONT_FAMILIES.findIndex(
      (f) => f.toLowerCase() === a.family.toLowerCase(),
    );
    const commonB = COMMON_LOCAL_FONT_FAMILIES.findIndex(
      (f) => f.toLowerCase() === b.family.toLowerCase(),
    );
    const commonDelta =
      (commonA === -1 ? Number.MAX_SAFE_INTEGER : commonA) -
      (commonB === -1 ? Number.MAX_SAFE_INTEGER : commonB);
    return commonDelta === 0 ? a.family.localeCompare(b.family) : commonDelta;
  });
}

function fontSourceRank(source: FontSource): number {
  if (source === "Current") return 0;
  if (source === "Document") return 1;
  if (source === "Imported") return 2;
  if (source === "Google") return 3;
  if (source === "Local") return 4;
  return 5;
}

/* ------------------------------------------------------------------ */
/*  Shared constants                                                   */
/* ------------------------------------------------------------------ */

export const FIELD =
  "min-w-0 rounded-md bg-panel-input px-3 py-[7px] text-panel-text-1 transition-colors focus-within:ring-1 focus-within:ring-panel-accent/30";
export const LABEL = "text-[11px] font-medium text-panel-text-3";
export const RESPONSIVE_GRID = "grid grid-cols-[repeat(auto-fit,minmax(118px,1fr))] gap-3";
export const EMPTY_STYLES: Record<string, string> = {};

const EMPTY_FILTER_VALUE = "none";
const BOX_SHADOW_PRESETS = {
  none: "none",
  soft: "0 12px 36px rgba(0, 0, 0, 0.28)",
  lift: "0 18px 54px rgba(0, 0, 0, 0.38)",
  glow: "0 0 0 1px rgba(60, 230, 172, 0.34), 0 18px 56px rgba(60, 230, 172, 0.2)",
} as const;

export type BoxShadowPreset = keyof typeof BOX_SHADOW_PRESETS | "custom";

export {
  buildClipPathValue,
  buildInsetClipPathSides,
  buildInsetClipPathValue,
  getClipPathInsetPx,
  inferClipPathPreset,
  parseInsetClipPathSides,
  type ClipPathInsetSides,
} from "./clipPathHelpers";

/* ------------------------------------------------------------------ */
/*  Shared types                                                       */
/* ------------------------------------------------------------------ */

export interface ParsedNumericToken {
  value: number;
  unit: string;
}

/* ------------------------------------------------------------------ */
/*  Pure utility functions                                             */
/* ------------------------------------------------------------------ */

export function colorFromCss(value: string): ParsedColor {
  return parseCssColor(value) ?? { red: 0, green: 0, blue: 0, alpha: 1 };
}

export function parseNumericValue(value: string | undefined): number | null {
  if (!value) return null;
  const parsed = Number.parseFloat(value);
  return Number.isFinite(parsed) ? parsed : null;
}

export function formatTimingValue(seconds: number): string {
  if (!Number.isFinite(seconds) || seconds < 0) return "0.00s";
  return `${seconds.toFixed(2)}s`;
}

export function formatNumericValue(value: number): string {
  const rounded = roundToCenti(value);
  return Number.isInteger(rounded)
    ? `${rounded}`
    : rounded.toFixed(2).replace(/0+$/, "").replace(/\.$/, "");
}

export function parseNumericToken(value: string | undefined): ParsedNumericToken | null {
  if (!value) return null;
  const match = value.trim().match(/^(-?\d+(?:\.\d+)?)([a-z%]*)$/i);
  if (!match) return null;
  const parsed = Number.parseFloat(match[1]);
  if (!Number.isFinite(parsed)) return null;
  return { value: parsed, unit: match[2] ?? "" };
}

export function parsePxMetricValue(value: string): number | null {
  const token = parseNumericToken(value);
  if (!token) return null;
  if (token.unit && token.unit.toLowerCase() !== "px") return null;
  return token.value;
}

function clampPanelNumber(value: number, min: number, max: number, fallback: number): number {
  if (!Number.isFinite(value)) return fallback;
  return Math.max(min, Math.min(max, value));
}

export function normalizePanelPxValue(
  value: string,
  options: { min?: number; max?: number; fallback?: number } = {},
): string | null {
  const token = parseNumericToken(value.trim());
  if (!token) return null;
  if (token.unit && token.unit.toLowerCase() !== "px") return null;
  const next = clampPanelNumber(
    token.value,
    options.min ?? Number.NEGATIVE_INFINITY,
    options.max ?? Number.POSITIVE_INFINITY,
    options.fallback ?? 0,
  );
  return `${formatNumericValue(next)}px`;
}

export function formatPxMetricValue(value: number): string {
  return `${formatNumericValue(value)}px`;
}

export function normalizeTextMetricValue(
  property: "letter-spacing" | "line-height",
  value: string,
) {
  const trimmed = value.trim();
  if (!trimmed || trimmed === "normal") return trimmed || "normal";
  const token = parseNumericToken(trimmed);
  if (!token) return trimmed;
  if (property === "letter-spacing") {
    return token.unit ? trimmed : `${formatNumericValue(token.value)}px`;
  }
  if (token.unit) return trimmed;
  return token.value > 4 ? `${formatNumericValue(token.value)}px` : formatNumericValue(token.value);
}

function splitCssFunctions(value: string): string[] {
  const source = value.trim();
  const functions: string[] = [];
  let depth = 0;
  let start = 0;

  for (let index = 0; index < source.length; index += 1) {
    const char = source[index];
    if (char === "(") depth += 1;
    if (char === ")") depth = Math.max(0, depth - 1);
    if (/\s/.test(char) && depth === 0) {
      const part = source.slice(start, index).trim();
      if (part) functions.push(part);
      start = index + 1;
    }
  }

  const lastPart = source.slice(start).trim();
  if (lastPart) functions.push(lastPart);
  return functions;
}

export function getCssFilterFunctionPx(value: string | undefined, name: string): number {
  const normalized = value?.trim();
  if (!normalized || normalized === EMPTY_FILTER_VALUE) return 0;
  const escapedName = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
  const match = new RegExp(`(?:^|\\s)${escapedName}\\((-?\\d+(?:\\.\\d+)?)px\\)`, "i").exec(
    normalized,
  );
  if (!match) return 0;
  const parsed = Number.parseFloat(match[1]);
  return Number.isFinite(parsed) ? Math.max(0, parsed) : 0;
}

export function setCssFilterFunctionPx(
  value: string | undefined,
  name: string,
  nextPx: number,
): string {
  const nextValue = clampPanelNumber(nextPx, 0, 200, 0);
  const functions = splitCssFunctions(value && value.trim() !== EMPTY_FILTER_VALUE ? value : "");
  const lowerName = name.toLowerCase();
  const filtered = functions.filter((entry) => !entry.toLowerCase().startsWith(`${lowerName}(`));
  if (nextValue > 0) filtered.push(`${name}(${formatNumericValue(nextValue)}px)`);
  return filtered.length > 0 ? filtered.join(" ") : EMPTY_FILTER_VALUE;
}

export function inferBoxShadowPreset(value: string | undefined): BoxShadowPreset {
  const normalized = value?.trim() || "none";
  for (const [preset, shadow] of Object.entries(BOX_SHADOW_PRESETS)) {
    if (normalized === shadow) return preset as BoxShadowPreset;
  }
  return normalized === "none" ? "none" : "custom";
}

export function buildBoxShadowPresetValue(
  preset: BoxShadowPreset,
  fallback: string | undefined,
): string {
  if (preset === "custom") return fallback?.trim() || "none";
  return BOX_SHADOW_PRESETS[preset];
}

export function buildStrokeWidthStyleUpdates(
  nextWidth: string,
  currentBorderStyle: string | undefined,
): Array<[property: string, value: string]> {
  const updates: Array<[property: string, value: string]> = [["border-width", nextWidth]];
  const token = parseNumericToken(nextWidth);
  const style = currentBorderStyle?.trim().toLowerCase() || "none";
  if (token && token.value > 0 && (style === "none" || style === "hidden")) {
    updates.push(["border-style", "solid"]);
  }
  return updates;
}

export function buildStrokeStyleUpdates(
  nextStyle: string,
  currentBorderWidth: string | undefined,
): Array<[property: string, value: string]> {
  const updates: Array<[property: string, value: string]> = [["border-style", nextStyle]];
  const style = nextStyle.trim().toLowerCase();
  if (!style || style === "none" || style === "hidden") return updates;

  const token = parseNumericToken(currentBorderWidth?.trim() || "0");
  if (!token || token.value <= 0) {
    updates.push(["border-width", "1px"]);
  }
  return updates;
}

export function adjustNumericToken(
  value: string,
  direction: 1 | -1,
  modifiers?: { shiftKey?: boolean; altKey?: boolean },
): string | null {
  const token = parseNumericToken(value);
  if (!token) return null;

  const baseStep = modifiers?.altKey ? 0.1 : modifiers?.shiftKey ? 10 : 1;
  const nextValue = token.value + baseStep * direction;
  return `${formatNumericValue(nextValue)}${token.unit}`;
}

export function extractBackgroundImageUrl(value: string | undefined): string {
  if (!value) return "";
  const lowerValue = value.toLowerCase();
  const urlStart = lowerValue.indexOf("url(");
  if (urlStart < 0) return "";

  let index = urlStart + 4;
  while (
    index < value.length &&
    (value[index] === " " ||
      value[index] === "\n" ||
      value[index] === "\r" ||
      value[index] === "\t" ||
      value[index] === "\f")
  ) {
    index += 1;
  }

  const quote = value[index] === '"' || value[index] === "'" ? value[index] : null;
  if (quote) {
    index += 1;
    const endQuote = value.indexOf(quote, index);
    return endQuote >= index ? value.slice(index, endQuote) : "";
  }

  const endParen = value.indexOf(")", index);
  if (endParen < index) return "";
  return value.slice(index, endParen).trim();
}

// ── GSAP runtime value readers (used by PropertyPanel) ────────────────────

// Core transform channels the panel ALWAYS reads live — even before a just-set
// value (e.g. rotationX) has re-parsed into `gsapAnimations`. Without this the
// cube + fields drop the prop and flicker to 0 on every commit; gsap.getProperty
// reflects the in-place instant patch, so it's the true current value.
// fallow-ignore-next-line complexity
const ALWAYS_READ_CHANNELS = [
  "x",
  "y",
  "rotation",
  "rotationX",
  "rotationY",
  "rotationZ",
  "z",
  "scale",
  "transformPerspective",
  "opacity",
];

/** Every property key the panel should read for an element: animated props + the
 * always-read transform channels. */
function collectPanelPropKeys(gsapAnimations: GsapAnimation[]): Set<string> {
  const keys = new Set<string>(ALWAYS_READ_CHANNELS);
  for (const anim of gsapAnimations) {
    if (anim.keyframes) {
      for (const kf of anim.keyframes.keyframes) {
        for (const p of Object.keys(kf.properties)) keys.add(p);
      }
    }
    for (const p of Object.keys(anim.properties)) keys.add(p);
  }
  return keys;
}

export function readGsapRuntimeValuesForPanel(
  gsapAnimId: string | null,
  gsapAnimations: GsapAnimation[],
  element: DomEditSelection,
  previewIframeRef: React.RefObject<HTMLIFrameElement | null>,
): Record<string, number> | null {
  if (!gsapAnimId || gsapAnimations.length === 0) return null;
  const iframe = previewIframeRef?.current;
  if (!iframe?.contentWindow) return null;
  const selector = element.id ? `#${element.id}` : element.selector;
  if (!selector) return null;
  try {
    const gsap = (
      iframe.contentWindow as unknown as {
        gsap?: { getProperty: (el: Element, prop: string) => number | string };
      }
    ).gsap;
    if (!gsap?.getProperty) return null;
    const el = iframe.contentDocument?.querySelector(selector);
    if (!el) return null;
    const propKeys = collectPanelPropKeys(gsapAnimations);
    const result: Record<string, number> = {};
    for (const prop of propKeys) {
      const v = Number(gsap.getProperty(el, prop));
      if (Number.isFinite(v)) result[prop] = roundToCenti(v);
    }
    return Object.keys(result).length > 0 ? result : null;
  } catch {
    return null;
  }
}

export function readGsapBorderRadiusForPanel(
  gsapRuntimeValues: Record<string, number> | null,
  gsapAnimations: GsapAnimation[],
  element: DomEditSelection,
  previewIframeRef: React.RefObject<HTMLIFrameElement | null>,
): { tl: number; tr: number; br: number; bl: number } | null {
  if (!gsapRuntimeValues || !("borderRadius" in gsapRuntimeValues)) {
    const hasBRProp = gsapAnimations.some(
      (a) =>
        "borderRadius" in a.properties ||
        a.keyframes?.keyframes.some((kf) => "borderRadius" in kf.properties),
    );
    if (!hasBRProp) return null;
  }
  const iframe = previewIframeRef?.current;
  const selector = element.id ? `#${element.id}` : element.selector;
  if (!iframe?.contentDocument || !selector) return null;
  try {
    const el = iframe.contentDocument.querySelector(selector);
    if (!el || !iframe.contentWindow) return null;
    const cs = iframe.contentWindow.getComputedStyle(el);
    const parse = (v: string) => Number.parseFloat(v) || 0;
    return {
      tl: parse(cs.borderTopLeftRadius),
      tr: parse(cs.borderTopRightRadius),
      br: parse(cs.borderBottomRightRadius),
      bl: parse(cs.borderBottomLeftRadius),
    };
  } catch {
    return null;
  }
}
