import maplibregl from "maplibre-gl";
import polylabel from "polylabel";
import { BACKGROUND_LAYER_OPTIONS, BackgroundLayerVisibility } from "@/uhm/lib/map/styles/backgroundLayers";
import { Feature, FeatureCollection, Geometry } from "@/uhm/lib/editor/state/useEditorState";
import {
    FEATURE_STATE_SOURCE_IDS,
    PATH_ARROW_ICON_ID,
    RASTER_BASE_LAYER_ID,
    RASTER_BASE_SOURCE_ID,
    PATH_ARROW_SOURCE_ID
} from "@/uhm/lib/map/constants";
import { PATH_RENDER_BY_TYPE } from "@/uhm/lib/map/styles/style";
import { getBackgroundRasterSourceSpecification } from "@/uhm/api/tiles";
import { newId } from "@/uhm/lib/utils/id";
import { normalizeGeoTypeKey } from "@/uhm/lib/map/geo/geoTypeMap";
import { normalizeBoundWithId, normalizeFeatureBoundWith } from "@/uhm/lib/editor/geometry/geometryBinding";
import type { EntityLabelCandidate } from "@/uhm/types/geo";

type Coordinate = [number, number];
type PolygonCoordinates = Coordinate[][];
type FeatureLabelInfo = {
    entityId: string;
    label: string;
    timeEnd: number | null;
};
const rasterBaseVisibilityGenerationByMap = new WeakMap<maplibregl.Map, number>();

const resolverCache = new WeakMap<
    FeatureCollection,
    Map<number | null | undefined, (feature: Feature) => string | null>
>();

const featureLabelInfoCache = new WeakMap<
    Feature,
    Map<number | null | undefined, FeatureLabelInfo | null>
>();

export function applyBackgroundLayerVisibility(
    map: maplibregl.Map,
    visibility: BackgroundLayerVisibility
) {
    syncRasterBaseVisibility(map, visibility[RASTER_BASE_LAYER_ID]);

    for (const layer of BACKGROUND_LAYER_OPTIONS) {
        if (layer.id === RASTER_BASE_LAYER_ID) continue;
        const nextVisibility = visibility[layer.id] ? "visible" : "none";

        if (map.getLayer(layer.id)) {
            map.setLayoutProperty(layer.id, "visibility", nextVisibility);
        }

        const groupedLayerIds = getBackgroundGroupLayerIds(map, layer.id);
        for (const groupedLayerId of groupedLayerIds) {
            if (!map.getLayer(groupedLayerId)) continue;
            map.setLayoutProperty(groupedLayerId, "visibility", nextVisibility);
        }
    }
}

export function syncRasterBaseVisibility(map: maplibregl.Map, shouldShow: boolean) {
    const generation = nextRasterBaseVisibilityGeneration(map);
    const isCurrentRequest = () => rasterBaseVisibilityGenerationByMap.get(map) === generation;

    if (shouldShow) {
        void ensureRasterBaseLayer(map, isCurrentRequest).catch((error) => {
            console.error("Failed to load proxied raster background.", error);
            if (isCurrentRequest()) {
                removeRasterBaseLayer(map);
            }
        });
        return;
    }
    removeRasterBaseLayer(map);
}

function nextRasterBaseVisibilityGeneration(map: maplibregl.Map) {
    const next = (rasterBaseVisibilityGenerationByMap.get(map) || 0) + 1;
    rasterBaseVisibilityGenerationByMap.set(map, next);
    return next;
}

export async function ensureRasterBaseLayer(
    map: maplibregl.Map,
    isCurrentRequest: () => boolean = () => true
) {
    if (!map.getSource(RASTER_BASE_SOURCE_ID)) {
        const source = await createRasterBaseSource();
        if (!isCurrentRequest()) return;
        if (map.getSource(RASTER_BASE_SOURCE_ID)) {
            // Another caller already added the source while we were waiting.
        } else {
            map.addSource(RASTER_BASE_SOURCE_ID, source);
        }
    }

    if (!isCurrentRequest()) return;

    const beforeId = getRasterBaseInsertBeforeLayerId(map);
    if (!map.getLayer(RASTER_BASE_LAYER_ID)) {
        map.addLayer(createRasterBaseLayer(), beforeId);
    } else if (beforeId && beforeId !== RASTER_BASE_LAYER_ID) {
        map.moveLayer(RASTER_BASE_LAYER_ID, beforeId);
    }

    if (!isCurrentRequest()) return;
    map.setLayoutProperty(RASTER_BASE_LAYER_ID, "visibility", "visible");
}

export function removeRasterBaseLayer(map: maplibregl.Map) {
    if (map.getLayer(RASTER_BASE_LAYER_ID)) {
        map.removeLayer(RASTER_BASE_LAYER_ID);
    }

    if (map.getSource(RASTER_BASE_SOURCE_ID)) {
        map.removeSource(RASTER_BASE_SOURCE_ID);
    }
}

export function createRasterBaseSource() {
    return getBackgroundRasterSourceSpecification();
}

export function createRasterBaseLayer() {
    return {
        id: RASTER_BASE_LAYER_ID,
        type: "raster" as const,
        source: RASTER_BASE_SOURCE_ID,
        paint: {
            "raster-opacity": 0.92,
            "raster-resampling": "linear" as const,
        },
    };
}

function getRasterBaseInsertBeforeLayerId(map: maplibregl.Map): string | undefined {
    const style = map.getStyle();
    const layers = style?.layers || [];

    return layers.find((layer) => {
        return layer.id !== "background" && layer.id !== RASTER_BASE_LAYER_ID;
    })?.id;
}

function getBackgroundGroupLayerIds(
    map: maplibregl.Map,
    groupId: string
): string[] {
    const style = map.getStyle();
    if (!style?.layers?.length) return [];

    return style.layers
        .filter((layer) => {
            const metadata = (layer as { metadata?: Record<string, unknown> }).metadata;
            return metadata?.uhmBackgroundGroupId === groupId;
        })
        .map((layer) => layer.id);
}

export function getSelectableLayers(map: maplibregl.Map): string[] {
    const selectableSources = ["countries", "places", PATH_ARROW_SOURCE_ID];
    const style = map.getStyle();
    if (!style || !style.layers) return [];

    return style.layers
        .filter((layer) => "source" in layer && selectableSources.includes(layer.source as string))
        .map((layer) => layer.id);
}

export function filterDraftByBinding(
    fc: FeatureCollection,
    selectedFeatureIds: (string | number)[],
    highlightFeatures?: FeatureCollection | null,
    isPreviewMode?: boolean
): FeatureCollection {
    const selectedIds = new Set(selectedFeatureIds.map(String));
    if (highlightFeatures?.features) {
        for (const f of highlightFeatures.features) {
            if (f.properties?.id != null) selectedIds.add(String(f.properties.id));
        }
    }

    const childIds = new Set<string>();
    const parentIds = new Set<string>();
    const featureParentMap = new Map<string, string>(); // childId -> parentId

    for (const feature of fc.features) {
        const featureId = String(feature.properties.id);
        const parentId = normalizeFeatureBoundWith(feature);
        if (parentId) {
            childIds.add(featureId);
            parentIds.add(parentId);
            featureParentMap.set(featureId, parentId);
        }
    }

    if (selectedIds.size === 0) {
        return { ...fc, features: fc.features.filter((f) => !childIds.has(String(f.properties.id))) };
    }

    const activeParents = new Set<string>();
    for (const id of selectedIds) {
        if (parentIds.has(id)) {
            activeParents.add(id);
        } else {
            const parentId = featureParentMap.get(id);
            if (parentId) {
                activeParents.add(parentId);
            }
        }
    }

    return {
        ...fc,
        features: fc.features.filter((feature) => {
            const featureId = String(feature.properties.id);
            const parentId = featureParentMap.get(featureId);

            // 1. If this feature is a parent and its hierarchy is active, hide it (only in preview/replay modes)
            if (isPreviewMode && activeParents.has(featureId)) {
                return false;
            }

            // 2. If this feature is a child of an active parent, show it
            if (parentId && activeParents.has(parentId)) {
                return true;
            }

            // 3. By default, hide all child geometries that are not part of the active hierarchy
            return !childIds.has(featureId);
        }),
    };
}

export function filterDraftByGeometryVisibility(
    fc: FeatureCollection,
    visibility: Record<string, boolean> | null | undefined
): FeatureCollection {
    if (!visibility) return fc;

    return {
        ...fc,
        features: fc.features.filter((feature) => {
            const id = String(feature.properties.id);
            // Kiểm tra ẩn theo ID cụ thể (ưu tiên cao nhất)
            if (visibility[id] === false) return false;

            const key = getFeatureSemanticType(feature);
            if (!key) return true;
            // Kiểm tra ẩn theo loại (semantic type)
            return visibility[key] !== false;
        }),
    };
}

export function splitDraftFeatures(fc: FeatureCollection) {
    const polygons = {
        type: "FeatureCollection",
        features: fc.features.filter((f) =>
            f.geometry.type !== "Point" && f.geometry.type !== "MultiPoint"
        ),
    } as FeatureCollection;

    const points = {
        type: "FeatureCollection",
        features: fc.features.filter((f) =>
            f.geometry.type === "Point" || f.geometry.type === "MultiPoint"
        ),
    } as FeatureCollection;

    return { polygons, points };
}

export function decoratePointFeaturesWithLabels(
    fc: FeatureCollection,
    labelContext: FeatureCollection = fc,
    timelineYear?: number | null
): FeatureCollection {
    const getLabel = getFeatureLabelResolver(labelContext, timelineYear);
    let changed = false;
    const nextFeatures = fc.features.map((feature) => {
        const point_label = getLabel(feature);
        if (feature.properties.point_label === point_label) {
            return feature;
        }
        changed = true;
        return {
            ...feature,
            properties: {
                ...feature.properties,
                point_label,
            },
        };
    });
    return changed ? { ...fc, features: nextFeatures } : fc;
}

export function decorateLineFeaturesWithLabels(
    fc: FeatureCollection,
    labelContext: FeatureCollection = fc,
    timelineYear?: number | null
): FeatureCollection {
    const getLabel = getFeatureLabelResolver(labelContext, timelineYear);
    let changed = false;
    const nextFeatures = fc.features.map((feature) => {
        const line_label = isLineGeometry(feature.geometry) ? getLabel(feature) : null;
        if (feature.properties.line_label === line_label) {
            return feature;
        }
        changed = true;
        return {
            ...feature,
            properties: {
                ...feature.properties,
                line_label,
            },
        };
    });
    return changed ? { ...fc, features: nextFeatures } : fc;
}

const polygonLabelFeaturesCache = new WeakMap<Feature, { label: string; feature: Feature }>();

export function buildPolygonLabelFeatureCollection(
    fc: FeatureCollection,
    labelContext: FeatureCollection = fc,
    timelineYear?: number | null
): FeatureCollection {
    const getLabel = getFeatureLabelResolver(labelContext, timelineYear);
    const features: Feature[] = [];

    for (const feature of fc.features) {
        const label = getLabel(feature);
        if (!label) continue;

        const cached = polygonLabelFeaturesCache.get(feature);
        if (cached && cached.label === label) {
            features.push(cached.feature);
            continue;
        }

        const labelPoint = getPolygonLabelPoint(feature.geometry, feature.properties.id);
        if (!labelPoint) continue;

        const labelFeature: Feature = {
            type: "Feature",
            properties: {
                ...feature.properties,
                id: `${feature.properties.id}:polygon-label`,
                polygon_label: label,
            },
            geometry: {
                type: "Point",
                coordinates: labelPoint,
            },
        };
        polygonLabelFeaturesCache.set(feature, { label, feature: labelFeature });
        features.push(labelFeature);
    }

    return { type: "FeatureCollection", features };
}

export function setSelectedFeatureState(
    map: maplibregl.Map,
    id: string | number | null,
    selected: boolean
) {
    if (id === null) return;
    for (const sourceId of FEATURE_STATE_SOURCE_IDS) {
        if (!map.getSource(sourceId)) continue;
        map.setFeatureState({ source: sourceId, id }, { selected });
    }
}

export function fitMapToFeatureCollection(
    map: maplibregl.Map,
    fc: FeatureCollection,
    padding?: number | maplibregl.PaddingOptions,
    options?: {
        duration?: number;
        maxZoom?: number;
        pointZoom?: number;
    }
): boolean {
    const bbox = getFeatureCollectionBBox(fc);
    if (!bbox) return false;

    const resolvedPadding = typeof padding === "number" || padding ? padding : 58;
    const duration = options?.duration ?? 0;
    const maxZoom = options?.maxZoom ?? 7;
    const pointZoom = options?.pointZoom ?? 6;

    const lngSpan = Math.abs(bbox.maxLng - bbox.minLng);
    const latSpan = Math.abs(bbox.maxLat - bbox.minLat);
    if (lngSpan < 0.000001 && latSpan < 0.000001) {
        map.easeTo({
            center: [bbox.minLng, bbox.minLat],
            zoom: pointZoom,
            padding: resolvedPadding,
            duration,
        });
        return true;
    }

    map.fitBounds(
        [
            [bbox.minLng, bbox.minLat],
            [bbox.maxLng, bbox.maxLat],
        ],
        {
            padding: resolvedPadding,
            maxZoom,
            duration,
        }
    );
    return true;
}

export function getFeatureCollectionBBox(
    fc: FeatureCollection
): { minLng: number; minLat: number; maxLng: number; maxLat: number } | null {
    const points = fc.features.flatMap((feature) => collectCoordinatePairs(feature.geometry.coordinates));
    if (!points.length) return null;

    let minLng = Number.POSITIVE_INFINITY;
    let minLat = Number.POSITIVE_INFINITY;
    let maxLng = Number.NEGATIVE_INFINITY;
    let maxLat = Number.NEGATIVE_INFINITY;

    for (const [lng, lat] of points) {
        minLng = Math.min(minLng, lng);
        minLat = Math.min(minLat, lat);
        maxLng = Math.max(maxLng, lng);
        maxLat = Math.max(maxLat, lat);
    }

    return { minLng, minLat, maxLng, maxLat };
}

export function collectCoordinatePairs(value: unknown): Array<[number, number]> {
    if (!Array.isArray(value)) return [];
    if (
        value.length >= 2 &&
        typeof value[0] === "number" &&
        typeof value[1] === "number" &&
        Number.isFinite(value[0]) &&
        Number.isFinite(value[1])
    ) {
        return [[value[0], value[1]]];
    }
    return value.flatMap((item) => collectCoordinatePairs(item));
}

export function getGeometryRepresentativePoint(geometry: Geometry): Coordinate | null {
    if (geometry.type === "Point") {
        return normalizeCoordinate(geometry.coordinates);
    }

    if (geometry.type === "MultiPoint") {
        return getAverageCoordinate(geometry.coordinates);
    }

    if (geometry.type === "LineString") {
        return getLineMidpointCoordinate(geometry.coordinates);
    }

    if (geometry.type === "MultiLineString") {
        let bestLine: Coordinate[] | null = null;
        let bestLength = -1;
        for (const line of geometry.coordinates) {
            const length = getLineLength(line);
            if (length > bestLength) {
                bestLength = length;
                bestLine = line;
            }
        }
        return bestLine ? getLineMidpointCoordinate(bestLine) : null;
    }

    if (geometry.type === "Polygon" || geometry.type === "MultiPolygon") {
        return getPolygonLabelPoint(geometry);
    }

    return null;
}

const pathArrowGeometriesCache = new WeakMap<Geometry, Geometry[]>();
const pathArrowGeometriesL2Cache = new Map<string, Geometry[]>();
const polygonLabelPointL2Cache = new Map<string, Coordinate | null>();
const MAX_L2_CACHE_SIZE = 1000;

export function clearGeometryCaches() {
    pathArrowGeometriesL2Cache.clear();
    polygonLabelPointL2Cache.clear();
}

function getGeometryFingerprint(geometry: Geometry): string {
    const coords = geometry.coordinates;
    if (!Array.isArray(coords)) return geometry.type;

    if (typeof coords[0] === "number") {
        return `${geometry.type}:${coords[0]},${coords[1]}`;
    }

    const flat = coords.flat(4) as number[];
    if (flat.length === 0) return geometry.type;

    const len = flat.length;
    const first = flat[0];
    const last = flat[len - 1];
    const mid = flat[Math.floor(len / 2)];
    return `${geometry.type}:${len}:${first}:${mid}:${last}`;
}

export function buildPathArrowFeatureCollection(fc: FeatureCollection): FeatureCollection {
    const features: Feature[] = [];

    for (const feature of fc.features) {
        if (!isPathFeature(feature)) continue;

        let arrowGeometries = pathArrowGeometriesCache.get(feature.geometry);
        if (!arrowGeometries) {
            const featureId = feature.properties?.id;
            const fingerprint = getGeometryFingerprint(feature.geometry);
            const cacheKey = featureId ? `${featureId}:${fingerprint}` : null;

            if (cacheKey && pathArrowGeometriesL2Cache.has(cacheKey)) {
                arrowGeometries = pathArrowGeometriesL2Cache.get(cacheKey)!;
                pathArrowGeometriesCache.set(feature.geometry, arrowGeometries);
            } else {
                arrowGeometries = [];
                const coordinateGroups = getLineCoordinateGroups(feature.geometry);
                const featureType = getFeatureSemanticType(feature);
                const isRetreat = featureType === "retreat_route";
                for (const coordinates of coordinateGroups) {
                    const geometry = buildPathArrowGeometry(coordinates, isRetreat);
                    if (geometry) arrowGeometries.push(geometry);
                }
                pathArrowGeometriesCache.set(feature.geometry, arrowGeometries);
                if (cacheKey) {
                    if (pathArrowGeometriesL2Cache.size >= MAX_L2_CACHE_SIZE) {
                        pathArrowGeometriesL2Cache.clear();
                    }
                    pathArrowGeometriesL2Cache.set(cacheKey, arrowGeometries);
                }
            }
        }

        for (const geometry of arrowGeometries) {
            features.push({
                type: "Feature",
                properties: { ...feature.properties },
                geometry,
            });
        }
    }

    return {
        type: "FeatureCollection",
        features,
    };
}

export function isPathFeature(feature: Feature): boolean {
    const featureType = getFeatureSemanticType(feature);
    return Boolean(featureType && PATH_RENDER_BY_TYPE[featureType]);
}

export function getFeatureSemanticType(feature: Feature): string | null {
    const value = feature.properties.type || feature.properties.entity_type_id || null;
    return normalizeGeoTypeKey(value);
}

export function buildPathArrowGeometry(coords: [number, number][], isRetreatRoute = false): Geometry | null {
    const sourceCoords = removeDuplicatePathCoords(coords);
    if (sourceCoords.length < 2) return null;

    const origin = sourceCoords[0];
    const originLatRad = toRadians(origin[1]);
    const cosOriginLat = Math.max(Math.cos(originLatRad), 0.000001);
    const projected = sourceCoords.map((coord) => projectLngLat(coord, origin, cosOriginLat));
    const measured = buildMeasuredPath(projected);
    const totalLength = measured[measured.length - 1]?.distance || 0;
    if (totalLength <= 0) return null;

    const headLength = clampNumber(totalLength * 0.24, totalLength * 0.12, totalLength * 0.45);
    const bodyEndDistance = Math.max(totalLength - headLength, totalLength * 0.35);
    const bodyPoints = measured
        .filter((point) => point.distance < bodyEndDistance)
        .map(({ x, y, distance }) => ({ x, y, distance }));
    bodyPoints.push(pointAtDistance(measured, bodyEndDistance));

    if (bodyPoints.length < 2) return null;

    const tailWidth = clampNumber(totalLength * 0.02, 5, 40000);
    const shoulderWidth = clampNumber(totalLength * 0.1, 10, 100000);
    const headWidth = shoulderWidth * 2.0;

    const base = bodyPoints[bodyPoints.length - 1];
    const tip = pointAtDistance(measured, totalLength);
    const headNormal = normalFromSegment(base, tip) || normalAt(bodyPoints, bodyPoints.length - 1);
    const headHalf = headWidth / 2;

    if (isRetreatRoute) {
        // Segmented Arrow (MultiPolygon)
        const rings: [number, number][][] = [];

        // 1. Generate body segments
        const segmentLength = totalLength * 0.10; // Dash length
        const gapLength = totalLength * 0.04;    // Gap length

        let currentD = 0;
        while (currentD < bodyEndDistance) {
            const startD = currentD;
            const endD = Math.min(startD + segmentLength, bodyEndDistance - gapLength);

            if (endD - startD > totalLength * 0.01) {
                const segmentPoints: MeasuredPoint[] = [];
                segmentPoints.push(pointAtDistance(measured, startD));
                
                for (const p of bodyPoints) {
                    if (p.distance > startD && p.distance < endD) {
                        segmentPoints.push(p);
                    }
                }
                
                segmentPoints.push(pointAtDistance(measured, endD));

                const leftBody: ProjectedPoint[] = [];
                const rightBody: ProjectedPoint[] = [];

                for (let i = 0; i < segmentPoints.length; i += 1) {
                    const point = segmentPoints[i];
                    const normal = normalAt(segmentPoints, i);
                    const progress = bodyEndDistance > 0
                        ? Math.pow(clampNumber(point.distance / bodyEndDistance, 0, 1), 0.9)
                        : 0;
                    const width = tailWidth + (shoulderWidth - tailWidth) * progress;
                    const half = width / 2;
                    leftBody.push({
                        x: point.x + normal.x * half,
                        y: point.y + normal.y * half,
                    });
                    rightBody.push({
                        x: point.x - normal.x * half,
                        y: point.y - normal.y * half,
                    });
                }

                const ring = [
                    ...leftBody,
                    ...rightBody.reverse(),
                    leftBody[0],
                ].map((point) => unprojectLngLat(point, origin, cosOriginLat));

                if (ring.length >= 4) {
                    rings.push(ring);
                }
            }

            currentD += segmentLength + gapLength;
        }

        // 2. Generate head segment (standalone arrowhead chevron/triangle)
        const headBaseLeft = {
            x: base.x + headNormal.x * headHalf,
            y: base.y + headNormal.y * headHalf,
        };
        const headBaseRight = {
            x: base.x - headNormal.x * headHalf,
            y: base.y - headNormal.y * headHalf,
        };
        const headRing = [
            { x: base.x, y: base.y },
            headBaseLeft,
            { x: tip.x, y: tip.y },
            headBaseRight,
            { x: base.x, y: base.y },
        ].map((point) => unprojectLngLat(point, origin, cosOriginLat));

        rings.push(headRing);

        return {
            type: "MultiPolygon",
            coordinates: rings.map(r => [r]),
        };
    } else {
        // Continuous Arrow (Polygon)
        const leftBody: ProjectedPoint[] = [];
        const rightBody: ProjectedPoint[] = [];

        for (let i = 0; i < bodyPoints.length; i += 1) {
            const point = bodyPoints[i];
            const normal = normalAt(bodyPoints, i);
            const progress = bodyEndDistance > 0
                ? Math.pow(clampNumber(point.distance / bodyEndDistance, 0, 1), 0.9)
                : 0;
            const width = tailWidth + (shoulderWidth - tailWidth) * progress;
            const half = width / 2;
            leftBody.push({
                x: point.x + normal.x * half,
                y: point.y + normal.y * half,
            });
            rightBody.push({
                x: point.x - normal.x * half,
                y: point.y - normal.y * half,
            });
        }

        const headBaseLeft = {
            x: base.x + headNormal.x * headHalf,
            y: base.y + headNormal.y * headHalf,
        };
        const headBaseRight = {
            x: base.x - headNormal.x * headHalf,
            y: base.y - headNormal.y * headHalf,
        };

        const ring = [
            ...leftBody,
            headBaseLeft,
            { x: tip.x, y: tip.y },
            headBaseRight,
            ...rightBody.reverse(),
            leftBody[0],
        ].map((point) => unprojectLngLat(point, origin, cosOriginLat));

        if (ring.length < 4) return null;
        return {
            type: "Polygon",
            coordinates: [ring],
        };
    }
}

export type ProjectedPoint = {
    x: number;
    y: number;
};

export type MeasuredPoint = ProjectedPoint & {
    distance: number;
};

export function removeDuplicatePathCoords(coords: [number, number][]): [number, number][] {
    const result: [number, number][] = [];
    for (const coord of coords) {
        const last = result[result.length - 1];
        if (last && last[0] === coord[0] && last[1] === coord[1]) continue;
        result.push(coord);
    }
    return result;
}

export function projectLngLat(
    coord: [number, number],
    origin: [number, number],
    cosOriginLat: number
): ProjectedPoint {
    const earthRadiusMeters = 6371008.8;
    return {
        x: toRadians(coord[0] - origin[0]) * earthRadiusMeters * cosOriginLat,
        y: toRadians(coord[1] - origin[1]) * earthRadiusMeters,
    };
}

export function unprojectLngLat(
    point: ProjectedPoint,
    origin: [number, number],
    cosOriginLat: number
): [number, number] {
    const earthRadiusMeters = 6371008.8;
    return [
        origin[0] + toDegrees(point.x / (earthRadiusMeters * cosOriginLat)),
        origin[1] + toDegrees(point.y / earthRadiusMeters),
    ];
}

export function buildMeasuredPath(points: ProjectedPoint[]): MeasuredPoint[] {
    let distance = 0;
    return points.map((point, index) => {
        if (index > 0) {
            distance += distanceProjected(points[index - 1], point);
        }
        return {
            ...point,
            distance,
        };
    });
}

export function pointAtDistance(points: MeasuredPoint[], targetDistance: number): MeasuredPoint {
    if (targetDistance <= 0) return points[0];
    for (let i = 1; i < points.length; i += 1) {
        const prev = points[i - 1];
        const next = points[i];
        if (targetDistance > next.distance) continue;
        const segmentLength = next.distance - prev.distance;
        const t = segmentLength > 0 ? (targetDistance - prev.distance) / segmentLength : 0;
        return {
            x: prev.x + (next.x - prev.x) * t,
            y: prev.y + (next.y - prev.y) * t,
            distance: targetDistance,
        };
    }
    return points[points.length - 1];
}

export function normalAt(points: ProjectedPoint[], index: number): ProjectedPoint {
    const prev = points[Math.max(0, index - 1)];
    const next = points[Math.min(points.length - 1, index + 1)];
    return normalFromSegment(prev, next) || { x: 0, y: 1 };
}

export function normalFromSegment(a: ProjectedPoint, b: ProjectedPoint): ProjectedPoint | null {
    const dx = b.x - a.x;
    const dy = b.y - a.y;
    const length = Math.hypot(dx, dy);
    if (length <= 0) return null;
    return {
        x: -dy / length,
        y: dx / length,
    };
}

export function distanceProjected(a: ProjectedPoint, b: ProjectedPoint): number {
    return Math.hypot(b.x - a.x, b.y - a.y);
}

export function toRadians(value: number): number {
    return (value * Math.PI) / 180;
}

export function toDegrees(value: number): number {
    return (value * 180) / Math.PI;
}

export function ensurePathArrowIcon(map: maplibregl.Map): boolean {
    if (map.hasImage(PATH_ARROW_ICON_ID)) return true;
    const imageData = createPathArrowImageData();
    if (!imageData) return false;
    map.addImage(PATH_ARROW_ICON_ID, imageData, { pixelRatio: 2 });
    return true;
}

export function createPathArrowImageData(): ImageData | null {
    const size = 56;
    if (typeof document === "undefined") return null;
    const canvas = document.createElement("canvas");
    canvas.width = size;
    canvas.height = size;
    const ctx = canvas.getContext("2d");
    if (!ctx) return null;

    ctx.clearRect(0, 0, size, size);

    ctx.strokeStyle = "#0f172a";
    ctx.fillStyle = "#38bdf8";
    ctx.lineWidth = 4;
    ctx.lineJoin = "round";
    ctx.lineCap = "round";

    ctx.beginPath();
    ctx.moveTo(8, 16);
    ctx.lineTo(28, 16);
    ctx.lineTo(28, 10);
    ctx.lineTo(46, 28);
    ctx.lineTo(28, 46);
    ctx.lineTo(28, 40);
    ctx.lineTo(8, 40);
    ctx.closePath();
    ctx.fill();
    ctx.stroke();

    return ctx.getImageData(0, 0, size, size);
}

export function buildTypeMatchExpression(
    valueByType: Record<string, string | number | boolean>,
    fallback: string | number | boolean
): maplibregl.ExpressionSpecification {
    const expression: unknown[] = ["match", getFeatureTypeExpression()];

    for (const [typeId, value] of Object.entries(valueByType)) {
        expression.push(typeId, value);
    }

    expression.push(fallback);
    return expression as maplibregl.ExpressionSpecification;
}

export function getFeatureTypeExpression(): maplibregl.ExpressionSpecification {
    return [
        "coalesce",
        ["get", "type"],
        ["get", "entity_type_id"],
        "",
    ] as maplibregl.ExpressionSpecification;
}

export function roundZoom(value: number): number {
    return Math.round(value * 10) / 10;
}

export function getFeatureLabelResolver(
    fc: FeatureCollection,
    timelineYear?: number | null
): (feature: Feature) => string | null {
    let yearMap = resolverCache.get(fc);
    if (!yearMap) {
        yearMap = new Map();
        resolverCache.set(fc, yearMap);
    }
    let resolver = yearMap.get(timelineYear);
    if (!resolver) {
        resolver = createFeatureLabelResolver(fc, timelineYear);
        yearMap.set(timelineYear, resolver);
    }
    return resolver;
}

function getSingleEntityFeatureLabelInfoCached(
    feature: Feature,
    timelineYear?: number | null
): FeatureLabelInfo | null {
    let yearMap = featureLabelInfoCache.get(feature);
    if (!yearMap) {
        yearMap = new Map();
        featureLabelInfoCache.set(feature, yearMap);
    }
    let info = yearMap.get(timelineYear);
    if (info === undefined) {
        info = getSingleEntityFeatureLabelInfo(feature, timelineYear);
        yearMap.set(timelineYear, info);
    }
    return info;
}

function createFeatureLabelResolver(
    fc: FeatureCollection,
    timelineYear?: number | null
): (feature: Feature) => string | null {
    const directLabelsByFeatureId = new Map<string, FeatureLabelInfo>();
    const inheritedLabelsByChildId = new Map<string, FeatureLabelInfo | null>();

    for (const feature of fc.features) {
        const labelInfo = getSingleEntityFeatureLabelInfoCached(feature, timelineYear);
        if (!labelInfo) continue;
        directLabelsByFeatureId.set(String(feature.properties.id), labelInfo);
    }

    for (const feature of fc.features) {
        const featureId = String(feature.properties.id);
        const parentId = normalizeBoundWithId(feature.properties.bound_with);
        if (!parentId) continue;

        const parentLabel = directLabelsByFeatureId.get(parentId);
        if (parentLabel) {
            mergeInheritedFeatureLabel(inheritedLabelsByChildId, featureId, parentLabel);
        }
    }

    return (feature) => {
        const featureId = String(feature.properties.id);
        const directEntityIds = getFeatureEntityIds(feature);
        let label: string | null = null;
        if (directEntityIds.length > 0) {
            label = directLabelsByFeatureId.get(featureId)?.label || null;
        } else {
            label = inheritedLabelsByChildId.get(featureId)?.label || null;
        }

        if (!label) {
            const geotype = feature.properties?.type || feature.properties?.entity_type_id;
            if (geotype === "region") {
                return "__Missing__";
            }
        }

        return label;
    };
}

function mergeInheritedFeatureLabel(
    labelsByFeatureId: Map<string, FeatureLabelInfo | null>,
    targetFeatureId: string,
    labelInfo: FeatureLabelInfo
) {
    const current = labelsByFeatureId.get(targetFeatureId);
    if (current === undefined) {
        labelsByFeatureId.set(targetFeatureId, labelInfo);
    } else if (current && current.entityId === labelInfo.entityId) {
        labelsByFeatureId.set(targetFeatureId, current);
    } else {
        labelsByFeatureId.set(targetFeatureId, null);
    }
}

function getSingleEntityFeatureLabelInfo(
    feature: Feature,
    timelineYear?: number | null
): FeatureLabelInfo | null {
    const candidates = getFeatureEntityLabelCandidates(feature);
    if (candidates.length > 0) {
        const timelineCandidate = getLatestTimelineEntityCandidate(candidates, timelineYear);
        if (!timelineCandidate) return null;
        return {
            entityId: timelineCandidate.id,
            label: timelineCandidate.name,
            timeEnd: normalizeLabelYear(timelineCandidate.time_end),
        };
    }

    const entityIds = getFeatureEntityIds(feature);
    if (entityIds.length !== 1) return null;

    const label = getSingleEntityName(feature);
    if (!label) return null;

    return { entityId: entityIds[0], label, timeEnd: null };
}

function getLatestTimelineEntityCandidate(
    candidates: EntityLabelCandidate[],
    timelineYear?: number | null
): EntityLabelCandidate | null {
    if (!candidates.length) return null;

    const activeCandidates = candidates.filter((candidate) =>
        isEntityCandidateVisibleAtYear(candidate, timelineYear)
    );
    if (!activeCandidates.length) return null;

    return activeCandidates.sort(compareEntityLabelCandidates)[0] || null;
}

function getFeatureEntityLabelCandidates(feature: Feature): EntityLabelCandidate[] {
    const rawCandidates = feature.properties.entity_label_candidates;
    if (!Array.isArray(rawCandidates)) return [];

    const byId = new Map<string, EntityLabelCandidate>();
    for (const raw of rawCandidates) {
        if (!raw || typeof raw !== "object") continue;
        const candidate = raw as EntityLabelCandidate;
        const id = String(candidate.id || "").trim();
        const name = String(candidate.name || "").trim();
        if (!id || !name) continue;
        byId.set(id, {
            id,
            name,
            time_start: normalizeLabelYear(candidate.time_start),
            time_end: normalizeLabelYear(candidate.time_end),
        });
    }

    return Array.from(byId.values());
}

function isEntityCandidateVisibleAtYear(
    candidate: EntityLabelCandidate,
    timelineYear?: number | null
): boolean {
    if (typeof timelineYear !== "number" || !Number.isFinite(timelineYear)) return true;

    const start = normalizeLabelYear(candidate.time_start);
    const end = normalizeLabelYear(candidate.time_end);
    if (start != null && timelineYear < start) return false;
    if (end != null && timelineYear > end) return false;
    return true;
}

function compareEntityLabelCandidates(a: EntityLabelCandidate, b: EntityLabelCandidate): number {
    const endA = normalizeLabelYear(a.time_end);
    const endB = normalizeLabelYear(b.time_end);
    const endScoreA = endA == null ? Number.NEGATIVE_INFINITY : endA;
    const endScoreB = endB == null ? Number.NEGATIVE_INFINITY : endB;
    if (endScoreA !== endScoreB) return endScoreB - endScoreA;

    const startA = normalizeLabelYear(a.time_start);
    const startB = normalizeLabelYear(b.time_start);
    const startScoreA = startA == null ? Number.NEGATIVE_INFINITY : startA;
    const startScoreB = startB == null ? Number.NEGATIVE_INFINITY : startB;
    if (startScoreA !== startScoreB) return startScoreB - startScoreA;

    return a.name.localeCompare(b.name);
}

function normalizeLabelYear(value: unknown): number | null {
    return typeof value === "number" && Number.isFinite(value) ? value : null;
}

function getFeatureEntityIds(feature: Feature): string[] {
    const rawEntityIds: unknown[] = Array.isArray(feature.properties.entity_ids)
        ? feature.properties.entity_ids
        : (typeof feature.properties.entity_id === "string" || typeof feature.properties.entity_id === "number"
            ? [feature.properties.entity_id]
            : []);

    return Array.from(new Set(
        rawEntityIds
            .filter((id): id is string | number => typeof id === "string" || typeof id === "number")
            .map((id) => String(id).trim())
            .filter((id) => id.length > 0)
    ));
}

function getSingleEntityName(feature: Feature): string | null {
    const directName = typeof feature.properties.entity_name === "string"
        ? feature.properties.entity_name.trim()
        : "";
    if (directName.length > 0) return directName;

    const names = Array.isArray(feature.properties.entity_names)
        ? Array.from(new Set(
            feature.properties.entity_names
                .filter((name): name is string => typeof name === "string")
                .map((name) => name.trim())
                .filter((name) => name.length > 0)
        ))
        : [];

    return names.length === 1 ? names[0] : null;
}

function isLineGeometry(geometry: Geometry): boolean {
    return geometry.type === "LineString" || geometry.type === "MultiLineString";
}

function normalizeCoordinate(value: unknown): Coordinate | null {
    if (!Array.isArray(value) || value.length < 2) return null;
    const lng = Number(value[0]);
    const lat = Number(value[1]);
    if (!Number.isFinite(lng) || !Number.isFinite(lat)) return null;
    return [lng, lat];
}

function getAverageCoordinate(coordinates: Coordinate[]): Coordinate | null {
    const valid = coordinates
        .map((coordinate) => normalizeCoordinate(coordinate))
        .filter((coordinate): coordinate is Coordinate => Boolean(coordinate));
    if (!valid.length) return null;

    const sum = valid.reduce(
        (acc, coordinate) => ({
            lng: acc.lng + coordinate[0],
            lat: acc.lat + coordinate[1],
        }),
        { lng: 0, lat: 0 }
    );
    return [sum.lng / valid.length, sum.lat / valid.length];
}

function getLineMidpointCoordinate(coordinates: Coordinate[]): Coordinate | null {
    const valid = coordinates
        .map((coordinate) => normalizeCoordinate(coordinate))
        .filter((coordinate): coordinate is Coordinate => Boolean(coordinate));
    if (!valid.length) return null;
    if (valid.length === 1) return valid[0];

    const totalLength = getLineLength(valid);
    if (totalLength <= 0) return valid[Math.floor(valid.length / 2)];

    const halfway = totalLength / 2;
    let travelled = 0;
    for (let i = 1; i < valid.length; i += 1) {
        const prev = valid[i - 1];
        const next = valid[i];
        const segmentLength = getCoordinateDistance(prev, next);
        if (travelled + segmentLength >= halfway) {
            const ratio = segmentLength > 0 ? (halfway - travelled) / segmentLength : 0;
            return [
                prev[0] + (next[0] - prev[0]) * ratio,
                prev[1] + (next[1] - prev[1]) * ratio,
            ];
        }
        travelled += segmentLength;
    }

    return valid[valid.length - 1];
}

function getLineLength(coordinates: Coordinate[]): number {
    let total = 0;
    for (let i = 1; i < coordinates.length; i += 1) {
        const prev = normalizeCoordinate(coordinates[i - 1]);
        const next = normalizeCoordinate(coordinates[i]);
        if (!prev || !next) continue;
        total += getCoordinateDistance(prev, next);
    }
    return total;
}

function getCoordinateDistance(a: Coordinate, b: Coordinate): number {
    const dx = b[0] - a[0];
    const dy = b[1] - a[1];
    return Math.sqrt(dx * dx + dy * dy);
}

function getLineCoordinateGroups(geometry: Geometry): Coordinate[][] {
    if (geometry.type === "LineString") return [geometry.coordinates];
    if (geometry.type === "MultiLineString") return geometry.coordinates;
    return [];
}

const polygonLabelPointCache = new WeakMap<Geometry, Coordinate | null>();

function getPolygonLabelPoint(geometry: Geometry, featureId?: string | number): Coordinate | null {
    if (polygonLabelPointCache.has(geometry)) {
        return polygonLabelPointCache.get(geometry)!;
    }

    const fingerprint = getGeometryFingerprint(geometry);
    const cacheKey = featureId ? `${featureId}:${fingerprint}` : null;

    if (cacheKey && polygonLabelPointL2Cache.has(cacheKey)) {
        const result = polygonLabelPointL2Cache.get(cacheKey)!;
        polygonLabelPointCache.set(geometry, result);
        return result;
    }

    let result: Coordinate | null = null;
    if (geometry.type === "Polygon") {
        result = getPolygonLabelCandidate(geometry.coordinates)?.point || null;
    } else if (geometry.type === "MultiPolygon") {
        let best: { point: Coordinate; distance: number } | null = null;
        for (const polygon of geometry.coordinates) {
            const candidate = getPolygonLabelCandidate(polygon);
            if (!candidate) continue;
            if (!best || candidate.distance > best.distance) {
                best = candidate;
            }
        }
        result = best?.point || null;
    }

    polygonLabelPointCache.set(geometry, result);
    if (cacheKey) {
        if (polygonLabelPointL2Cache.size >= MAX_L2_CACHE_SIZE) {
            polygonLabelPointL2Cache.clear();
        }
        polygonLabelPointL2Cache.set(cacheKey, result);
    }
    return result;
}

function getPolygonLabelCandidate(polygon: PolygonCoordinates): { point: Coordinate; distance: number } | null {
    const outerRing = polygon[0];
    if (!outerRing || outerRing.length < 3) return null;

    const bbox = getRingBbox(outerRing);
    if (!bbox) return null;

    const width = bbox.maxX - bbox.minX;
    const height = bbox.maxY - bbox.minY;
    if (width <= 0 || height <= 0) {
        const fallback: Coordinate = [bbox.minX, bbox.minY];
        return { point: fallback, distance: 0 };
    }

    const precision = Math.max(Math.max(width, height) / 100, 0.0001);
    const result = polylabel(polygon, precision);
    const x = result[0];
    const y = result[1];

    if (!Number.isFinite(x) || !Number.isFinite(y)) {
        return { point: [bbox.minX + width / 2, bbox.minY + height / 2], distance: 0 };
    }

    return { point: [x, y], distance: Number.isFinite(result.distance) ? result.distance : 0 };
}

function getRingBbox(ring: Coordinate[]): { minX: number; minY: number; maxX: number; maxY: number } | null {
    if (!ring.length) return null;

    let minX = Number.POSITIVE_INFINITY;
    let minY = Number.POSITIVE_INFINITY;
    let maxX = Number.NEGATIVE_INFINITY;
    let maxY = Number.NEGATIVE_INFINITY;

    for (const [x, y] of ring) {
        minX = Math.min(minX, x);
        minY = Math.min(minY, y);
        maxX = Math.max(maxX, x);
        maxY = Math.max(maxY, y);
    }

    if (!Number.isFinite(minX) || !Number.isFinite(minY) || !Number.isFinite(maxX) || !Number.isFinite(maxY)) {
        return null;
    }

    return { minX, minY, maxX, maxY };
}

export function buildClientFeatureId(): string {
    return newId();
}

export function clampNumber(value: number, min: number, max: number): number {
    if (value < min) return min;
    if (value > max) return max;
    return value;
}

export function hashStringToColor(str: string): string {
    let hash = 5381;
    for (let i = 0; i < str.length; i++) {
        hash = (hash * 33) ^ str.charCodeAt(i);
    }
    // Use Knuth's multiplicative hashing multiplier to scatter consecutive/close hash values
    const scattered = Math.abs(hash * 2654435761);
    const hue = scattered % 360;
    
    // Vary saturation and lightness slightly to increase color diversity and uniqueness
    const saturation = 70 + (scattered % 20); // 70% to 90%
    const lightness = 45 + ((scattered >> 5) % 15); // 45% to 60%
    
    return `hsl(${hue}, ${saturation}%, ${lightness}%)`;
}

export function decorateFeaturesWithEntityColors(fc: FeatureCollection): FeatureCollection {
    let changed = false;
    const nextFeatures = fc.features.map((feature) => {
        const geomType = feature.geometry?.type;
        if (geomType === "Point" || geomType === "MultiPoint") {
            // Point - giữ nguyên màu của preset/icon
            return feature;
        }

        let entity_color: string | undefined;
        if (geomType === "LineString" || geomType === "MultiLineString") {
            const entityIds = getFeatureEntityIds(feature);
            if (entityIds.length > 0) {
                const sortedCombined = [...entityIds].sort().join("+");
                entity_color = hashStringToColor(sortedCombined);
            }
        } else if (geomType === "Polygon" || geomType === "MultiPolygon") {
            const geoId = String(feature.properties?.id || "");
            if (geoId) {
                entity_color = hashStringToColor(geoId);
            }
        }

        if (feature.properties.entity_color === entity_color) {
            return feature;
        }
        changed = true;
        return {
            ...feature,
            properties: {
                ...feature.properties,
                entity_color,
            },
        };
    });
    return changed ? { ...fc, features: nextFeatures } : fc;
}
