import { system, } from '@minecraft/server';
import WBVector3 from '../../util/wbVector3';
export class TargetController {
    /**
     * Initialize a new TargetController.
     *
     * This method should not be called directly. Use `TargetController.get` instead.
     * @param entity The entity that needs to search for targets
     */
    constructor(entity) {
        this.entity = entity;
        this.targets = new Set();
    }
    /**
     * Get a TargetController. Ensures only one instance per entity.
     * @param entity The entity that needs to search for targets
     * @returns A TargetController for this entity
     */
    static get(entity) {
        let instance = this.INSTANCES.get(entity);
        if (!instance) {
            instance = new TargetController(entity);
            this.INSTANCES.set(entity, instance);
        }
        return instance;
    }
    static forEach(callbackfn) {
        return this.INSTANCES.forEach(callbackfn);
    }
    stopScanTask() {
        if (this.scanTaskHandle) {
            system.clearRun(this.scanTaskHandle);
            this.scanTaskHandle = undefined;
        }
    }
    startScanTask() {
        this.stopScanTask();
        const options = this.getOptions();
        if (!options) {
            console.warn(`Entity ${this.entity.typeId} attempted to start scan task but no options are available`);
            return;
        }
        system.runInterval(() => this.scanForTargets(), options.scanInterval * 20);
    }
    /**
     * Start scanning for targets.
     * @param options Scan options
     */
    start(options) {
        this.options = options;
        this.startScanTask();
    }
    /**
     * Stop scanning for targets.
     *
     * Any and all active targets will be lost.
     */
    stop() {
        this.stopScanTask();
        this.loseTargets();
        this.options = undefined;
    }
    isStarted() {
        return this.scanTaskHandle !== undefined;
    }
    /**
     * Stop and unload this TargetController instance.
     */
    unload() {
        this.stop();
        TargetController.INSTANCES.delete(this.entity);
    }
    getOptions() {
        if (typeof this.options === 'function') {
            return this.options(this.entity);
        }
        return this.options;
    }
    /**
     * Check whether an entity is being targeted by this entity.
     * @param entity An entity
     * @returns Whether the entity is a target
     */
    isTarget(entity) {
        return this.targets.has(entity);
    }
    scanForTargets() {
        const options = this.getOptions();
        if (!options)
            return;
        // If we (no longer) want any targets, lose all active targets and return early.
        if (options.maxNumTargets === 0) {
            this.loseTargets();
            return;
        }
        const { dimension } = this.entity;
        const followRange = options.followRange ?? options.sightRange;
        // Find all potential targets. We narrow down later.
        // - minDistance is 0.001 to exclude the searching entity.
        // - maxDistance is followRange since no entities outside that range can be tracked.
        const eqo = (options.filter)
            ? {
                ...options.filter,
                location: this.entity.location,
                minDistance: Math.max(0.001, options.filter.minDistance || 0),
                maxDistance: followRange,
            }
            : {
                location: this.entity.location,
                minDistance: 0.001,
                maxDistance: followRange,
            };
        const potentialTargets = dimension.getEntities(eqo);
        const newTargets = [];
        const confirmedActiveTargets = new Set();
        const maxNumTargets = options.maxNumTargets ?? 1;
        for (const target of potentialTargets) {
            const isActiveTarget = this.isTarget(target);
            // In the worst case, we get a completely new set of targets. However, once we
            // come across an active target that is still valid, we can decrease this value.
            // Check if we've already found enough targets for the worst case scenario.
            // If so, we no longer need to process other candidates.
            if (!isActiveTarget && newTargets.length >= (maxNumTargets - confirmedActiveTargets.size)) {
                // eslint-disable-next-line no-continue
                continue;
            }
            // Get the direction and distance between entity and target
            const headLocation = this.entity.getHeadLocation();
            const direction = WBVector3.fromMC(target.getHeadLocation()).subtract(headLocation);
            const distance = direction.length();
            let isValid = target.isValid();
            if (isValid && !isActiveTarget) {
                // If this is a candidate, make sure they are within sight range
                isValid = distance <= options.sightRange;
            }
            if (isValid && options.filterFunc) {
                // Apply custom filtering function
                isValid = options.filterFunc(target);
            }
            if (isValid && !options.canSeeThroughBlocks) {
                // Make sure the target can be seen (if necessary)
                const hit = dimension.getBlockFromRay(headLocation, direction, {
                    maxDistance: distance,
                });
                isValid = (hit === undefined);
            }
            if (isActiveTarget) {
                // If this is an active target that is still valid, update the worst case.
                if (isValid)
                    confirmedActiveTargets.add(target);
                // If this active target is no longer valid, lose it.
                else
                    this.loseTarget(target);
            }
            else if (isValid) {
                newTargets.push(target);
            }
        }
        // Lose any active targets that were not seen
        for (const target of this.getTargets()) {
            if (!confirmedActiveTargets.has(target)) {
                this.loseTarget(target);
            }
        }
        // Calculate how many new targets can be added
        const numNewTargets = maxNumTargets - this.targets.size;
        if (numNewTargets > 0) {
            // Acquire as many new targets as allowed
            newTargets.slice(0, numNewTargets).forEach((t) => this.acquireTarget(t));
        }
    }
    getTargets() {
        return [...this.targets.values()];
    }
    getTarget() {
        return this.targets.values().next()?.value;
    }
    getNumTargets() {
        return this.targets.size;
    }
    /**
     * Acquire a new target.
     *
     * If the entity was already a target, nothing happens.
     * @param target The target to acquire
     * @returns `false` if this entity was already a target, `true` otherwise
     */
    acquireTarget(target) {
        if (this.targets.has(target))
            return false;
        // Update the target
        this.targets.add(target);
        this.entity.setDynamicProperty('twb_wisu:has_target', true);
        // Invoke event
        const options = this.getOptions();
        options?.onTargetAcquired?.({ source: this, target });
        return true;
    }
    /**
     * Lose a target.
     *
     * If the entity was not a target, nothing happens.
     * @param target The target to lose
     * @returns `true` if the entity was a target, `false` otherwise
     */
    loseTarget(target) {
        // Update the target
        const removed = this.targets.delete(target);
        if (!removed)
            return false;
        if (this.targets.size === 0) {
            this.entity.setDynamicProperty('twb_wisu:has_target', false);
        }
        // Invoke events
        const options = this.getOptions();
        options?.onTargetLost?.({ source: this, target });
        return true;
    }
    /**
     * Immediately lose all targets.
     */
    loseTargets() {
        if (this.targets.size === 0)
            return;
        // Update the target
        const targets = [...this.targets.values()];
        this.targets.clear();
        // Invoke events
        const options = this.getOptions();
        if (options?.onTargetLost !== undefined) {
            targets.forEach((target) => options.onTargetLost({ source: this, target }));
        }
    }
}
TargetController.INSTANCES = new Map();
