import is from '@pilotlab/is';
import { IPromise, Result } from '@pilotlab/result';
import { ProgressTracker, ProgressCommand, ProgressRunArgs } from '@pilotlab/progress';
import { DataType } from './attributeEnums';
import IAttributeSetReturn from './interfaces/iAttributeSetReturn';
import IAttribute from './interfaces/iAttribute';
import IAttributes from './interfaces/iAttributes';
import IAttributeChangeOptions from './interfaces/iAttributeChangeOptions';
import AttributeChangeOptions from './attributeChangeOptions';
import { Animation, IAnimationBatch, AnimationBatch } from '@pilotlab/animation';
import IAttributeUpdateTracker from './interfaces/iAttributeUpdateTracker';
import DataTools from '../dataTools';


export class AttributeUpdateTracker implements IAttributeUpdateTracker {
    constructor(
        attributesNew:IAttributes,
        changeOptions:IAttributeChangeOptions = AttributeChangeOptions.default,
        progressTracker?:ProgressTracker,
        attributesChanged?:IAttributes
    ) {
        this.attributesNew = attributesNew;
        this.changeOptions = changeOptions;
        this.p_result = new Result<IAttributeUpdateTracker>();

        if (is.notEmpty(progressTracker)) this.progressTracker = progressTracker;

        /// IMPORTANT:
        /// Be sure to use a factory to create an empty collection here if no attributes were changed,
        /// or this code will break tests with UnhandledPromiseRejectionWarning.
        this.p_attributesChanged = is.notEmpty(attributesChanged) ? attributesChanged : attributesNew.create.collection.instance();

        this.animation = new AnimationBatch(Animation.animate, [], false);

        this.isAllowAttributeCreation = true;
    }


    get progressTracker():ProgressTracker {
        if (is.empty(this.p_progressTracker)) {
            this.progressTracker = new ProgressTracker();
        }

        return this.p_progressTracker;
    }
    set progressTracker(value:ProgressTracker) {
        if (is.empty(value)) return;
        this.p_progressTracker = value;

        this.p_progressTracker.completed.listenOnce(() => {
            this.animation.initialize();
            this.p_result.resolve(this);
        });
    }
    protected p_progressTracker:ProgressTracker;


    attributesNew:IAttributes;
    changeOptions:IAttributeChangeOptions;
    animation:IAnimationBatch;
    isAllowAttributeCreation:boolean;


    get result():Result<IAttributeUpdateTracker> { return this.p_result; }
    protected p_result:Result<any>;


    get isChanged():boolean { return this.p_isChanged; }
    protected p_isChanged:boolean = false;


    get attributesChanged():IAttributes { return this.p_attributesChanged; }
    protected p_attributesChanged:IAttributes;


    /**
     * This is resolved once the tracker has finished updating all attributes,
     * but before animations are initialized after the update cycle.
     */
    then(onDone:(value:IAttributeUpdateTracker) => any, onError?:(error:Error) => void):IPromise<any> {
        return this.p_result.then(onDone, onError);
    }


    update(attributesToUpdate:IAttributes):void {
        for (let i = 0; i < this.attributesNew.list.size; i++) {
            const attributeNew:IAttribute = <IAttribute>this.attributesNew.list.item(i);
            const attributeToUpdate:IAttribute = is.notEmpty(attributesToUpdate) ? attributesToUpdate.get(attributeNew.key) : null;
            if (is.empty(attributeToUpdate) || attributeToUpdate.isEmpty) {
                if (this.isAllowAttributeCreation) {
                    attributesToUpdate.add(attributeNew);
                    this.p_isChanged = true;
                    this.p_attributesChanged.addByPath(attributeNew, attributeNew.path);
                }
                continue;
            }

            if (attributeToUpdate.create.getBaseDataType(attributeToUpdate.dataType) === DataType.COLLECTION) this.p_queueAttributesUpdate(attributeToUpdate.value, attributeNew);
            this.p_queueAttributeUpdate(attributeToUpdate, attributeNew);
        }
    }


    protected p_queueCommand(command:ProgressCommand, key?:string):void {
        if (is.empty(command)) return;

        if (is.empty(key)) this.progressTracker.queueCommand(command, [this.attributesNew, this.changeOptions]);
        else {
            let dataAttribute:IAttribute = this.attributesNew.get(key);
            if (is.empty(dataAttribute) || dataAttribute.isEmpty) return;
            this.progressTracker.queueCommand(command, [dataAttribute.value, this]);
        }
    }


    protected p_queueAttributeUpdate(attributeToUpdate:IAttribute, attributeNew:IAttribute):void {
        if (is.empty(attributeNew)) attributeNew = this.attributesNew.get(attributeToUpdate.key);
        if (attributeNew.isEmpty) return;

        this.progressTracker.queue((args:ProgressRunArgs) => {
            let attributeSetReturn:IAttributeSetReturn = attributeToUpdate.set(
                attributeNew.value,
                this.changeOptions
            );

            if (is.notEmpty(attributeSetReturn.animation) && attributeSetReturn.animation.animations.size > 0) {
                this.animation.animations.append(attributeSetReturn.animation.animations);
            }

            if (attributeSetReturn.isChanged) {
                this.p_isChanged = true;
                this.p_attributesChanged.addByPath(attributeToUpdate, attributeToUpdate.path);
            }

            args.result.resolve();
        }, this, 'updating ' + attributeToUpdate.key + ' attribute');
    }


    protected p_queueAttributesUpdate(attributesToUpdate:IAttributes, attributeNew:IAttribute):void {
        if (is.empty(attributesToUpdate) || is.empty(attributeNew)) return;

        if (
            attributeNew.isEmpty
            || attributeNew.create.getBaseDataType(attributeNew.dataType) !== DataType.COLLECTION
            || is.empty(attributeNew.value)
            || attributeNew.value.size === 0
        ) return;

        this.progressTracker.queue((args:ProgressRunArgs) => {
            let updateTracker:AttributeUpdateTracker = new AttributeUpdateTracker(attributeNew.value, this.changeOptions, args.subTracker, this.p_attributesChanged);
            attributesToUpdate.updateTracked(updateTracker);
            return updateTracker.result;
        }, this, 'updating ' + attributeNew.key + ' data');
    }
} // End class


export default AttributeUpdateTracker;
