import maplibregl from "maplibre-gl";
import { BACKGROUND_LAYER_OPTIONS, BackgroundLayerVisibility } from "@/uhm/lib/map/styles/backgroundLayers";
import { Feature, FeatureCollection, Geometry } from "@/uhm/lib/editor/state/useEditorState";
import {
    DEFAULT_POINT_ICON_ID,
    FEATURE_STATE_SOURCE_IDS,
    PATH_ARROW_ICON_ID,
    POINT_ICON_URL,
    RASTER_BASE_INSERT_BEFORE_LAYER_ID,
    RASTER_BASE_LAYER_ID,
    RASTER_BASE_SOURCE_ID,
} from "@/uhm/lib/map/constants";
import { PATH_RENDER_BY_TYPE } from "@/uhm/lib/map/styles/style";
import { getRasterTileTemplateUrl } from "@/uhm/api/tiles";
import { newId } from "@/uhm/lib/utils/id";

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;
        if (!map.getLayer(layer.id)) continue;
        map.setLayoutProperty(
            layer.id,
            "visibility",
            visibility[layer.id] ? "visible" : "none"
        );
    }
}

export function syncRasterBaseVisibility(map: maplibregl.Map, shouldShow: boolean) {
    if (shouldShow) {
        ensureRasterBaseLayer(map);
        return;
    }
    removeRasterBaseLayer(map);
}

export function ensureRasterBaseLayer(map: maplibregl.Map) {
    if (!map.getSource(RASTER_BASE_SOURCE_ID)) {
        map.addSource(RASTER_BASE_SOURCE_ID, createRasterBaseSource());
    }

    if (!map.getLayer(RASTER_BASE_LAYER_ID)) {
        const beforeId = map.getLayer(RASTER_BASE_INSERT_BEFORE_LAYER_ID)
            ? RASTER_BASE_INSERT_BEFORE_LAYER_ID
            : undefined;
        map.addLayer(createRasterBaseLayer(), beforeId);
    }

    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 {
        type: "raster" as const,
        tiles: [getRasterTileTemplateUrl()],
        tileSize: 256,
        minzoom: 0,
        maxzoom: 6,
    };
}

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,
        },
    };
}

export function getSelectableLayers(map: maplibregl.Map): string[] {
    return [
        "countries-fill",
        "countries-line",
        "routes-line",
        "routes-path-arrow-fill",
        "routes-path-arrow-line",
        "routes-path-hit",
        "places-circle",
        "places-symbol",
    ].filter((layerId) => Boolean(map.getLayer(layerId)));
}

export function filterDraftByBinding(
    fc: FeatureCollection,
    selectedFeatureIds: (string | number)[],
    highlightFeatures?: FeatureCollection | null
): 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>();
    for (const feature of fc.features) {
        for (const id of normalizeBindingIds(feature.properties.binding)) {
            childIds.add(id);
        }
    }

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

    const selectedChildren = new Set<string>();
    for (const feature of fc.features) {
        if (selectedIds.has(String(feature.properties.id))) {
            for (const id of normalizeBindingIds(feature.properties.binding)) {
                selectedChildren.add(id);
            }
        }
    }

    return {
        ...fc,
        features: fc.features.filter((feature) => {
            const featureId = String(feature.properties.id);
            if (selectedIds.has(featureId)) return true;
            if (selectedChildren.has(featureId)) return true;
            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 key = getFeatureSemanticType(feature);
            if (!key) return true;
            return visibility[key] !== false;
        }),
    };
}

export function normalizeBindingIds(rawBinding: unknown): string[] {
    if (!Array.isArray(rawBinding)) return [];
    const deduped: string[] = [];
    const seen = new Set<string>();
    for (const rawId of rawBinding) {
        if (typeof rawId !== "string" && typeof rawId !== "number") continue;
        const id = String(rawId).trim();
        if (!id || seen.has(id)) continue;
        seen.add(id);
        deduped.push(id);
    }
    return deduped;
}

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 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
): boolean {
    const bbox = getFeatureCollectionBBox(fc);
    if (!bbox) return false;

    const resolvedPadding = typeof padding === "number" || padding ? padding : 58;

    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: 6,
            padding: resolvedPadding,
            duration: 0,
        });
        return true;
    }

    map.fitBounds(
        [
            [bbox.minLng, bbox.minLat],
            [bbox.maxLng, bbox.maxLat],
        ],
        {
            padding: resolvedPadding,
            maxZoom: 7,
            duration: 0,
        }
    );
    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 buildPathArrowFeatureCollection(fc: FeatureCollection): FeatureCollection {
    const features = fc.features
        .map((feature) => {
            if (!isPathFeature(feature) || feature.geometry.type !== "LineString") return null;
            const geometry = buildPathArrowGeometry(feature.geometry.coordinates);
            if (!geometry) return null;
            return {
                type: "Feature" as const,
                properties: { ...feature.properties },
                geometry,
            };
        })
        .filter((feature): feature is Feature => feature !== null);

    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;
    if (!value) return null;
    const normalized = String(value).trim().toLowerCase();
    return normalized.length ? normalized : null;
}

export function buildPathArrowGeometry(coords: [number, number][]): 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.005, 8000, 40000);
    const shoulderWidth = clampNumber(totalLength * 0.015, 18000, 100000);
    const headWidth = shoulderWidth * 2.0;

    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 base = bodyPoints[bodyPoints.length - 1];
    const tip = pointAtDistance(measured, totalLength);
    const headNormal = normalFromSegment(base, tip) || normalAt(bodyPoints, bodyPoints.length - 1);
    const headHalf = headWidth / 2;
    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 addPointSymbolLayer(map: maplibregl.Map) {
    void ensurePointAssetIcon(map).then((hasPointIcon) => {
        try {
            if (!hasPointIcon || !map.getSource("places") || map.getLayer("places-symbol")) return;

            map.addLayer({
                id: "places-symbol",
                type: "symbol",
                source: "places",
                layout: {
                    "icon-image": DEFAULT_POINT_ICON_ID,
                    "icon-size": 0.06,
                    "icon-anchor": "center",
                    "icon-allow-overlap": true,
                },
            });

            if (map.getLayer("places-circle")) {
                map.setLayoutProperty("places-circle", "visibility", "none");
            }
        } catch (err) {
            console.warn("Add point symbol layer skipped", err);
        }
    });
}

export async function ensurePointAssetIcon(map: maplibregl.Map): Promise<boolean> {
    if (map.hasImage(DEFAULT_POINT_ICON_ID)) return true;

    try {
        const image = await map.loadImage(POINT_ICON_URL);
        if (!map.hasImage(DEFAULT_POINT_ICON_ID)) {
            map.addImage(DEFAULT_POINT_ICON_ID, image.data);
        }
        return true;
    } catch (error) {
        console.error(`Failed to load point icon asset: ${POINT_ICON_URL}`, error);
        return false;
    }
}

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 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;
}
