"use strict";
/*---------------------------------------------------------------------------------------------
* 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
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.ElementLocateManager = exports.ElementPicker = exports.LocateResponse = exports.LocateOptions = exports.SnapStatus = exports.LocateFilterStatus = exports.LocateAction = void 0;
const core_bentley_1 = require("@itwin/core-bentley");
const core_geometry_1 = require("@itwin/core-geometry");
const HitDetail_1 = require("./HitDetail");
const IModelApp_1 = require("./IModelApp");
const Pixel_1 = require("./render/Pixel");
const Tool_1 = require("./tools/Tool");
const ViewRect_1 = require("./common/ViewRect");
/** The possible actions for which a locate filter can be called.
 * @public
 * @extensions
 */
var LocateAction;
(function (LocateAction) {
    LocateAction[LocateAction["Identify"] = 0] = "Identify";
    LocateAction[LocateAction["AutoLocate"] = 1] = "AutoLocate";
})(LocateAction || (exports.LocateAction = LocateAction = {}));
/** Values to return from a locate filter.
 * Return `Reject` to indicate the element is unacceptable.
 * @public
 * @extensions
 */
var LocateFilterStatus;
(function (LocateFilterStatus) {
    LocateFilterStatus[LocateFilterStatus["Accept"] = 0] = "Accept";
    LocateFilterStatus[LocateFilterStatus["Reject"] = 1] = "Reject";
})(LocateFilterStatus || (exports.LocateFilterStatus = LocateFilterStatus = {}));
/**
 * @public
 * @extensions
 */
var SnapStatus;
(function (SnapStatus) {
    SnapStatus[SnapStatus["Success"] = 0] = "Success";
    SnapStatus[SnapStatus["Aborted"] = 1] = "Aborted";
    SnapStatus[SnapStatus["NoElements"] = 2] = "NoElements";
    SnapStatus[SnapStatus["Disabled"] = 100] = "Disabled";
    SnapStatus[SnapStatus["NoSnapPossible"] = 200] = "NoSnapPossible";
    SnapStatus[SnapStatus["NotSnappable"] = 300] = "NotSnappable";
    SnapStatus[SnapStatus["FilteredByApp"] = 600] = "FilteredByApp";
    SnapStatus[SnapStatus["FilteredByAppQuietly"] = 700] = "FilteredByAppQuietly";
})(SnapStatus || (exports.SnapStatus = SnapStatus = {}));
/** Options that customize the way element location (i.e. *picking*) works.
 * @public
 * @extensions
 */
class LocateOptions {
    /** If true, also test graphics from view decorations. */
    allowDecorations = false;
    /** If true, also test graphics with non-locatable flag set. */
    allowNonLocatable = false;
    /** Maximum number of hits to return. */
    maxHits = 20;
    /** The [[HitSource]] identifying the caller. */
    hitSource = HitDetail_1.HitSource.DataPoint;
    /** If true, also test graphics from an IModelConnection other than the one associated with the Viewport. This can occur if, e.g., a
     * [[TiledGraphicsProvider]] is used to display graphics from a different iModel into the [[Viewport]].
     * @note If you override this, you must be prepared to properly handle [[HitDetail]]s originating from other IModelConnections.
     * @see [[HitDetail.iModel]] and [[HitDetail.isExternalIModelHit]]
     */
    allowExternalIModels = false;
    /** If true, then the world point of a hit on a model will preserve any transforms applied to the model at display time,
     * such as those supplied by a [[ModelDisplayTransformProvider]] or [PlanProjectionSettings.elevation]($common).
     * Otherwise, the world point will be multiplied by the inverse of any such transforms to correlate it with the model's true coordinate space.
     */
    preserveModelDisplayTransforms = false;
    /** Make a copy of this LocateOptions. */
    clone() {
        const other = new LocateOptions();
        other.allowDecorations = this.allowDecorations;
        other.allowNonLocatable = this.allowNonLocatable;
        other.maxHits = this.maxHits;
        other.hitSource = this.hitSource;
        other.allowExternalIModels = this.allowExternalIModels;
        return other;
    }
    setFrom(other) {
        this.allowDecorations = other.allowDecorations;
        this.allowNonLocatable = other.allowNonLocatable;
        this.maxHits = other.maxHits;
        this.hitSource = other.hitSource;
        this.allowExternalIModels = other.allowExternalIModels;
    }
    init() {
        this.allowDecorations = this.allowNonLocatable = this.allowExternalIModels = false;
        this.maxHits = 20;
        this.hitSource = HitDetail_1.HitSource.DataPoint;
    }
}
exports.LocateOptions = LocateOptions;
/**
 * @public
 * @extensions
 */
class LocateResponse {
    snapStatus = SnapStatus.Success;
    reason;
    explanation = "";
    /** @internal */
    clone() {
        const other = new LocateResponse();
        other.snapStatus = this.snapStatus;
        other.reason = this.reason;
        other.explanation = this.explanation;
        return other;
    }
    /** @internal */
    setFrom(other) {
        this.snapStatus = other.snapStatus;
        this.reason = other.reason;
        this.explanation = other.explanation;
    }
}
exports.LocateResponse = LocateResponse;
/**
 * @public
 * @extensions
 */
class ElementPicker {
    viewport;
    pickPointWorld = new core_geometry_1.Point3d();
    hitList;
    empty() {
        this.pickPointWorld.setZero();
        this.viewport = undefined;
        if (this.hitList)
            this.hitList.empty();
        else
            this.hitList = new HitDetail_1.HitList();
    }
    /** return the HitList for the last Pick performed. Optionally allows the caller to take ownership of the list. */
    getHitList(takeOwnership) {
        const list = (0, core_bentley_1.expectDefined)(this.hitList);
        if (takeOwnership)
            this.hitList = undefined;
        return list;
    }
    getNextHit() { return this.hitList ? this.hitList.getNextHit() : undefined; }
    /** Return a hit from the list of hits created the last time pickElements was called. */
    getHit(i) { return this.hitList ? this.hitList.getHit(i) : undefined; }
    resetCurrentHit() {
        if (this.hitList)
            this.hitList.resetCurrentHit();
    }
    comparePixel(pixel1, pixel2, distXY1, distXY2) {
        const priority1 = pixel1.computeHitPriority();
        const priority2 = pixel2.computeHitPriority();
        if (priority1 < priority2)
            return -1;
        if (priority1 > priority2)
            return 1;
        if (distXY1 < distXY2)
            return -1;
        if (distXY1 > distXY2)
            return 1;
        if (pixel1.distanceFraction > pixel2.distanceFraction)
            return -1;
        if (pixel1.distanceFraction < pixel2.distanceFraction)
            return 1;
        return 0;
    }
    /** Generate a list of elements that are close to a given point.
     * @param vp Viewport to use for pick
     * @param pickPointWorld Pick location in world coordinates
     * @param pickRadiusView Pick radius in pixels
     * @param options Pick options to use
     * @param excludedElements Optional ids to not draw during pick. Allows hits for geometry obscured by these ids to be returned.
     * @returns The number of hits in the hitList of this object.
     */
    doPick(vp, pickPointWorld, pickRadiusView, options, excludedElements) {
        if (this.hitList && this.hitList.length > 0 && vp === this.viewport && pickPointWorld.isAlmostEqual(this.pickPointWorld)) {
            this.hitList.resetCurrentHit();
            return this.hitList.length;
        }
        this.empty(); // empty the hit list
        this.viewport = vp;
        this.pickPointWorld.setFrom(pickPointWorld);
        const pickPointView = vp.worldToView(pickPointWorld);
        const testPointView = new core_geometry_1.Point2d(Math.floor(pickPointView.x + 0.5), Math.floor(pickPointView.y + 0.5));
        let pixelRadius = Math.floor(pickRadiusView + 0.5);
        const rect = new ViewRect_1.ViewRect(testPointView.x - pixelRadius, testPointView.y - pixelRadius, testPointView.x + pixelRadius, testPointView.y + pixelRadius);
        if (rect.isNull)
            return 0;
        const receiver = (pixels) => {
            if (undefined === pixels)
                return;
            testPointView.x = vp.cssPixelsToDevicePixels(testPointView.x);
            testPointView.y = vp.cssPixelsToDevicePixels(testPointView.y);
            pixelRadius = vp.cssPixelsToDevicePixels(pixelRadius);
            const elmHits = new Map();
            const testPoint = core_geometry_1.Point2d.createZero();
            for (testPoint.x = testPointView.x - pixelRadius; testPoint.x <= testPointView.x + pixelRadius; ++testPoint.x) {
                for (testPoint.y = testPointView.y - pixelRadius; testPoint.y <= testPointView.y + pixelRadius; ++testPoint.y) {
                    const pixel = pixels.getPixel(testPoint.x, testPoint.y);
                    if (undefined === pixel || undefined === pixel.elementId || core_bentley_1.Id64.isInvalid(pixel.elementId))
                        continue; // no geometry at this location...
                    const distXY = testPointView.distance(testPoint);
                    if (distXY > pixelRadius)
                        continue; // ignore corners. it's a locate circle not square...
                    const oldPoint = elmHits.get(pixel.elementId);
                    if (undefined !== oldPoint) {
                        if (this.comparePixel(pixel, pixels.getPixel(oldPoint.x, oldPoint.y), distXY, testPointView.distance(oldPoint)) < 0)
                            oldPoint.setFrom(testPoint); // new hit is better, update location...
                    }
                    else {
                        elmHits.set(pixel.elementId, testPoint.clone());
                    }
                }
            }
            if (0 === elmHits.size)
                return;
            for (const elmPoint of elmHits.values()) {
                const pixel = pixels.getPixel(elmPoint.x, elmPoint.y);
                if (undefined === pixel || undefined === pixel.elementId)
                    continue;
                const hitPointWorld = vp.getPixelDataWorldPoint({
                    pixels,
                    x: elmPoint.x,
                    y: elmPoint.y,
                    preserveModelDisplayTransforms: options.preserveModelDisplayTransforms,
                });
                if (!hitPointWorld)
                    continue;
                const hit = new HitDetail_1.HitDetail({
                    ...pixel.toHitProps(vp),
                    testPoint: pickPointWorld,
                    viewport: vp,
                    hitSource: options.hitSource,
                    hitPoint: hitPointWorld,
                    distXY: testPointView.distance(elmPoint),
                });
                const hitList = (0, core_bentley_1.expectDefined)(this.hitList);
                hitList.addHit(hit);
                if (hitList.hits.length > options.maxHits)
                    hitList.hits.length = options.maxHits; // truncate array...
            }
            result = (0, core_bentley_1.expectDefined)(this.hitList).length;
        };
        const args = {
            receiver,
            rect,
            selector: Pixel_1.Pixel.Selector.All,
            excludeNonLocatable: !options.allowNonLocatable,
            excludedElements,
        };
        let result = 0;
        vp.readPixels(args);
        return result;
    }
    testHit(hit, vp, pickPointWorld, pickRadiusView, options) {
        if (0 === this.doPick(vp, pickPointWorld, pickRadiusView, options))
            return false;
        return (0, core_bentley_1.expectDefined)(this.hitList).hits.some((thisHit) => hit.isSameHit(thisHit));
    }
}
exports.ElementPicker = ElementPicker;
/**
 * @public
 * @extensions
 */
class ElementLocateManager {
    hitList;
    currHit;
    options = new LocateOptions();
    picker = new ElementPicker();
    /** get the full message key for a locate failure  */
    static getFailureMessageKey(key) { return `LocateFailure.${key}`; }
    onInitialized() { }
    get apertureInches() { return 0.11; }
    get touchApertureInches() { return 0.22; }
    clear() { this.setCurrHit(undefined); }
    setHitList(list) { this.hitList = list; }
    setCurrHit(hit) { this.currHit = hit; }
    getNextHit() { return this.hitList ? this.hitList.getNextHit() : undefined; }
    /** return the current path from either the snapping logic or the pre-locating systems. */
    getPreLocatedHit() {
        // NOTE: Check AccuSnap first as Tentative is used to build intersect snap. For normal snaps when a Tentative is active there should be no AccuSnap.
        const fromAccuSnap = IModelApp_1.IModelApp.accuSnap.getHitAndList(this);
        const preLocated = fromAccuSnap ?? IModelApp_1.IModelApp.tentativePoint.getHitAndList(this);
        if (preLocated) {
            const excludedElements = (preLocated.isElementHit ? new Set([preLocated.sourceId]) : undefined);
            if (excludedElements || !fromAccuSnap) {
                // NOTE: For tentative snap, get new hit list at snap point; want reset to cycle hits using adjusted point location...
                const point = (fromAccuSnap ? preLocated.hitPoint : preLocated.getPoint());
                const vp = preLocated.viewport;
                this.picker.empty();
                this.picker.doPick(vp, point, (vp.pixelsFromInches(this.apertureInches) / 2.0) + 1.5, this.options, excludedElements);
                this.setHitList(this.picker.getHitList(true));
                if (excludedElements) {
                    if (undefined === this.hitList)
                        this.hitList = new HitDetail_1.HitList();
                    this.hitList.insertHit(0, preLocated);
                }
            }
        }
        if (this.hitList)
            this.hitList.resetCurrentHit();
        return preLocated;
    }
    async filterHit(hit, _action, out) {
        // Tools must opt-in to locate of transient geometry as it requires special treatment.
        if (!this.options.allowDecorations && !hit.isElementHit) {
            if (hit.isModelHit)
                out.reason = ElementLocateManager.getFailureMessageKey("RealityModel");
            else if (hit.isMapHit)
                out.reason = ElementLocateManager.getFailureMessageKey("Map");
            else
                out.reason = ElementLocateManager.getFailureMessageKey("Transient");
            return LocateFilterStatus.Reject;
        }
        // Tools must opt-in to locate geometry from external iModels.
        if (!this.options.allowExternalIModels && hit.isExternalIModelHit) {
            out.reason = ElementLocateManager.getFailureMessageKey("ExternalIModel");
            return LocateFilterStatus.Reject;
        }
        if (undefined !== hit.subCategoryId && !hit.isExternalIModelHit) {
            const appearance = hit.viewport.getSubCategoryAppearance(hit.subCategoryId);
            if (appearance.dontLocate) {
                out.reason = ElementLocateManager.getFailureMessageKey("NotLocatableSubCategory");
                return LocateFilterStatus.Reject;
            }
        }
        const tool = IModelApp_1.IModelApp.toolAdmin.activeTool;
        if (!(tool && tool instanceof Tool_1.InteractiveTool))
            return LocateFilterStatus.Accept;
        const status = await tool.filterHit(hit, out);
        if (LocateFilterStatus.Reject === status)
            out.reason = ElementLocateManager.getFailureMessageKey("ByApp");
        return status;
    }
    initLocateOptions() { this.options.init(); }
    initToolLocate() {
        this.initLocateOptions();
        this.clear();
        this.picker.empty();
        IModelApp_1.IModelApp.tentativePoint.clear(true);
    }
    async _doLocate(response, newSearch, testPoint, vp, source, filterHits) {
        if (!vp)
            return;
        // the "newSearch" flag indicates whether the caller wants us to conduct a new search at the testPoint, or just continue returning paths from the previous search.
        if (newSearch) {
            const hit = this.getPreLocatedHit();
            // if we're snapped to something, that path has the highest priority and becomes the active hit.
            if (hit) {
                if (!filterHits || LocateFilterStatus.Accept === await this.filterHit(hit, LocateAction.Identify, response))
                    return hit;
                response = new LocateResponse(); // we have the reason and explanation we want.
            }
            this.picker.empty();
            this.picker.doPick(vp, testPoint, (vp.pixelsFromInches(Tool_1.InputSource.Touch === source ? this.touchApertureInches : this.apertureInches) / 2.0) + 1.5, this.options);
            const hitList = this.picker.getHitList(true);
            this.setHitList(hitList);
        }
        let newHit;
        while (undefined !== (newHit = this.getNextHit())) {
            if (!filterHits || LocateFilterStatus.Accept === await this.filterHit(newHit, LocateAction.Identify, response))
                return newHit;
            response = new LocateResponse(); // we have the reason and explanation we want.
        }
        return undefined;
    }
    async doLocate(response, newSearch, testPoint, view, source, filterHits = true) {
        response.reason = ElementLocateManager.getFailureMessageKey("NoElements");
        response.explanation = "";
        const hit = await this._doLocate(response, newSearch, testPoint, view, source, filterHits);
        this.setCurrHit(hit);
        // if we found a hit, remove it from the list of remaining hits near the current search point.
        if (hit && this.hitList)
            this.hitList.removeHitsFrom(hit.sourceId);
        return hit;
    }
}
exports.ElementLocateManager = ElementLocateManager;
//# sourceMappingURL=ElementLocateManager.js.map