import is from '@pilotlab/is';
import Debug from "@pilotlab/debug";
import { NodesBase, INodeChangeOptions } from '@pilotlab/nodes';
import { ProgressTracker } from '@pilotlab/progress';
import { Signal } from '@pilotlab/signals';
import IAttribute from './interfaces/iAttribute';
import AttributeCreateOptions from './attributeCreateOptions';
import AttributeSetReturn from './attributeSetReturn';
import { DataType } from './attributeEnums';
import AttributeChangeOptions from './attributeChangeOptions';
import IAttributeChangeOptions from './interfaces/iAttributeChangeOptions';
import IAttributeCreateOptions from './interfaces/iAttributeCreateOptions';
import IAttributes from './interfaces/iAttributes';
import IAttributeUpdateTracker from './interfaces/iAttributeUpdateTracker';
import AttributeUpdateTracker from './attributeUpdateTracker';
import IAttributeFactory from './interfaces/iAttributeFactory';
import AttributeEventArgs from './attributeEventArgs';
import IAttributeSetReturn from './interfaces/iAttributeSetReturn';
import AttributeBase from './attributeBase';


export abstract class AttributesBase<
    TAttribute extends AttributeBase<any, any, any, any>,
    TAttributes extends AttributesBase<any, any>
>
extends NodesBase<TAttribute>
implements IAttributes {
    constructor(
        factory:IAttributeFactory,
        parent?:TAttribute,
        pathSegment:string = '.'
    ) {
        super(factory, parent, pathSegment);
        this.p_factory = factory;
    }


    /*====================================================================*
     START: Factory
     *====================================================================*/
    get create():IAttributeFactory { return this.p_factory; }
    protected p_factory:IAttributeFactory;


    /*====================================================================*
     START: Properties
     *====================================================================*/
    get copy():TAttributes {
        return <TAttributes>this.create.collection.fromObject(this.toObject());
    }


    /*====================================================================*
     START: Signals
     *====================================================================*/
    saveTriggered:Signal<AttributeEventArgs<TAttribute>> = new Signal<AttributeEventArgs<TAttribute>>();
    /// IMPORTANT: This signal should only be dispatched from internalChanged.
    changed:Signal<AttributeEventArgs<TAttribute>> = new Signal<AttributeEventArgs<TAttribute>>(false);


    /*====================================================================*
     START: Methods
     *====================================================================*/
    /**
     * If an attribute exists at the given path, it will be returned.
     * Otherwise, if createOptions is supplied, a new attribute will
     * first be created with the given parameters, then returned.
     */
    getOrCreate(
        path:string,
        createOptions?:IAttributeCreateOptions,
        segmentCreateOptions?:IAttributeCreateOptions
    ):TAttribute {
        if (is.empty(createOptions)) createOptions = new AttributeCreateOptions(null, DataType.COLLECTION);
        if (is.empty(segmentCreateOptions)) segmentCreateOptions = new AttributeCreateOptions(null, DataType.COLLECTION);
        return super.getOrCreate(path, createOptions, segmentCreateOptions);
    }


    get(
        path:string,
        createOptions?:IAttributeCreateOptions,
        changeOptions:IAttributeChangeOptions = AttributeChangeOptions.default
    ):any {
        if (is.empty(createOptions)) return this.getByPath(path);
        const segmentCreateOptions:IAttributeCreateOptions = new AttributeCreateOptions(null, DataType.COLLECTION, null, null, null, changeOptions);
        return this.getOrCreate(path, createOptions, segmentCreateOptions);
    }


    getOnly(paths:string[]):TAttributes {
        let collection:TAttributes = <TAttributes>this.create.collection.instance();

        paths.forEach((path:string) => {
            let child:TAttribute = this.get(path);
            /// Return the child attributes by reference here, since that's what get() does.
            /// If the user doesn't want to modify the actual attributes, they can call .copy on the returned collection.
            if (is.notEmpty(child) && !child.isEmpty) collection.add(child, null, AttributeChangeOptions.zero);
        });

        return collection;
    }


    getAll(key?:string, options?:any, filter?:(data:any) => boolean, sort?:(a:any, b:any) => number):TAttributes {
        let collectionIn:TAttributes;
        let collectionOut:TAttributes = <TAttributes>this.create.collection.instance();

        /// Retrieve only child attribute collections with a child key that matches the key argument.
        if (is.notEmpty(key)) {
            collectionIn = <TAttributes>this.create.collection.instance();
            this.forEach((childLevel1:TAttribute) => {
                if (this.create.getBaseDataType(childLevel1.dataType) === DataType.COLLECTION) {
                    childLevel1.value.forEach((nodeLevel2:TAttribute) => {
                        if (nodeLevel2.key === key) {
                            let isInclude:boolean = true;
                            /// Were any options passed in?
                            if (is.notEmpty(options)) {
                                /// If a key was passed in as an option, check to see whether the value of the attribute matches the key.
                                if (is.notEmpty(options.key)) if (nodeLevel2.value !== options.key) isInclude = false;
                            }

                            if (isInclude) collectionIn.set(<string>childLevel1.key, childLevel1.value);
                        }
                    });
                    return true;
                }
                return true;
            });
        } else collectionIn = <TAttributes><IAttributes>this;

        if (is.notEmpty(filter)) {
            collectionIn.list.forEach((child:TAttribute):boolean => {
                if (filter(child.toObject())) collectionOut.set(<string>child.key, child.value);
                return true;
            });
        } else collectionOut = collectionIn;

        if (is.notEmpty(sort)) collectionOut.sort(sort);
        else {
            collectionOut.list.sort((a:TAttribute, b:TAttribute) => {
                if (a.key > b.key) return 1;
                else if (a.key < b.key) return -1;
                return 0;
            });
        }

        return <TAttributes>collectionOut;
    }


    /**
     * If an attribute doesn't already exist at the given path, one will be created.
     */
    set(
        path:string,
        value:any,
        dataType:(DataType | string) = DataType.ANY,
        label?:string,
        index?:number,
        changeOptions?:IAttributeChangeOptions
    ):IAttributeSetReturn {
        let returnValue:IAttributeSetReturn = new AttributeSetReturn<TAttribute>(null, false);
        if (is.empty(path, true)) return returnValue;

        let attribute:IAttribute = this.getByPath(path);

        if (is.empty(attribute) || attribute.isEmpty) {
            const createOptions:IAttributeCreateOptions = new AttributeCreateOptions(value, dataType, label, index, null, changeOptions);
            const segmentCreateOptions:IAttributeCreateOptions = new AttributeCreateOptions(null, DataType.COLLECTION, null, null, null, changeOptions);
            attribute = this.getOrCreate(path, createOptions, segmentCreateOptions);
            returnValue.attribute = attribute;
            returnValue.isChanged = true;
        } else {
            returnValue = attribute.set(value, changeOptions);
        }

        return returnValue;
    }


    protected p_add(
        child:TAttribute,
        index?:number,
        changeOptions?:IAttributeChangeOptions
    ):void {
        super.p_add(child, index, changeOptions);

        if (
            is.notEmpty(child) &&
            is.notEmpty(child.key)
        ) {
            /// START: Create getters and setters.
            if (
                is.notEmpty(this.parent) &&
                !this.parent.hasOwnProperty(child.key) &&
                this.parent.isAllowAutoProperties
            ) {
                const childKey:string = child.key;
                const childDataType:(DataType | string) = child.dataType;
                Object.defineProperty(this.parent, childKey, {
                    get: (): any => {
                        const attribute:IAttribute = this.get(childKey);
                        if (is.notEmpty(attribute)) {
                            if (this.create.getBaseDataType(attribute.dataType) === DataType.COLLECTION) return attribute;
                            else return attribute.value;
                        } else return this.create.instance();
                    },
                    set: (value: any) => {
                        this.set(childKey, value, childDataType, null, null, AttributeChangeOptions.default.durationZero);
                    },
                    enumerable: true,
                    configurable: true
                });
            }

            if (!this.hasOwnProperty(child.key)) {
                const childKey:string = child.key;
                Object.defineProperty(this, childKey, {
                    get: ():any => { return this.get(childKey); },
                    enumerable: true,
                    configurable: true
                });
            }
            /// END: Create getters and setters.
        }
    }


    deleteByKey(
        key:string,
        changeOptions:IAttributeChangeOptions = AttributeChangeOptions.default
    ):TAttribute { return super.deleteByKey(key, changeOptions); }


    interruptAnimationAll():void {
        this.p_list.forEach(function (child:TAttribute):boolean {
            if (is.empty(child)) return true;
            child.interruptAnimation();
            return true;
        });
    }


    set isAllowSave(value:boolean) {
        for (let i:number = 0; i < this.p_list.size; i++) {
            this.p_list.item(i).isAllowSave = value;
        }
    }


    /*====================================================================*
     START: Update
     *====================================================================*/
    /**
     * <pre><code>
     *    /// Just quietly initialize the data entity, but don't save to the data store or signal a change.
     *    /// This is used when reloading data from the store to initialize the app.
     *
     *    myComponent.data.update(data, AttributeChangeOptions.noSaveOrSignal);
     *
     *    /// Update the data, and signal changes, but don't save to the data store.
     *    /// Used for data entities that don't need to be saved to a store.
     *
     *    myComponent.data.update(data, nodeChangeOptions.flags(AttributeChangeOptions.SIGNAL_CHANGE));
     *
     *    /// Update the data and save to the data store, but don’t signal a change.
     *    /// Used for batch saving, when you may want to signal changes only at the end of the process.
     *
     *    myComponent.data.update(data, nodeChangeOptions.flags(AttributeChangeOptions.SAVE));
     *
     *    /// And, finally, the default mode. Update the data, save to the store and signal the change.
     *
     *    myComponent.data.update(data, AttributeChangeOptions.saveAndSignal);
     * </code></pre>
     */
    update(
        data:(IAttributes | Object | Array<any> | string),
        changeOptions:IAttributeChangeOptions = AttributeChangeOptions.default,
        progressTracker?:ProgressTracker
    ):IAttributeUpdateTracker {
        let attributesNew:IAttributes = this.create.collection.fromAny(data);

        let updateTracker:IAttributeUpdateTracker = new AttributeUpdateTracker(attributesNew, changeOptions, progressTracker);

        if (is.empty(attributesNew) || attributesNew.list.size === 0) {
            updateTracker.progressTracker.run();
            return updateTracker;
        }

        this.updateTracked(updateTracker);

        return updateTracker;
    }


    /**
     * Cascades data down through all attributes and attribute
     * collections to update any that need to be changed.
     */
    updateTracked(updateTracker:IAttributeUpdateTracker):void {
        updateTracker.update(this);
        updateTracker.progressTracker.run();
    }


    internalAttachedAll(changeOptions:INodeChangeOptions):void {
        if (is.notEmpty(this.parent)) {
            this.isSignalChange = this.parent.isSignalChange;
            this.isAllowSave = this.parent.isAllowSave;
        }

        super.internalAttachedAll(changeOptions);
    }
} // End class


export default AttributesBase;
