"use client";
import { useEffect, useState } from "react";
import { useMap } from "react-leaflet";

export default function useMapPolygonDraw(polygons, addPolygon, removePolygon) {
    const map = useMap();
    const nodeCatchDistance = 30; // px
    const nodeHighlightDistance = 30; // px
    const [currentPolygon, setCurrentPolygon] = useState([]);
    const [highlightNodes, setHighlightNodes] = useState([]);

    useEffect(() => {
        setCurrentPolygon([]);
        setHighlightNodes([]);
    }, [polygons])

    function latlngEqual(latlng1, latlng2, epsilon = 1e-9) {
        return Math.abs(latlng1.lat - latlng2.lat) < epsilon && Math.abs(latlng1.lng - latlng2.lng) < epsilon;
    }

    function layerDistance(latlng1, latlng2) {
        // Return the pixel distance between latlng1 and latlng2 as they appear on the map
        const {x: x1, y: y1} = map.latLngToLayerPoint(latlng1);
        const {x: x2, y: y2} = map.latLngToLayerPoint(latlng2);
        return Math.sqrt((x1 - x2)**2 + (y1 - y2)**2);
    }

    function isDrawing() {
        return currentPolygon.length > 0;
    }

    function areSegmentsIntersecting(p1, p2, p3, p4) {
        // Return true if the segments (p1, p2) and (p3, p4) are strictly intersecting, else false
        const direction = (a, b, c) => {
            return (c.lng - a.lng) * (b.lat - a.lat) - (b.lng - a.lng) * (c.lat - a.lat);
        };

        const d1 = direction(p3, p4, p1);
        const d2 = direction(p3, p4, p2);
        const d3 = direction(p1, p2, p3);
        const d4 = direction(p1, p2, p4);

        return ((d1 > 0 && d2 < 0) || (d1 < 0 && d2 > 0)) && ((d3 > 0 && d4 < 0) || (d3 < 0 && d4 > 0));
    }

    function isIntersecting(segment, pointArray, isPolygon) {
        // Return true if segment intersects one of the pointArray segments according to areSegmentsIntersecting
        // Moreover if isPolygon, then it verifies if segment intersects the segment closing pointArray
        const length = pointArray.length;

        for (let i = 0; i < length-1; i++) {
            if (areSegmentsIntersecting(segment[0], segment[1], pointArray[i], pointArray[i+1])) {
                return true;
            }
        }

        if (isPolygon && length > 2) {
            return areSegmentsIntersecting(segment[0], segment[1], pointArray[length-1], pointArray[0]);
        } else {
            return false;
        }
    }

    function isInPolygon(latlng, polygon) {
        // Return true if latlng is strictly inside polygon
        // Return false if latlng is outside polygon or on a vertex of the polygon
        // Return true or false if latlng is on the border
        if (latlngEqual(latlng, polygon[0])) return false;

        const length = polygon.length;
        const {lat: x, lng: y} = latlng;
        let inside = false;

        for (let i = 0, j = length - 1; i < length; j = i++) {
            if (latlngEqual(latlng, polygon[j])) return false;

            const {lat: xi, lng: yi} = polygon[i];
            const {lat: xj, lng: yj} = polygon[j];
            const intersects = ((yi > y) !== (yj > y)) && (x < ((xj - xi) * (y - yi)) / (yj - yi) + xi);

            if (intersects) inside = !inside;
        }

        return inside;
    }

    function isClockwise(points) {
        // Return true if the tab describes a clockwise polygon (Shoelace formula)
        let sum = 0;
        for (let i = 0; i < points.length; i++) {
            const curr = points[i];
            const next = points[(i + 1) % points.length];
            sum += (next.lng - curr.lng) * (next.lat + curr.lat);
        }
        return sum > 0;
    };
    
    function getPolygonIndex(latlng) {
        // Return the index of the polygon where latlng is according to isInPolygon
        return polygons.findIndex(polygon => isInPolygon(latlng, polygon));
    }

    function getEventLatLng(e) {
        // Return the closest latlng to e.latlng among the existing nodes including the first node of currentPolygon
        // If the closest distance is superior to nodeCatchDistance, then e.latlng is returned
        const closeNodes = [];
        // Existing nodes
        for (const polygon of polygons) {
            for (const node of polygon) {
                const d = layerDistance(e.latlng, node);
                if (d < nodeCatchDistance) {
                    closeNodes.push([d, node]);
                }
            }
        }
        // First node of currentPolygon
        if (isDrawing()) {
            const d = layerDistance(e.latlng, currentPolygon[0]);
            if (d < nodeCatchDistance) {
                closeNodes.push([d, currentPolygon[0]]);
            }
        }
        // If there is no close node
        if (closeNodes.length == 0) {
            return e.latlng;
        // Else return the closest close node
        } else {
            return closeNodes.reduce( (min, current) => { return current[0] < min[0] ? current : min } )[1];
        }
    }

    function handleLeftClick(e) {
        setHighlightNodes([]);
        const latlng = getEventLatLng(e);
        const length = currentPolygon.length;

        // If it is the first node
        if (!isDrawing()) {
            // If the point is not in an existing polygon
            if (getPolygonIndex(latlng) == -1) {
                setCurrentPolygon([latlng]);
            }

        // If it is the last node
        } else if (latlngEqual(latlng, currentPolygon[0])) {
            // If the current polygon is a polygon (at least 3 points)
            if (length >= 3) {
                // If the current polygon is not circling an existing polygon
                for (const polygon of polygons) {
                    // meanPoint exists and is strictly inside polygon
                    const meanPoint = {
                        lat: (polygon[0].lat + polygon[1].lat + polygon[2].lat) / 3,
                        lng: (polygon[0].lng + polygon[1].lng + polygon[2].lng) / 3
                    };
                    if (isInPolygon(meanPoint, currentPolygon)) return;
                }
                // Making the new polygon clockwise to simplify some algorithms
                if (!isClockwise(currentPolygon)) currentPolygon.reverse();
                addPolygon(currentPolygon);
                setCurrentPolygon([]);
            }

        // If it is an intermediate node
        } else {
            // Is the polygon closing to early ?
            for (const point of currentPolygon) if (latlngEqual(point, latlng)) return;
            // Is the new point making the current polygon intersect with itself ?
            if (isIntersecting([latlng, currentPolygon[length-1]], currentPolygon, false)) return;
            // Is the new point inside a polygon ?
            if (getPolygonIndex(latlng) != -1) return;
            // Is the new point making the current polygon intersect with another polygon ?
            for (const polygon of polygons) {
                // Strict intersection
                if (isIntersecting([latlng, currentPolygon[length-1]], polygon, true)) return;
                // Intersection by joining two non adjacent nodes of polygon
                let tab = [-1, -1];
                for (let i = 0; i < polygon.length; i++) {
                    if (latlngEqual(latlng, polygon[i])) tab[0] = i;
                    if (latlngEqual(currentPolygon[length-1], polygon[i])) tab[1] = i;
                }
                if (
                    tab[0] != -1 && tab[1] != -1 &&
                    (tab[0] != (tab[1] + 1) % polygon.length) &&
                    (tab[1] != (tab[0] + 1) % polygon.length)
                ) return;
            }
            setCurrentPolygon([...currentPolygon, latlng]);
        }
    }

    function handleRightClick(e) {
        setHighlightNodes([]);
        // If isDrawing, cancel the currentPolygon
        if (isDrawing()) {
            setCurrentPolygon([]);
        // If not isDrawing, remove the clicked polygon
        } else {
            const i = getPolygonIndex(e.latlng);
            if (i != -1) removePolygon(i);
        }
    }

    function handleMouseMove(e) {
        const nodes = [];
        for (const polygon of polygons) {
            for (const node of polygon) {
                if (layerDistance(node, e.latlng) < nodeHighlightDistance && node != currentPolygon[0]) nodes.push(node);
            }
        }
        setHighlightNodes(nodes);
    }

    return { currentPolygon, highlightNodes, handleLeftClick, handleRightClick, handleMouseMove };
}
