/*---------------------------------------------------------------------------------------------
* Copyright (c) Bentley Systems, Incorporated. All rights reserved.
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/
/** @packageDocumentation
 * @module LocatingElements
 */
import { assert, Id64 } from "@itwin/core-bentley";
import { Arc3d, LineSegment3d, LineString3d, Path, Point3d } from "@itwin/core-geometry";
import { LinePixels } from "@itwin/core-common";
import { IModelApp } from "./IModelApp";
import { IconSprites } from "./Sprites";
import { GraphicType } from "./common/render/GraphicType";
/**
 * @public
 * @extensions
 */
export var SnapMode;
(function (SnapMode) {
    SnapMode[SnapMode["Nearest"] = 1] = "Nearest";
    SnapMode[SnapMode["NearestKeypoint"] = 2] = "NearestKeypoint";
    SnapMode[SnapMode["MidPoint"] = 4] = "MidPoint";
    SnapMode[SnapMode["Center"] = 8] = "Center";
    SnapMode[SnapMode["Origin"] = 16] = "Origin";
    SnapMode[SnapMode["Bisector"] = 32] = "Bisector";
    SnapMode[SnapMode["Intersection"] = 64] = "Intersection";
})(SnapMode || (SnapMode = {}));
/**
 * @public
 * @extensions
 */
export var SnapHeat;
(function (SnapHeat) {
    SnapHeat[SnapHeat["None"] = 0] = "None";
    SnapHeat[SnapHeat["NotInRange"] = 1] = "NotInRange";
    SnapHeat[SnapHeat["InRange"] = 2] = "InRange";
})(SnapHeat || (SnapHeat = {}));
/** The procedure that generated this Hit.
 * @public
 * @extensions
 */
export var HitSource;
(function (HitSource) {
    HitSource[HitSource["None"] = 0] = "None";
    HitSource[HitSource["FromUser"] = 1] = "FromUser";
    HitSource[HitSource["MotionLocate"] = 2] = "MotionLocate";
    HitSource[HitSource["AccuSnap"] = 3] = "AccuSnap";
    HitSource[HitSource["TentativeSnap"] = 4] = "TentativeSnap";
    HitSource[HitSource["DataPoint"] = 5] = "DataPoint";
    HitSource[HitSource["Application"] = 6] = "Application";
    HitSource[HitSource["EditAction"] = 7] = "EditAction";
    HitSource[HitSource["EditActionSS"] = 8] = "EditActionSS";
})(HitSource || (HitSource = {}));
/** What was being tested to generate this hit. This is not the element or
 * GeometricPrimitive that generated the Hit, it is an indication of whether it is an edge or interior hit.
 * @public
 * @extensions
 */
export var HitGeomType;
(function (HitGeomType) {
    HitGeomType[HitGeomType["None"] = 0] = "None";
    HitGeomType[HitGeomType["Point"] = 1] = "Point";
    HitGeomType[HitGeomType["Segment"] = 2] = "Segment";
    HitGeomType[HitGeomType["Curve"] = 3] = "Curve";
    HitGeomType[HitGeomType["Arc"] = 4] = "Arc";
    HitGeomType[HitGeomType["Surface"] = 5] = "Surface";
})(HitGeomType || (HitGeomType = {}));
/** Classification of GeometricPrimitive that generated the Hit.
 * @public
 * @extensions
 */
export var HitParentGeomType;
(function (HitParentGeomType) {
    HitParentGeomType[HitParentGeomType["None"] = 0] = "None";
    HitParentGeomType[HitParentGeomType["Wire"] = 1] = "Wire";
    HitParentGeomType[HitParentGeomType["Sheet"] = 2] = "Sheet";
    HitParentGeomType[HitParentGeomType["Solid"] = 3] = "Solid";
    HitParentGeomType[HitParentGeomType["Mesh"] = 4] = "Mesh";
    HitParentGeomType[HitParentGeomType["Text"] = 5] = "Text";
})(HitParentGeomType || (HitParentGeomType = {}));
/**
 * @public
 * @extensions
 */
export var HitPriority;
(function (HitPriority) {
    HitPriority[HitPriority["WireEdge"] = 0] = "WireEdge";
    HitPriority[HitPriority["PlanarEdge"] = 1] = "PlanarEdge";
    HitPriority[HitPriority["NonPlanarEdge"] = 2] = "NonPlanarEdge";
    HitPriority[HitPriority["SilhouetteEdge"] = 3] = "SilhouetteEdge";
    HitPriority[HitPriority["PlanarSurface"] = 4] = "PlanarSurface";
    HitPriority[HitPriority["NonPlanarSurface"] = 5] = "NonPlanarSurface";
    HitPriority[HitPriority["Unknown"] = 6] = "Unknown";
})(HitPriority || (HitPriority = {}));
/**
 * @public
 * @extensions
 */
export var HitDetailType;
(function (HitDetailType) {
    HitDetailType[HitDetailType["Hit"] = 1] = "Hit";
    HitDetailType[HitDetailType["Snap"] = 2] = "Snap";
    HitDetailType[HitDetailType["Intersection"] = 3] = "Intersection";
})(HitDetailType || (HitDetailType = {}));
;
/** A HitDetail stores the result when locating geometry displayed in a view.
 * It holds an approximate location on an element (or decoration) from a *pick*.
 * @public
 * @extensions
 */
export class HitDetail {
    /** The point in world coordinates that was used as the initial locate point. */
    get testPoint() { return this._props.testPoint; }
    /** The viewport in which the locate operation was performed. */
    get viewport() { return this._props.viewport; }
    /** The procedure that requested the locate operation. */
    get hitSource() { return this._props.hitSource; }
    /** The approximate location in world coordinates on the geometry identified by this HitDetail. */
    get hitPoint() { return this._props.hitPoint; }
    /** The source of the geometry. This may be a persistent element Id, or a transient Id used for, e.g., pickable decorations. */
    get sourceId() { return this._props.sourceId; }
    /** The hit geometry priority/classification. */
    get priority() { return this._props.priority; }
    /** The xy distance to the hit in view coordinates. */
    get distXY() { return this._props.distXY; }
    /** The distance in view coordinates between the hit and the near plane. */
    get distFraction() { return this._props.distFraction; }
    /** The [SubCategory]($backend) to which the hit geometry belongs. */
    get subCategoryId() { return this._props.subCategoryId; }
    /** The class of the hit geometry. */
    get geometryClass() { return this._props.geometryClass; }
    /** The Id of the [[ModelState]] from which the hit originated. */
    get modelId() { return this._props.modelId; }
    /** The IModelConnection from which the hit originated.
     * This should almost always be left undefined, unless the hit is known to have originated from an iModel
     * other than the one associated with the viewport.
     * @internal
     */
    get sourceIModel() { return this._props.sourceIModel; }
    /** @internal */
    get transformFromSourceIModel() { return this._props.transformFromSourceIModel; }
    /** @internal chiefly for debugging */
    get tileId() { return this._props.tileId; }
    /** True if the hit originated from a reality model classifier.
     * @alpha
     */
    get isClassifier() { return this._props.isClassifier; }
    /** Information about the [ViewAttachment]($backend) within which the hit geometry resides, if any.
     * @note Only [[SheetViewState]]s can have view attachments.
     * @beta
     */
    get viewAttachment() { return this._props.path?.viewAttachment; }
    /** Describes the path by which the hit was located through a series of attached views.
     * @beta
     */
    get path() { return this._props.path; }
    /** @internal */
    constructor(arg0, viewport, hitSource, hitPoint, sourceId, priority, distXY, distFraction, subCategoryId, geometryClass, modelId, sourceIModel, tileId, isClassifier) {
        if (arg0 instanceof Point3d) {
            assert(undefined !== viewport && undefined !== hitSource && undefined !== hitPoint && undefined !== sourceId);
            assert(undefined !== priority && undefined !== distXY && undefined !== distFraction);
            this._props = {
                testPoint: arg0,
                viewport,
                hitSource,
                hitPoint,
                sourceId,
                priority,
                distXY,
                distFraction,
                subCategoryId,
                geometryClass,
                modelId,
                sourceIModel,
                tileId,
                isClassifier,
            };
        }
        else {
            // Ignore an empty path.
            const path = arg0.path?.sectionDrawingAttachment || arg0.path?.viewAttachment ? arg0.path : undefined;
            // Tempting to use { ...arg0 } but spread operator omits getters so, e.g., if input is a HitDetail we would lose all the properties.
            this._props = {
                testPoint: arg0.testPoint,
                viewport: arg0.viewport,
                hitSource: arg0.hitSource,
                hitPoint: arg0.hitPoint,
                sourceId: arg0.sourceId,
                priority: arg0.priority,
                distXY: arg0.distXY,
                distFraction: arg0.distFraction,
                subCategoryId: arg0.subCategoryId,
                geometryClass: arg0.geometryClass,
                modelId: arg0.modelId,
                sourceIModel: arg0.sourceIModel,
                transformFromSourceIModel: arg0.transformFromSourceIModel,
                tileId: arg0.tileId,
                isClassifier: arg0.isClassifier,
                path,
            };
        }
    }
    /** Get the type of HitDetail.
     * @returns HitDetailType.Hit if this is a HitDetail, HitDetailType.Snap if it is a SnapDetail
     */
    getHitType() { return HitDetailType.Hit; }
    /** Get the *hit point* for this HitDetail. Returns the approximate point on the element that caused the hit when not a SnapDetail or IntersectDetail.
     * For a snap that is *hot*, the *exact* point on the Element for the snap mode is returned, otherwise the close point on the hit geometry is returned.
     */
    getPoint() { return this.hitPoint; }
    /** Determine if this HitPoint is from the same source as another HitDetail. */
    isSameHit(otherHit) { return (undefined !== otherHit && this.sourceId === otherHit.sourceId && this.iModel === otherHit.iModel); }
    /** Return whether sourceId is for a persistent element and not a pickable decoration. */
    get isElementHit() { return !Id64.isInvalid(this.sourceId) && !Id64.isTransient(this.sourceId); }
    // return whether the sourceId is for a model (reality models etc.)
    get isModelHit() {
        return this.modelId === this.sourceId;
    }
    // return whether the hit point is from map.
    get isMapHit() { return 0 !== this.viewport.mapLayerFromHit(this).length; }
    /** Create a deep copy of this HitDetail */
    clone() {
        return new HitDetail(this);
    }
    /** Draw this HitDetail as a Decoration. Causes the picked element to *flash* */
    draw(_context) {
        this.viewport.flashedId = this.sourceId;
    }
    /** Get the tooltip content for this HitDetail. */
    async getToolTip() {
        let toolTipPromise = this.isElementHit ? IModelApp.viewManager.overrideElementToolTip(this) : IModelApp.viewManager.getDecorationToolTip(this);
        for (const toolTipProvider of IModelApp.viewManager.toolTipProviders)
            toolTipPromise = toolTipProvider.augmentToolTip(this, toolTipPromise);
        return toolTipPromise;
    }
    /** The IModelConnection from which the hit originated. In some cases this may not be the same as the iModel associated with the Viewport -
     * for example, if a [[TiledGraphicsProvider]] is used to display graphics from a different iModel in the viewport.
     * This HitDetail's element, subcategory, and model Ids are defined in the context of this IModelConnection.
     */
    get iModel() {
        return this.sourceIModel ?? this.viewport.iModel;
    }
    /** Returns true if this hit originated from an [[IModelConnection]] other than the one associated with the [[Viewport]].
     * @see [[iModel]].
     */
    get isExternalIModelHit() {
        return this.iModel !== this.viewport.iModel;
    }
}
/** A SnapDetail is generated from the result of a snap request. In addition to the HitDetail about the reason the element was *picked*,
 * it holds the *exact* point on the element from the snapping logic, plus additional information that varies with the type of element and snap mode.
 * @public
 * @extensions
 */
export class SnapDetail extends HitDetail {
    /** Constructor for SnapDetail.
     * @param from The HitDetail that created this snap
     * @param snapMode The SnapMode used to create this SnapDetail
     * @param heat The SnapHeat of this SnapDetail
     * @param snapPoint The snapped point in the element
     */
    constructor(from, snapMode = SnapMode.Nearest, heat = SnapHeat.None, snapPoint) {
        super(from);
        this.snapMode = snapMode;
        this.heat = heat;
        this.snapPoint = Point3d.fromJSON(snapPoint ? snapPoint : from.hitPoint);
        this.adjustedPoint = this.snapPoint.clone();
        this.sprite = IconSprites.getSpriteFromUrl(SnapDetail.getSnapSpriteUrl(snapMode));
    }
    /** Returns `HitDetailType.Snap` */
    getHitType() { return HitDetailType.Snap; }
    /** Get the snap point if this SnapDetail is *hot*, the pick point otherwise. */
    getPoint() { return this.isHot ? this.snapPoint : super.getPoint(); }
    /** Return true if the pick point was closer than the snap aperture from the generated snap point. */
    get isHot() { return this.heat !== SnapHeat.None; }
    /** Determine whether the [[adjustedPoint]] is different than the [[snapPoint]]. This happens, for example, when points are adjusted for grids, acs plane snap, and AccuDraw. */
    get isPointAdjusted() { return !this.adjustedPoint.isExactEqual(this.snapPoint); }
    /** Change the snap point. */
    setSnapPoint(point, heat) {
        this.snapPoint.setFrom(point);
        this.adjustedPoint.setFrom(point);
        this.heat = heat;
    }
    /** Set curve primitive and HitGeometryType for this SnapDetail. */
    setCurvePrimitive(primitive, localToWorld, geomType) {
        this.primitive = primitive;
        this.geomType = undefined;
        // Only HitGeomType.Point and HitGeomType.Surface are valid without a curve primitive.
        if (undefined === this.primitive) {
            if (HitGeomType.Point === geomType || HitGeomType.Surface === geomType)
                this.geomType = geomType;
            return;
        }
        if (undefined !== localToWorld)
            this.primitive.tryTransformInPlace(localToWorld);
        if (this.primitive instanceof Arc3d)
            this.geomType = HitGeomType.Arc;
        else if (this.primitive instanceof LineSegment3d)
            this.geomType = HitGeomType.Segment;
        else if (this.primitive instanceof LineString3d)
            this.geomType = HitGeomType.Segment;
        else
            this.geomType = HitGeomType.Curve;
        // Set curve primitive geometry type override...
        //  - HitGeomType.Point with arc/ellipse denotes center.
        //  - HitGeomType.Surface with any curve primitive denotes an interior hit.
        if (undefined !== geomType && HitGeomType.None !== geomType)
            this.geomType = geomType;
    }
    /** Make a copy of this SnapDetail. */
    clone() {
        const val = new SnapDetail(this, this.snapMode, this.heat, this.snapPoint);
        val.sprite = this.sprite;
        val.geomType = this.geomType;
        val.parentGeomType = this.parentGeomType;
        val.adjustedPoint.setFrom(this.adjustedPoint);
        if (undefined !== this.primitive)
            val.primitive = this.primitive.clone();
        if (undefined !== this.normal)
            val.normal = this.normal.clone();
        return val;
    }
    getCurvePrimitive(singleSegment = true) {
        if (!singleSegment || undefined === this.primitive)
            return this.primitive;
        if (this.primitive instanceof LineString3d) {
            const ls = this.primitive;
            if (ls.points.length > 2) {
                const loc = ls.closestPoint(this.snapPoint, false);
                const nSegments = ls.points.length - 1;
                const uSegRange = (1.0 / nSegments);
                let segmentNo = Math.floor(loc.fraction / uSegRange);
                if (segmentNo >= nSegments)
                    segmentNo = nSegments - 1;
                return LineSegment3d.create(ls.points[segmentNo], ls.points[segmentNo + 1]);
            }
        }
        return this.primitive;
    }
    draw(context) {
        if (undefined !== this.primitive) {
            let singleSegment = false;
            switch (this.snapMode) {
                case SnapMode.Center:
                case SnapMode.Origin:
                case SnapMode.Bisector:
                    break; // Snap point for these is computed using entire linestring, not just the hit segment...
                default: {
                    singleSegment = true;
                    break;
                }
            }
            const builder = context.createGraphicBuilder(GraphicType.WorldOverlay);
            const outline = context.viewport.hilite.color.adjustedForContrast(context.viewport.view.backgroundColor, 50);
            const centerLine = context.viewport.hilite.color.adjustedForContrast(outline, 175);
            const path = Path.create(this.getCurvePrimitive(singleSegment));
            builder.setSymbology(outline, outline, 6);
            builder.addPath(path);
            builder.setSymbology(centerLine, centerLine, 2);
            builder.addPath(path);
            context.addDecorationFromBuilder(builder);
            return;
        }
        super.draw(context);
    }
    static getSnapSpriteUrl(snapType) {
        switch (snapType) {
            case SnapMode.Nearest: return `${IModelApp.publicPath}sprites/SnapPointOn.png`;
            case SnapMode.NearestKeypoint: return `${IModelApp.publicPath}sprites/SnapKeypoint.png`;
            case SnapMode.MidPoint: return `${IModelApp.publicPath}sprites/SnapMidpoint.png`;
            case SnapMode.Center: return `${IModelApp.publicPath}sprites/SnapCenter.png`;
            case SnapMode.Origin: return `${IModelApp.publicPath}sprites/SnapOrigin.png`;
            case SnapMode.Bisector: return `${IModelApp.publicPath}sprites/SnapBisector.png`;
            case SnapMode.Intersection: return `${IModelApp.publicPath}sprites/SnapIntersection.png`;
        }
        return "";
    }
}
/**
 * @public
 * @extensions
 */
export class IntersectDetail extends SnapDetail {
    constructor(from, heat = SnapHeat.None, snapPoint, otherPrimitive, otherId) {
        super(from, SnapMode.Intersection, heat, snapPoint);
        this.otherPrimitive = otherPrimitive;
        this.otherId = otherId;
        this.primitive = from.primitive;
        this.normal = from.normal; // Preserve normal from primary snap location for AccuDraw smart rotation...
    }
    draw(context) {
        if (undefined !== this.primitive && undefined !== this.otherPrimitive) {
            const builder = context.createGraphicBuilder(GraphicType.WorldOverlay);
            const outline = context.viewport.hilite.color.adjustedForContrast(context.viewport.view.backgroundColor, 50);
            const centerLine = context.viewport.hilite.color.adjustedForContrast(outline, 175);
            const path1 = Path.create(this.primitive);
            const path2 = Path.create(this.otherPrimitive);
            builder.setSymbology(outline, outline, 6);
            builder.addPath(path1);
            builder.addPath(path2);
            builder.setSymbology(centerLine, centerLine, 2);
            builder.addPath(path1);
            builder.setSymbology(centerLine, centerLine, 2, LinePixels.Code2);
            builder.addPath(path2);
            context.addDecorationFromBuilder(builder);
            return;
        }
        super.draw(context);
    }
}
/** The result of a "locate" is a sorted list of objects that satisfied the search criteria (a HitList). Earlier hits in the list
 * are somehow *better* than those later on.
 * @public
 * @extensions
 */
export class HitList {
    constructor() {
        this.hits = [];
        this.currHit = -1;
    }
    get length() { return this.hits.length; }
    empty() {
        this.hits.length = 0;
        this.currHit = -1;
    }
    resetCurrentHit() { this.currHit = -1; }
    /** Get a hit from a particular index into a HitList
     * return the requested hit from the HitList or undefined
     */
    getHit(hitNum) {
        if (hitNum < 0)
            hitNum = this.length - 1;
        return (hitNum >= this.length) ? undefined : this.hits[hitNum];
    }
    /** When setting one or more indices to undefined you must call dropNulls afterwards */
    setHit(i, p) {
        if (i < 0 || i >= this.length)
            return;
        this.hits[i] = p;
    }
    dropNulls() {
        const hits = this.hits;
        this.hits = [];
        for (const hit of hits)
            this.hits.push(hit);
    }
    getNextHit() {
        this.currHit++;
        return this.getCurrentHit();
    }
    getCurrentHit() { return -1 === this.currHit ? undefined : this.getHit(this.currHit); }
    setCurrentHit(hit) {
        this.resetCurrentHit();
        for (let thisHit; undefined !== (thisHit = this.getNextHit());) {
            if (thisHit === hit)
                return;
        }
    }
    /** remove the current hit from the list. */
    removeCurrentHit() { this.removeHit(this.currHit); }
    /** remove a hit in the list. */
    removeHit(hitNum) {
        if (hitNum < 0) // Support -1 == END
            hitNum = this.length - 1;
        if (hitNum <= this.currHit)
            this.currHit = -1;
        if (hitNum >= this.length) // Locate calls GetNextHit, which increments currHit, until it goes beyond the end of size of the array.
            return; // Then Reset call RemoteCurrentHit, which passes in currHit. When it is out of range, we do nothing.
        this.hits.splice(hitNum, 1);
    }
    /** search through list and remove any hits that contain a specified element id. */
    removeHitsFrom(sourceId) {
        let removedOne = false;
        // walk backwards through list so we don't have to worry about what happens on remove
        for (let i = this.length - 1; i >= 0; i--) {
            const thisHit = this.hits[i];
            if (thisHit && sourceId === thisHit.sourceId) {
                removedOne = true;
                this.removeHit(i);
            }
        }
        return removedOne;
    }
    getPriorityZOverride(priority) {
        switch (priority) {
            case HitPriority.WireEdge:
            case HitPriority.PlanarEdge:
            case HitPriority.NonPlanarEdge:
                return 0;
            case HitPriority.SilhouetteEdge:
                return 1;
            case HitPriority.PlanarSurface:
            case HitPriority.NonPlanarSurface:
                return 2;
            default:
                return 3;
        }
    }
    /** compare two hits for insertion into list. */
    compare(hit1, hit2) {
        if (!hit1 || !hit2)
            return 0;
        const zOverride1 = this.getPriorityZOverride(hit1.priority);
        const zOverride2 = this.getPriorityZOverride(hit2.priority);
        // Prefer edges over surfaces, this is more important than z because we know the edge isn't obscured...
        if (zOverride1 < zOverride2)
            return -1;
        if (zOverride1 > zOverride2)
            return 1;
        // Compare xy distance from pick point, prefer hits closer to center...
        if (hit1.distXY < hit2.distXY)
            return -1;
        if (hit1.distXY > hit2.distXY)
            return 1;
        // Compare distance fraction, prefer hits closer to eye...
        if (hit1.distFraction > hit2.distFraction)
            return -1;
        if (hit1.distFraction < hit2.distFraction)
            return 1;
        // Compare geometry class, prefer path/region hits over surface hits when all else is equal...
        if (hit1.priority < hit2.priority)
            return -1;
        if (hit1.priority > hit2.priority)
            return 1;
        return 0;
    }
    /** Add a new hit to the list. Hits are sorted according to their priority and distance. */
    addHit(newHit) {
        if (0 === this.hits.length) {
            this.hits.push(newHit);
            return 0;
        }
        let index = 0;
        for (; index < this.hits.length; ++index) {
            const oldHit = this.hits[index];
            const comparison = this.compare(newHit, oldHit);
            if (comparison < 0)
                break;
        }
        this.hits.splice(index, 0, newHit);
        return index;
    }
    /** Insert a new hit into the list at the supplied index. */
    insertHit(i, hit) {
        if (i < 0 || i >= this.length)
            this.hits.push(hit);
        else
            this.hits.splice(i, 0, hit);
    }
}
//# sourceMappingURL=HitDetail.js.map