import {Manager, ServiceManager} from './ServiceManager.js'
import {systemObject,ServiceStatusCodes,basicResponse,systemVariable,customAction, Actions, VarResponse, VarStatusCodes, ErrorCodes, systemError} from './DataModels/Types.js'



/**Abstract class defining a Comunication Engine for data I/O with a server.
 * 
 * @prop toBeSubscribed {Map<string,number>} -  Variables waiting to be subscribed for updates. It is a key-number map. 
 * The number represent how many UI element times requested updates from that variable.
 * Variables are purged once subscribed. If subscription fails with "NO-NET" 
 * or "CANT-SUB" error the var is kept for later subscription, if fails with "WONT-SUB" or "NOT-EXIST" it will be purged from list.
 * 
 * @prop toBeUnsubscribed {Set<string>} - List of Variables waiting to be unsubscribed from updates. 
 * 
 * @prop subscribedVar  {Map<string,number>} - List of Variables currently subscribed for updates. It is a key-number map.
 * The number represent the number of UI-elements registered with the same variable, 
 * usually one, but for special cases could be more.
 * 
 * @prop aggregationTime_ms {number} - Time the system will wait before sending subscruiption/unsubscription, so that variable
 * can be aggregated and make moreefficient network calls.
*/
export abstract class DataCommsEngine {
    
    manager:ServiceManager = Manager;
    status:string = ServiceStatusCodes.Down
    name:string
    VarDispatchErrorCases:string[]
    VarErrorUnsubCases:string[]
    VarErrorNoActCases:string[]

    toBeSubscribed     = new Map<string,number>()
    toBeUnsubscribed   = new Set<string>()
    subscribedVar      = new Map<string,number>()
    sub_timerID:number = null
    unsub_timerID:number = null
    aggregationTime_ms:number = 10


    constructor( EngineName:string ){
        this.name = EngineName || "DataEngine";
        
        this.VarDispatchErrorCases = [
            ErrorCodes.VarNotExist, ErrorCodes.WontSubcribe, ErrorCodes.Unauthorized, 
            ErrorCodes.UnknownError, ErrorCodes.CantUnSubcribe];

        this.VarErrorNoActCases = [ErrorCodes.BadValue, ErrorCodes.CantUnSubcribe, 
            ErrorCodes.Unauthorized];
        
        this.VarErrorUnsubCases = [ErrorCodes.CantSubcribe, ErrorCodes.NoNetwork]
    }

    serializeSysObject(target:systemObject):string
    {
        if(typeof target.name !== "string" || target.name.includes(":") ||
           typeof target.system !== "string" || target.system.includes(":"))  return null;

        return ( target.system + ":" + target.name );
    }
    deserializeSysObject(target:string):systemObject
    {
        let tmp = target.split(":");
        if(tmp.length !== 2 ) return null;
        return { system : tmp[0], name:tmp[1]};
    }

    RequestSubscription(target:systemObject){
        let ser_obj = this.serializeSysObject(target);
        if(ser_obj === null) throw Error("CANNOT SUBSCRIBE variable " + target.name);
        
        if(this.subscribedVar.has(ser_obj))
        {
            // case already subscribed, just bump the number of subscribed var
            let idx = this.subscribedVar.get(ser_obj)
            this.subscribedVar.set(ser_obj, idx + 1);
            return;
        }
        let count  = this.toBeSubscribed.get(ser_obj) || 0;
        this.toBeSubscribed.set(ser_obj, count + 1);
        
        // this case just fill the subscribelist,willbe submitted after init
        if(this.status === ServiceStatusCodes.Down ||
            this.status === ServiceStatusCodes.Warming ) return;
        
        if(this.sub_timerID) clearTimeout(this.sub_timerID);

        this.sub_timerID = window.setTimeout( this._subcribe.bind(this), this.aggregationTime_ms );
    }

    RequestUnsubscription(target:systemObject){
        let ser_obj = this.serializeSysObject(target);
        if(ser_obj === null || !this.subscribedVar.has(ser_obj)) throw Error("CANNOT UNSUBSCRIBE variable " + target.name);

        let count = this.subscribedVar.get(ser_obj);
        if(count > 1 ) {
            // the variable needs to remain subscribed untill there 
            // are related UI element connected
            this.subscribedVar.set(ser_obj, count - 1) ; 
            return ;
        }
        this.toBeUnsubscribed.add(ser_obj);

        if(this.unsub_timerID) clearTimeout(this.unsub_timerID);
        this.unsub_timerID = window.setTimeout( this._unsubcribe.bind(this), this.aggregationTime_ms );
    }

    async _subcribe(){
        let targets = Array.from(this.toBeSubscribed.keys()).map( t => this.deserializeSysObject(t));
        let response = await this.Subscribe( targets );
        this.updateSubscriberLists(response);
        this.UpdateVars(response, VarStatusCodes.Subscribed, Actions.Subscribe);
    }

    updateSubscriberLists(response:VarResponse[]){
        for (let rsp of response) 
        {
            let var_id = this.serializeSysObject(rsp);
            if(rsp.success) 
            {
                let count = this.toBeSubscribed.get(var_id);
                count +=  ( this.subscribedVar.get(var_id) || 0 );
                this.subscribedVar.set(var_id, count );
                this.toBeSubscribed.delete(var_id);
            }
            else 
            {
                let code = rsp.error ? rsp.error.code : ErrorCodes.UnknownError;
                // keep in list for next try later in case of these errors
                if(code !== ErrorCodes.NoNetwork && code !== ErrorCodes.CantSubcribe)
                    this.toBeSubscribed.delete(var_id);
            }
        }
    }

    isVarSubscribed(varID:systemObject):boolean
    {
        let id = this.serializeSysObject(varID);
        return this.subscribedVar.has(id);
    }

    UpdateVars(response:VarResponse[], ok_status:VarStatusCodes, action:string = "")
    {
        let var_upd:systemVariable[] = [];

        for( let rsp of response ){
            let var_idx = new systemVariable(rsp);
            if(rsp.success) 
            {
                var_idx.status = ok_status ;
                if(rsp.value !== null && rsp.value !== undefined) var_idx.value  = rsp.value;
            }
            else 
            {
                let code = rsp.error ? rsp.error.code : ErrorCodes.UnknownError;
                
                if( this.VarDispatchErrorCases.includes(code))
                    this.manager.CreateAndDispatchError(rsp.system,code, rsp.name,action);

                if(this.VarErrorUnsubCases.includes(code)) 
                    var_idx.status = VarStatusCodes.Unsubscribed;
                
                else if(this.VarErrorNoActCases.includes(code)) // no modify status, unless is "pending"
                {   
                    let _var = this.manager.dataTree.GetVar(rsp);
                    var_idx.status = _var.status === VarStatusCodes.Pending ? VarStatusCodes.Subscribed : null ;
                }
                else var_idx.status = VarStatusCodes.Error;
            }
            var_upd.push(var_idx);
        }
        this.manager.Update(var_upd);
    }

    async _unsubcribe()
    {
        let targets = Array.from(this.toBeUnsubscribed).map( t => this.deserializeSysObject(t));
        let response = await this.Unsubscribe( targets );
        for (let rsp of response)
        {
            let var_id = this.serializeSysObject(rsp);
            if(rsp.success) this.subscribedVar.delete(var_id);
            this.toBeUnsubscribed.delete(var_id);            
        }
        this.UpdateVars(response, VarStatusCodes.Unsubscribed, Actions.Unsubscribe);
    }


    async _init()
    {
        this.status = ServiceStatusCodes.Warming;
        let resp = await this.Initialize() ;
        
        if(resp.success) this.status = ServiceStatusCodes.Ready ;
        else 
        {
            this.status = ServiceStatusCodes.Error ;
            let code = resp.error ? resp.error.code : ErrorCodes.UnknownError;
            let err = new systemError(this.name, code, this.name, Actions.Init);
            this.manager.DispatchError(err);
        }
        if(this.toBeSubscribed.size > 0) this._subcribe();
    }

    /**
     * Abstract method. Action Initialize. Place here anything that is needed for initialization of this engine.
     * @abstract
     * @return {basicResponse} - return status of initialization action.
     */
    Initialize() : Promise<basicResponse> {return null;}

    /**
     * Abstract method. Action Subscribe. It subscribes the list of variables names for automatic updates.
     * @abstract
     * @param {systemObject[]} variables - variables names to be subscribed
     * @return {Promise<VarResponse[]>}  - Response of the action.
     */
    Subscribe(variables:systemObject[]) : Promise<VarResponse[]> {return null;}

    /**
     * Abstract method. Action Unsubscribe. It unubscribes the list of variables names from automatic updates.
     * @abstract
     * @param {systemObject[]} variables - variables names to be unsubscribed
     * @return {Promise<VarResponse[]>} - Response of the action.
     */
    Unsubscribe(variables:systemObject[]) : Promise<VarResponse[]>  {return null;}

    /**
     * Abstract method. Action Write, this can be called by a UI element. 
     * It writes to server the provided list of values to the relative variables.
     * @abstract
     * @param {systemObject[]} targets - variables names to be unsubscribed
     * @param values {any[]} - values related to variables to be written
     * @return {Promise<VarResponse[]>}
     */
    Write( targets:systemObject[], values:any[] ) : Promise<VarResponse[]> { return null;}
    
    /**
     * Abstract method. Action Read, this can be called by a UI element. 
     * Forces a list of variables to be read from server even if not scheduled.
     * @abstract
     * @param names list of variable to be read
     * @return  {Promise<VarResponse[]>}
     */
    Read( targets:systemObject[] ) : Promise<VarResponse[]> { return null; }

    /**
     * Action Update. It updates a list of variable values and statuses in the DataManager.
     * The updates will be automatically dispatched to all UI component connected to those variables.
     * @param data A list of variable updates, properties (like status or value) that are null will not be updated.
     */
    UpdateData( data: systemVariable[]) : void {
        this.manager.Update(data);
    }

    /**
     * Container for Engine dependent Actions. They can be called by UI elements via the function "runAction" providing the key.
     */
    customActions: {
        [key:string] : customAction
    }

}

