
import { _setDerivedOpts, BaseObservable } from './base.js';
import { DebugNameData } from './debugName.js';
import { assertFn } from '../assert.js';
import { strictEquals } from '../equals.js';
import { BugIndicatingError, onBugIndicatingError } from '../errors.js';
import '../event.js';
import { DisposableStore } from '../lifecycle.js';
import { getLogger } from './logging.js';

function derived(computeFnOrOwner, computeFn) {
    if (computeFn !== undefined) {
        return ( new Derived(( new DebugNameData(computeFnOrOwner, undefined, computeFn)), computeFn, undefined, undefined, undefined, strictEquals));
    }
    return ( new Derived(( new DebugNameData(undefined, undefined, computeFnOrOwner)), computeFnOrOwner, undefined, undefined, undefined, strictEquals));
}
function derivedWithSetter(owner, computeFn, setter) {
    return ( new DerivedWithSetter(( new DebugNameData(owner, undefined, computeFn)), computeFn, undefined, undefined, undefined, strictEquals, setter));
}
function derivedOpts(options, computeFn) {
    return ( new Derived(( new DebugNameData(options.owner, options.debugName, options.debugReferenceFn)), computeFn, undefined, undefined, options.onLastObserverRemoved, options.equalsFn ?? strictEquals));
}
_setDerivedOpts(derivedOpts);
function derivedHandleChanges(options, computeFn) {
    return ( new Derived(( new DebugNameData(options.owner, options.debugName, undefined)), computeFn, options.createEmptyChangeSummary, options.handleChange, undefined, options.equalityComparer ?? strictEquals));
}
function derivedWithStore(computeFnOrOwner, computeFnOrUndefined) {
    let computeFn;
    let owner;
    if (computeFnOrUndefined === undefined) {
        computeFn = computeFnOrOwner;
        owner = undefined;
    }
    else {
        owner = computeFnOrOwner;
        computeFn = computeFnOrUndefined;
    }
    const store = ( new DisposableStore());
    return ( new Derived(( new DebugNameData(owner, undefined, computeFn)), r => {
        store.clear();
        return computeFn(r, store);
    }, undefined, undefined, () => store.dispose(), strictEquals));
}
function derivedDisposable(computeFnOrOwner, computeFnOrUndefined) {
    let computeFn;
    let owner;
    if (computeFnOrUndefined === undefined) {
        computeFn = computeFnOrOwner;
        owner = undefined;
    }
    else {
        owner = computeFnOrOwner;
        computeFn = computeFnOrUndefined;
    }
    let store = undefined;
    return ( new Derived(( new DebugNameData(owner, undefined, computeFn)), r => {
        if (!store) {
            store = ( new DisposableStore());
        }
        else {
            store.clear();
        }
        const result = computeFn(r);
        if (result) {
            store.add(result);
        }
        return result;
    }, undefined, undefined, () => {
        if (store) {
            store.dispose();
            store = undefined;
        }
    }, strictEquals));
}
var DerivedState;
(function (DerivedState) {
    DerivedState[DerivedState["initial"] = 0] = "initial";
    DerivedState[DerivedState["dependenciesMightHaveChanged"] = 1] = "dependenciesMightHaveChanged";
    DerivedState[DerivedState["stale"] = 2] = "stale";
    DerivedState[DerivedState["upToDate"] = 3] = "upToDate";
})(DerivedState || (DerivedState = {}));
class Derived extends BaseObservable {
    get debugName() {
        return this._debugNameData.getDebugName(this) ?? '(anonymous)';
    }
    constructor(_debugNameData, _computeFn, createChangeSummary, _handleChange, _handleLastObserverRemoved = undefined, _equalityComparator) {
        super();
        this._debugNameData = _debugNameData;
        this._computeFn = _computeFn;
        this.createChangeSummary = createChangeSummary;
        this._handleChange = _handleChange;
        this._handleLastObserverRemoved = _handleLastObserverRemoved;
        this._equalityComparator = _equalityComparator;
        this.state = DerivedState.initial;
        this.value = undefined;
        this.updateCount = 0;
        this.dependencies = ( new Set());
        this.dependenciesToBeRemoved = ( new Set());
        this.changeSummary = undefined;
        this._isUpdating = false;
        this._isComputing = false;
        this._isReaderValid = false;
        this.changeSummary = this.createChangeSummary?.();
    }
    onLastObserverRemoved() {
        this.state = DerivedState.initial;
        this.value = undefined;
        for (const d of this.dependencies) {
            d.removeObserver(this);
        }
        this.dependencies.clear();
        this._handleLastObserverRemoved?.();
    }
    get() {
        if (this._isComputing) {
            throw ( new BugIndicatingError('Cyclic deriveds are not supported yet!'));
        }
        if (this.observers.size === 0) {
            let result;
            try {
                this._isReaderValid = true;
                result = this._computeFn(this, this.createChangeSummary?.());
            }
            finally {
                this._isReaderValid = false;
            }
            this.onLastObserverRemoved();
            return result;
        }
        else {
            do {
                if (this.state === DerivedState.dependenciesMightHaveChanged) {
                    for (const d of this.dependencies) {
                        d.reportChanges();
                        if (this.state === DerivedState.stale) {
                            break;
                        }
                    }
                }
                if (this.state === DerivedState.dependenciesMightHaveChanged) {
                    this.state = DerivedState.upToDate;
                }
                this._recomputeIfNeeded();
            } while (this.state !== DerivedState.upToDate);
            return this.value;
        }
    }
    _recomputeIfNeeded() {
        if (this.state === DerivedState.upToDate) {
            return;
        }
        const emptySet = this.dependenciesToBeRemoved;
        this.dependenciesToBeRemoved = this.dependencies;
        this.dependencies = emptySet;
        const hadValue = this.state !== DerivedState.initial;
        const oldValue = this.value;
        this.state = DerivedState.upToDate;
        let didChange = false;
        this._isComputing = false;
        try {
            const changeSummary = this.changeSummary;
            this.changeSummary = this.createChangeSummary?.();
            try {
                this._isReaderValid = true;
                this.value = this._computeFn(this, changeSummary);
            }
            finally {
                this._isReaderValid = false;
                for (const o of this.dependenciesToBeRemoved) {
                    o.removeObserver(this);
                }
                this.dependenciesToBeRemoved.clear();
            }
            didChange = hadValue && !(this._equalityComparator(oldValue, this.value));
            getLogger()?.handleDerivedRecomputed(this, {
                oldValue,
                newValue: this.value,
                change: undefined,
                didChange,
                hadValue,
            });
        }
        catch (e) {
            onBugIndicatingError(e);
        }
        this._isComputing = false;
        if (didChange) {
            for (const r of this.observers) {
                r.handleChange(this, undefined);
            }
        }
    }
    toString() {
        return `LazyDerived<${this.debugName}>`;
    }
    beginUpdate(_observable) {
        if (this._isUpdating) {
            throw ( new BugIndicatingError('Cyclic deriveds are not supported yet!'));
        }
        this.updateCount++;
        this._isUpdating = true;
        try {
            const propagateBeginUpdate = this.updateCount === 1;
            if (this.state === DerivedState.upToDate) {
                this.state = DerivedState.dependenciesMightHaveChanged;
                if (!propagateBeginUpdate) {
                    for (const r of this.observers) {
                        r.handlePossibleChange(this);
                    }
                }
            }
            if (propagateBeginUpdate) {
                for (const r of this.observers) {
                    r.beginUpdate(this);
                }
            }
        }
        finally {
            this._isUpdating = false;
        }
    }
    endUpdate(_observable) {
        this.updateCount--;
        if (this.updateCount === 0) {
            const observers = [...this.observers];
            for (const r of observers) {
                r.endUpdate(this);
            }
        }
        assertFn(() => this.updateCount >= 0);
    }
    handlePossibleChange(observable) {
        if (this.state === DerivedState.upToDate && ( this.dependencies.has(observable)) && !( this.dependenciesToBeRemoved.has(observable))) {
            this.state = DerivedState.dependenciesMightHaveChanged;
            for (const r of this.observers) {
                r.handlePossibleChange(this);
            }
        }
    }
    handleChange(observable, change) {
        if (( this.dependencies.has(observable)) && !( this.dependenciesToBeRemoved.has(observable))) {
            let shouldReact = false;
            try {
                shouldReact = this._handleChange ? this._handleChange({
                    changedObservable: observable,
                    change,
                    didChange: (o) => o === observable,
                }, this.changeSummary) : true;
            }
            catch (e) {
                onBugIndicatingError(e);
            }
            const wasUpToDate = this.state === DerivedState.upToDate;
            if (shouldReact && (this.state === DerivedState.dependenciesMightHaveChanged || wasUpToDate)) {
                this.state = DerivedState.stale;
                if (wasUpToDate) {
                    for (const r of this.observers) {
                        r.handlePossibleChange(this);
                    }
                }
            }
        }
    }
    readObservable(observable) {
        if (!this._isReaderValid) {
            throw ( new BugIndicatingError('The reader object cannot be used outside its compute function!'));
        }
        observable.addObserver(this);
        const value = observable.get();
        this.dependencies.add(observable);
        this.dependenciesToBeRemoved.delete(observable);
        return value;
    }
    addObserver(observer) {
        const shouldCallBeginUpdate = !( this.observers.has(observer)) && this.updateCount > 0;
        super.addObserver(observer);
        if (shouldCallBeginUpdate) {
            observer.beginUpdate(this);
        }
    }
    removeObserver(observer) {
        const shouldCallEndUpdate = ( this.observers.has(observer)) && this.updateCount > 0;
        super.removeObserver(observer);
        if (shouldCallEndUpdate) {
            observer.endUpdate(this);
        }
    }
}
class DerivedWithSetter extends Derived {
    constructor(debugNameData, computeFn, createChangeSummary, handleChange, handleLastObserverRemoved = undefined, equalityComparator, set) {
        super(debugNameData, computeFn, createChangeSummary, handleChange, handleLastObserverRemoved, equalityComparator);
        this.set = set;
    }
}

export { Derived, DerivedWithSetter, derived, derivedDisposable, derivedHandleChanges, derivedOpts, derivedWithSetter, derivedWithStore };
