
import { DebugNameData } from './debugName.js';
import { assertFn } from '../assert.js';
import '../arrays.js';
import { onBugIndicatingError, BugIndicatingError } from '../errors.js';
import '../event.js';
import { DisposableStore, toDisposable, trackDisposable, markAsDisposed } from '../lifecycle.js';
import { getLogger } from './logging.js';

function autorun(fn) {
    return ( new AutorunObserver(( new DebugNameData(undefined, undefined, fn)), fn, undefined, undefined));
}
function autorunOpts(options, fn) {
    return ( new AutorunObserver(( new DebugNameData(options.owner, options.debugName, options.debugReferenceFn ?? fn)), fn, undefined, undefined));
}
function autorunHandleChanges(options, fn) {
    return ( new AutorunObserver(( new DebugNameData(options.owner, options.debugName, options.debugReferenceFn ?? fn)), fn, options.createEmptyChangeSummary, options.handleChange));
}
function autorunWithStoreHandleChanges(options, fn) {
    const store = ( new DisposableStore());
    const disposable = autorunHandleChanges({
        owner: options.owner,
        debugName: options.debugName,
        debugReferenceFn: options.debugReferenceFn ?? fn,
        createEmptyChangeSummary: options.createEmptyChangeSummary,
        handleChange: options.handleChange,
    }, (reader, changeSummary) => {
        store.clear();
        fn(reader, changeSummary, store);
    });
    return toDisposable(() => {
        disposable.dispose();
        store.dispose();
    });
}
function autorunWithStore(fn) {
    const store = ( new DisposableStore());
    const disposable = autorunOpts({
        owner: undefined,
        debugName: undefined,
        debugReferenceFn: fn,
    }, reader => {
        store.clear();
        fn(reader, store);
    });
    return toDisposable(() => {
        disposable.dispose();
        store.dispose();
    });
}
function autorunIterableDelta(getValue, handler, getUniqueIdentifier = v => v) {
    const lastValues = ( new Map());
    return autorunOpts({ debugReferenceFn: getValue }, (reader) => {
        const newValues = ( new Map());
        const removedValues = ( new Map(lastValues));
        for (const value of getValue(reader)) {
            const id = getUniqueIdentifier(value);
            if (( lastValues.has(id))) {
                removedValues.delete(id);
            }
            else {
                newValues.set(id, value);
                lastValues.set(id, value);
            }
        }
        for (const id of ( removedValues.keys())) {
            lastValues.delete(id);
        }
        if (newValues.size || removedValues.size) {
            handler({ addedValues: [...( newValues.values())], removedValues: [...( removedValues.values())] });
        }
    });
}
var AutorunState;
(function (AutorunState) {
    AutorunState[AutorunState["dependenciesMightHaveChanged"] = 1] = "dependenciesMightHaveChanged";
    AutorunState[AutorunState["stale"] = 2] = "stale";
    AutorunState[AutorunState["upToDate"] = 3] = "upToDate";
})(AutorunState || (AutorunState = {}));
class AutorunObserver {
    get debugName() {
        return this._debugNameData.getDebugName(this) ?? '(anonymous)';
    }
    constructor(_debugNameData, _runFn, createChangeSummary, _handleChange) {
        this._debugNameData = _debugNameData;
        this._runFn = _runFn;
        this.createChangeSummary = createChangeSummary;
        this._handleChange = _handleChange;
        this.state = AutorunState.stale;
        this.updateCount = 0;
        this.disposed = false;
        this.dependencies = ( new Set());
        this.dependenciesToBeRemoved = ( new Set());
        this._isReaderValid = false;
        this.changeSummary = this.createChangeSummary?.();
        this._runIfNeeded();
        trackDisposable(this);
    }
    dispose() {
        this.disposed = true;
        for (const o of this.dependencies) {
            o.removeObserver(this);
        }
        this.dependencies.clear();
        markAsDisposed(this);
    }
    _runIfNeeded() {
        if (this.state === AutorunState.upToDate) {
            return;
        }
        const emptySet = this.dependenciesToBeRemoved;
        this.dependenciesToBeRemoved = this.dependencies;
        this.dependencies = emptySet;
        this.state = AutorunState.upToDate;
        const isDisposed = this.disposed;
        try {
            if (!isDisposed) {
                getLogger()?.handleAutorunTriggered(this);
                const changeSummary = this.changeSummary;
                try {
                    this.changeSummary = this.createChangeSummary?.();
                    this._isReaderValid = true;
                    this._runFn(this, changeSummary);
                }
                catch (e) {
                    onBugIndicatingError(e);
                }
                finally {
                    this._isReaderValid = false;
                }
            }
        }
        finally {
            for (const o of this.dependenciesToBeRemoved) {
                o.removeObserver(this);
            }
            this.dependenciesToBeRemoved.clear();
        }
    }
    toString() {
        return `Autorun<${this.debugName}>`;
    }
    beginUpdate() {
        if (this.state === AutorunState.upToDate) {
            this.state = AutorunState.dependenciesMightHaveChanged;
        }
        this.updateCount++;
    }
    endUpdate() {
        try {
            if (this.updateCount === 1) {
                do {
                    if (this.state === AutorunState.dependenciesMightHaveChanged) {
                        this.state = AutorunState.upToDate;
                        for (const d of this.dependencies) {
                            d.reportChanges();
                            if (this.state === AutorunState.stale) {
                                break;
                            }
                        }
                    }
                    this._runIfNeeded();
                } while (this.state !== AutorunState.upToDate);
            }
        }
        finally {
            this.updateCount--;
        }
        assertFn(() => this.updateCount >= 0);
    }
    handlePossibleChange(observable) {
        if (this.state === AutorunState.upToDate && ( this.dependencies.has(observable)) && !( this.dependenciesToBeRemoved.has(observable))) {
            this.state = AutorunState.dependenciesMightHaveChanged;
        }
    }
    handleChange(observable, change) {
        if (( this.dependencies.has(observable)) && !( this.dependenciesToBeRemoved.has(observable))) {
            try {
                const shouldReact = this._handleChange ? this._handleChange({
                    changedObservable: observable,
                    change,
                    didChange: (o) => o === observable,
                }, this.changeSummary) : true;
                if (shouldReact) {
                    this.state = AutorunState.stale;
                }
            }
            catch (e) {
                onBugIndicatingError(e);
            }
        }
    }
    readObservable(observable) {
        if (!this._isReaderValid) {
            throw ( new BugIndicatingError('The reader object cannot be used outside its compute function!'));
        }
        if (this.disposed) {
            return observable.get();
        }
        observable.addObserver(this);
        const value = observable.get();
        this.dependencies.add(observable);
        this.dependenciesToBeRemoved.delete(observable);
        return value;
    }
}
(function (autorun) {
    autorun.Observer = AutorunObserver;
})(autorun);

export { AutorunObserver, autorun, autorunHandleChanges, autorunIterableDelta, autorunOpts, autorunWithStore, autorunWithStoreHandleChanges };
