import {
  Map as MapLibreMap,
  NavigationControl,
  Popup,
  addProtocol,
} from "../vendor/maplibre/maplibre-gl.mjs";
import { yardGeoJSON } from "./data/yard.js";
import { resolveFeatureLinks, resolveFeatureStyle } from "./lib/feature-types.js";

const MAP_MAX_ZOOM = 24;
const MANDAN_BOUNDS = [
  [-101.05, 46.74],
  [-100.75, 46.93],
];
const TIMELINE_PRESETS = [
  { id: "purchase", label: "Axvigs purchase house", date: "2021-02-01" },
  { id: "trees-2021", label: "2021 trees planted", date: "2021-09-22" },
  { id: "today", label: "Today", date: getTodayDateString() },
];

const mapElement = document.getElementById("map");
const statusElement = document.getElementById("map-status");
const legend = document.getElementById("legend");
const protocol = new globalThis.pmtiles.Protocol();
addProtocol("pmtiles", protocol.tile);
const basemapLayers = globalThis.basemaps
  .layers("protomaps", globalThis.basemaps.namedFlavor("light"), { lang: "en" })
  .filter((layer) => layer.type !== "symbol");

const map = new MapLibreMap({
  container: mapElement,
  center: [-100.89875, 46.82682],
  zoom: 20,
  maxZoom: MAP_MAX_ZOOM,
  maxBounds: MANDAN_BOUNDS,
  style: {
    version: 8,
    sources: {
      protomaps: {
        type: "vector",
        url: `pmtiles://${new URL("./tiles/mandan.pmtiles", document.baseURI).href}`,
        attribution:
          '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap contributors</a>',
      },
    },
    layers: basemapLayers,
  },
});

map.addControl(new NavigationControl(), "bottom-right");

let selectedTimelineDate = TIMELINE_PRESETS.at(-1).date;
let renderedLayerIds = [];
let renderedSourceIds = [];
let renderedLayerHandlers = [];

map.on("load", () => {
  renderYardForDate(selectedTimelineDate);
  addTimelineControl();
  map.fitBounds(getGeoJSONBounds(yardGeoJSON), {
    padding: 70,
    maxZoom: MAP_MAX_ZOOM,
    duration: 0,
  });
});

map.on("sourcedata", (event) => {
  if (event.sourceId === "protomaps" && event.isSourceLoaded) {
    statusElement.hidden = true;
  }
});

map.on("error", (event) => {
  const message = event.error?.message || "The vector tile archive could not be loaded.";
  statusElement.textContent = `Map unavailable: ${message}`;
  statusElement.hidden = false;
});

function renderYardForDate(date) {
  removeRenderedYard();
  const features = yardGeoJSON.features.filter((feature) => featureExistsOnDate(feature, date));

  features.forEach((feature, index) => addFeatureLayer(feature, index));
  renderLegend(features);
  syncTimelineButtons();
}

function removeRenderedYard() {
  for (const { layerId, event, handler } of renderedLayerHandlers) {
    map.off(event, layerId, handler);
  }
  for (const layerId of renderedLayerIds.reverse()) {
    if (map.getLayer(layerId)) {
      map.removeLayer(layerId);
    }
  }
  for (const sourceId of renderedSourceIds) {
    if (map.getSource(sourceId)) {
      map.removeSource(sourceId);
    }
  }
  renderedLayerIds = [];
  renderedSourceIds = [];
  renderedLayerHandlers = [];
}

function addFeatureLayer(feature, index) {
  const sourceId = `yard-source-${index}`;
  const layerId = `yard-layer-${index}`;
  const style = resolveFeatureStyle(feature.properties.kind, feature.properties.details);
  const layer = buildMapLibreLayer(layerId, sourceId, feature, style);

  map.addSource(sourceId, { type: "geojson", data: feature });
  map.addLayer(layer);
  renderedSourceIds.push(sourceId);
  renderedLayerIds.push(layerId);

  const handleMouseEnter = () => {
    map.getCanvas().style.cursor = "pointer";
  };
  const handleMouseLeave = () => {
    map.getCanvas().style.cursor = "";
  };
  const handleClick = (event) => {
    const coordinates =
      feature.geometry.type === "Point"
        ? feature.geometry.coordinates
        : [event.lngLat.lng, event.lngLat.lat];
    new Popup({ offset: 10 })
      .setLngLat(coordinates)
      .setHTML(buildPopupContent(feature.properties))
      .addTo(map);
  };

  for (const [event, handler] of [
    ["mouseenter", handleMouseEnter],
    ["mouseleave", handleMouseLeave],
    ["click", handleClick],
  ]) {
    map.on(event, layerId, handler);
    renderedLayerHandlers.push({ layerId, event, handler });
  }
}

function buildMapLibreLayer(id, source, feature, style) {
  if (feature.geometry.type === "Polygon" || feature.geometry.type === "MultiPolygon") {
    return {
      id,
      source,
      type: "fill",
      paint: {
        "fill-color": style.fillColor ?? style.color ?? "#333333",
        "fill-opacity": style.fillOpacity ?? 0.25,
        "fill-outline-color": style.color ?? "#333333",
      },
    };
  }

  if (feature.geometry.type === "LineString" || feature.geometry.type === "MultiLineString") {
    const layer = {
      id,
      source,
      type: "line",
      paint: {
        "line-color": style.color ?? "#333333",
        "line-opacity": style.opacity ?? 1,
        "line-width": style.weight ?? 2,
      },
    };
    if (style.dashArray) {
      layer.paint["line-dasharray"] = style.dashArray.split(/\s+/).map(Number);
    }
    return layer;
  }

  return {
    id,
    source,
    type: "circle",
    paint: {
      "circle-color": style.fillColor ?? style.color ?? "#333333",
      "circle-opacity": style.fillOpacity ?? 0.85,
      "circle-stroke-color": style.color ?? "#333333",
      "circle-stroke-width": style.weight ?? 2,
      "circle-radius": getCircleRadius(style, feature.geometry.coordinates[1]),
    },
  };
}

function getCircleRadius(style, latitude) {
  const radiusMeters =
    style.radiusMeters ??
    (typeof style.iconDiameterMeters === "number" ? style.iconDiameterMeters / 2 : null);
  if (radiusMeters === null) {
    return style.radius ?? 5;
  }

  const metersPerPixelAtZoomZero = 156543.03392 * Math.cos((latitude * Math.PI) / 180);
  return [
    "interpolate",
    ["exponential", 2],
    ["zoom"],
    15,
    (radiusMeters * 2 ** 15) / metersPerPixelAtZoomZero,
    MAP_MAX_ZOOM,
    (radiusMeters * 2 ** MAP_MAX_ZOOM) / metersPerPixelAtZoomZero,
  ];
}

function featureExistsOnDate(feature, date) {
  const { plantedDate, removedDate } = feature.properties;
  return (!plantedDate || plantedDate <= date) && (!removedDate || date <= removedDate);
}

const timelineButtons = new globalThis.Map();

function addTimelineControl() {
  class TimelineControl {
    onAdd() {
      const container = document.createElement("div");
      container.className = "maplibregl-ctrl timeline-control";

      const title = document.createElement("div");
      title.className = "timeline-control-title";
      title.textContent = "Timeline";

      const buttonGroup = document.createElement("div");
      buttonGroup.className = "timeline-buttons";
      buttonGroup.setAttribute("role", "group");
      buttonGroup.setAttribute("aria-label", "Choose map date");

      for (const preset of TIMELINE_PRESETS) {
        const button = document.createElement("button");
        button.type = "button";
        button.textContent = preset.label;
        button.title = preset.date;
        button.addEventListener("click", () => {
          selectedTimelineDate = preset.date;
          renderYardForDate(selectedTimelineDate);
        });
        timelineButtons.set(preset.id, button);
        buttonGroup.append(button);
      }

      container.append(title, buttonGroup);
      return container;
    }

    onRemove() {}
  }

  map.addControl(new TimelineControl(), "top-right");
  syncTimelineButtons();
}

function syncTimelineButtons() {
  for (const preset of TIMELINE_PRESETS) {
    const button = timelineButtons.get(preset.id);
    if (!button) continue;
    const isSelected = preset.date === selectedTimelineDate;
    button.classList.toggle("is-selected", isSelected);
    button.setAttribute("aria-pressed", String(isSelected));
  }
}

function renderLegend(features) {
  const summary = new globalThis.Map();

  for (const feature of features) {
    const key = feature.properties.kind;
    const style = resolveFeatureStyle(key, feature.properties.details);
    const existing = summary.get(key) ?? {
      label: feature.properties.label,
      count: 0,
      color: style.fillColor ?? style.color ?? "#333333",
    };
    existing.count += 1;
    summary.set(key, existing);
  }

  legend.replaceChildren(
    ...[...summary.values()].map((entry) => {
      const row = document.createElement("div");
      row.className = "legend-item";
      const swatch = document.createElement("span");
      swatch.className = "legend-swatch";
      swatch.style.background = entry.color;
      const label = document.createElement("span");
      label.textContent = `${entry.label} (${entry.count})`;
      row.append(swatch, label);
      return row;
    }),
  );
}

function getTodayDateString(date = new Date()) {
  const year = date.getFullYear();
  const month = String(date.getMonth() + 1).padStart(2, "0");
  const day = String(date.getDate()).padStart(2, "0");
  return `${year}-${month}-${day}`;
}

function getGeoJSONBounds(collection) {
  let west = Infinity;
  let south = Infinity;
  let east = -Infinity;
  let north = -Infinity;

  visitCoordinates(collection.features.map((feature) => feature.geometry.coordinates), ([lon, lat]) => {
    west = Math.min(west, lon);
    south = Math.min(south, lat);
    east = Math.max(east, lon);
    north = Math.max(north, lat);
  });
  return [
    [west, south],
    [east, north],
  ];
}

function visitCoordinates(coordinates, callback) {
  if (typeof coordinates[0] === "number") {
    callback(coordinates);
    return;
  }
  for (const coordinate of coordinates) {
    visitCoordinates(coordinate, callback);
  }
}

function buildPopupContent(properties) {
  const { kind, label, name, plantedDate, removedDate, details, parentId, groupIds } = properties;
  const entries = Object.entries(details ?? {}).filter(
    ([key, value]) => value !== null && value !== undefined && shouldShowDetailField(key),
  );
  const lifecycleEntries = [];
  const relationshipEntries = [];
  const linkEntries = resolveFeatureLinks(kind, details);

  if (plantedDate) lifecycleEntries.push(["plantedDate", plantedDate]);
  if (removedDate) lifecycleEntries.push(["removedDate", removedDate]);
  if (parentId) relationshipEntries.push(["parentFeature", parentId]);
  if (groupIds?.length) relationshipEntries.push(["groups", groupIds.join(", ")]);

  const title = `<strong>${escapeHtml(name)}</strong>`;
  const subtitle = name === label ? "" : `<div>${escapeHtml(label)}</div>`;
  const allEntries = [...lifecycleEntries, ...entries, ...relationshipEntries];
  const detailRows = allEntries
    .map(
      ([key, value]) =>
        `<div><span>${escapeHtml(formatDetailLabel(key))}:</span> ${escapeHtml(formatDetailValue(key, value))}</div>`,
    )
    .join("");
  const linkRows = linkEntries
    .map(
      (link) =>
        `<div><a href="${escapeHtml(link.url)}" target="_blank" rel="noreferrer">${escapeHtml(link.label)}</a></div>`,
    )
    .join("");
  return `${title}${subtitle}<div>${detailRows}${linkRows}</div>`;
}

function shouldShowDetailField(key) {
  return !key.endsWith("Url") && key !== "gardenOrgId";
}

function formatDetailLabel(key) {
  const labels = {
    canopyDiameterMeters: "Canopy diameter",
    heightMeters: "Height",
    plantedDate: "Planted",
    removedDate: "Removed",
    commonName: "Common name",
    genus: "Genus",
    cultivar: "Cultivar",
    bloomColor: "Bloom color",
    bloomSeason: "Bloom season",
    species: "Species",
    fenceType: "Fence type",
    axis: "Axis",
    offsetFeet: "Offset",
    spacingFeet: "Spacing",
    note: "Note",
    parentFeature: "Parent",
    groups: "Groups",
  };
  return labels[key] ?? key;
}

function formatDetailValue(key, value) {
  if (key === "canopyDiameterMeters" || key === "heightMeters") return `${value} m`;
  if (key === "offsetFeet" || key === "spacingFeet") return `${value} ft`;
  return String(value);
}

function escapeHtml(value) {
  return String(value)
    .replaceAll("&", "&amp;")
    .replaceAll("<", "&lt;")
    .replaceAll(">", "&gt;")
    .replaceAll('"', "&quot;")
    .replaceAll("'", "&#39;");
}
