import type {
  ExpressionSpecification,
  Map as MapLibreMap,
  StyleImageInterface
} from "maplibre-gl";
import { WORKBENCH_INTERACTION_BEFORE_ID } from "./layers";
import { SUPPLY_LAYER_VISUALS } from "./map-layer-visuals";
import { SOURCE_LAYERS } from "./sources";

export const FLOW_LINE_LAYER_ID = "workbench-network-flow-forward";
export const FLOW_REVERSE_LINE_LAYER_ID = "workbench-network-flow-reverse";

const FLOW_FORWARD_IMAGE_ID = "workbench-network-flow-pattern-forward";
const FLOW_REVERSE_IMAGE_ID = "workbench-network-flow-pattern-reverse";
const FLOW_STATE_DIRECTION = "flowDirection";
const FLOW_PATTERN_WIDTH = 64;
const FLOW_PATTERN_HEIGHT = 8;
const FLOW_PATTERN_DURATION_MS = 1_200;

export type PipeFlowCalculation = {
  pipeId: string | number;
  velocity: number;
};

export type PipeFlowDirection = -1 | 0 | 1;

export type PipeFlowSummary = {
  total: number;
  forward: number;
  reverse: number;
  stopped: number;
  invalid: number;
};

type NormalizedPipeFlowCalculation = {
  pipeId: string | number;
  direction: PipeFlowDirection;
};

export type FlowOverlayState = {
  calculations: Map<string, NormalizedPipeFlowCalculation>;
  revision: number;
  visible: boolean;
};

export function createFlowOverlayState(): FlowOverlayState {
  return {
    calculations: new Map(),
    revision: 0,
    visible: false
  };
}

export async function showFlowOverlay(
  map: MapLibreMap,
  _state: FlowOverlayState,
  reducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches
) {
  ensureFlowImages(map, reducedMotion);
  ensureFlowLayers(map);
  setFlowMockVisibility(map, _state.calculations.size === 0);
  _state.visible = true;
  setFlowLayerVisibility(map, "visible");
}

export function hideFlowOverlay(map: MapLibreMap, _state: FlowOverlayState) {
  _state.visible = false;
  setFlowLayerVisibility(map, "none");
}

export async function replacePipeFlowCalculations(
  map: MapLibreMap,
  state: FlowOverlayState,
  records: readonly PipeFlowCalculation[]
): Promise<PipeFlowSummary> {
  const revision = ++state.revision;
  const { calculations: next, invalid } = normalizePipeFlowCalculations(records);
  const changes: FlowStateChange[] = [];
  setFlowMockVisibility(map, false);

  state.calculations.forEach((_, key) => {
    if (!next.has(key)) changes.push({ type: "remove", key });
  });
  next.forEach((calculation, key) => {
    const previous = state.calculations.get(key);
    if (
      previous?.direction !== calculation.direction
    ) {
      changes.push({ type: "set", key, calculation });
    }
  });

  if (state.visible && changes.length > 0) {
    setFlowLayerVisibility(map, "none");
  }
  await applyFlowStateChanges(map, state, changes, revision);
  if (state.visible && revision === state.revision) {
    setFlowLayerVisibility(map, "visible");
  }
  return summarizePipeFlowCalculations(state.calculations.values(), invalid);
}

export function updatePipeFlowCalculation(
  map: MapLibreMap,
  state: FlowOverlayState,
  record: PipeFlowCalculation
): PipeFlowSummary {
  const calculation = normalizePipeFlowCalculation(record);
  if (!calculation) throw new Error("INVALID_FLOW_CALCULATION");
  state.revision += 1;
  const key = String(calculation.pipeId);
  const previous = state.calculations.get(key);
  if (
    previous?.direction !== calculation.direction
  ) {
    setPipeFlowState(map, calculation);
    state.calculations.set(key, calculation);
  }
  if (state.visible) setFlowLayerVisibility(map, "visible");
  return summarizePipeFlowCalculations(state.calculations.values(), 0);
}

export function clearPipeFlowCalculations(
  map: MapLibreMap,
  state: FlowOverlayState
) {
  return replacePipeFlowCalculations(map, state, []).then((summary) => {
    if (state.visible) {
      setFlowMockVisibility(map, true);
    }
    return summary;
  });
}

export function getPipeFlowDirection(velocity: number): PipeFlowDirection {
  if (!Number.isFinite(velocity) || velocity === 0) return 0;
  return velocity < 0 ? -1 : 1;
}

type FlowStateChange =
  | { type: "remove"; key: string }
  | {
    type: "set";
    key: string;
    calculation: NormalizedPipeFlowCalculation;
  };

function normalizePipeFlowCalculations(records: readonly PipeFlowCalculation[]) {
  const calculations = new Map<string, NormalizedPipeFlowCalculation>();
  let invalid = 0;
  records.forEach((record) => {
    const normalized = normalizePipeFlowCalculation(record);
    if (!normalized) {
      invalid += 1;
      return;
    }
    calculations.set(String(normalized.pipeId), normalized);
  });
  return { calculations, invalid };
}

function normalizePipeFlowCalculation(
  record: PipeFlowCalculation
): NormalizedPipeFlowCalculation | null {
  if (
    (typeof record.pipeId !== "string" && typeof record.pipeId !== "number")
    || String(record.pipeId).trim().length === 0
    || !Number.isFinite(record.velocity)
  ) {
    return null;
  }
  return {
    pipeId: record.pipeId,
    direction: getPipeFlowDirection(record.velocity)
  };
}

function summarizePipeFlowCalculations(
  calculations: Iterable<NormalizedPipeFlowCalculation>,
  invalid: number
): PipeFlowSummary {
  const summary: PipeFlowSummary = {
    total: 0,
    forward: 0,
    reverse: 0,
    stopped: 0,
    invalid
  };
  for (const calculation of calculations) {
    summary.total += 1;
    if (calculation.direction > 0) summary.forward += 1;
    else if (calculation.direction < 0) summary.reverse += 1;
    else summary.stopped += 1;
  }
  return summary;
}

function setPipeFlowState(
  map: MapLibreMap,
  calculation: NormalizedPipeFlowCalculation
) {
  map.setFeatureState(
    {
      source: "pipes",
      sourceLayer: SOURCE_LAYERS.pipes,
      id: calculation.pipeId
    },
    {
      [FLOW_STATE_DIRECTION]: calculation.direction
    }
  );
}

async function applyFlowStateChanges(
  map: MapLibreMap,
  state: FlowOverlayState,
  changes: readonly FlowStateChange[],
  revision: number
) {
  const batchSize = 2_000;
  let offset = 0;

  await new Promise<void>((resolve) => {
    const applyBatch = () => {
      if (revision !== state.revision) {
        resolve();
        return;
      }
      const end = Math.min(offset + batchSize, changes.length);
      for (; offset < end; offset += 1) {
        const change = changes[offset];
        if (change.type === "remove") {
          removePipeFlowState(map, change.key);
          state.calculations.delete(change.key);
        } else {
          setPipeFlowState(map, change.calculation);
          state.calculations.set(change.key, change.calculation);
        }
      }
      if (offset >= changes.length) {
        resolve();
        return;
      }
      window.requestAnimationFrame(applyBatch);
    };
    applyBatch();
  });
}

function removePipeFlowState(map: MapLibreMap, id: string | number) {
  const target = {
    source: "pipes",
    sourceLayer: SOURCE_LAYERS.pipes,
    id
  };
  map.removeFeatureState(target, FLOW_STATE_DIRECTION);
}

function ensureFlowImages(map: MapLibreMap, reducedMotion: boolean) {
  if (!map.hasImage(FLOW_FORWARD_IMAGE_ID)) {
    map.addImage(
      FLOW_FORWARD_IMAGE_ID,
      createFlowPatternImage(1, reducedMotion),
      { pixelRatio: 1 }
    );
  }
  if (!map.hasImage(FLOW_REVERSE_IMAGE_ID)) {
    map.addImage(
      FLOW_REVERSE_IMAGE_ID,
      createFlowPatternImage(-1, reducedMotion),
      { pixelRatio: 1 }
    );
  }
}

function createFlowPatternImage(
  direction: -1 | 1,
  reducedMotion: boolean
): StyleImageInterface {
  let map: MapLibreMap | null = null;
  let lastOffset = Number.NaN;
  const data = new Uint8Array(FLOW_PATTERN_WIDTH * FLOW_PATTERN_HEIGHT * 4);

  const draw = (offset: number) => {
    for (let y = 0; y < FLOW_PATTERN_HEIGHT; y += 1) {
      const edgeFade = Math.sin((Math.PI * (y + 0.5)) / FLOW_PATTERN_HEIGHT);
      for (let x = 0; x < FLOW_PATTERN_WIDTH; x += 1) {
        const wrapped = (
          x - offset + FLOW_PATTERN_WIDTH
        ) % FLOW_PATTERN_WIDTH;
        const pulse = wrapped < 18
          ? Math.sin((Math.PI * wrapped) / 18)
          : 0;
        const alpha = Math.round(255 * pulse * edgeFade);
        const index = (y * FLOW_PATTERN_WIDTH + x) * 4;
        data[index] = 94;
        data[index + 1] = 234;
        data[index + 2] = 212;
        data[index + 3] = alpha;
      }
    }
  };

  draw(direction > 0 ? 0 : FLOW_PATTERN_WIDTH - 1);

  return {
    width: FLOW_PATTERN_WIDTH,
    height: FLOW_PATTERN_HEIGHT,
    data,
    onAdd(nextMap) {
      map = nextMap;
    },
    render() {
      if (reducedMotion) return false;
      const progress = (performance.now() % FLOW_PATTERN_DURATION_MS)
        / FLOW_PATTERN_DURATION_MS;
      const offset = Math.floor(
        progress * FLOW_PATTERN_WIDTH * direction
        + FLOW_PATTERN_WIDTH
      ) % FLOW_PATTERN_WIDTH;
      if (offset === lastOffset) {
        map?.triggerRepaint();
        return false;
      }
      lastOffset = offset;
      draw(offset);
      map?.triggerRepaint();
      return true;
    }
  };
}

const mockDirectionExpression: ExpressionSpecification = [
  "case",
  [
    "==",
    ["%", ["to-number", ["id"], 0], 2],
    0
  ],
  1,
  -1
];

const directionExpression = (
  direction: -1 | 1,
  visibleOpacity: number,
  mockVisible = false
): ExpressionSpecification => [
  "case",
  [
    "==",
    [
      "coalesce",
      ["feature-state", FLOW_STATE_DIRECTION],
      mockVisible ? mockDirectionExpression : 0
    ],
    direction
  ],
  visibleOpacity,
  0
];

function ensureFlowLayers(map: MapLibreMap) {
  if (!map.getLayer(FLOW_LINE_LAYER_ID)) {
    map.addLayer({
      id: FLOW_LINE_LAYER_ID,
      type: "line",
      source: "pipes",
      "source-layer": SOURCE_LAYERS.pipes,
      minzoom: SUPPLY_LAYER_VISUALS.pipes.minzoom,
      layout: flowLineLayout(),
      paint: {
        "line-pattern": FLOW_FORWARD_IMAGE_ID,
        "line-width": 4,
        "line-opacity": directionExpression(1, 0.92)
      }
    }, WORKBENCH_INTERACTION_BEFORE_ID);
  }

  if (!map.getLayer(FLOW_REVERSE_LINE_LAYER_ID)) {
    map.addLayer({
      id: FLOW_REVERSE_LINE_LAYER_ID,
      type: "line",
      source: "pipes",
      "source-layer": SOURCE_LAYERS.pipes,
      minzoom: SUPPLY_LAYER_VISUALS.pipes.minzoom,
      layout: flowLineLayout(),
      paint: {
        "line-pattern": FLOW_REVERSE_IMAGE_ID,
        "line-width": 4,
        "line-opacity": directionExpression(-1, 0.92)
      }
    }, WORKBENCH_INTERACTION_BEFORE_ID);
  }

}

function flowLineLayout() {
  return {
    visibility: "visible" as const,
    "line-cap": "round" as const,
    "line-join": "round" as const
  };
}

function setFlowLayerVisibility(
  map: MapLibreMap,
  visibility: "visible" | "none"
) {
  [
    FLOW_LINE_LAYER_ID,
    FLOW_REVERSE_LINE_LAYER_ID
  ].forEach((layerId) => {
    if (map.getLayer(layerId)) {
      map.setLayoutProperty(layerId, "visibility", visibility);
    }
  });
}

function setFlowMockVisibility(
  map: MapLibreMap,
  visible: boolean
) {
  const opacityByLayer = [
    [FLOW_LINE_LAYER_ID, "line-opacity", 1, 0.92],
    [FLOW_REVERSE_LINE_LAYER_ID, "line-opacity", -1, 0.92]
  ] as const;
  opacityByLayer.forEach(([layerId, property, direction, opacity]) => {
    if (map.getLayer(layerId)) {
      map.setPaintProperty(
        layerId,
        property,
        directionExpression(direction, opacity, visible)
      );
    }
  });
}
