
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/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 autorunDelta(observable, handler) {
    let _lastValue;
    return autorunOpts({ debugReferenceFn: handler }, (reader) => {
        const newValue = observable.read(reader);
        const lastValue = _lastValue;
        _lastValue = newValue;
        handler({ lastValue, newValue });
    });
}
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._isRunning = false;
        this._changeSummary = this.createChangeSummary?.();
        getLogger()?.handleAutorunCreated(this);
        this._run();
        trackDisposable(this);
    }
    dispose() {
        this._disposed = true;
        for (const o of this._dependencies) {
            o.removeObserver(this);
        }
        this._dependencies.clear();
        getLogger()?.handleAutorunDisposed(this);
        markAsDisposed(this);
    }
    _run() {
        const emptySet = this._dependenciesToBeRemoved;
        this._dependenciesToBeRemoved = this._dependencies;
        this._dependencies = emptySet;
        this._state = AutorunState.upToDate;
        try {
            if (!this._disposed) {
                getLogger()?.handleAutorunStarted(this);
                const changeSummary = this._changeSummary;
                try {
                    this._changeSummary = this.createChangeSummary?.();
                    this._isRunning = true;
                    this._runFn(this, changeSummary);
                }
                catch (e) {
                    onBugIndicatingError(e);
                }
                finally {
                    this._isRunning = false;
                }
            }
        }
        finally {
            if (!this._disposed) {
                getLogger()?.handleAutorunFinished(this);
            }
            for (const o of this._dependenciesToBeRemoved) {
                o.removeObserver(this);
            }
            this._dependenciesToBeRemoved.clear();
        }
    }
    toString() {
        return `Autorun<${this.debugName}>`;
    }
    beginUpdate(_observable) {
        if (this._state === AutorunState.upToDate) {
            this._state = AutorunState.dependenciesMightHaveChanged;
        }
        this._updateCount++;
    }
    endUpdate(_observable) {
        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;
                            }
                        }
                    }
                    if (this._state !== AutorunState.upToDate) {
                        this._run();
                    }
                } while (this._state !== AutorunState.upToDate);
            }
        }
        finally {
            this._updateCount--;
        }
        assertFn(() => this._updateCount >= 0);
    }
    handlePossibleChange(observable) {
        if (this._state === AutorunState.upToDate && this._isDependency(observable)) {
            this._state = AutorunState.dependenciesMightHaveChanged;
        }
    }
    handleChange(observable, change) {
        if (this._isDependency(observable)) {
            getLogger()?.handleAutorunDependencyChanged(this, observable, change);
            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);
            }
        }
    }
    _isDependency(observable) {
        return ( this._dependencies.has(observable)) && !( this._dependenciesToBeRemoved.has(observable));
    }
    readObservable(observable) {
        if (!this._isRunning) {
            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;
    }
    debugGetState() {
        return {
            isRunning: this._isRunning,
            updateCount: this._updateCount,
            dependencies: this._dependencies,
            state: this._state,
        };
    }
    debugRerun() {
        if (!this._isRunning) {
            this._run();
        }
        else {
            this._state = AutorunState.stale;
        }
    }
}
(function (autorun) {
    autorun.Observer = AutorunObserver;
})(autorun || (autorun = {}));

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