import { triggerEvent, utilities } from '@cornerstonejs/core';
import { vec3 } from 'gl-matrix';
import createPolylineToolData from './createPolylineToolData';
import findAnnotationsForInterpolation from './findAnnotationForInterpolation';
import EventTypes from '../../../enums/Events';
import * as annotationState from '../../../stateManagement/annotation';
import selectHandles from './selectHandles';
import updateChildInterpolationUID from './updateChildInterpolationUID';
import { createPolylineHole } from '../../../eventListeners/annotations/contourSegmentation/contourSegmentationCompleted';
const { PointsManager } = utilities;
const dP = 0.2;
function interpolate(viewportData) {
    if (!viewportData.annotation) {
        return;
    }
    const { isInterpolationUpdate, annotation } = viewportData;
    queueMicrotask(() => {
        try {
            if (isInterpolationUpdate) {
                annotation.isInterpolationUpdate = true;
                annotation.autoGenerated = false;
            }
            startInterpolation(viewportData);
        }
        finally {
            if (isInterpolationUpdate) {
                annotation.autoGenerated = true;
            }
        }
    });
}
function startInterpolation(viewportData) {
    const { annotation: toolData } = viewportData;
    updateChildInterpolationUID(toolData);
    const { interpolationData, interpolationList } = findAnnotationsForInterpolation(toolData, viewportData) || {};
    if (!interpolationData || !interpolationList) {
        return;
    }
    const eventData = {
        toolName: toolData.metadata.toolName,
        toolType: toolData.metadata.toolName,
        viewport: viewportData.viewport,
    };
    for (let i = 0; i < interpolationList.length; i++) {
        if (interpolationList[i]) {
            _linearlyInterpolateBetween(interpolationList[i].list, interpolationList[i].pair, interpolationData, eventData);
        }
    }
    const { id, renderingEngineId, element } = viewportData.viewport;
    const eventDetails = {
        annotation: toolData,
        element,
        viewportId: id,
        renderingEngineId,
    };
    if (interpolationList.length) {
        triggerEvent(viewportData.viewport.element, EventTypes.ANNOTATION_INTERPOLATION_PROCESS_COMPLETED, eventDetails);
    }
}
function _linearlyInterpolateBetween(indices, annotationPair, interpolationData, eventData) {
    const annotation0 = interpolationData.get(annotationPair[0])[0];
    const annotation1 = interpolationData.get(annotationPair[1])[0];
    const c1 = _generateClosedContour(annotation0.data.contour.polyline);
    const c2 = _generateClosedContour(annotation1.data.contour.polyline);
    const { c1Interp, c2Interp } = _generateInterpolationContourPair(c1, c2);
    c1Interp.kIndex = annotationPair[0];
    c2Interp.kIndex = annotationPair[1];
    indices.forEach(function (index) {
        _linearlyInterpolateContour(c1Interp, c2Interp, index, annotationPair, interpolationData, c1.x.length > c2.x.length, eventData);
    });
}
function getPointCount(pointArray) {
    let sum = 0;
    for (let i = 0; i < pointArray.I.length; i++) {
        if (pointArray.I[i]) {
            sum++;
        }
    }
    return sum;
}
function _linearlyInterpolateContour(c1Interp, c2Interp, sliceIndex, annotationPair, interpolationData, c1HasMoreNodes, eventData) {
    const [startIndex, endIndex] = annotationPair;
    const zInterp = (sliceIndex - startIndex) / (endIndex - startIndex);
    const annotation0 = interpolationData.get(startIndex)[0];
    const annotation1 = interpolationData.get(endIndex)[0];
    const interpolated3DPoints = _generateInterpolatedOpenContour(c1Interp, c2Interp, zInterp, c1HasMoreNodes);
    const nearestAnnotation = zInterp > 0.5 ? annotation1 : annotation0;
    const handlePoints = selectHandles(interpolated3DPoints);
    if (interpolationData.has(sliceIndex)) {
        _editInterpolatedContour(interpolated3DPoints, handlePoints, sliceIndex, nearestAnnotation, eventData);
    }
    else {
        _addInterpolatedContour(interpolated3DPoints, handlePoints, sliceIndex, nearestAnnotation, eventData);
    }
}
function _addInterpolatedContour(interpolated3DPoints, handlePoints, sliceIndex, referencedToolData, eventData) {
    const points = interpolated3DPoints.points;
    const { viewport } = eventData;
    const interpolatedAnnotation = createPolylineToolData(points, handlePoints, referencedToolData);
    const viewRef = viewport.getViewReference({ sliceIndex });
    if (!viewRef) {
        throw new Error(`Can't find slice ${sliceIndex}`);
    }
    Object.assign(interpolatedAnnotation.metadata, viewRef);
    annotationState.state.addAnnotation(interpolatedAnnotation, viewport.element);
    referencedToolData.onInterpolationComplete?.(interpolatedAnnotation, referencedToolData);
    const { parentAnnotationUID } = referencedToolData;
    if (parentAnnotationUID) {
        const parentReferenced = annotationState.state.getAnnotation(parentAnnotationUID);
        const parentAnnotation = _findExistingAnnotation(parentReferenced, sliceIndex, eventData);
        createPolylineHole(viewport, parentAnnotation, interpolatedAnnotation);
    }
}
function _findExistingAnnotation(referencedToolData, sliceIndex, eventData) {
    const { viewport } = eventData;
    const annotations = annotationState.state.getAnnotations(referencedToolData.metadata.toolName, viewport.element);
    for (let i = 0; i < annotations.length; i++) {
        const annotation = annotations[i];
        if (annotation.interpolationUID === referencedToolData.interpolationUID &&
            annotation.metadata.sliceIndex === sliceIndex) {
            return annotation;
        }
    }
}
function _editInterpolatedContour(interpolated3DPoints, handlePoints, sliceIndex, referencedToolData, eventData) {
    const oldAnnotationData = _findExistingAnnotation(referencedToolData, sliceIndex, eventData);
    const points = interpolated3DPoints.points;
    const interpolatedAnnotation = createPolylineToolData(points, handlePoints, oldAnnotationData);
    Object.assign(oldAnnotationData, {
        metadata: interpolatedAnnotation.metadata,
        data: interpolatedAnnotation.data,
    });
}
function _generateInterpolatedOpenContour(c1ir, c2ir, zInterp, c1HasMoreNodes) {
    const indices = c1HasMoreNodes ? c1ir.I : c2ir.I;
    const c1 = PointsManager.fromXYZ(c1ir);
    const c2 = PointsManager.fromXYZ(c2ir);
    const { length } = c1;
    const cInterp = PointsManager.create3(length);
    const vecSubtract = vec3.create();
    const vecResult = vec3.create();
    const c1Source = PointsManager.create3(length);
    c1Source.kIndex = c1ir.kIndex;
    const c2Source = PointsManager.create3(length);
    c2Source.kIndex = c2ir.kIndex;
    for (let i = 0; i < c1ir.x.length; i++) {
        if (indices[i]) {
            const c1point = c1.getPoint(i);
            const c2point = c2.getPoint(i);
            c1Source.push(c1point);
            c2Source.push(c2point);
            vec3.sub(vecSubtract, c2point, c1point);
            cInterp.push(vec3.scaleAndAdd(vecResult, c1point, vecSubtract, zInterp));
        }
    }
    cInterp.sources = [c1Source, c2Source];
    return cInterp;
}
function _generateInterpolationContourPair(c1, c2) {
    const cumPerim1 = _getCumulativePerimeter(c1);
    const cumPerim2 = _getCumulativePerimeter(c2);
    const interpNodes = Math.max(Math.ceil(cumPerim1[cumPerim1.length - 1] / dP), Math.ceil(cumPerim2[cumPerim2.length - 1] / dP));
    const cumPerim1Norm = _normalisedCumulativePerimeter(cumPerim1);
    const cumPerim2Norm = _normalisedCumulativePerimeter(cumPerim2);
    const numNodes1 = interpNodes + c2.x.length;
    const numNodes2 = interpNodes + c1.x.length;
    const perim1Interp = _getInterpolatedPerim(numNodes1, cumPerim1Norm);
    const perim2Interp = _getInterpolatedPerim(numNodes2, cumPerim2Norm);
    const perim1Ind = _getIndicatorArray(numNodes1 - 2, c1.x.length);
    const perim2Ind = _getIndicatorArray(numNodes2 - 2, c2.x.length);
    const nodesPerSegment1 = _getNodesPerSegment(perim1Interp, perim1Ind);
    const nodesPerSegment2 = _getNodesPerSegment(perim2Interp, perim2Ind);
    const c1i = _getSuperSampledContour(c1, nodesPerSegment1);
    const c2i = _getSuperSampledContour(c2, nodesPerSegment2);
    _shiftSuperSampledContourInPlace(c1i, c2i);
    return _reduceContoursToOriginNodes(c1i, c2i);
}
function _reduceContoursToOriginNodes(c1i, c2i) {
    const c1Interp = {
        x: [],
        y: [],
        z: [],
        I: [],
    };
    const c2Interp = {
        x: [],
        y: [],
        z: [],
        I: [],
    };
    for (let i = 0; i < c1i.x.length; i++) {
        if (c1i.I[i] || c2i.I[i]) {
            c1Interp.x.push(c1i.x[i]);
            c1Interp.y.push(c1i.y[i]);
            c1Interp.z.push(c1i.z[i]);
            c1Interp.I.push(c1i.I[i]);
            c2Interp.x.push(c2i.x[i]);
            c2Interp.y.push(c2i.y[i]);
            c2Interp.z.push(c2i.z[i]);
            c2Interp.I.push(c2i.I[i]);
        }
    }
    return {
        c1Interp,
        c2Interp,
    };
}
function _shiftSuperSampledContourInPlace(c1i, c2i) {
    const c1iLength = c1i.x.length;
    const optimal = {
        startingNode: 0,
        totalSquaredXYLengths: Infinity,
    };
    for (let startingNode = 0; startingNode < c1iLength; startingNode++) {
        let node = startingNode;
        let totalSquaredXYLengths = 0;
        for (let iteration = 0; iteration < c1iLength; iteration++) {
            totalSquaredXYLengths +=
                (c1i.x[node] - c2i.x[iteration]) ** 2 +
                    (c1i.y[node] - c2i.y[iteration]) ** 2 +
                    (c1i.z[node] - c2i.z[iteration]) ** 2;
            node++;
            if (node === c1iLength) {
                node = 0;
            }
        }
        if (totalSquaredXYLengths < optimal.totalSquaredXYLengths) {
            optimal.totalSquaredXYLengths = totalSquaredXYLengths;
            optimal.startingNode = startingNode;
        }
    }
    const node = optimal.startingNode;
    _shiftCircularArray(c1i.x, node);
    _shiftCircularArray(c1i.y, node);
    _shiftCircularArray(c1i.z, node);
    _shiftCircularArray(c1i.I, node);
}
function _shiftCircularArray(arr, count) {
    count -= arr.length * Math.floor(count / arr.length);
    const slicedArray = arr.splice(0, count);
    arr.push(...slicedArray);
    return arr;
}
function _getSuperSampledContour(c, nodesPerSegment) {
    const ci = {
        x: [],
        y: [],
        z: [],
        I: [],
    };
    for (let n = 0; n < c.x.length - 1; n++) {
        ci.x.push(c.x[n]);
        ci.y.push(c.y[n]);
        ci.z.push(c.z[n]);
        ci.I.push(true);
        const xSpacing = (c.x[n + 1] - c.x[n]) / (nodesPerSegment[n] + 1);
        const ySpacing = (c.y[n + 1] - c.y[n]) / (nodesPerSegment[n] + 1);
        const zSpacing = (c.z[n + 1] - c.z[n]) / (nodesPerSegment[n] + 1);
        for (let i = 0; i < nodesPerSegment[n] - 1; i++) {
            ci.x.push(ci.x[ci.x.length - 1] + xSpacing);
            ci.y.push(ci.y[ci.y.length - 1] + ySpacing);
            ci.z.push(ci.z[ci.z.length - 1] + zSpacing);
            ci.I.push(false);
        }
    }
    return ci;
}
function _getNodesPerSegment(perimInterp, perimInd) {
    const idx = [];
    for (let i = 0; i < perimInterp.length; ++i) {
        idx[i] = i;
    }
    idx.sort(function (a, b) {
        return perimInterp[a] < perimInterp[b] ? -1 : 1;
    });
    const perimIndSorted = [];
    for (let i = 0; i < perimInd.length; i++) {
        perimIndSorted.push(perimInd[idx[i]]);
    }
    const indicesOfOriginNodes = perimIndSorted.reduce(function (arr, elementValue, i) {
        if (elementValue) {
            arr.push(i);
        }
        return arr;
    }, []);
    const nodesPerSegment = [];
    for (let i = 0; i < indicesOfOriginNodes.length - 1; i++) {
        nodesPerSegment.push(indicesOfOriginNodes[i + 1] - indicesOfOriginNodes[i]);
    }
    return nodesPerSegment;
}
function _getIndicatorArray(numFalse, numTrue) {
    const perimInd = new Array(numFalse + numTrue);
    perimInd.fill(false, 0, numFalse);
    perimInd.fill(true, numFalse, numFalse + numTrue);
    return perimInd;
}
function _getInterpolatedPerim(numNodes, cumPerimNorm) {
    const diff = 1 / (numNodes - 1);
    const linspace = [diff];
    for (let i = 1; i < numNodes - 2; i++) {
        linspace.push(linspace[linspace.length - 1] + diff);
    }
    return linspace.concat(cumPerimNorm);
}
function _normalisedCumulativePerimeter(cumPerim) {
    const cumPerimNorm = [];
    for (let i = 0; i < cumPerim.length; i++) {
        cumPerimNorm.push(cumPerim[i] / cumPerim[cumPerim.length - 1]);
    }
    return cumPerimNorm;
}
function _getCumulativePerimeter(contour) {
    const cumulativePerimeter = [0];
    for (let i = 1; i < contour.x.length; i++) {
        const lengthOfSegment = Math.sqrt((contour.x[i] - contour.x[i - 1]) ** 2 +
            (contour.y[i] - contour.y[i - 1]) ** 2 +
            (contour.z[i] - contour.z[i - 1]) ** 2);
        cumulativePerimeter.push(cumulativePerimeter[i - 1] + lengthOfSegment);
    }
    return cumulativePerimeter;
}
function _generateClosedContour(points) {
    const c = {
        x: [],
        y: [],
        z: [],
    };
    for (let i = 0; i < points.length; i++) {
        c.x[i] = points[i][0];
        c.y[i] = points[i][1];
        c.z[i] = points[i][2];
    }
    c.x.push(c.x[0]);
    c.y.push(c.y[0]);
    c.z.push(c.z[0]);
    return c;
}
export default interpolate;
