import { CompositeLayer } from "@deck.gl/core";
import { DataFilterExtension, PathStyleExtension } from "@deck.gl/extensions";
import { PathLayer } from "@deck.gl/layers";
import _ from "lodash";
import { isClose } from "../../../utils/measurement";
import { getFromAccessor, hasUpdateTriggerChanged, } from "../../utils/layerTools";
import { getCumulativeDistance, getFractionPositionSegmentIndices, interpolateDataOnTrajectory, } from "../utils/trajectory";
export var SubLayerId;
(function (SubLayerId) {
    SubLayerId["DASHED_PATH"] = "dashed-paths";
    SubLayerId["SOLID_PATH"] = "solid-paths";
})(SubLayerId || (SubLayerId = {}));
export class DashedSectionsPathLayer extends CompositeLayer {
    updateState({ changeFlags }) {
        var _a, _b, _c, _d, _e, _f;
        if (!this.isLoaded)
            return;
        const data = this.props.data;
        const { getDashedSectionsAlongPath, getCumulativePathDistance, getPath, } = this.props;
        if (!Array.isArray(data))
            throw new Error(`Expected loaded data to be a list, instead got ${typeof data}`);
        const dataChanged = changeFlags.dataChanged;
        const pathChanged = hasUpdateTriggerChanged(changeFlags, "getPath");
        const cumulativePathDistanceChanged = hasUpdateTriggerChanged(changeFlags, "getCumulativePathDistance");
        const sectionsChanged = hasUpdateTriggerChanged(changeFlags, "getDashedSectionsAlongPath");
        const filterValueChanged = hasUpdateTriggerChanged(changeFlags, "getFilterValue");
        if (dataChanged ||
            pathChanged ||
            cumulativePathDistanceChanged ||
            filterValueChanged ||
            sectionsChanged) {
            // Each path will need to be split into two data sets based on whether the section is dashed or not.
            const dashedPathSubLayerData = [];
            const normalPathSubLayerData = [];
            const pathCache = (_a = this.state.pathCache) !== null && _a !== void 0 ? _a : new Map();
            const cumulativePathDistanceCache = (_b = this.state.cumulativePathDistanceCache) !== null && _b !== void 0 ? _b : new Map();
            const sectionsCache = (_c = this.state.sectionsCache) !== null && _c !== void 0 ? _c : new Map();
            for (let index = 0; index < data.length; index++) {
                const datum = data[index];
                const itemContext = { data, index, target: [] };
                // Only recompute from accessors if relevant changeflags (or data) has changed since last time
                if (pathChanged || dataChanged) {
                    pathCache.set(index, getFromAccessor(getPath, datum, itemContext));
                }
                if (cumulativePathDistanceChanged || dataChanged) {
                    let cumulativeDistance = getFromAccessor(getCumulativePathDistance, datum, itemContext);
                    if (!cumulativeDistance) {
                        cumulativeDistance = getCumulativeDistance(pathCache.get(index));
                    }
                    cumulativePathDistanceCache.set(index, cumulativeDistance);
                }
                if (sectionsChanged || dataChanged) {
                    const sections = (_d = getFromAccessor(getDashedSectionsAlongPath, datum, itemContext)) !== null && _d !== void 0 ? _d : [];
                    const chunkedSections = typeof sections[0] === "number"
                        ? _.chunk(sections, 2)
                        : sections;
                    sectionsCache.set(index, chunkedSections);
                }
                const path = pathCache.get(index);
                const sections = sectionsCache.get(index);
                const cumulativePathDistance = cumulativePathDistanceCache.get(index);
                let prevSectionEnd = 0;
                // If a data-filter extension is present, we might need to split the filter value array along the path.
                let filterValue;
                const { filterEnabled, getFilterValue } = this
                    .props;
                const hasFilterExtension = (_e = this.props.extensions) === null || _e === void 0 ? void 0 : _e.some((e) => e instanceof DataFilterExtension);
                if (hasFilterExtension && filterEnabled && getFilterValue) {
                    filterValue = getFromAccessor(getFilterValue, datum, itemContext);
                }
                // ! We assume sections are sorted
                for (const section of sections) {
                    const dashedSectionStart = section[0];
                    const dashedSectionEnd = (_f = section[1]) !== null && _f !== void 0 ? _f : 1;
                    // There's a gap between these dashed segments, so we add a normal path
                    if (dashedSectionStart > prevSectionEnd) {
                        const normalSection = {
                            id: `path-${index}-normal-section-${normalPathSubLayerData.length}`,
                            pathType: "normal",
                            properties: {
                                filterValue: getSectionFilterValue(filterValue, cumulativePathDistance, prevSectionEnd, dashedSectionStart),
                            },
                            path: getSectionPathPositions(path, cumulativePathDistance, prevSectionEnd, dashedSectionStart),
                        };
                        normalPathSubLayerData.push(this.getSubLayerRow(normalSection, datum, index));
                    }
                    const dashedSection = {
                        id: `path-${index}-dashed-section-${normalPathSubLayerData.length}`,
                        pathType: "dashed",
                        properties: {
                            filterValue: getSectionFilterValue(filterValue, cumulativePathDistance, dashedSectionStart, dashedSectionEnd),
                        },
                        path: getSectionPathPositions(path, cumulativePathDistance, dashedSectionStart, dashedSectionEnd),
                    };
                    dashedPathSubLayerData.push(this.getSubLayerRow(dashedSection, datum, index));
                    prevSectionEnd = dashedSectionEnd;
                }
                if (prevSectionEnd < 1) {
                    const normalSection = {
                        id: `path-${index}-normal-section-${normalPathSubLayerData.length}`,
                        pathType: "normal",
                        properties: {
                            filterValue: getSectionFilterValue(filterValue, cumulativePathDistance, prevSectionEnd, 1),
                        },
                        path: getSectionPathPositions(path, cumulativePathDistance, prevSectionEnd, 1),
                    };
                    normalPathSubLayerData.push(this.getSubLayerRow(normalSection, datum, index));
                }
            }
            this.setState({
                dashedPathSubLayerData,
                normalPathSubLayerData,
                pathCache,
                sectionsCache,
                cumulativePathDistanceCache,
            });
        }
    }
    getSubLayerId(sublayerId) {
        return this.getSubLayerProps({ id: sublayerId }).id;
    }
    getPickingInfo(params) {
        const sourceInfo = super.getPickingInfo(params);
        if (!sourceInfo.coordinate || !sourceInfo.object)
            return sourceInfo;
        if (sourceInfo.coordinate.length === 3 &&
            this.props.positionFormat === "XY") {
            // Occasionally some other picking logic (f.ex. the re-triggered picking from the Map wrapper) will fire this with a 3D coordinate, regardless of position-format. To avoid picking the wrong segment, we discard the z-coord
            sourceInfo.coordinate = sourceInfo.coordinate.slice(0, 2);
        }
        const coordinate = sourceInfo.coordinate;
        const zScale = this.props.modelMatrix ? this.props.modelMatrix[10] : 1;
        if (coordinate[2] !== undefined) {
            coordinate[2] /= Math.max(0.001, zScale);
        }
        let path = this.state.pathCache.get(sourceInfo.index);
        const sections = this.state.sectionsCache.get(sourceInfo.index);
        const cumulativePathDistance = this.state.cumulativePathDistanceCache.get(sourceInfo.index);
        if (!path || !cumulativePathDistance || !sections) {
            throw new Error("Expected cached accessor values for hovered object");
        }
        if (sourceInfo.coordinate.length === 2) {
            path = path.map((v) => v.slice(0, 2));
        }
        const hoveredDistance = interpolateDataOnTrajectory(sourceInfo.coordinate, cumulativePathDistance, path);
        if (hoveredDistance == null)
            return sourceInfo;
        const hoveredPositionAlong = hoveredDistance / cumulativePathDistance.at(-1);
        // ? Possibly allow more nuanced info here?
        const hoveredSectionIndex = sections.findIndex(([from, to]) => from <= hoveredPositionAlong && to >= hoveredPositionAlong);
        return Object.assign(Object.assign({}, sourceInfo), { dashedSectionIndex: hoveredSectionIndex, positionAlong: hoveredDistance });
    }
    renderLayers() {
        const otherProps = _.omit(this.props, "getPath");
        return [
            new PathLayer(Object.assign(Object.assign({}, this.getSubLayerProps(Object.assign(Object.assign({}, otherProps), { id: SubLayerId.DASHED_PATH, data: this.state.dashedPathSubLayerData, getColor: this.getSubLayerAccessor(this.props.getColor), extensions: [
                    ...this.props.extensions.filter((f) => !(f instanceof PathStyleExtension)),
                    new PathStyleExtension({
                        dash: true,
                        highPrecisionDash: true,
                    }),
                ], updateTriggers: {
                    getFilterValue: this.props.updateTriggers["getFilterValue"],
                } }))), { 
                // ! These props gets overriden if included inside getSubLayerProps
                getDashArray: this.props.getSectionDashArray, dashGapPickable: true, getFilterValue: (d) => { var _a; return (_a = d.properties) === null || _a === void 0 ? void 0 : _a["filterValue"]; } })),
            new PathLayer(Object.assign(Object.assign({}, this.getSubLayerProps(Object.assign(Object.assign({}, otherProps), { id: SubLayerId.SOLID_PATH, data: this.state.normalPathSubLayerData, getColor: this.getSubLayerAccessor(this.props.getColor), parameters: this.props.parameters, updateTriggers: {
                    getFilterValue: this.props.updateTriggers["getFilterValue"],
                } }))), { getFilterValue: (d) => { var _a; return (_a = d.properties) === null || _a === void 0 ? void 0 : _a["filterValue"]; } })),
        ];
    }
}
DashedSectionsPathLayer.layerName = "DashedSectionsPathLayer";
DashedSectionsPathLayer.defaultProps = Object.assign(Object.assign({}, PathLayer.defaultProps), { getSectionDashArray: { type: "accessor", value: [5, 5] }, getDashedSectionsAlongPath: {
        type: "accessor",
        // eslint-disable-next-line @typescript-eslint/no-explicit-any
        value: (object) => object.dashSectionsAlongPath,
    }, getCumulativePathDistance: {
        type: "accessor",
        // If not provided, we manually compute it from the path
        value: undefined,
    } });
function getSectionFilterValue(filterValue, cumulativePathDistance, startFraction, endFraction) {
    if (!Array.isArray(filterValue))
        return filterValue;
    if (startFraction === 0 && endFraction === 1)
        return [...filterValue];
    if (filterValue.length !== cumulativePathDistance.length) {
        throw new Error("Expected filter value array to be same length as path computation array");
    }
    const { sliceStart, sliceEnd, startSegment, endSegment } = computeSliceIndices(filterValue, cumulativePathDistance, startFraction, endFraction);
    const startFilterValue = startSegment[0];
    const endFilterValue = endSegment[1];
    // Build a segment of positions
    const segment = [
        startFilterValue,
        ...filterValue.slice(sliceStart, sliceEnd),
        endFilterValue,
    ];
    return segment;
}
function getSectionPathPositions(path, cumulativePathDistance, startFraction, endFraction) {
    if (startFraction === 0 && endFraction === 1)
        return [...path];
    if (path.length === 0)
        return [];
    if (cumulativePathDistance.length !== path.length) {
        throw new Error("Expected path measurements array to be same length as path array");
    }
    const { sliceStart, sliceEnd, startSegment, endSegment } = computeSliceIndices(path, cumulativePathDistance, startFraction, endFraction);
    const startPosition = _.zipWith(startSegment[0], startSegment[1], (pl, pu) => {
        return pl + startSegment[2] * (pu - pl);
    });
    const endPosition = _.zipWith(endSegment[0], endSegment[1], (pl, pu) => {
        return pl + endSegment[2] * (pu - pl);
    });
    // Build a segment of positions
    const segment = [
        startPosition,
        ...path.slice(sliceStart, sliceEnd),
        endPosition,
    ];
    return segment;
}
function computeSliceIndices(dataArr, cumulativePathDistance, startFraction, endFraction) {
    if (cumulativePathDistance.length !== dataArr.length) {
        throw new Error("Expected distance and data array to be same length as path array");
    }
    const [lowerIndexStart, upperIndexStart, sectionFractionStart] = getFractionPositionSegmentIndices(startFraction, dataArr, cumulativePathDistance);
    const [lowerIndexEnd, upperIndexEnd, sectionFractionEnd] = getFractionPositionSegmentIndices(endFraction, dataArr, cumulativePathDistance);
    const sliceStartOffset = isClose(sectionFractionStart, 1) ? 1 : 0;
    const sliceEndOffset = isClose(sectionFractionEnd, 0) ? 2 : 1;
    return {
        startSegment: [
            dataArr[lowerIndexStart],
            dataArr[upperIndexStart],
            sectionFractionStart,
        ],
        endSegment: [
            dataArr[lowerIndexEnd],
            dataArr[upperIndexEnd],
            sectionFractionEnd,
        ],
        sliceStart: upperIndexStart + sliceStartOffset,
        sliceEnd: lowerIndexEnd + sliceEndOffset,
    };
}
//# sourceMappingURL=dashedSectionsPathLayer.js.map