import { CompositeLayer } from "@deck.gl/core";
import { DataFilterExtension } from "@deck.gl/extensions";
import { PathLayer, PolygonLayer } from "@deck.gl/layers";
import { GL } from "@luma.gl/constants";
import _ from "lodash";
import { blendColors } from "../../../utils/Color";
import { getFromAccessor, hasUpdateTriggerChanged, } from "../../utils/layerTools";
import { getSize, LINE } from "../utils/features";
import { buildMarkerPath2D } from "../utils/markers";
import { getCumulativeDistance, getFractionPositionSegmentIndices, getPositionAndAngleOnTrajectoryPath, } from "../utils/trajectory";
export var SubLayerId;
(function (SubLayerId) {
    SubLayerId["MARKERS_2D"] = "markers-2d";
    SubLayerId["MARKERS_2D_OUTLINE"] = "markers-2d--outline";
    SubLayerId["MARKERS_2D_PICKING"] = "markers-2d--picking";
})(SubLayerId || (SubLayerId = {}));
/**
 * Wells sub-layer for displaying assorted markers along the trajectory of a well. The layer
 * is * primarily designed to be used in 2D views, so the markers are built by simple flat
 * lines, and are aligned along the XY path of the trajectories. For 3D markers, see the
 * `WellMarkersLayer` class
 */
export class FlatWellMarkersLayer extends CompositeLayer {
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    constructor(...args) {
        super(...args);
        this.getMarkerColor = this.getMarkerColor.bind(this);
    }
    updateState({ changeFlags, props, oldProps, }) {
        var _a, _b;
        if (!this.isLoaded)
            return;
        const data = this.props.data;
        if (props.highlightedObjectIndex !== oldProps.highlightedObjectIndex) {
            this.setState({
                highlightedSourceIndex: props.highlightedObjectIndex,
            });
        }
        const markersUpdate = hasUpdateTriggerChanged(changeFlags, "getMarkers");
        const trajectoryUpdate = hasUpdateTriggerChanged(changeFlags, "getTrajectoryPath");
        const cumulativeDistanceUpdate = hasUpdateTriggerChanged(changeFlags, "getCumulativePathDistance");
        if (!changeFlags.dataChanged &&
            !markersUpdate &&
            !trajectoryUpdate &&
            !cumulativeDistanceUpdate)
            return;
        if (!this.props.getTrajectoryPath)
            return;
        if (!Array.isArray(data))
            throw new Error(`Expected data to be a list, instead got ${typeof data}`);
        const cumulativePathDistanceCache = (_a = this.state.cumulativePathDistanceCache) !== null && _a !== void 0 ? _a : new Map();
        const markers = [];
        let markerDataOffset = 0;
        for (let index = 0; index < data.length; index++) {
            const itemContext = { data, index, target: [] };
            const datum = data[index];
            const trajectoryPath = getFromAccessor(this.props.getTrajectoryPath, datum, itemContext);
            if (cumulativeDistanceUpdate || changeFlags.dataChanged) {
                const cumulativeDistance = (_b = getFromAccessor(props.getCumulativePathDistance, datum, itemContext
                // The typing is a little misleading here; the *default* accessor could
                // potentially return undefined, so we need to include a fallback here
                )) !== null && _b !== void 0 ? _b : getCumulativeDistance(trajectoryPath);
                cumulativePathDistanceCache.set(index, cumulativeDistance);
            }
            const cumulativePathDistance = cumulativePathDistanceCache.get(index);
            const trajectoryMarkers = getFromAccessor(this.props.getMarkers, datum, itemContext);
            for (let markerIndex = 0; markerIndex < trajectoryMarkers.length; markerIndex++) {
                const { type, positionAlongPath, properties } = trajectoryMarkers[markerIndex];
                const posFragment = _.clamp(positionAlongPath, 0, 1);
                const [angle, worldPosition] = getPositionAndAngleOnTrajectoryPath(posFragment, trajectoryPath, cumulativePathDistance, 
                // We'll disregard 3D positioning here; the markers look nicer when they're "flat"
                undefined, false);
                const markerData = {
                    type: type,
                    position: worldPosition,
                    angle: angle,
                    positionAlongPath: posFragment,
                    properties: properties,
                    // Link source data
                    sourceObject: datum,
                    sourceIndex: index,
                    markerIndex: markerIndex + markerDataOffset,
                };
                markers.push(markerData);
                // ! We want to show perforations as a spike on both sides of the line. Since they will rendered as disconnected lines, we need to inject a second, rotated row for it
                if (type === "perforation") {
                    const markerData2 = Object.assign(Object.assign({}, markerData), { angle: angle + Math.PI });
                    markers.push(markerData2);
                }
            }
            markerDataOffset += trajectoryMarkers.length;
        }
        this.setState({ markers, cumulativePathDistanceCache });
    }
    filterSubLayer(context) {
        if (context.layer.id ===
            this.getSubLayerProps({ id: SubLayerId.MARKERS_2D_PICKING }).id) {
            return context.isPicking;
        }
        return true;
    }
    getPickingInfo({ info }) {
        var _a, _b;
        const hoveredIndex = (_b = (_a = info.object) === null || _a === void 0 ? void 0 : _a.markerIndex) !== null && _b !== void 0 ? _b : -1;
        this.setState({ hoveredMarkerIndex: hoveredIndex });
        return info;
    }
    updateAutoHighlight(info) {
        if (info.sourceLayer !== this && this.props.autoHighlight) {
            // Assume this is from the source
            this.setState({ highlightedSourceIndex: info.index });
        }
    }
    // We've done our own style of binding, so we override the base one to reflect that.
    getSubLayerAccessor(accessor) {
        if (typeof accessor === "function") {
            const objectInfo = {
                index: -1,
                // @ts-expect-error (TS2322) -- Accessing resolved data
                data: this.props.data,
                target: [],
            };
            // @ts-expect-error (TS2416) -- deck.gl types these with In as the source data, which is technically wrong, so we ignore this error
            return (x) => {
                objectInfo.index = x.sourceIndex;
                // @ts-expect-error (TS2349) -- Out is never a function
                return accessor(x.sourceObject, objectInfo);
            };
        }
        return accessor;
    }
    getMarkerColor(d, ctx) {
        const markerIndex = d.markerIndex;
        // Check if this marker or any related marker is being hovered
        const isHighlighted = this.props.autoHighlight &&
            (markerIndex === this.state.hoveredMarkerIndex ||
                d.sourceIndex === this.state.highlightedSourceIndex);
        // Get base color
        let baseColor;
        if (this.props.getMarkerColor) {
            baseColor = getFromAccessor(this.props.getMarkerColor, d, ctx);
        }
        else if (this.props.getLineColor) {
            baseColor = getFromAccessor(this.props.getLineColor, 
            // @ts-expect-error -- accessors are hard :O
            d.sourceObject, ctx);
        }
        else {
            baseColor = [0, 0, 0, 255];
        }
        if (!isHighlighted) {
            return baseColor;
        }
        // Mimic auto-highlight behavior
        let highlightColor;
        if (typeof this.props.highlightColor === "function") {
            console.warn("highlightColor from function is not supported in FlatWellMarkersLayer");
            highlightColor = [0, 0, 128, 128];
        }
        else {
            highlightColor = this.props.highlightColor;
        }
        return blendColors(baseColor, highlightColor);
    }
    renderLayers() {
        var _a, _b, _c, _d, _e, _f;
        const { cumulativePathDistanceCache } = this.state;
        const lineWidthAccessor = this.getSubLayerAccessor(this.props.getLineWidth);
        const sourceFilterAccessor = this.getSubLayerAccessor(this.props.getFilterValue);
        const markerFilterAccessor = (d, ctx) => {
            const sourcePathDistance = cumulativePathDistanceCache.get(d.sourceIndex);
            const sourceFilterValue = getFromAccessor(sourceFilterAccessor, d, ctx);
            return getMarkerFilterValue(d, sourceFilterValue, sourcePathDistance);
        };
        const sharedSubLayerProps = Object.assign(Object.assign({}, this.props), { data: this.state.markers, billboard: this.props.lineBillboard, widthScale: this.props.lineWidthScale, widthUnits: this.props.lineWidthUnits, getWidth: lineWidthAccessor, getPath: (d) => buildMarkerPath2D(d.type, d.position, d.angle), updateTriggers: {
                getPath: (_a = this.props.updateTriggers) === null || _a === void 0 ? void 0 : _a["getTrajectoryPath"],
                getColor: [
                    this.state.hoveredMarkerIndex,
                    this.state.highlightedSourceIndex,
                    ...((_c = (_b = this.props.updateTriggers) === null || _b === void 0 ? void 0 : _b["getLineColor"]) !== null && _c !== void 0 ? _c : []),
                    ...((_e = (_d = this.props.updateTriggers) === null || _d === void 0 ? void 0 : _d["getMarkerColor"]) !== null && _e !== void 0 ? _e : []),
                ],
            } });
        // TODO: Distinct layers based on 2D or 3D views, as they should render completely differently based on context
        // TODO: Better scaling for markers. For instance, a screen marker should always be the same amount bigger than it's parent line
        return [
            new PathLayer(Object.assign(Object.assign({}, this.getSubLayerProps(Object.assign(Object.assign({}, sharedSubLayerProps), { id: SubLayerId.MARKERS_2D_OUTLINE, parameters: {
                    [GL.POLYGON_OFFSET_FACTOR]: 1,
                    [GL.POLYGON_OFFSET_UNITS]: 1,
                } }))), { visible: this.props.outline, getColor: [0, 0, 0], getWidth: getSize(LINE, lineWidthAccessor, 1), 
                // Disable this layer for picking, instead using MARKERS_2D_PICKING
                pickable: false, autoHighlight: false, highlightedObjectIndex: -1, getFilterValue: markerFilterAccessor })),
            new PathLayer(Object.assign(Object.assign({}, this.getSubLayerProps(Object.assign(Object.assign({}, sharedSubLayerProps), { id: SubLayerId.MARKERS_2D }))), { getColor: this.getMarkerColor, getFilterValue: markerFilterAccessor, 
                // Disable this layer for picking, instead using MARKERS_2D_PICKING
                pickable: false, autoHighlight: false, highlightedObjectIndex: -1 })),
            // To make markers easier to pick, we render them as filled polygons whilst picking
            new PolygonLayer(Object.assign(Object.assign({}, this.getSubLayerProps(Object.assign(Object.assign({}, sharedSubLayerProps), { id: SubLayerId.MARKERS_2D_PICKING, updateTriggers: {
                    getPolygon: (_f = this.props.updateTriggers) === null || _f === void 0 ? void 0 : _f["getTrajectoryPath"],
                } }))), { visible: this.props.pickable, filled: true, stroked: true, 
                // Only render pick-able markers
                extensions: [
                    ...this.props.extensions,
                    new DataFilterExtension({ categorySize: 1 }),
                ], getFilterValue: markerFilterAccessor, getFilterCategory: (d) => Number(d.type !== "perforation"), getPolygon: (d) => buildMarkerPath2D(d.type, d.position, d.angle), getLineWidth: lineWidthAccessor, lineWidthScale: this.props.lineWidthScale, lineWidthUnits: this.props.lineWidthUnits, 
                // Disable auto-highlight, since we've implemented it manually
                autoHighlight: false, highlightedObjectIndex: -1 })),
        ];
    }
}
FlatWellMarkersLayer.layerName = "FlatWellMarkersLayer";
FlatWellMarkersLayer.defaultProps = {
    getMarkers: { type: "accessor", value: [] },
    // Default to undefined so we can fall back to trajectory color
    getMarkerColor: undefined,
    getCumulativePathDistance: undefined,
};
function getMarkerFilterValue(markerData, sourceFilterValue, cumulativePathDistance) {
    if (!Array.isArray(sourceFilterValue))
        return sourceFilterValue !== null && sourceFilterValue !== void 0 ? sourceFilterValue : 1;
    const [lowerIndex] = getFractionPositionSegmentIndices(markerData.positionAlongPath, sourceFilterValue, cumulativePathDistance);
    return sourceFilterValue[lowerIndex];
}
//# sourceMappingURL=flatWellMarkersLayer.js.map