import type { FlatStyleLike, StyleVariables } from "ol/style/flat";

import { calculateClassification } from "@utils/breaksClassification";
import { parseColor } from "@utils/parseColor";

import {
  createDefaultLayerStyleStates,
  GRADIENT_PALETTES,
  RAINBOW_PALETTES,
  SINGLE_COLOR_PALETTES,
} from "./styleEditorPresets";
import type {
  DefaultLayerStyleId,
  LayerStyleState,
  ResolvedLayerStyle,
  StyleConfig,
  StyleValidationResult,
} from "./styleEditorTypes";

export const MIN_CLASS_COUNT = 2;
export const MAX_CLASS_COUNT = 10;

const clampIndex = (value: number, length: number) =>
  Math.min(Math.max(Math.round(value) || 0, 0), Math.max(length - 1, 0));

const arraysEqual = <T>(left: readonly T[] = [], right: readonly T[] = []) =>
  left.length === right.length && left.every((value, index) => value === right[index]);

const withOpacity = (color: string, opacity: number) => {
  const parsed = parseColor(color);
  return `rgba(${parsed.r}, ${parsed.g}, ${parsed.b}, ${opacity})`;
};

const formatBoundary = (value: number) =>
  Number.isInteger(value) ? String(value) : Number(value.toFixed(3)).toString();

export const rgbaToHex = (rgba: string) => {
  try {
    const color = parseColor(rgba);
    const toHex = (value: number) => Math.round(value).toString(16).padStart(2, "0");
    return `#${toHex(color.r)}${toHex(color.g)}${toHex(color.b)}`;
  } catch {
    return "#000000";
  }
};

export const hexToRgba = (hex: string) => {
  const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
  return result
    ? `rgba(${parseInt(result[1], 16)}, ${parseInt(result[2], 16)}, ${parseInt(
        result[3],
        16,
      )}, 1)`
    : "rgba(0, 0, 0, 1)";
};

export const getDefaultCustomColors = (
  segments: number,
  existingColors: string[] = [],
) => {
  const nextColors = [...existingColors];
  const baseColors = RAINBOW_PALETTES[0].colors;
  while (nextColors.length < segments) {
    nextColors.push(baseColors[nextColors.length % baseColors.length]);
  }
  return nextColors.slice(0, segments);
};

const createEqualBoundaries = (minimum: number, maximum: number, segments: number) => {
  if (minimum === maximum) {
    return Array.from({ length: segments + 1 }, () => minimum);
  }
  return Array.from(
    { length: segments + 1 },
    (_, index) => minimum + ((maximum - minimum) * index) / segments,
  );
};

const normalizeCalculatedBoundaries = (
  calculated: number[],
  values: number[],
  segments: number,
) => {
  const finiteValues = values.filter(Number.isFinite).sort((a, b) => a - b);
  if (finiteValues.length === 0) return [];
  const minimum = finiteValues[0];
  const maximum = finiteValues[finiteValues.length - 1];
  if (minimum === maximum) return createEqualBoundaries(minimum, maximum, segments);

  const candidates = Array.from(
    new Set([minimum, ...calculated.filter(Number.isFinite), maximum]),
  )
    .filter((value) => value >= minimum && value <= maximum)
    .sort((a, b) => a - b);

  if (candidates.length === segments + 1) return candidates;
  return createEqualBoundaries(minimum, maximum, segments);
};

export const resolveBoundaries = (
  values: number[],
  styleConfig: StyleConfig,
): number[] => {
  const segments = Math.min(
    MAX_CLASS_COUNT,
    Math.max(MIN_CLASS_COUNT, Math.round(styleConfig.segments)),
  );
  if (styleConfig.classificationMethod === "custom_breaks") {
    return [...(styleConfig.customBreaks || [])];
  }
  const finiteValues = values.filter(Number.isFinite);
  if (finiteValues.length === 0) return [];
  const calculated = calculateClassification(finiteValues, segments, "pretty_breaks");
  return normalizeCalculatedBoundaries(calculated, finiteValues, segments);
};

export const getDefaultCustomBreaks = ({
  segments,
  property,
  layerId,
  elevationRange,
  diameterRange,
  currentJunctionCalData,
  currentPipeCalData,
}: {
  segments: number;
  property: string;
  layerId?: string;
  elevationRange?: [number, number];
  diameterRange?: [number, number];
  currentJunctionCalData?: any[];
  currentPipeCalData?: any[];
}) => {
  let values: number[] = [];
  if (layerId === "junctions" && property === "elevation" && elevationRange) {
    values = elevationRange;
  } else if (layerId === "pipes" && property === "diameter" && diameterRange) {
    values = diameterRange;
  } else if (layerId === "junctions") {
    values = (currentJunctionCalData || []).map((item: any) => Number(item.value));
  } else if (layerId === "pipes") {
    values = (currentPipeCalData || []).map((item: any) => Number(item.value));
  }
  const finiteValues = values.filter(Number.isFinite);
  if (!property || finiteValues.length === 0) {
    return Array.from({ length: segments + 1 }, (_, index) => index);
  }
  const minimum = Math.min(...finiteValues);
  const maximum = Math.max(...finiteValues);
  if (minimum === maximum) {
    const padding = Math.max(Math.abs(minimum) * 0.01, 1);
    return createEqualBoundaries(minimum - padding, maximum + padding, segments);
  }
  return normalizeCalculatedBoundaries(
    calculateClassification(finiteValues, segments, "pretty_breaks"),
    finiteValues,
    segments,
  );
};

export const normalizeCustomBreaks = (breaks: number[], segments: number) => {
  const finite = breaks.filter(Number.isFinite).slice(0, segments + 1);
  if (finite.length === segments + 1) return finite;
  if (finite.length >= 2) {
    return createEqualBoundaries(finite[0], finite[finite.length - 1], segments);
  }
  return Array.from({ length: segments + 1 }, (_, index) => finite[0] ?? index);
};

export const validateStyleConfig = (styleConfig: StyleConfig): StyleValidationResult => {
  const errors: string[] = [];
  if (!styleConfig.property) errors.push("请选择分级属性");
  if (
    !Number.isInteger(styleConfig.segments) ||
    styleConfig.segments < MIN_CLASS_COUNT ||
    styleConfig.segments > MAX_CLASS_COUNT
  ) {
    errors.push(`分类数量必须是 ${MIN_CLASS_COUNT}-${MAX_CLASS_COUNT} 的整数`);
  }
  if (styleConfig.classificationMethod === "custom_breaks") {
    const boundaries = styleConfig.customBreaks || [];
    if (boundaries.length !== styleConfig.segments + 1) {
      errors.push(`需要 ${styleConfig.segments + 1} 个区间边界`);
    } else if (boundaries.some((value) => !Number.isFinite(value))) {
      errors.push("区间边界必须是有限数字");
    } else if (boundaries.some((value, index) => index > 0 && value <= boundaries[index - 1])) {
      errors.push("区间边界必须严格递增");
    }
  }
  if (styleConfig.colorType === "custom") {
    const colors = styleConfig.customColors || [];
    if (colors.length !== styleConfig.segments) {
      errors.push(`需要 ${styleConfig.segments} 个自定义颜色`);
    } else if (colors.some((color) => {
      try {
        parseColor(color);
        return false;
      } catch {
        return true;
      }
    })) {
      errors.push("自定义颜色格式无效");
    }
  }
  if (!Number.isFinite(styleConfig.opacity) || styleConfig.opacity < 0 || styleConfig.opacity > 1) {
    errors.push("透明度必须在 0 到 1 之间");
  }
  const paletteIndexes: Array<[number, number]> = [
    [styleConfig.singlePaletteIndex, SINGLE_COLOR_PALETTES.length],
    [styleConfig.gradientPaletteIndex, GRADIENT_PALETTES.length],
    [styleConfig.rainbowPaletteIndex, RAINBOW_PALETTES.length],
  ];
  if (
    paletteIndexes.some(
      ([index, length]) => !Number.isInteger(index) || index < 0 || index >= length,
    )
  ) {
    errors.push("色板索引无效");
  }
  if (
    [
      styleConfig.minSize,
      styleConfig.maxSize,
      styleConfig.minStrokeWidth,
      styleConfig.maxStrokeWidth,
      styleConfig.fixedStrokeWidth,
    ].some((value) => !Number.isFinite(value) || value <= 0)
  ) {
    errors.push("符号尺寸必须大于 0");
  }
  if (styleConfig.minSize > styleConfig.maxSize) errors.push("节点最小尺寸不能大于最大尺寸");
  if (styleConfig.minStrokeWidth > styleConfig.maxStrokeWidth) {
    errors.push("管线最小宽度不能大于最大宽度");
  }
  return { valid: errors.length === 0, errors };
};

export const hydrateStoredLayerStyleStates = (
  document: unknown,
  expectedVersion: number,
): LayerStyleState[] => {
  const defaults = createDefaultLayerStyleStates();
  if (!document || typeof document !== "object") return defaults;

  const storedDocument = document as {
    version?: number;
    layers?: Partial<Record<DefaultLayerStyleId, StyleConfig>>;
  };
  if (storedDocument.version !== expectedVersion || !storedDocument.layers) {
    return defaults;
  }

  return defaults.map((state) => {
    const stored = storedDocument.layers?.[state.layerId as DefaultLayerStyleId];
    if (!stored || !validateStyleConfig(stored).valid) return state;
    return {
      ...state,
      styleConfig: {
        ...stored,
        customBreaks: [...(stored.customBreaks || [])],
        customColors: [...(stored.customColors || [])],
      },
      legendConfig: { ...state.legendConfig, property: stored.property },
      isActive: false,
    };
  });
};

export const selectStoredLayerStyles = (states: LayerStyleState[]) =>
  Object.fromEntries(
    states
      .filter((state) => state.layerId === "junctions" || state.layerId === "pipes")
      .map((state) => [state.layerId, state.styleConfig]),
  ) as Partial<Record<DefaultLayerStyleId, StyleConfig>>;

export const requiresStyleApply = (
  applied: StyleConfig | undefined,
  draft: StyleConfig,
) =>
  !applied ||
  applied.property !== draft.property ||
  applied.classificationMethod !== draft.classificationMethod ||
  applied.segments !== draft.segments ||
  (draft.classificationMethod === "custom_breaks" &&
    !arraysEqual(applied.customBreaks, draft.customBreaks));

export const resolveStyleColors = (
  styleConfig: StyleConfig,
  classCount = styleConfig.segments,
): string[] => {
  if (styleConfig.colorType === "single") {
    const palette = SINGLE_COLOR_PALETTES[
      clampIndex(styleConfig.singlePaletteIndex, SINGLE_COLOR_PALETTES.length)
    ];
    return Array.from({ length: classCount }, () => palette.color);
  }
  if (styleConfig.colorType === "gradient") {
    const palette = GRADIENT_PALETTES[
      clampIndex(styleConfig.gradientPaletteIndex, GRADIENT_PALETTES.length)
    ];
    const start = parseColor(palette.start);
    const end = parseColor(palette.end);
    return Array.from({ length: classCount }, (_, index) => {
      const ratio = classCount > 1 ? index / (classCount - 1) : 0;
      return `rgba(${Math.round(start.r + (end.r - start.r) * ratio)}, ${Math.round(
        start.g + (end.g - start.g) * ratio,
      )}, ${Math.round(start.b + (end.b - start.b) * ratio)}, 1)`;
    });
  }
  if (styleConfig.colorType === "rainbow") {
    const palette = RAINBOW_PALETTES[
      clampIndex(styleConfig.rainbowPaletteIndex, RAINBOW_PALETTES.length)
    ];
    return Array.from(
      { length: classCount },
      (_, index) => palette.colors[index % palette.colors.length],
    );
  }
  return getDefaultCustomColors(classCount, styleConfig.customColors || []);
};

export const resolveDimensions = ({
  layerType,
  styleConfig,
  classCount = styleConfig.segments,
}: {
  layerType: string;
  styleConfig: StyleConfig;
  classCount?: number;
}) => {
  const interpolate = (minimum: number, maximum: number, index: number) => {
    const ratio = classCount > 1 ? index / (classCount - 1) : 0;
    return minimum + (maximum - minimum) * ratio;
  };
  if (layerType === "linestring") {
    return Array.from({ length: classCount }, (_, index) =>
      styleConfig.adjustWidthByProperty
        ? interpolate(styleConfig.minStrokeWidth, styleConfig.maxStrokeWidth, index)
        : styleConfig.fixedStrokeWidth,
    );
  }
  return Array.from({ length: classCount }, (_, index) =>
    interpolate(styleConfig.minSize, styleConfig.maxSize, index),
  );
};

export const resolveLayerStyle = ({
  layerType,
  styleConfig,
  values,
}: {
  layerType: string;
  styleConfig: StyleConfig;
  values: number[];
}): ResolvedLayerStyle | null => {
  const boundaries = resolveBoundaries(values, styleConfig);
  if (boundaries.length !== styleConfig.segments + 1) return null;
  const colors = resolveStyleColors(styleConfig, styleConfig.segments);
  const dimensions = resolveDimensions({ layerType, styleConfig });
  const isConstant = boundaries.every((value) => value === boundaries[0]);
  const labels = isConstant
    ? [`${formatBoundary(boundaries[0])}`]
    : Array.from(
        { length: styleConfig.segments },
        (_, index) => `${formatBoundary(boundaries[index])} - ${formatBoundary(boundaries[index + 1])}`,
      );
  return { boundaries, colors, dimensions, labels, isConstant };
};

const valueExpression = (property: string): any[] => ["get", property];

const buildVariableCase = (property: string, classCount: number, variablePrefix: string) => {
  const expression: any[] = ["case"];
  for (let index = 0; index < classCount - 1; index += 1) {
    expression.push(
      ["<=", valueExpression(property), ["var", `tj_break_${index + 1}`]],
      ["var", `${variablePrefix}_${index}`],
    );
  }
  expression.push(["var", `${variablePrefix}_${classCount - 1}`]);
  return expression;
};

export const buildDynamicStyleTemplate = ({
  layerType,
  property,
  classCount,
}: {
  layerType: string;
  property: string;
  classCount: number;
}): FlatStyleLike => {
  const color = buildVariableCase(property, classCount, "tj_color");
  const dimension = buildVariableCase(property, classCount, "tj_size");
  if (layerType === "linestring") {
    return { "stroke-color": color, "stroke-width": dimension };
  }
  return {
    "circle-fill-color": color,
    "circle-radius": ["interpolate", ["linear"], ["zoom"], 12, 1, 24, dimension],
    "circle-stroke-color": color,
    "circle-stroke-width": 2,
  };
};

export const buildStyleVariables = (
  styleConfig: StyleConfig,
  resolvedStyle: ResolvedLayerStyle,
): StyleVariables => {
  const variables: StyleVariables = {};
  resolvedStyle.boundaries.slice(1, -1).forEach((boundary, index) => {
    variables[`tj_break_${index + 1}`] = boundary;
  });
  resolvedStyle.colors.forEach((color, index) => {
    variables[`tj_color_${index}`] = withOpacity(color, styleConfig.opacity);
  });
  resolvedStyle.dimensions.forEach((dimension, index) => {
    variables[`tj_size_${index}`] = dimension;
  });
  return variables;
};

export const buildContourDefinitions = ({
  styleConfig,
  breaks,
  colors,
}: {
  styleConfig: StyleConfig;
  breaks: number[];
  colors: string[];
}) =>
  colors.map((color, index) => {
    const parsed = parseColor(color);
    return {
      threshold: [breaks[index], breaks[index + 1]],
      color: [parsed.r, parsed.g, parsed.b, Math.round(styleConfig.opacity * 255)],
      strokeWidth: 0,
    };
  });
