import is from "@pilotlab/is";
import { NodeType } from '@pilotlab/nodes';
import { Debug } from '@pilotlab/debug';
import { ProgressTracker } from '@pilotlab/progress';
import { IPromise, Result } from '@pilotlab/result';
import { Signal } from '@pilotlab/signals';
import { DataType } from './attributes/attributeEnums';
import IAttribute from './attributes/interfaces/iAttribute';
import IAttributes from './attributes/interfaces/iAttributes';
import IAttributeChangeOptions from './attributes/interfaces/iAttributeChangeOptions';
import IAttributeUpdateTracker from './attributes/interfaces/iAttributeUpdateTracker';
import AttributeBase from './attributes/attributeBase';
import AttributeEventArgs from './attributes/attributeEventArgs';
import AttributeRoot from './attributes/attributeRoot';
import Attributes from './attributes/attributes';
import AttributeChangeOptions from './attributes/attributeChangeOptions';
import AttributeCreateOptions from './attributes/attributeCreateOptions';
import IData from './interfaces/iData';
import IDataStore from './interfaces/iDataStore';
import { Identifier } from '@pilotlab/ids';
import { KeyAutoGenerationType } from "./dataEnums";
import { SHA1 } from '@pilotlab/sha1';
import { ThrottlingCommand } from '@pilotlab/commands';


Debug.setCategoryMode('data_save', false);


export class Data
extends AttributeBase<Attributes, AttributeBase<any, any, any, any>, Attributes, AttributeRoot>
implements IData {
    constructor(
        data?:(Data | IAttributes | Object | string),
        isStoreKey:boolean = true,
        dataStore?:IDataStore,
        isSaveDataTypes:boolean = false,
        isAutoSave:boolean = false,
        key?:string,
        label?:string,
        progressTracker?:ProgressTracker,
        isInitialize:boolean = true
    ) {
        super(AttributeRoot.create, data, DataType.COLLECTION, label, key, NodeType.ROOT, false);
        this.isStoreKey = isStoreKey;
        if (isInitialize) this.initialize(data, dataStore, isSaveDataTypes, isAutoSave, progressTracker);
    }


    initialize(
        data?:(Data | IAttributes | Object | string),
        dataStore?:IDataStore,
        isSaveDataTypes:boolean = false,
        isAutoSave:boolean = false,
        progressTracker?:ProgressTracker
    ):IPromise<any> {
        return super.p_initialize([data, dataStore, isSaveDataTypes, isAutoSave, progressTracker]);
    }


    protected p_onInitializeStarted(result:Result<any>, args:any[]):IPromise<any> {
        const data:(Data | Attributes | Object | string) = args[0];
        const dataStore:IDataStore = args[1];
        const isSaveDataTypes:boolean = args[2];
        const isAutoSave:boolean = args[3];
        const progressTracker:ProgressTracker = args[4];

        super.p_onInitializeStarted(new Result<any>(), [data, DataType.COLLECTION, NodeType.ROOT]).then(() => {
            this.p_value.changed.listen((args:AttributeEventArgs<Data>) => { this.p_onChanged(args); });

            if (is.notEmpty(dataStore)) this.store = dataStore;

            if (!this.isStoreKey) {
                if (this.p_keyAutoGenerationType === KeyAutoGenerationType.SESSION_UNIQUE) {
                    this.p_key = Identifier.getSessionUniqueInteger().toString();
                } else this.p_key = Identifier.generate();
            } else {
                this.p_key = Identifier.generate();
            }

            this._isSaveDataTypes = isSaveDataTypes;
            this._isAutoSave = isAutoSave;

            this._saveCommand = new ThrottlingCommand(
                ():IPromise<any> => {
                    let result:Result<boolean> = new Result<boolean>();
                    let data:any = this.toObject(this.isSaveDataTypes);
                    if (is.empty(data) || is.empty(this.key) || typeof this.key !== 'string') return result.resolve(false);

                    //----- Reset _isAutoSaveQueued
                    this._isAutoSaveQueued = false;

                    if (!this.hash.omitFromDataStore && this.p_isAutoHash) {
                        SHA1.hash(JSON.stringify(data)).then((hash:string) => {
                            data['hash'] = hash;

                            Debug.log('saving to database', 'Data._saveCommand', 'data_save');
                            Debug.log(data, 'Data._saveCommand', 'data_save');
                            this._dataStore.save(data, <string>this.key).then((response:any) => {
                                if (is.notEmpty(response) && typeof response === 'string') this._revision = response;
                                result.resolve(response);
                                this.saved.dispatch(response);
                            });
                        });
                    } else {
                        Debug.log('saving to database', 'Data._saveCommand', 'data_save');
                        Debug.log(data, 'Data._saveCommand', 'data_save');
                        this._dataStore.save(data, <string>this.key).then((response:any) => {
                            if (is.notEmpty(response) && typeof response === 'string') this._revision = response;
                            result.resolve(response);
                            this.saved.dispatch(response);
                        });
                    }

                    return result;
                },
                null, this, this._saveDebounceDelayInMilliseconds
            );

            this.p_value.update(data, AttributeChangeOptions.signal.durationZero, progressTracker).then(() => result.resolve(data));
        });

        return result;
    }


    /*====================================================================*
     START: Properties
     *====================================================================*/
    /**
     * If the key is stored, this will be a globally unique ID, used to identify the data
     * object associated with this entity in the data store.
     */
    set key(value:string) {
        this.p_key = value;
        if (this.isStoreKey) this.save();
    }
    isStoreKey:boolean = true;
    protected p_keyAutoGenerationType:KeyAutoGenerationType = KeyAutoGenerationType.SESSION_UNIQUE;


    /**
     * The revision ID, which may be managed by the database and can be used to determine
     * if the data associated with this entity is up-to-date.
     */
    get revision():string { return this._revision; }
    private _revision:string;


    /**
     * An SHA-1 hash representing the precise contents of the associated blob or text.
     * Can be used to check the uniqueness of a data object to avoid storing duplicates.
     */
    get hash():IAttribute {
        return this.p_value.get('hash', new AttributeCreateOptions('', DataType.STRING, null, null, null, AttributeChangeOptions.zero));
    }


    get isAutoHash():boolean { return this.p_isAutoHash; }
    set isAutoHash(value:boolean) { this.p_isAutoHash = value; }
    protected p_isAutoHash:boolean = false;


    //===== DataStore
    /**
     * The data store where the data associated with this entity will be stored.
     */
    get store():IDataStore { return this._dataStore; }
    set store(value:IDataStore) {
        this._dataStore = value;
        this.storeSet.dispatch(value);
    }
    private _dataStore:IDataStore;


    //===== Save
    /**
     * Determines whether a dataType property is saved with the data objects,
     * which can be used to verify the data's type when the object is deserialized.
     */
    get isSaveDataTypes():boolean { return this._isSaveDataTypes; }
    set isSaveDataTypes(value:boolean) { this._isSaveDataTypes = value; }
    private _isSaveDataTypes:boolean;


    /**
     * Toggles auto-save functionality.
     * Can be used to pause auto-saving while changes to the data are batched.
     */
    get isAutoSave():boolean { return this._isAutoSave; }
    set isAutoSave(value:boolean) {
        if (this._isAutoSave === value) return;
        this._isAutoSave = value;
        if (value && this._isAutoSaveQueued) this.save();
    }
    private _isAutoSave:boolean = false;


    /**
     * This will be set to true if autoSave() was called while isAutoSave was false.
     * This triggers a save of the data immediately after isAutoSave is set to true.
     */
    private _isAutoSaveQueued:boolean = false;


    /*====================================================================*
     START: Signals
     *====================================================================*/
    storeSet:Signal<IDataStore> = new Signal<IDataStore>(true);


    /**
     * Dispatched after a response is received back from the data store when data has been saved.
     * Completes with the response from the data store.
     */
    saved:Signal<any> = new Signal<any>(false);


    protected p_onChanged(args:AttributeEventArgs<Data>):void {
        if (is.empty(args)) return;
        if (args.changeOptions.isSave && is.notEmpty(this.key)) {
            this.autoSave();
            this.saveTriggered.dispatch(args);
        }
    }


    /*====================================================================*
     START: Signals
     *====================================================================*/
    saveTriggered:Signal<AttributeEventArgs<Data>> = new Signal<AttributeEventArgs<Data>>(false);


    /*====================================================================*
     START: Public Methods
     *====================================================================*/
    update(
        data:(Data | Attributes | Object | string),
        changeOptions:IAttributeChangeOptions = AttributeChangeOptions.default,
        progressTracker?:ProgressTracker
    ):IAttributeUpdateTracker {
        return this.p_value.update(data, changeOptions, progressTracker);
    }


    //===== Save
    private _saveDebounceDelayInMilliseconds:number = 125;
    private _saveCommand:ThrottlingCommand;


    save():IPromise<any> {
        if (is.empty(this._dataStore)) {
            //----- Save immediately after the data store has been set.
            this.storeSet.listenOnce(() => this.save());
            return Result.resolve<any>(null);
        }

        return this._saveCommand.debounce();
    }


    autoSave():void {
        if (!this.isAutoSave) this._isAutoSaveQueued = true;
        else if (is.notEmpty(this.key)) this.save();
    }


    delete():IPromise<boolean> {
        if (is.empty(this.key) || typeof this.key !== 'string') return Result.resolve<boolean>(false);

        let result = new Result<boolean>();
        this._dataStore.delete(<string>this.key).then((isSuccess:boolean) => {
            result.resolve(isSuccess);
        }, (e:Error) => { result.reject(e); });

        return result;
    }
} // End of class


export default Data;
