import type {
  ExpressionSpecification,
  Map as MapLibreMap,
  SymbolLayerSpecification
} from "maplibre-gl";
import { MAP_STYLE_TOKENS } from "./map-colors";
import { SOURCE_LAYERS } from "./sources";

export const VALUE_LABEL_LAYER_IDS = {
  pipes: "workbench-value-label-pipes",
  junctions: "workbench-value-label-junctions",
  valves: "workbench-value-label-valves",
  reservoirs: "workbench-value-label-reservoirs"
} as const;

export type ValueLabelSourceId = keyof typeof VALUE_LABEL_LAYER_IDS;

export type ValueLabelStyle = {
  sourceId: ValueLabelSourceId;
  property: string;
  precision: number;
  unit: string;
  showValue: boolean;
  showId: boolean;
  textSize: number;
  textColor: string;
};

const LABEL_SOURCE_IDS = Object.keys(VALUE_LABEL_LAYER_IDS) as ValueLabelSourceId[];

export function createValueLabelLayers(): SymbolLayerSpecification[] {
  return LABEL_SOURCE_IDS.map((sourceId) => {
    const line = sourceId === "pipes";
    return {
      id: VALUE_LABEL_LAYER_IDS[sourceId],
      type: "symbol",
      source: sourceId,
      "source-layer": SOURCE_LAYERS[sourceId],
      minzoom: sourceId === "junctions" ? 15 : 10,
      layout: {
        visibility: "none",
        "symbol-placement": line ? "line-center" : "point",
        "text-field": "",
        "text-size": 12,
        "text-offset": line ? [0, 0] : [0, -1.35],
        "text-anchor": line ? "center" : "bottom",
        "text-padding": 5,
        "text-allow-overlap": false,
        "text-ignore-placement": false,
        "text-keep-upright": true
      },
      paint: {
        "text-color": MAP_STYLE_TOKENS.label.primary,
        "text-halo-color": MAP_STYLE_TOKENS.label.inverse,
        "text-halo-width": 2,
        "text-halo-blur": 0.4,
        "text-opacity": 0.96
      }
    };
  });
}

export function applyValueLabelStyle(
  map: MapLibreMap,
  style: ValueLabelStyle
) {
  if (!validateValueLabelStyle(style)) {
    throw new Error("INVALID_VALUE_LABEL_STYLE");
  }
  const layerId = VALUE_LABEL_LAYER_IDS[style.sourceId];
  if (!map.getLayer(layerId)) {
    throw new Error("VALUE_LABEL_LAYER_UNAVAILABLE");
  }
  map.setFilter(
    layerId,
    style.showValue ? ["has", style.property] : null
  );
  map.setLayoutProperty(
    layerId,
    "text-field",
    createValueLabelTextExpression(style)
  );
  map.setLayoutProperty(layerId, "text-size", style.textSize);
  map.setPaintProperty(layerId, "text-color", style.textColor);
  map.setLayoutProperty(layerId, "visibility", "visible");
}

export function clearValueLabels(
  map: MapLibreMap,
  targetSourceId?: ValueLabelSourceId
) {
  const sourceIds = targetSourceId ? [targetSourceId] : LABEL_SOURCE_IDS;
  sourceIds.forEach((sourceId) => {
    const layerId = VALUE_LABEL_LAYER_IDS[sourceId];
    if (map.getLayer(layerId)) {
      map.setLayoutProperty(layerId, "visibility", "none");
    }
  });
}

function createValueLabelTextExpression(
  style: ValueLabelStyle
): ExpressionSpecification {
  const idExpression: ExpressionSpecification = [
    "to-string",
    ["coalesce", ["get", "id"], ["id"]]
  ];
  const valueExpression: ExpressionSpecification = [
    "concat",
    [
      "number-format",
      ["to-number", ["get", style.property], 0],
      {
        "min-fraction-digits": style.precision,
        "max-fraction-digits": style.precision
      }
    ],
    style.unit ? ` ${style.unit}` : ""
  ];
  if (style.showId && style.showValue) {
    return ["concat", idExpression, " · ", valueExpression];
  }
  return style.showId ? idExpression : valueExpression;
}

export function getNumericFeatureProperties(properties: Record<string, unknown> | null | undefined) {
  return Object.entries(properties ?? {})
    .filter((entry): entry is [string, number] => typeof entry[1] === "number" && Number.isFinite(entry[1]))
    .sort(([a], [b]) => a.localeCompare(b));
}

function validateValueLabelStyle(style: ValueLabelStyle) {
  return style.sourceId in VALUE_LABEL_LAYER_IDS
    && /^[A-Za-z_][A-Za-z0-9_]{0,63}$/.test(style.property)
    && Number.isInteger(style.precision)
    && style.precision >= 0
    && style.precision <= 3
    && style.unit.length <= 16
    && (style.showValue || style.showId)
    && Number.isFinite(style.textSize)
    && style.textSize >= 10
    && style.textSize <= 18
    && /^#[\dA-F]{6}$/i.test(style.textColor);
}
