/**
 * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info.
 *
 * @author David Sehnal <david.sehnal@gmail.com>
 */

import { Map as ImmutableMap, OrderedSet } from 'immutable';
import { Transform } from '../transform';
import { StateTree } from './immutable';
import { StateTreeBuilder } from './builder';
import { StateObjectCell } from 'mol-state/object';
import { shallowEqual } from 'mol-util/object';
import { UUID } from 'mol-util';

export { TransientTree }

class TransientTree implements StateTree {
    transforms = this.tree.transforms as ImmutableMap<Transform.Ref, Transform>;
    children = this.tree.children as ImmutableMap<Transform.Ref, OrderedSet<Transform.Ref>>;

    private changedNodes = false;
    private changedChildren = false;
    private _childMutations: Map<Transform.Ref, OrderedSet<Transform.Ref>> | undefined = void 0;
    private _transformMutations: Map<Transform.Ref, Transform> | undefined = void 0;

    private get childMutations() {
        if (this._childMutations) return this._childMutations;
        this._childMutations = new Map();
        return this._childMutations;
    }

    get root() { return this.transforms.get(Transform.RootRef)! }

    build(): StateTreeBuilder.Root {
        return new StateTreeBuilder.Root(this);
    }

    asTransient() {
        return this.asImmutable().asTransient();
    }

    private addChild(parent: Transform.Ref, child: Transform.Ref) {
        if (!this.changedChildren) {
            this.changedChildren = true;
            this.children = this.children.asMutable();
        }

        if (this.childMutations.has(parent)) {
            this.childMutations.get(parent)!.add(child);
        } else {
            const set = (this.children.get(parent) as OrderedSet<Transform.Ref>).asMutable();
            set.add(child);
            this.children.set(parent, set);
            this.childMutations.set(parent, set);
        }
    }

    private removeChild(parent: Transform.Ref, child: Transform.Ref) {
        if (!this.changedChildren) {
            this.changedChildren = true;
            this.children = this.children.asMutable();
        }

        if (this.childMutations.has(parent)) {
            this.childMutations.get(parent)!.remove(child);
        } else {
            const set = (this.children.get(parent) as OrderedSet<Transform.Ref>).asMutable();
            set.remove(child);
            this.children.set(parent, set);
            this.childMutations.set(parent, set);
        }
    }

    private clearRoot() {
        const parent = Transform.RootRef;
        if (this.children.get(parent).size === 0) return;
        const set = OrderedSet<Transform.Ref>();
        if (!this.changedChildren) {
            this.changedChildren = true;
            this.children = this.children.asMutable();
        }
        this.children.set(parent, set);
        this.childMutations.set(parent, set);
    }

    add(transform: Transform) {
        const ref = transform.ref;

        if (this.transforms.has(transform.ref)) {
            const node = this.transforms.get(transform.ref);
            if (node.parent !== transform.parent) alreadyPresent(transform.ref);
        }

        const children = this.children.get(transform.parent);
        if (!children) parentNotPresent(transform.parent);

        if (!children.has(transform.ref)) {
            this.addChild(transform.parent, transform.ref);
        }

        if (!this.children.has(transform.ref)) {
            if (!this.changedChildren) {
                this.changedChildren = true;
                this.children = this.children.asMutable();
            }
            this.children.set(transform.ref, OrderedSet());
        }

        if (!this.changedNodes) {
            this.changedNodes = true;
            this.transforms = this.transforms.asMutable();
        }

        this.transforms.set(ref, transform);
        return this;
    }

    /** Calls Transform.definition.params.areEqual if available, otherwise uses shallowEqual to check if the params changed */
    setParams(ref: Transform.Ref, params: unknown) {
        ensurePresent(this.transforms, ref);

        const transform = this.transforms.get(ref)!;
        const def = transform.transformer.definition;
        if (def.params && def.params.areEqual) {
            if (def.params.areEqual(transform.params, params)) return false;
        } else {
            if (shallowEqual(transform.params, params)) {
                return false;
            }
        }

        if (this._transformMutations && this._transformMutations.has(transform.ref)) {
            const mutated = this._transformMutations.get(transform.ref)!;
            (mutated.params as any) = params;
            (mutated.version as UUID) = UUID.create22();
        } else {
            this.set(Transform.withParams(transform, params));
        }

        return true;
    }

    setCellState(ref: Transform.Ref, state: Partial<StateObjectCell.State>) {
        ensurePresent(this.transforms, ref);

        if (this._transformMutations && this._transformMutations.has(ref)) {
            const transform = this._transformMutations.get(ref)!;
            const old = transform.cellState;
            (transform.cellState as StateObjectCell.State) = { ...old, ...state };
            return transform;
        } else {
            const transform = this.transforms.get(ref);
            const newT = Transform.withCellState(transform, state);
            this.set(newT);
            return newT;
        }
    }

    private set(transform: Transform) {
        ensurePresent(this.transforms, transform.ref);

        if (!this.changedNodes) {
            this.changedNodes = true;
            this.transforms = this.transforms.asMutable();
        }

        if (!this._transformMutations) {
            this._transformMutations = new Map();
        }
        this._transformMutations.set(transform.ref, transform);

        this.transforms.set(transform.ref, transform);
        return this;
    }

    remove(ref: Transform.Ref): Transform[] {
        const node = this.transforms.get(ref);
        if (!node) return [];

        const st = StateTree.subtreePostOrder(this, node);
        if (ref === Transform.RootRef) {
            st.pop();
            if (st.length === 0) return st;
            this.clearRoot();
        } else {
            if (st.length === 0) return st;
            this.removeChild(node.parent, node.ref);
        }

        if (!this.changedNodes) {
            this.changedNodes = true;
            this.transforms = this.transforms.asMutable();
        }

        for (const n of st) {
            this.transforms.delete(n.ref);
            this.children.delete(n.ref);
            if (this._childMutations) this._childMutations.delete(n.ref);
        }

        return st;
    }

    asImmutable() {
        if (!this.changedNodes && !this.changedChildren && !this._childMutations) return this.tree;
        if (this._childMutations) this._childMutations.forEach(fixChildMutations, this.children);
        return StateTree.create(
            this.changedNodes ? this.transforms.asImmutable() : this.transforms,
            this.changedChildren ? this.children.asImmutable() : this.children);
    }

    constructor(private tree: StateTree) {

    }
}

function fixChildMutations(this: ImmutableMap<Transform.Ref, OrderedSet<Transform.Ref>>, m: OrderedSet<Transform.Ref>, k: Transform.Ref) { this.set(k, m.asImmutable()); }

function alreadyPresent(ref: Transform.Ref) {
    throw new Error(`Transform '${ref}' is already present in the tree.`);
}

function parentNotPresent(ref: Transform.Ref) {
    throw new Error(`Parent '${ref}' must be present in the tree.`);
}

function ensurePresent(nodes: StateTree.Nodes, ref: Transform.Ref) {
    if (!nodes.has(ref)) {
        throw new Error(`Node '${ref}' is not present in the tree.`);
    }
}