import is from '@pilotlab/is';
import { List } from '@pilotlab/collections';
import { Signal } from '@pilotlab/signals';
import { Identifier } from '@pilotlab/ids';
import { IPromise, Result } from '@pilotlab/result';
import { NodeType, NodeChangeType, NodeBase } from '@pilotlab/nodes';
import { AttributeChangeActions, AttributesWrapType, DataType } from './attributeEnums';
import {
    IAnimationBatch,
} from '@pilotlab/animation';
import DataTools from '../dataTools';
import AttributesBase from './attributesBase';
import AttributeSetReturn from './attributeSetReturn';
import AttributeChangeOptions from './attributeChangeOptions';
import IAttributes from './interfaces/iAttributes';
import IAttributeFactory from './interfaces/iAttributeFactory';
import IAttributeChangeOptions from './interfaces/iAttributeChangeOptions';
import IAttributeSetReturn from './interfaces/iAttributeSetReturn';
import IAttributeUpdateTracker from './interfaces/iAttributeUpdateTracker';
import AttributeEventArgs from './attributeEventArgs';
import Attributes from './attributes';
import AttributeRoot from './attributeRoot';
import {NodeEventArgs} from "../../../nodes/src/nodeEventArgs";
import INodeChangeOptions from "../../../nodes/src/interfaces/iNodeChangeOptions";
import Debug from "../../../debug";
import INode from "../../../nodes/src/interfaces/iNode";


export abstract class AttributeBase<
    TValue,
    TNode extends AttributeBase<any, any, any, any>,
    TNodes extends Attributes,
    TRoot extends AttributeRoot>
extends NodeBase<TNode, TNodes, TRoot> {
    protected constructor(
        factory:IAttributeFactory,
        value?:any,
        dataType?:(DataType | string),
        label?:string,
        key:string = '',
        nodeType:NodeType = NodeType.BASIC,
        isInitialize:boolean = true
    ) {
        super(factory, nodeType, key, false);

        this.p_value = null;
        if (is.notEmpty(label, true)) this.p_label = label;
        this.p_dataType = DataType.UNDEFINED;
        this.p_changed = new Signal<AttributeEventArgs<TNode>>(false);
        this.p_isSignalChange = true;
        this.p_isAllowSave = true;
        this.p_isAllowAutoPropterties = true;
        this.p_returnValue = new AttributeSetReturn<TNode>(<TNode><AttributeBase<any, any, any, any>>this, false);
        this.p_formatter = (value:any) => value;

        if (isInitialize) this.initialize(value, dataType, nodeType);
    }


    protected p_onInitializeStarted(result:Result<any>, args:any[]):IPromise<any> {
        const value:any = args[0];
        const dataType:(DataType | string) = args[1];
        const nodeType:NodeType = args[2];

        super.p_onInitializeStarted(new Result<any>(), [nodeType]).then(() => {
            /// Set the data type first, then the value
            if (is.notEmpty(dataType)) this.p_dataType = dataType;
            else {
                if (is.notEmpty(value, false)) this.p_dataType = DataTools.getDataType(value);
                else this.p_dataType = DataType.OBJECT;
            }

            if (this.create.getBaseDataType(this.p_dataType) === DataType.COLLECTION) {
                this.p_wrapAttributes(value, AttributesWrapType.CLEAR_BASE);
            } else if (is.notEmpty(value, false)) this.p_value = value;

            result.resolve(value);
        });

        return result;
    }


    /**
     * All data associated with this entity will be saved to the provided attribute collection,
     * and all change events will be propagated through that collection.
     */
    protected p_wrapAttributes(value?:any, wrapType:AttributesWrapType = AttributesWrapType.OVERWRITE_BASE):void {
        let changeOptions:IAttributeChangeOptions = AttributeChangeOptions.noSaveOrSignal;
        let attributes:IAttributes;

        /// If an Attributes instance was passed as the value, it will be directly assigned to this.p_value.
        if (is.notEmpty(value)) { attributes = this.create.collection.fromAny(value, this); }
        if (is.empty(attributes)) attributes = this.create.collection.instance(this);

        this.p_value = attributes;

        if (is.notEmpty(this.p_value)) {
            if (wrapType === AttributesWrapType.CLEAR_BASE) {
                /// Do nothing.
            } else if (wrapType === AttributesWrapType.CLEAR_WRAPPED) {
                attributes.clear(changeOptions);
                attributes.update(this.p_value, changeOptions);
            } else if (wrapType === AttributesWrapType.OVERWRITE_BASE) {
                this.p_value.update(attributes, changeOptions);
                attributes.clear(AttributeChangeOptions.noSaveOrSignal);
                attributes.update(this.p_value, changeOptions);
            } else if (wrapType === AttributesWrapType.OVERWRITE_WRAPPED) {
                attributes.update(this.p_value, changeOptions);
            }
        }
    }


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


    /*====================================================================*
     START: Properties
     *====================================================================*/
    protected p_returnValue:IAttributeSetReturn;


    /**
     * A UI-friendly title for the attribute that can be presented to users.
     * If no label is set, the key of the node will be returned.
     */
    get label():string { return is.notEmpty(this.p_label, true) ? this.p_label : <string>this.key; }
    set label(value:string) { this.p_label = value; }
    protected p_label:string;


    /**
     * A brief description of that attribute that can be used to supply info to the user
     * about how the attribute should be used.
     */
    get summary():string { return is.notEmpty(this.p_summary, true) ? this.p_summary : ''; }
    set summary(value:string) { this.p_summary = value; }
    protected p_summary:string;


    /**
     * The value of the associated data.
     * This property cannot be set directly. You must use the set() method to change the value.
     * This allows attributes to be wrapped in an AttributeReader instance, which effectively
     * renders them immutable.
     */
    get value():TValue { return <TValue>this.p_value; }
    set value(value:TValue) { this.set(value, this.isSignalChange ? AttributeChangeOptions.save : AttributeChangeOptions.zero); }
    get formatted():TValue {
        let value:TValue = <TValue>this.p_value;
        value = this.p_format(value);
        return value;
    }
    protected p_value:any;
    protected p_valueTarget:any; /// Used for animation.


    get isCollection():boolean {
        return (
            this.nodeType === NodeType.ROOT ||
            this.nodeType === NodeType.COLLECTION ||
            this.create.getBaseDataType(this.dataType) === DataType.COLLECTION
        );
    }


    /// This property will hold dynamically generated getters and setters
    /// for each child attribute that is added to this attribute.
    get attributes():IAttributes { return this.p_value; }
    get children():TNodes { return this.isCollection ? this.p_value : this.p_children; }


    get valuePrevious():TValue { return <TValue>this.p_valuePrevious; }
    protected p_valuePrevious:any;


    get dataType():(DataType | string) { return this.p_dataType; }
    protected p_dataType:(DataType | string);


    /**
     * Attributes will be considered empty if no key or value has been set.
     */
    get isEmpty():boolean {
        if (is.undefined(this.p_value)) return true;
        if (is.empty(this.p_value, false)) return true;
        if (is.empty(this.key, true)) return true;
        if (is.empty(this.dataType, true)) return true;
        if (this.dataType === DataType.UNDEFINED) return true;
        if (this.dataType === DataType.NULL) return true;

        return false;
    }


    /**
     * If this function returns true, the attribute will be omitted from data transfer objects
     * when saving data to the data store.
     */
    get omitFromDataStore():(value:TValue) => boolean { return this.p_omitFromDataStore; }
    set omitFromDataStore(fn:(value:TValue) => boolean) { this.p_omitFromDataStore = fn; }
    protected p_omitFromDataStore:(value:TValue) => boolean;


    get formatter():(value:any) => TValue { return this.p_formatter; }
    set formatter(value:(value:any) => TValue) { this.p_formatter = value; }
    protected p_formatter:(value:any) => TValue;
    protected p_format(value:any):TValue { return this.p_formatter(value); }


    get copy():TNode { return <TNode>this.create.fromObject(this.toObject()); }


    /**
     * An automatically generated unique ID that is used when animating attribute values.
     */
    get animationKey():string {
        if (is.empty(this._animationKey)) this._animationKey = this.key + '_' + Identifier.getSessionUniqueInteger();
        return this._animationKey;
    }
    private _animationKey:string;
    protected p_animation:IAnimationBatch;


    get isEditable():boolean { return this.p_isEditable; }
    set isEditable(value:boolean) {
        this.p_isEditable = value;
    }
    protected p_isEditable:boolean;


    get isHidden():boolean { return this.p_isHidden; }
    set isHidden(value:boolean) {
        this.p_isHidden = value;
    }
    protected p_isHidden:boolean;


    get isAllowSave():boolean { return this.p_isAllowSave; }
    set isAllowSave(value:boolean) {
        if (this.isCollection) this.children.isAllowSave = value;
        this.p_isAllowSave = value;
    }
    protected p_isAllowSave:boolean;


    /**
     * Should we allow accessor properties to be generated automatically when
     * a new child Attribute is added to our Attribute collection?
     */
    get isAllowAutoProperties():boolean { return this.p_isAllowAutoPropterties; }
    set isAllowAutoProperties(value:boolean) { this.p_isAllowAutoPropterties = value; }
    protected p_isAllowAutoPropterties:boolean;


    /*====================================================================*
     START: Signals
     *====================================================================*/
    get changed():Signal<AttributeEventArgs<TNode>> { return this.p_changed; }
    protected p_changed:Signal<AttributeEventArgs<TNode>>;


    /*====================================================================*
     START: Internal
     *====================================================================*/
    /// Overridden to take isSave into account.
    internalChanged(args:AttributeEventArgs<TNode>):void {
        if (!this.isSignalChange && !this.isAllowSave) return;
        if (
            (args.changeOptions.isSignalChange || args.changeOptions.isSave) && this.isSignalChange
        ) this.changed.dispatch(args);

        if (is.notEmpty(this.parentCollection) && (is.notEmpty(this.parentCollection.internalChanged))) {
            this.parentCollection.internalChanged(args);
        }
    }


    /*====================================================================*
     START: Public Methods
     *====================================================================*/
    /**
     * Save the value to the attribute.
     */
    set(
        value:any,
        changeOptions:IAttributeChangeOptions = AttributeChangeOptions.default,
    ):IAttributeSetReturn {
        let returnValue:IAttributeSetReturn = this.p_returnValue;
        if (is.empty(value)) return returnValue;

        if (this.create.getBaseDataType(this.p_dataType) === DataType.COLLECTION) {
            /// This attribute holds a collection...
            if (is.notEmpty(this.p_value) && (this.p_value instanceof AttributesBase)) {
                let tracker:IAttributeUpdateTracker = this.p_value.update(value, changeOptions);
                returnValue.updateTracker = tracker;
                tracker.then((tracker:IAttributeUpdateTracker) => {
                    returnValue.isChanged = tracker.isChanged;
                });
            }
        } else {
            value = this.p_format(value);
            if (this.p_value === value) {
                if (changeOptions.action === AttributeChangeActions.SAVE) {
                    this.doChanged(changeOptions.action);
                }

                return returnValue;
            }

            this.p_valuePrevious = this.p_valueTarget;
            this.p_value = value;
            this.p_valueTarget = value;
            returnValue.isChanged = true;
            this.doChanged(changeOptions.action);
        }

        return returnValue;
    }


    /**
     * Interrupt any animated value transitions that are in effect.
     */
    interruptAnimation():void { if (is.notEmpty(this.p_animation)) this.p_animation.interrupt(); }


    /**
     * Manually trigger a dispatch on the 'changed' signal.
     */
    doChanged(action:AttributeChangeActions):void {
        if (!this.isSignalChange && !this.isAllowSave) return;
        else if (action === AttributeChangeActions.SIGNAL_CHANGE && !this.isSignalChange) return;
        else if (action === AttributeChangeActions.SAVE && !this.isAllowSave) return;

        let args:AttributeEventArgs<TNode> = new AttributeEventArgs<TNode>(
            <TNode><AttributeBase<any, any, any, any>>this,
            NodeChangeType.UPDATED,
            new AttributeChangeOptions(action).durationZero
        );

        this.internalChanged(args);
    }


    toObject(isIncludeDataTypes:boolean = true, isIncludeLabels:boolean = true, appendToObject?:any, isForceInclude:boolean = false):any {
        if (!isForceInclude && (is.notEmpty(this.p_omitFromDataStore) && this.p_omitFromDataStore(this.p_value))) return appendToObject;

        let object:any = {};

        if (
            isIncludeDataTypes ||
            (isIncludeLabels && is.notEmpty(this.label, true) && this.label !== this.key)
        ) {
            if (isIncludeDataTypes) object['dataType'] = this.dataType.toString();

            if (isIncludeLabels && (is.notEmpty(this.label, true) && this.label !== this.key)) object['label'] = this.label;

            object['isEditable'] = this.isEditable;
            object['isHidden'] = this.isHidden;

            if (this.p_value instanceof AttributesBase) {
                object['value'] = (<IAttributes>this.p_value).toObject(isIncludeDataTypes, isIncludeLabels, isForceInclude);
            } else object['value'] = this.formatted;
        } else {
            if (this.p_value instanceof AttributesBase) {
                object = (<IAttributes>this.p_value).toObject(isIncludeDataTypes, isIncludeLabels, isForceInclude);
            } else object = this.formatted;
        }

        if (is.notEmpty(appendToObject)) {
            appendToObject[this.key] = object;
            return appendToObject;
        } else return object;
    }


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

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


export default AttributeBase;
