const contextStack: Observer[] = [];

export class Observer<T = unknown> {
    private effect: () => T;

    dependencies: Set<Observable> = new Set();

    constructor(effect: { (): T }) {
        this.effect = effect;
        this.execute();
    }

    execute() {
        this.cleanup();
        contextStack.push(this);
        try {
            this.effect();
        } finally {
            contextStack.pop();
        }
    }

    cleanup() {
        for (const dep of this.dependencies.values()) {
            dep.observers.delete(this);
        }
        this.dependencies.clear();
    }
}

export class Observable<T = unknown> {
    private value: T;

    observers: Set<Observer> = new Set();

    constructor(init: T) {
        this.value = init;
    }

    get() {
        const activeObserver = contextStack[contextStack.length - 1];
        if (activeObserver) {
            this.observers.add(activeObserver);
            activeObserver.dependencies.add(this);
        }
        return this.value;
    }

    set(next: T) {
        this.value = next;
        for (const observer of Array.from(this.observers.values())) {
            observer.execute();
        }
    }
}

export class Mapping<T extends unknown> {
    private observable: Observable<T>;

    private observer: Observer<T>;

    constructor(mapping: { (): T }) {
        this.observable = new Observable(undefined as T);
        this.observer = new Observer(() => {
            const result = mapping();
            this.observable.set(result);
            return result;
        });
    }

    get() {
        return this.observable.get();
    }
}
