export interface Point {
    x: number;
    y: number;
}

export type Polygon = Point[];

export const signedDistanceToEdge = (edgeStart: Point, edgeEnd: Point, point: Point): number =>
    (edgeEnd.x - edgeStart.x) * (point.y - edgeStart.y)
    - (edgeEnd.y - edgeStart.y) * (point.x - edgeStart.x);

export const iterateEdges = (
    polygon: Polygon,
    callback: (current: Point, next: Point, index: number) => boolean | void,
): void => {
    if (!Array.isArray(polygon) || polygon.length === 0) {
        return;
    }
    for (let index = 0; index < polygon.length; index += 1) {
        const current = polygon[index];
        const next = polygon[(index + 1) % polygon.length];
        if (callback(current, next, index) === false) {
            break;
        }
    }
};

export const forEachVertex = (
    polygon: Polygon,
    callback: (current: Point, previous: Point, index: number) => boolean | void,
): void => {
    if (!Array.isArray(polygon) || polygon.length === 0) {
        return;
    }
    for (let index = 0; index < polygon.length; index += 1) {
        const current = polygon[index];
        const prev = polygon[(index - 1 + polygon.length) % polygon.length];
        if (callback(current, prev, index) === false) {
            break;
        }
    }
};

export const lineIntersection = (p1: Point, p2: Point, cp1: Point, cp2: Point): Point => {
    const A1 = p2.y - p1.y;
    const B1 = p1.x - p2.x;
    const C1 = A1 * p1.x + B1 * p1.y;

    const A2 = cp2.y - cp1.y;
    const B2 = cp1.x - cp2.x;
    const C2 = A2 * cp1.x + B2 * cp1.y;

    const det = A1 * B2 - A2 * B1;
    if (Math.abs(det) < 1e-6) {
        return { x: cp1.x, y: cp1.y };
    }
    return {
        x: (B2 * C1 - B1 * C2) / det,
        y: (A1 * C2 - A2 * C1) / det,
    };
};

export const clipPolygon = (subject: Polygon, clipper: Polygon): Polygon => {
    if (!Array.isArray(subject) || !subject.length) {
        return [];
    }
    let output = subject;
    iterateEdges(clipper, (cp1, cp2) => {
        const input = output;
        output = [];
        if (!Array.isArray(input) || !input.length) {
            return false;
        }
        forEachVertex(input, (current, prev) => {
            const currentInside = signedDistanceToEdge(cp1, cp2, current) >= 0;
            const prevInside = signedDistanceToEdge(cp1, cp2, prev) >= 0;
            if (currentInside) {
                if (!prevInside) {
                    output.push(lineIntersection(prev, current, cp1, cp2));
                }
                output.push(current);
            } else if (prevInside) {
                output.push(lineIntersection(prev, current, cp1, cp2));
            }
            return true;
        });
        return output.length > 0;
    });
    return output;
};

export const isPointInsideConvex = (point: Point, polygon: Polygon): boolean => {
    if (!polygon?.length) {
        return false;
    }
    let sign = 0;
    let inside = true;
    iterateEdges(polygon, (a, b) => {
        const cross = signedDistanceToEdge(a, b, point);
        if (cross === 0) {
            return true;
        }
        const currentSign = cross > 0 ? 1 : -1;
        if (sign === 0) {
            sign = currentSign;
            return true;
        }
        if (sign !== currentSign) {
            inside = false;
            return false;
        }
        return true;
    });
    return inside;
};

export const polygonCentroid = (polygon: Polygon): Point => {
    if (!polygon?.length) {
        return { x: 0, y: 0 };
    }
    let area = 0;
    let cx = 0;
    let cy = 0;
    iterateEdges(polygon, (current, next) => {
        const cross = current.x * next.y - next.x * current.y;
        area += cross;
        cx += (current.x + next.x) * cross;
        cy += (current.y + next.y) * cross;
    });
    if (Math.abs(area) < 1e-6) {
        let sumX = 0;
        let sumY = 0;
        forEachVertex(polygon, (point) => {
            sumX += point.x;
            sumY += point.y;
        });
        return {
            x: sumX / polygon.length,
            y: sumY / polygon.length,
        };
    }
    const areaFactor = 1 / (3 * area);
    return {
        x: cx * areaFactor,
        y: cy * areaFactor,
    };
};
