export type GradientKind = "linear" | "radial" | "conic";

export type RadialSizeKeyword =
  | "closest-side"
  | "closest-corner"
  | "farthest-side"
  | "farthest-corner";

export interface GradientStop {
  color: string;
  position: number;
}

export interface GradientModel {
  kind: GradientKind;
  repeating: boolean;
  angle: number;
  centerX: number;
  centerY: number;
  shape: "circle" | "ellipse";
  radialSize: RadialSizeKeyword;
  stops: GradientStop[];
}

const RADIAL_SIZE_KEYWORDS: RadialSizeKeyword[] = [
  "closest-side",
  "closest-corner",
  "farthest-side",
  "farthest-corner",
];

function isWhitespace(char: string | undefined): boolean {
  return char === " " || char === "\n" || char === "\r" || char === "\t" || char === "\f";
}

function isDigit(char: string | undefined): boolean {
  return char != null && char >= "0" && char <= "9";
}

function isSimpleNumber(value: string): boolean {
  if (!value) return false;
  let index = value[0] === "-" ? 1 : 0;
  let digits = 0;

  while (isDigit(value[index])) {
    index += 1;
    digits += 1;
  }

  if (value[index] === ".") {
    index += 1;
    while (isDigit(value[index])) {
      index += 1;
      digits += 1;
    }
  }

  return digits > 0 && index === value.length;
}

function parseCssNumber(value: string | undefined): number | null {
  if (!value) return null;
  const trimmed = value.trim();
  if (!isSimpleNumber(trimmed)) return null;
  const parsed = Number(trimmed);
  return Number.isFinite(parsed) ? parsed : null;
}

function splitCssWhitespace(value: string): string[] {
  const tokens: string[] = [];
  let current = "";

  for (const char of value) {
    if (isWhitespace(char)) {
      if (current) {
        tokens.push(current);
        current = "";
      }
      continue;
    }
    current += char;
  }

  if (current) tokens.push(current);
  return tokens;
}

function hasCssWord(value: string, word: string): boolean {
  return splitCssWhitespace(value.toLowerCase()).includes(word);
}

function parsePercentToken(value: string | undefined, fallback: number): number {
  if (!value?.endsWith("%")) return fallback;
  const parsed = parseCssNumber(value.slice(0, -1));
  return parsed == null ? fallback : clamp(parsed, 0, 100);
}

function parseAngleToken(value: string | undefined): number | null {
  const trimmed = value?.trim().toLowerCase();
  if (!trimmed?.endsWith("deg")) return null;
  return parseCssNumber(trimmed.slice(0, -3));
}

function trailingPercentStart(value: string): number | null {
  if (!value.endsWith("%")) return null;
  const withoutUnit = value.slice(0, -1).trimEnd();
  let start = withoutUnit.length;

  while (start > 0 && (isDigit(withoutUnit[start - 1]) || withoutUnit[start - 1] === ".")) {
    start -= 1;
  }

  if (start > 0 && withoutUnit[start - 1] === "-") {
    start -= 1;
  }

  const token = withoutUnit.slice(start);
  if (!isSimpleNumber(token)) return null;
  if (start === 0 || !isWhitespace(withoutUnit[start - 1])) return null;
  return start;
}

function clamp(value: number, min: number, max: number): number {
  return Math.min(max, Math.max(min, value));
}

function round(value: number): number {
  return Math.round(value * 100) / 100;
}

function parsePercent(value: string | undefined, fallback: number): number {
  const parsed = parseCssNumber(value);
  return parsed == null ? fallback : clamp(parsed, 0, 100);
}

function parseColorStop(raw: string): { color: string; position: number | null } {
  const trimmed = raw.trim();
  const percentStart = trailingPercentStart(trimmed);
  if (percentStart == null) return { color: trimmed, position: null };

  const withoutUnit = trimmed.slice(0, -1).trimEnd();
  return {
    color: withoutUnit.slice(0, percentStart).trim(),
    position: parsePercent(withoutUnit.slice(percentStart), 0),
  };
}

function normalizeStops(stops: Array<{ color: string; position: number | null }>): GradientStop[] {
  if (stops.length === 0) {
    return [
      { color: "rgba(60, 230, 172, 0.18)", position: 0 },
      { color: "rgba(255, 255, 255, 0.04)", position: 100 },
    ];
  }

  if (stops.length === 1) {
    return [
      { color: stops[0].color, position: 0 },
      { color: stops[0].color, position: 100 },
    ];
  }

  const result = stops.map((stop, index) => ({
    color: stop.color,
    position: stop.position ?? (index / (stops.length - 1)) * 100,
  }));

  return result.map((stop) => ({
    color: stop.color,
    position: round(clamp(stop.position, 0, 100)),
  }));
}

function splitGradientArgs(value: string): string[] {
  const parts: string[] = [];
  let current = "";
  let depth = 0;

  for (const char of value) {
    if (char === "(") depth += 1;
    if (char === ")") depth = Math.max(0, depth - 1);

    if (char === "," && depth === 0) {
      if (current.trim()) parts.push(current.trim());
      current = "";
      continue;
    }

    current += char;
  }

  if (current.trim()) parts.push(current.trim());
  return parts;
}

function directionToAngle(value: string): number | null {
  const normalized = value.trim().toLowerCase();
  const map: Record<string, number> = {
    "to top": 0,
    "to top right": 45,
    "to right top": 45,
    "to right": 90,
    "to bottom right": 135,
    "to right bottom": 135,
    "to bottom": 180,
    "to bottom left": 225,
    "to left bottom": 225,
    "to left": 270,
    "to top left": 315,
    "to left top": 315,
  };
  return normalized in map ? map[normalized] : null;
}

function parseLinearArgs(parts: string[]): GradientModel {
  const first = parts[0] ?? "";
  const angleFromDirection = directionToAngle(first);
  const parsedAngle = parseAngleToken(first);
  const firstIsAngle = parsedAngle != null;
  const angle = parsedAngle ?? angleFromDirection ?? 180;
  const stopParts = firstIsAngle || angleFromDirection != null ? parts.slice(1) : parts;

  return {
    kind: "linear",
    repeating: false,
    angle,
    centerX: 50,
    centerY: 50,
    shape: "ellipse",
    radialSize: "farthest-corner",
    stops: normalizeStops(stopParts.map(parseColorStop)),
  };
}

function parseRadialArgs(parts: string[]): GradientModel {
  const first = parts[0] ?? "";
  const firstLower = first.toLowerCase();
  const hasConfig =
    hasCssWord(firstLower, "at") ||
    hasCssWord(firstLower, "circle") ||
    hasCssWord(firstLower, "ellipse") ||
    firstLower.includes("closest-") ||
    firstLower.includes("farthest-");
  const config = hasConfig ? first : "";
  const stopParts = hasConfig ? parts.slice(1) : parts;
  const configLower = config.toLowerCase();
  const configTokens = splitCssWhitespace(configLower);
  const atIndex = configTokens.indexOf("at");

  const shape = hasCssWord(configLower, "circle") ? "circle" : "ellipse";
  const radialSize =
    RADIAL_SIZE_KEYWORDS.find((keyword) => configTokens.includes(keyword)) ?? "farthest-corner";

  return {
    kind: "radial",
    repeating: false,
    angle: 180,
    centerX: parsePercentToken(configTokens[atIndex + 1], 50),
    centerY: parsePercentToken(configTokens[atIndex + 2], 50),
    shape,
    radialSize,
    stops: normalizeStops(stopParts.map(parseColorStop)),
  };
}

function parseConicArgs(parts: string[]): GradientModel {
  const first = parts[0] ?? "";
  const firstLower = first.toLowerCase();
  const hasConfig = hasCssWord(firstLower, "from") || hasCssWord(firstLower, "at");
  const config = hasConfig ? first : "";
  const stopParts = hasConfig ? parts.slice(1) : parts;
  const configTokens = splitCssWhitespace(config.toLowerCase());
  const fromIndex = configTokens.indexOf("from");
  const atIndex = configTokens.indexOf("at");
  const angle = parseAngleToken(configTokens[fromIndex + 1]);

  return {
    kind: "conic",
    repeating: false,
    angle: angle ?? 0,
    centerX: parsePercentToken(configTokens[atIndex + 1], 50),
    centerY: parsePercentToken(configTokens[atIndex + 2], 50),
    shape: "ellipse",
    radialSize: "farthest-corner",
    stops: normalizeStops(stopParts.map(parseColorStop)),
  };
}

export function buildDefaultGradientModel(fallbackColor?: string): GradientModel {
  return {
    kind: "linear",
    repeating: false,
    angle: 135,
    centerX: 50,
    centerY: 50,
    shape: "ellipse",
    radialSize: "farthest-corner",
    stops: normalizeStops([
      {
        color:
          fallbackColor && fallbackColor !== "transparent"
            ? fallbackColor
            : "rgba(60, 230, 172, 0.18)",
        position: 0,
      },
      { color: "rgba(255, 255, 255, 0.04)", position: 100 },
    ]),
  };
}

export function parseGradient(value: string | undefined): GradientModel | null {
  if (!value || value === "none") return null;
  const trimmed = value.trim();
  const openParenIndex = trimmed.indexOf("(");
  if (openParenIndex <= 0 || !trimmed.endsWith(")")) return null;

  const functionName = trimmed.slice(0, openParenIndex).toLowerCase();
  const kindByFunctionName: Record<string, { kind: GradientKind; repeating: boolean }> = {
    "linear-gradient": { kind: "linear", repeating: false },
    "radial-gradient": { kind: "radial", repeating: false },
    "conic-gradient": { kind: "conic", repeating: false },
    "repeating-linear-gradient": { kind: "linear", repeating: true },
    "repeating-radial-gradient": { kind: "radial", repeating: true },
    "repeating-conic-gradient": { kind: "conic", repeating: true },
  };
  const parsedFunction = kindByFunctionName[functionName];
  if (!parsedFunction) return null;

  const { kind, repeating } = parsedFunction;
  const parts = splitGradientArgs(trimmed.slice(openParenIndex + 1, -1));

  const parsed =
    kind === "linear"
      ? parseLinearArgs(parts)
      : kind === "radial"
        ? parseRadialArgs(parts)
        : parseConicArgs(parts);

  return { ...parsed, repeating };
}

function formatStop(stop: GradientStop): string {
  return `${stop.color} ${round(stop.position)}%`;
}

export function serializeGradient(model: GradientModel): string {
  const fn = `${model.repeating ? "repeating-" : ""}${model.kind}-gradient`;
  const stops = model.stops.map(formatStop).join(", ");

  if (model.kind === "linear") {
    return `${fn}(${round(model.angle)}deg, ${stops})`;
  }

  if (model.kind === "radial") {
    return `${fn}(${model.shape} ${model.radialSize} at ${round(model.centerX)}% ${round(
      model.centerY,
    )}%, ${stops})`;
  }

  return `${fn}(from ${round(model.angle)}deg at ${round(model.centerX)}% ${round(
    model.centerY,
  )}%, ${stops})`;
}

function blendChannel(start: number, end: number, ratio: number): number {
  return Math.round(start + (end - start) * ratio);
}

function formatHex(channel: number): string {
  return channel.toString(16).padStart(2, "0");
}

function interpolateGradientStopColor(model: GradientModel, position: number): string {
  const clampedPosition = clamp(position, 0, 100);
  const sortedStops = [...model.stops].sort((a, b) => a.position - b.position);
  const exact = sortedStops.find((stop) => Math.abs(stop.position - clampedPosition) < 0.001);
  if (exact) return exact.color;

  const right = sortedStops.find((stop) => stop.position > clampedPosition) ?? sortedStops.at(-1);
  const left =
    [...sortedStops].reverse().find((stop) => stop.position < clampedPosition) ?? sortedStops[0];
  if (!left || !right) return sortedStops[0]?.color ?? "rgba(255, 255, 255, 1)";
  if (left === right) return left.color;

  const leftColor = left.color;
  const rightColor = right.color;
  const leftParsed = leftColor ? parseColorString(leftColor) : null;
  const rightParsed = rightColor ? parseColorString(rightColor) : null;
  if (!leftParsed || !rightParsed) return left.color;

  const ratio = (clampedPosition - left.position) / Math.max(1, right.position - left.position);
  const red = blendChannel(leftParsed.red, rightParsed.red, ratio);
  const green = blendChannel(leftParsed.green, rightParsed.green, ratio);
  const blue = blendChannel(leftParsed.blue, rightParsed.blue, ratio);
  const alpha = round(leftParsed.alpha + (rightParsed.alpha - leftParsed.alpha) * ratio);

  if (alpha >= 1) {
    return `#${formatHex(red)}${formatHex(green)}${formatHex(blue)}`.toUpperCase();
  }

  return `rgba(${red}, ${green}, ${blue}, ${alpha})`;
}

export function insertGradientStop(model: GradientModel, position: number): GradientModel {
  const clampedPosition = round(clamp(position, 0, 100));
  const color = interpolateGradientStopColor(model, clampedPosition);
  const nextStops = [...model.stops, { color, position: clampedPosition }].sort(
    (a, b) => a.position - b.position,
  );
  return {
    ...model,
    stops: nextStops,
  };
}

function parseColorString(
  value: string,
): { red: number; green: number; blue: number; alpha: number } | null {
  const trimmed = value.trim().toLowerCase();
  if (trimmed === "transparent") {
    return { red: 0, green: 0, blue: 0, alpha: 0 };
  }

  const hex = trimmed.match(/^#([0-9a-f]{6})$/i);
  if (hex) {
    return {
      red: Number.parseInt(hex[1].slice(0, 2), 16),
      green: Number.parseInt(hex[1].slice(2, 4), 16),
      blue: Number.parseInt(hex[1].slice(4, 6), 16),
      alpha: 1,
    };
  }

  const rgba = trimmed.match(
    /^rgba?\(\s*([0-9.]+)\s*,\s*([0-9.]+)\s*,\s*([0-9.]+)(?:\s*,\s*([0-9.]+))?\s*\)$/i,
  );
  if (!rgba) return null;

  return {
    red: Number.parseFloat(rgba[1]),
    green: Number.parseFloat(rgba[2]),
    blue: Number.parseFloat(rgba[3]),
    alpha: rgba[4] != null ? Number.parseFloat(rgba[4]) : 1,
  };
}
