import type { Map as MapLibreMap } from "maplibre-gl";
import { MAP_STYLE_TOKENS } from "./map-colors";
import { SUPPLY_LAYER_VISUALS } from "./map-layer-visuals";
import {
  createSupplyActiveOpacity,
  createSupplyStateColor,
  supplyFacilityOpacityExpression,
  supplyPipeColorExpression,
  supplyPipeDashExpression
} from "./supply-style-expressions";

export type LineLayerGroupStylePatch = {
  color?: string;
  opacity?: number;
  widthMultiplier?: number;
  dash?: "original" | "solid" | "dashed";
};

export type PointLayerGroupStylePatch = {
  fillColor?: string;
  strokeColor?: string;
  opacity?: number;
  radiusMultiplier?: number;
  strokeMultiplier?: number;
  iconMultiplier?: number;
};

export type LayerGroupStylePatch = LineLayerGroupStylePatch | PointLayerGroupStylePatch;
export type LayerGroupGeometry = "line" | "point";

export const LAYER_GROUP_GEOMETRIES = {
  pipes: "line",
  junctions: "point",
  valves: "point",
  reservoirs: "point"
} as const satisfies Record<string, LayerGroupGeometry>;

export type LayerGroupId = keyof typeof LAYER_GROUP_GEOMETRIES;

const LINE_CORE_LAYERS: Record<Extract<LayerGroupId, "pipes">, string> = {
  pipes: "pipes-flow"
};

const LINE_DEFAULT_WIDTHS = {
  "pipes-flow": SUPPLY_LAYER_VISUALS.pipes.width,
  "pipes-casing": SUPPLY_LAYER_VISUALS.pipes.casingWidth,
  "pipes-inactive": SUPPLY_LAYER_VISUALS.pipes.width,
  "pipes-inactive-casing": SUPPLY_LAYER_VISUALS.pipes.casingWidth,
  "pipes-hover-outline": SUPPLY_LAYER_VISUALS.pipes.hoverOutlineWidth,
  "pipes-hover": SUPPLY_LAYER_VISUALS.pipes.hoverWidth
} as const;

const POINT_DEFAULT_COLORS = {
  junctions: MAP_STYLE_TOKENS.supply.junction,
  valves: MAP_STYLE_TOKENS.supply.valve,
  reservoirs: MAP_STYLE_TOKENS.supply.reservoir
} as const;

const POINT_DEFAULT_OPACITIES = {
  junctions: 0.9,
  valves: supplyFacilityOpacityExpression,
  reservoirs: 0
} as const;

const POINT_DEFAULT_VISUALS = {
  junctions: SUPPLY_LAYER_VISUALS.junctions,
  valves: SUPPLY_LAYER_VISUALS.valves,
  reservoirs: SUPPLY_LAYER_VISUALS.reservoirs
};

export function validateLayerGroupStylePatch(groupId: string, patch: unknown): patch is LayerGroupStylePatch {
  if (!(groupId in LAYER_GROUP_GEOMETRIES) || !patch || typeof patch !== "object" || Array.isArray(patch)) return false;
  const geometry = LAYER_GROUP_GEOMETRIES[groupId as LayerGroupId];
  const values = patch as Record<string, unknown>;
  const allowed = geometry === "line"
    ? ["color", "opacity", "widthMultiplier", "dash"]
    : ["fillColor", "strokeColor", "opacity", "radiusMultiplier", "strokeMultiplier", "iconMultiplier"];
  if (Object.keys(values).some((key) => !allowed.includes(key))) return false;
  if ("color" in values && !isColor(values.color)) return false;
  if ("fillColor" in values && !isColor(values.fillColor)) return false;
  if ("strokeColor" in values && !isColor(values.strokeColor)) return false;
  if ("opacity" in values && !inRange(values.opacity, 0, 1)) return false;
  if ("widthMultiplier" in values && !inRange(values.widthMultiplier, 0.5, 3)) return false;
  if ("radiusMultiplier" in values && !inRange(values.radiusMultiplier, 0.5, 3)) return false;
  if ("strokeMultiplier" in values && !inRange(values.strokeMultiplier, 0.5, 3)) return false;
  if ("iconMultiplier" in values && !inRange(values.iconMultiplier, 0.5, 3)) return false;
  if ("dash" in values && !["original", "solid", "dashed"].includes(String(values.dash))) return false;
  return true;
}

export function applyLayerGroupStyle(map: MapLibreMap, groupId: LayerGroupId, patch: LayerGroupStylePatch) {
  const geometry = LAYER_GROUP_GEOMETRIES[groupId];
  if (geometry === "line") applyLineStyle(map, groupId as "pipes", patch as LineLayerGroupStylePatch);
  else applyPointStyle(map, groupId as "junctions" | "valves" | "reservoirs", patch as PointLayerGroupStylePatch);
}

export function resetLayerGroupStyle(map: MapLibreMap, groupId: LayerGroupId) {
  const geometry = LAYER_GROUP_GEOMETRIES[groupId];
  if (geometry === "line") resetLineStyle(map, groupId as "pipes");
  else resetPointStyle(map, groupId as "junctions" | "valves" | "reservoirs");
}

function applyLineStyle(map: MapLibreMap, groupId: "pipes", patch: LineLayerGroupStylePatch) {
  const prefix = groupId;
  const coreId = LINE_CORE_LAYERS[groupId];
  if (patch.color !== undefined) setPaint(map, coreId, "line-color", createSupplyStateColor(patch.color));
  if (patch.opacity !== undefined) setPaint(map, coreId, "line-opacity", createSupplyActiveOpacity(patch.opacity));
  if (patch.dash !== undefined) {
    const dash = patch.dash === "solid"
      ? [1, 0]
      : patch.dash === "dashed"
        ? [3, 2]
        : supplyPipeDashExpression;
    setPaint(map, coreId, "line-dasharray", dash);
    setPaint(map, `${prefix}-casing`, "line-dasharray", dash);
  }
  if (patch.widthMultiplier !== undefined) {
    for (const [id, value] of Object.entries(LINE_DEFAULT_WIDTHS)) {
      setPaint(map, id, "line-width", scale(value, patch.widthMultiplier));
    }
  }
}

function resetLineStyle(map: MapLibreMap, groupId: "pipes") {
  const coreId = LINE_CORE_LAYERS[groupId];
  setPaint(map, coreId, "line-color", supplyPipeColorExpression);
  setPaint(map, coreId, "line-opacity", createSupplyActiveOpacity(0.92));
  setPaint(map, coreId, "line-dasharray", supplyPipeDashExpression);
  setPaint(map, `${groupId}-casing`, "line-dasharray", supplyPipeDashExpression);
  for (const [id, value] of Object.entries(LINE_DEFAULT_WIDTHS)) {
    setPaint(map, id, "line-width", value);
  }
}

function applyPointStyle(map: MapLibreMap, groupId: "junctions" | "valves" | "reservoirs", patch: PointLayerGroupStylePatch) {
  const coreId = groupId;
  if (patch.fillColor !== undefined) setPaint(map, coreId, "circle-color", createSupplyStateColor(patch.fillColor));
  if (patch.strokeColor !== undefined) setPaint(map, coreId, "circle-stroke-color", createSupplyStateColor(patch.strokeColor));
  if (patch.opacity !== undefined) setPaint(map, coreId, "circle-opacity", patch.opacity);
  if (patch.radiusMultiplier !== undefined) {
    const visual = POINT_DEFAULT_VISUALS[groupId];
    const radii = {
      [coreId]: visual.radius,
      [`${groupId}-halo`]: visual.haloRadius,
      [`${groupId}-hover`]: visual.hoverRadius,
      [`${groupId}-selected`]: visual.selectedRadius,
      [`${groupId}-selected-outer`]: visual.selectedRadius
    };
    for (const [id, value] of Object.entries(radii)) {
      setPaint(map, id, "circle-radius", scale(value, patch.radiusMultiplier));
    }
  }
  if (patch.strokeMultiplier !== undefined) {
    setPaint(map, coreId, "circle-stroke-width", 2 * patch.strokeMultiplier);
  }
  if (patch.iconMultiplier !== undefined && "iconSize" in POINT_DEFAULT_VISUALS[groupId]) {
    setLayout(
      map,
      `${groupId}-symbol`,
      "icon-size",
      scale(POINT_DEFAULT_VISUALS[groupId].iconSize, patch.iconMultiplier)
    );
  }
}

function resetPointStyle(map: MapLibreMap, groupId: "junctions" | "valves" | "reservoirs") {
  const visual = POINT_DEFAULT_VISUALS[groupId];
  setPaint(map, groupId, "circle-color", createSupplyStateColor(POINT_DEFAULT_COLORS[groupId]));
  setPaint(map, groupId, "circle-opacity", POINT_DEFAULT_OPACITIES[groupId]);
  setPaint(map, groupId, "circle-stroke-width", 0);
  setPaint(map, groupId, "circle-radius", visual.radius);
  setPaint(map, `${groupId}-halo`, "circle-radius", visual.haloRadius);
  setPaint(map, `${groupId}-hover`, "circle-radius", visual.hoverRadius);
  setPaint(map, `${groupId}-selected`, "circle-radius", visual.selectedRadius);
  setPaint(map, `${groupId}-selected-outer`, "circle-radius", visual.selectedRadius);
  if ("iconSize" in visual) {
    setLayout(map, `${groupId}-symbol`, "icon-size", visual.iconSize);
  }
}

function scale(value: unknown, multiplier: number): unknown {
  return ["*", value, multiplier];
}

function setPaint(map: MapLibreMap, layerId: string, property: string, value: unknown) {
  if (map.getLayer(layerId)) map.setPaintProperty(layerId, property, (value ?? null) as never);
}

function setLayout(map: MapLibreMap, layerId: string, property: string, value: unknown) {
  if (map.getLayer(layerId) && typeof map.setLayoutProperty === "function") {
    map.setLayoutProperty(layerId, property, (value ?? null) as never);
  }
}

function inRange(value: unknown, min: number, max: number): value is number {
  return typeof value === "number" && Number.isFinite(value) && value >= min && value <= max;
}

function isColor(value: unknown): value is string {
  return typeof value === "string" && /^#[\da-f]{6}$/i.test(value);
}
