import { UMB_HINT_CONTEXT } from './hint.context-token.js';
import { UmbControllerBase } from '../../../../libs/class-api/index.js';
import { UmbArrayState, UmbObjectState } from '../../../../libs/observable-api/index.js';
export class UmbHintController extends UmbControllerBase {
    //
    #viewAlias;
    getViewAlias() {
        return this.#viewAlias;
    }
    #pathFilter;
    setPathFilter(filter) {
        this.#pathFilter = filter;
    }
    #scaffold;
    #inUnprovidingState;
    #parent;
    #parentHints;
    #hints;
    // Consider using weight to determine the visibility distance. [NL]
    //public readonly hasHints = this._hints.asObservablePart((x) => x.length > 0);
    updateScaffold(updates) {
        this.#scaffold.update(updates);
    }
    getScaffold() {
        return this.#scaffold.getValue();
    }
    constructor(host, args) {
        super(host);
        this.#scaffold = new UmbObjectState({});
        this.scaffold = this.#scaffold.asObservable();
        this.#hints = new UmbArrayState([], (x) => x.unique);
        this.hints = this.#hints.asObservable();
        this.firstHint = this.#hints.asObservablePart((x) => x[0]);
        this.#receiveHints = (hints) => {
            if (!hints) {
                // Parent properly lost, so lets assume the parent hints are empty: [NL]
                hints = [];
            }
            this.initiateChange();
            if (this.#parentHints) {
                // Remove the local messages that does not exist in the parent anymore:
                const toRemove = this.#parentHints.filter((hint) => !hints.find((m) => m.unique === hint.unique));
                this.remove(toRemove.map((hint) => hint.unique));
            }
            this.#parentHints = hints;
            hints.forEach((hint) => {
                // Remove first entry of hint.path, if it matches viewAlias.
                if (this.#viewAlias && hint.path[0] === this.#viewAlias) {
                    hint = { ...hint, path: hint.path.slice(1) };
                }
                this.#hints.appendOne(hint);
            });
            this.finishChange();
        };
        this.#propagateHints = (hints) => {
            if (!this.#parent)
                return;
            this.#parent.initiateChange();
            const viewAlias = this.getViewAlias();
            hints.forEach((hint) => {
                let newPath = hint.path;
                // If the hint path does not already contain the parent view alias as the first entry, we add it. (This will usually happen, but some Hint Contexts does not have a view alias as they)
                if (viewAlias && newPath[0] !== viewAlias) {
                    newPath = [viewAlias, ...hint.path];
                }
                this.#parent.addOne({ ...hint, path: newPath });
            });
            // Remove hints that are not in the local hints anymore:
            const toRemove = this.#parentHints?.filter((hint) => !hints.find((m) => m.unique === hint.unique));
            if (toRemove) {
                this.#parent.remove(toRemove.map((hint) => hint.unique));
            }
            this.#parent.finishChange();
        };
        this.#viewAlias = args?.viewAlias ?? null;
        if (args?.scaffold) {
            this.#scaffold.setValue(args?.scaffold);
        }
        this.#hints.sortBy((a, b) => (b.weight || 0) - (a.weight || 0));
    }
    #providerCtrl;
    #currentProvideHost;
    /**
     * Provide this validation context to a specific controller host.
     * This can be used to Host a validation context in a Workspace, but provide it on a certain scope, like a specific Workspace View.
     * @param {UmbClassInterface} controllerHost - The controller host to provide this validation context to.
     */
    provideAt(controllerHost) {
        if (this.#currentProvideHost === controllerHost)
            return;
        this.unprovide();
        this.#currentProvideHost = controllerHost;
        this.#providerCtrl = controllerHost.provideContext(UMB_HINT_CONTEXT, this);
    }
    unprovide() {
        if (this.#providerCtrl) {
            // We need to set this in Unprovide state, so this context can be provided again later.
            this.#inUnprovidingState = true;
            this.#providerCtrl.destroy();
            this.#providerCtrl = undefined;
            this.#inUnprovidingState = false;
            this.#currentProvideHost = undefined;
        }
    }
    asObservablePart(fn) {
        return this.#hints.asObservablePart(fn);
    }
    descendingHints(viewAlias) {
        if (viewAlias) {
            return this.#hints.asObservablePart((hints) => {
                return hints.filter((hint) => hint.path[0] === viewAlias);
            });
        }
        else {
            return this.hints;
        }
    }
    /**
     * @internal
     * @param {(path: Array<string>) => boolean} filter - A filter function to filter the hints by their path.
     * @returns {Observable<Array<UmbHint> | undefined>} An observable of an array of hints that match the filter.
     */
    // eslint-disable-next-line @typescript-eslint/naming-convention
    _internal_descendingHintsByFilter(filter) {
        return this.#hints.asObservablePart((hints) => {
            return hints.filter((hint) => filter(hint.path));
        });
    }
    inherit() {
        if (this.#viewAlias === null && this.#pathFilter === undefined) {
            throw new Error('A Hint Controller needs a view alias or pathFilter to be able to inherit from a parent.');
        }
        this.consumeContext(UMB_HINT_CONTEXT, (parent) => {
            this.inheritFrom(parent);
        }).skipHost();
        // Notice skipHost ^^, this is because we do not want it to consume it self, as this would be a match for this consumption, instead we will look at the parent and above. [NL]
    }
    inheritFrom(parent) {
        if (this.#parent === parent)
            return;
        if (this.#viewAlias === null && this.#pathFilter === undefined) {
            throw new Error('A Hint Controller needs a view alias or pathFilter to be able to inherit from a parent.');
        }
        this.#parent = parent;
        this.observe(this.#parent?.scaffold, (scaffold) => {
            if (scaffold) {
                this.#scaffold.update(scaffold);
            }
        });
        if (this.#viewAlias) {
            this.observe(parent?.descendingHints(this.#viewAlias), this.#receiveHints, 'observeParentHints');
        }
        else if (this.#pathFilter) {
            this.observe(parent?._internal_descendingHintsByFilter(this.#pathFilter), this.#receiveHints, 'observeParentHints');
        }
        this.observe(this.hints, this.#propagateHints, 'observeLocalMessages');
    }
    #receiveHints;
    #propagateHints;
    initiateChange() {
        this.#hints.mute();
    }
    finishChange() {
        this.#hints.unmute();
    }
    /**
     * Add a new hint
     * @param {HintType} hint - The hint to add
     * @returns {HintType['unique']} Unique value of the hint
     */
    addOne(hint) {
        const newHint = { ...this.#scaffold.getValue(), ...hint };
        newHint.unique ??= Symbol();
        newHint.weight ??= 0;
        newHint.text ??= '!';
        newHint.path ??= [];
        this.#hints.appendOne(newHint);
        return hint.unique;
    }
    /**
     * Add multiple rules
     * @param {HintType[]} hints - Array of hints to add
     */
    add(hints) {
        this.#hints.mute();
        hints.forEach((hint) => this.addOne(hint));
        this.#hints.unmute();
    }
    /**
     * Remove a hint
     * @param {HintType['unique']} unique Unique value of the hint to remove
     */
    removeOne(unique) {
        this.#hints.removeOne(unique);
    }
    /**
     * Remove multiple hints
     * @param {HintType['unique'][]} uniques Array of unique values to remove
     */
    remove(uniques) {
        this.#hints.remove(uniques);
    }
    /**
     * Check if a hint exists
     * @param {HintType['unique']} unique Unique value of the hint to check
     * @returns {boolean} True if the hint exists, false otherwise
     */
    has(unique) {
        return this.#hints.getHasOne(unique);
    }
    /**
     * Get all hints
     * @returns {HintType[]} Array of hints
     */
    getAll() {
        return this.#hints.getValue();
    }
    /**
     * Clear all hints
     */
    clear() {
        this.#hints.setValue([]);
    }
    destroy() {
        super.destroy();
        if (this.#inUnprovidingState === true) {
            return;
        }
        this.unprovide();
        this.#parentHints = undefined;
        this.#parent = undefined;
        this.#hints.destroy();
    }
}
