import {Testables} from "./testables";
// @ts-ignore
import {ComponentDataType, TestableCompositeData, TestableData} from "./store";
import {NestedTestableNodeData} from "./InMemoryTestableNode";
import {cloneDeep} from "lodash";
import {ExportImportTarget} from "./export/Exporters";
import {removeIds} from "./NodeHelpers";
import {ComponentsMeta} from "./ComponentsMeta";
import {exporterImporters} from "./export/CustomImportersAndExporters";
import {
    currentPreloadedDynamicData,
    PreloadedDynamicData,
    PreloadedDynamicValues
} from "./export/ComponentExporterImporter";
import {isTestableCompositeGroupType} from "./testableGroupTypes";

export interface TestableInterface<Options extends (object | false) = {}> {
    readonly id: string;
    readonly isRoot: boolean;

    /**
     * Returns the original reference to the testable data in the state!
     *
     * Other code is relying on this reference so don't change it!
     */
    getData(): Options extends false ? TestableCompositeData : TestableData<Options>;

    /**
     * A deep copy of the testable data is returned!
     */
    getNestedData(): NestedTestableNodeData;

    isDefault(): boolean;

    getMode(): TestableCompositeData['mode'];

    isPartial(): any;

}

export class Testable<Options extends (object | false) = {}> implements TestableInterface<Options> {
    private testable?: Options extends false ? TestableCompositeData : TestableData<Options>

    constructor(
        private testables: Testables,
        readonly id: string,
        readonly isRoot: boolean = false,
    ) {
    }

    /**
     * Returns the original reference to the testable data in the state!
     *
     * Other code is relying on this reference so don't change it!
     */
    getData(): typeof this.testable {
        if (!this.testable) {
            this.testable = this.testables.getNode(this.id);
        }

        return this.testable;
    }

    /**
     * A deep copy of the testable data is returned!
     */
    getNestedData(): NestedTestableNodeData {
        let dataToReturn: NestedTestableNodeData

        if (this.isPartial()) {
            dataToReturn = this.testables.getNode(this.id)!
        } else {
            dataToReturn = this.testables.getNodeWithChildren(this.id)!
        }

        return cloneDeep(dataToReturn)
    }

    getChildren(): Testable[] {
        return this.testables.getChildren(this.id).map(child => new Testable(this.testables, child.id));
    }

    isDefault(): boolean {
        // it is a composite group
        // and ony has one child
        const testable = this.getData();

        return testable?.type !== 'context' && this.testables.getChildren(testable?.id || '').length === 1;
    }

    getMode(): TestableCompositeData['mode'] {
        // get me the parent testable composite and then get me the mode from that
        // IF THIS IS a partial though, just get the mode based on the testable type (eg: filter for filters, test for conditions)
        // check whether this is a partial
        const parent = this.getParentData();

        if (this.isPartial()) {
            return (parent as TestableData).testableType === 'filter' ? 'filter' : 'test';
        }

        if (parent) {
            return (parent as TestableCompositeData).mode;
        }

        // we have no parent
        if (this.isGroup()) {
            return (this.getData() as TestableCompositeData).mode
        }
        // this is a testable and we have no parent so the mode will have to be its type (if a filter, then 'filter' else 'test')
        return (this.getData() as TestableData).testableType === 'filter'? 'filter' : 'test';
    }

    getType() : string {
        if (this.isPartial()) {
            return (this.getParent() as Testable).getType()
        }

        return (this.getData() as TestableData).type;
    }
    getTestableType(): ComponentDataType {
        return (this.getData() as TestableData).testableType;
    }

    getParent = () => {
        const parentData = this.getParentData();

        if (!parentData) {
            return undefined;
        }

        return new Testable(this.testables, parentData.id);
    }

    private getParentData = () => {
        return this.testables.getParent(this.id);
    }

    isPartial() {
        return this.testables.isPartial(this.id);
    }

    exists() {
        return !!this.testables.getNode(this.id);
    }

    export(target: ExportImportTarget) {
        let nestedData = this.getNestedData();

        if ('children' in nestedData) {
            nestedData.children = nestedData.children.map(testableData => {
                let testable: TestableInterface
                if (!testableData.id) {
                    testable = new LocalTestable(testableData as TestableData);
                } else {
                    testable = new Testable(this.testables, testableData.id);
                }

                return testable.export(target);
            })
        } else {
            let type = (nestedData as TestableData).type

            if (!('type' in nestedData)) {
                // this is partial, need to add support for it, we need to
                type = (this.getParent() as Testable).getType()
            }

            const customExportedData = exporterImporters[type]?.exportData?.(cloneDeep(nestedData), target)
            /*const customExportedData = exporters[testableType]?.(nestedData, target)*/

            if (customExportedData) {
                nestedData = customExportedData as typeof nestedData
            }

            if (target === 'server') {
                const preloadedDynamicValues = this.getDynamicValues()

                if (preloadedDynamicValues) {
                    currentPreloadedDynamicData.addPreloadedDynamicData(preloadedDynamicValues)
                }
            }
        }

        if (!this.isGroup()) {
            if ('children' in nestedData) {
                delete (nestedData as any).children
            }
        }
        if (target === 'preset') {
            // @ts-ignore
            nestedData = removeIds(nestedData)
        }

        return nestedData
    }

    isGroup(): boolean {
        return isTestableCompositeGroupType(this.getData()?.type)
    }

    getDynamicValues(): PreloadedDynamicValues[] {
        const dynamicValues = new PreloadedDynamicData([])

        if (this.isGroup()) {
            // this is a group so let's get the dynamic values from all children
            const children = this.getChildren();

            children.forEach(child => {
                const dynamicChildValues = child.getDynamicValues();

                if (dynamicChildValues.length) {
                    dynamicValues.addPreloadedDynamicData(dynamicChildValues)
                }
            })
        } else {
            // this is a testable so let's get the dynamic values for this testable if any
            const preloadedValues = exporterImporters[this.getType()]?.tagDynamicValues?.(this.getData(), 'server')

            if (preloadedValues) {
                dynamicValues.addPreloadedDynamicData(preloadedValues)
            }
        }

        return dynamicValues.items;
    }
}

export class LocalTestable<Options extends object = {}> implements TestableInterface<Options> {
    private localData: TestableData<Options>;

    constructor(
        localData: TestableData<Options>,
    ) {
        this.localData = cloneDeep(localData);
    }

    get id(): string {
        return this.localData?.id;
    }

    get isRoot(): boolean {
        return false;
    }

    getData() {
        return this.localData;
    }

    getMode(): TestableCompositeData["mode"] {
        return this.localData.testableType === 'filter' ? 'filter' : 'test';
    }

    getNestedData(): NestedTestableNodeData {
        return (this.localData);
    }

    isDefault(): boolean {
        return false;
    }

    isPartial(): any {
        return false;
    }
}
