import {YAPI, YErrorMsg, YModule, YFunction} from "yoctolib-esm/yocto_api_html";
import {commonSettings, globalSettings} from "./commonSettings";


interface loadImageAsDataUriCallback { (context:any,image: string): void; }
interface actionClassConstructor{ (context:string): Promise<StreamDeckAction> }


export  enum ControllerEnum  {
    KEYPAD = "Keypad",
    ENCODER = "Encoder"
}

export class PluginDispatcher {
    private static websocket: WebSocket | null = null;
    private static pluginUUID: string | null = null;
    private static settingsCache: any = {};   // not sure
    private static actions        : { [key: string]: StreamDeckAction } = {};
    private static actionsClasses   : { [key: string]: actionClassConstructor } = {};


    private static readonly DestinationEnum: any = {
        "HARDWARE_AND_SOFTWARE": 0,
        "HARDWARE_ONLY": 1,
        "SOFTWARE_ONLY": 2
    };


    public static run(inPort: string, inPluginUUID: string, inRegisterEvent: any, inInfo: any)
    {
        PluginDispatcher.pluginUUID = inPluginUUID
        // Open the web socket
        PluginDispatcher.websocket = new WebSocket("ws://127.0.0.1:" + inPort);
        PluginDispatcher.websocket.onopen = () => {
            PluginDispatcher.registerPlugin(PluginDispatcher.pluginUUID as string, inRegisterEvent);

        }
        PluginDispatcher.websocket.onmessage = (evt: MessageEvent) => {PluginDispatcher.onmessage( evt)};
        PluginDispatcher.websocket.onclose = () => {
        };

    };

    private static registerPlugin(inPluginUUID: string, inRegisterEvent: string) {
        let json = {
            "event": inRegisterEvent,
            "uuid": inPluginUUID
        };

        PluginDispatcher.websocket?.send(JSON.stringify(json));
    };

    public static  registerActionClass(ActionID:string, ConstructorClass: any):number
    {
        PluginDispatcher.actionsClasses[ActionID.toLowerCase()] = (async (uuid: string) => {
              let obj = new ConstructorClass(uuid);
              await obj.init();
              return obj;
        });
        return 1;

    }

    // internal Yfunction has detected a change, forward to PluginDispatcher so it
    // send it to all action using the same Yfunction type
    public static valueHasChanged(functiontype:string,source:YFunction,value:string)
    {   let keys:  string[]= Object.keys(PluginDispatcher.actions);

        for ( let i:number=0;i<keys.length;i++)
           {  if (PluginDispatcher.actions[keys[i]].ManagedFunctionType== functiontype)
               (PluginDispatcher.actions[keys[i]].valueChanged(source,value))

           }

    }



    private static async  onmessage( evt: MessageEvent) {
        let jsonObj: any = JSON.parse(evt.data);
        let event: string = jsonObj['event'];
        let action: string = jsonObj['action'];
        let context: string = jsonObj['context'];
        let jsonPayload: any = jsonObj['payload'] || {};
        console.log(">>>> received message ("+event+")")
        if (action!==undefined) action=action.toLowerCase();
        switch (event)
        {
            case "deviceDidConnect":
                break;
            case "willAppear":
                if (!(action in PluginDispatcher.actionsClasses)) throw new Error("Action " + action + " is not registered");
                if (!(context in PluginDispatcher.actions)) PluginDispatcher.actions[context] = await PluginDispatcher.actionsClasses[action](context)
                break;
        }


        if (context!='')
         { let source :StreamDeckAction | null  = null;
            if (context in PluginDispatcher.actions) source =  PluginDispatcher.actions[context];
            if (source!=null)
                switch (event)
                {
                    case  "keyDown":
                        await source.onKeyDown(context, jsonPayload['settings'], jsonPayload['coordinates'], jsonPayload['userDesiredState']);
                        break;
                    case "keyUp":
                        await source.onKeyUp(context, jsonPayload['settings'], jsonPayload['coordinates'], jsonPayload['userDesiredState']);
                        break;
                    case "touchTap" :
                         await source.touchTap(context, jsonPayload['settings'], jsonPayload['coordinates'], jsonPayload['tapPos'], jsonPayload['hold']);
                        break;
                    case "dialPress" :
                        if (jsonPayload['pressed'])
                            await source.onKeyDown(context, jsonPayload['settings'], jsonPayload['coordinates'], 0);
                        else
                            await source.onKeyUp(context, jsonPayload['settings'], jsonPayload['coordinates'], 0);

                         // await source.dialPress (context, jsonPayload['settings'], jsonPayload['coordinates'], jsonPayload['pressed']);
                        break;
                    case "dialRotate" :
                        await source.dialRotate (context, jsonPayload['settings'], jsonPayload['coordinates'],  jsonPayload['ticks'], jsonPayload['pressed']);
                        break;
                    case  "willAppear":
                        let controller : ControllerEnum  =ControllerEnum.KEYPAD;
                        if ("controller" in jsonPayload)
                        {
                            controller = jsonPayload['controller'] == "Encoder" ? ControllerEnum.ENCODER :ControllerEnum.KEYPAD;
                        }
                        await source.onWillAppear(context, controller, jsonPayload['settings'], jsonPayload['coordinates']);
                        break;
                    case  "sendToPlugin":
                        source.sendToPlugin(context, jsonPayload);
                        break;
                    case "didReceiveSettings":
                        await source.didReceiveSettings(context, jsonPayload['settings'], jsonPayload['coordinates']);
                        break;
                    case "didReceiveGlobalSettings":
                        await source.didReceiveGlobalSettings(context, jsonPayload['settings']);
                        break;
                    case "propertyInspectorDidAppear":
                        await source.propertyInspectorDidAppear(context);
                        break;
                    case "propertyInspectorDidDisappear":
                        await source.propertyInspectorDidDisappear(context);
                        break;
                }
              else
               {  if (event=="didReceiveGlobalSettings")
                   {  console.log("--> Plugin did received global settings") ;
                       let  globalsetting : globalSettings  = new globalSettings();
                       globalsetting.set(jsonPayload['settings']);

                       YoctopuceDeviceHandler.newHubList(globalsetting.hubAddrList)
                   }

               }
        }

    }


    protected static SetTitle(context: any, title: string) {
        let json = {
            "event": "setTitle",
            "context": context,
            "payload": {
                "title": title,
                "target": PluginDispatcher.DestinationEnum.HARDWARE_AND_SOFTWARE
            }
        };
        if (PluginDispatcher.websocket == null) throw new Error("SetTitle : socket no opened")
        PluginDispatcher.websocket.send(JSON.stringify(json));
    }

    protected  static SetSettings(context: any, settings: any) {
        let json = {
            "event": "setSettings",
            "context": context,
            "payload": settings
        };
        if (PluginDispatcher.websocket == null) throw new Error("SetSettings : socket no opened")
        PluginDispatcher.websocket.send(JSON.stringify(json));
    }

    protected static AddToSettings(context: any, newSettings: any) {
        PluginDispatcher.settingsCache[context] = newSettings;
    }

    public static getGlobalSettings(context:any)
    { var json = {
        "event": "getGlobalSettings",
        "context": PluginDispatcher.pluginUUID
    };
        if (PluginDispatcher.websocket == null) throw new Error("SetSettings : socket no opened")
        PluginDispatcher.websocket?.send(JSON.stringify(json));
    }

    private static setImage(context: any, imgUrl: string) {
        let json: any =
          {
              "event": "setImage",
              "context": context,
              "payload": {image: imgUrl || "", target: PluginDispatcher.DestinationEnum.HARDWARE_AND_SOFTWARE}
          }
        if (PluginDispatcher.websocket == null) throw new Error("SetSettings : socket no opened")
        PluginDispatcher.websocket.send(JSON.stringify(json));
    }

    public static HTMLImageElement2url( bg: HTMLImageElement , overlay: HTMLImageElement | null)
    {let canvas: HTMLCanvasElement = document.createElement("canvas");
        canvas.width = bg.naturalWidth;
        canvas.height = bg.naturalHeight;
        let ctx: CanvasRenderingContext2D = canvas.getContext("2d") as CanvasRenderingContext2D;
        ctx.drawImage(bg, 0, 0);
        if (overlay!=null)  ctx.drawImage(overlay, 0, 0);
        return  canvas.toDataURL("image/png");
    }

    public  static setBgImage(context :any, bg: HTMLImageElement , overlay: HTMLImageElement | null )
    {
        PluginDispatcher.setImage(context,PluginDispatcher.HTMLImageElement2url(bg,overlay))
    }

    private static loadImageAsDataUri(context: any, url: string, callback: loadImageAsDataUriCallback) {
        var image = new Image();
        image.onload = (e: Event) => {
            let canvas: HTMLCanvasElement = document.createElement("canvas");
            canvas.width = image.naturalWidth;
            canvas.height = image.naturalHeight;
            let ctx: CanvasRenderingContext2D = canvas.getContext("2d") as CanvasRenderingContext2D;
            ctx.drawImage(image, 0, 0);
            callback(context, canvas.toDataURL("image/png"));
        };
        image.src = url;
    };

    public static sendDataToPropertiesInpector(context:string,managedFunctionType:string,name:string, data:any)
    {
        var json = {
            "action": "com.yoctopuce."+managedFunctionType+".action",
            "event": "sendToPropertyInspector",
            "context": context,
            "payload": {"type": name ,"data": data}
        };

        PluginDispatcher.websocket?.send(JSON.stringify(json));
    }

    public static setFeedback(context:string, data:any)
    {
      var json = {
       "event": "setFeedback",
       "context": context,
       "payload": data

     }
    PluginDispatcher.websocket?.send(JSON.stringify(json));


  }
}

export class StreamDeckAction {
    protected UUID :string;
    public  get ManagedFunctionType() { return    "*invalid*" }

    public propertiesInspectoIsOpen : boolean = false;
    protected   controller : ControllerEnum = ControllerEnum.KEYPAD;

    static newImage(imageUrl:string):Promise<HTMLImageElement>
    {
        return new Promise<HTMLImageElement>((resolve, reject) => {
            let img: HTMLImageElement = new Image();
            img.onload = () => { resolve(img); }
            img.onerror = (msg) => { reject(msg); };
            img.src = imageUrl;
        })
    }

    public  static registerFunctionType()
    {
       throw new Error("plugin's registerFunctionType() method has not been overriden")
    }


    protected get settings(): commonSettings{ throw new Error("setting get has notbeen overidded"); }

    constructor(uuid:string)
    { this.UUID=uuid;

    }

    // call  direction :  up
    protected broadcastValueChange(source:YFunction, value:string)
     { PluginDispatcher.valueHasChanged(this.ManagedFunctionType,source,value);
     }

    // call  direction :  down
    public valueChanged(source:YFunction,value:string) {}
    protected  async functionArrival(hwdname:string)  {}
    protected  async functionRemoval(hwdname:string){}
    public async onKeyDown(context: any, settings: any, coordinates: any, userDesiredState: any) { }
    public async onKeyUp(context: any, settings: any, coordinates: any, userDesiredState: any) {  }
    public async touchTap(context: any, settings: any, coordinates: any, tapPos :any, hold:boolean){}
    //public async dialPress (context: any, settings: any, coordinates: any, pressed:boolean){}
    public async dialRotate (context: any, settings: any, coordinates: any,  ticks:number, pressed:boolean){}

    public async onWillAppear(context: any, controller: ControllerEnum, settings: any, coordinates: any)
    {   console.log("*** onWillAppear ***");
        this.controller = controller;
        console.log("plugin asking for global settings...")
        PluginDispatcher.getGlobalSettings(context);
    }

    public async didReceiveSettings(context: any, settings: any, coordinates: any)
     { this.settings.set(settings);
     }

    public async didReceiveGlobalSettings(context: any,  settings: any)
    {  console.log("plugin Received GlobalSettings")
    }

    public deviceListHasChanged(list: Array<any> )
      {

        if (this.settings.hwdName!="")
          for (let i:number=0;i<list.length;i++)
            if (list[i]["hwdName"] == this.settings.hwdName)
               if (list[i]["onlinechanged"])
               { if (list[i]["online"]) this.functionArrival(this.settings.hwdName)
                       else this.functionRemoval(this.settings.hwdName)
               }
          if  (this.propertiesInspectoIsOpen)
          {   console.log("Sending devlist  to PI (1) length="+list.length);
              this.sendToPlugin("devList", list );
          }

      }

    public   setBgImage(context :any, bg: HTMLImageElement    )
    { PluginDispatcher.setBgImage(context , bg, null )
    }

    public  async propertyInspectorDidAppear(context: any)
    {   this.propertiesInspectoIsOpen = true;
        let list  : any[] =   YoctopuceDeviceHandler.getFunctionsList(this.ManagedFunctionType);
        console.log("!!!!propertyInspectorDidAppear, sending devlist for "+this.ManagedFunctionType)
        console.log("Sending devlist  to PI (2), length="+list.length);
        this.sendToPlugin("devList",list);
    }

    public  async propertyInspectorDidDisappear(context: any)
    {   this.propertiesInspectoIsOpen = false;
    }

    public sendToPlugin( payloadName:string, payload:any)
    {
        PluginDispatcher.sendDataToPropertiesInpector(this.UUID,this.ManagedFunctionType,payloadName, payload)
    }



}

let deviceHandler : YoctopuceDeviceHandler | null =null;




export  class  YoctopuceDeviceHandler
{

    private static functions : { [key: string]: any[] } = {};
    private static plugins   : { [key: string]: StreamDeckAction[] } = {};

    private static registeredHubs : { [key: string]: boolean } = {};
    private static refreshTimeout :  number=0;
    private static refreshCount:number =0;

    public static registerFunctionType(functiontype : string)
    {   console.log("Adding support for function "+functiontype)
        if (!(functiontype in YoctopuceDeviceHandler.plugins))  YoctopuceDeviceHandler.plugins[functiontype ] =[];
        if (!(functiontype in YoctopuceDeviceHandler.functions))  YoctopuceDeviceHandler.functions[functiontype ] =[];
    }

    public static registerAction(functiontype :string , action : StreamDeckAction)
    {   console.log("New action  just arrived");
        YoctopuceDeviceHandler.plugins[functiontype ].push(action)
    }

    public static async run()
    {  YAPI.DisableExceptions();
        let errmsg : YErrorMsg= new YErrorMsg();
        if (await YAPI.InitAPI(YAPI.DETECT_NET, errmsg)!=YAPI.SUCCESS)
        { console.log("Failed to init Yoctopuce API:"+errmsg.msg)
            return;
        }
        await YAPI.RegisterDeviceArrivalCallback(  (m:YModule)=>{YoctopuceDeviceHandler.Arrival(m)});
        await YAPI.RegisterDeviceRemovalCallback(  (m:YModule)=>{YoctopuceDeviceHandler.Removal(m)});
        await YoctopuceDeviceHandler.refresh();
    }

    private static async refresh()

    {   try
        { YoctopuceDeviceHandler.refreshCount++;
        YAPI.HandleEvents()
        if  (YoctopuceDeviceHandler.refreshCount>=3)
        {
            YoctopuceDeviceHandler.refreshCount = 0;
            let errmsg: YErrorMsg = new YErrorMsg();
            YAPI.UpdateDeviceList(errmsg);
            console.log(".");
        }
        }
        catch (e) {console.log("refresh caused an exception "+ e);}

        YoctopuceDeviceHandler.refreshTimeout =  setTimeout(()=>{YoctopuceDeviceHandler.refresh()} ,100)  as unknown as number ; // WTF ?
    }

    private static async Arrival(m:YModule)
    {
        console.log("***arrival " + await m.get_serialNumber())
        let count: number = await m.functionCount();
        let moduleSerial = await m.get_serialNumber();
        let ModuleFriendlyName = await m.get_friendlyName();
        ModuleFriendlyName = ModuleFriendlyName.substr(0, ModuleFriendlyName.indexOf("."));
        let modulename = ModuleFriendlyName != "" ? ModuleFriendlyName : moduleSerial;

        let listChanged: { [key: string]: boolean } ={};

        for (let i: number = 0; i < count; i++)
        {  let functiontype :string = await  m.functionType(i);
           functiontype = functiontype.charAt(0).toLowerCase()+functiontype.substring(1);
           if  (functiontype in YoctopuceDeviceHandler.plugins)
            {
                listChanged[functiontype] = true;
                let fname: String = await m.functionName(i);
                let fid = await m.functionId(i);
                let hwdName = moduleSerial + "." + fid;
                let friendlyName = modulename + "." + (fname != '' ? fname : fid);
                let found: boolean = false;
                for (let j: number = 0; (j < YoctopuceDeviceHandler.functions[functiontype].length) && !found; j++)
                    if (YoctopuceDeviceHandler.functions[functiontype][j]["hwdName"] == hwdName) {
                        YoctopuceDeviceHandler.functions[functiontype][j]["friendlyname"] = friendlyName;
                        YoctopuceDeviceHandler.functions[functiontype][j]["onlinechanged"] = !YoctopuceDeviceHandler.functions[functiontype][j]["online"];
                        YoctopuceDeviceHandler.functions[functiontype][j]["online"] = true;
                        found = true;
                    }
                if (!found) YoctopuceDeviceHandler.functions[functiontype].push({"hwdName": hwdName, "friendlyname": friendlyName,"onlinechanged":true, "online": true});
            }
        }

        for (const functiontype in listChanged)
            for (let i=0;i<YoctopuceDeviceHandler.plugins[functiontype].length;i++ )
            {   console.log("sending arrival info to plugin "+functiontype+"["+i+"]")
                YoctopuceDeviceHandler.plugins[functiontype][i].deviceListHasChanged(YoctopuceDeviceHandler.functions[functiontype]);
            }
    }

    private static async Removal(m:YModule)
    {   console.log("**removal "+await m.get_serialNumber())
        let serial:string = await m.get_serialNumber();
        let listChanged: { [key: string]: boolean } ={};

        for (const functiontype in  YoctopuceDeviceHandler.functions)
           for (let i: number = 0; (i < YoctopuceDeviceHandler.functions[functiontype].length); i++)
              if (YoctopuceDeviceHandler.functions[functiontype][i]["hwdName"].startsWith(serial))
              {   YoctopuceDeviceHandler.functions[functiontype][i]["onlinechanged"] = YoctopuceDeviceHandler.functions[functiontype][i]["online"];
                  YoctopuceDeviceHandler.functions[functiontype][i]["online"] = false;
                  listChanged[functiontype] = true;
              }

        for (const functiontype in listChanged)
          for (let i=0;i<YoctopuceDeviceHandler.plugins[functiontype].length;i++ )
            YoctopuceDeviceHandler.plugins[functiontype][i].deviceListHasChanged(YoctopuceDeviceHandler.functions[functiontype] );

    }

    public static getFunctionsList(functiontype:string ) :any[]
    { return YoctopuceDeviceHandler.functions[functiontype];

    }

    public static Stop()
    {  clearTimeout(YoctopuceDeviceHandler.refreshTimeout);
        YoctopuceDeviceHandler.refreshTimeout=0;
        YAPI.FreeAPI();
        deviceHandler =null;
    }



    public static async newHubList(hubAddrList: string )
     {  console.log("+++ newHubList "+hubAddrList )
        let list : string[] = hubAddrList.split(',');
        for (let i :number  =0;i<list.length;i++) list[i]=list[i].trim()

        for (let key in YoctopuceDeviceHandler.registeredHubs)
         { YoctopuceDeviceHandler.registeredHubs[key]=false;
         }
        for (let i :number  =0;i<list.length;i++)
         if (list[i]!="")
        {  if (list[i] in YoctopuceDeviceHandler.registeredHubs)
           { YoctopuceDeviceHandler.registeredHubs[list[i]] =true;

           }
           else
            {   let address: string = list[i];
                let errmsg: YErrorMsg = new YErrorMsg();
                console.log("Preregistering hub "+ address)
                if (await YAPI.PreregisterHub(address, errmsg) != YAPI.SUCCESS)
                    console.log("PreregisterHub(" + address + ") failed (" + errmsg.msg + ")");
                else
                    YoctopuceDeviceHandler.registeredHubs[address] = true;
            }
         }

         for (let key in YoctopuceDeviceHandler.registeredHubs)

          {  if (!YoctopuceDeviceHandler.registeredHubs[key])
             {   console.log("unregistering hub "+ key)
                 await YAPI.UnregisterHub(key);
                 delete YoctopuceDeviceHandler.registeredHubs[key];
             }
         }
    /*
         let liststr :string="";
         for (let key in YoctopuceDeviceHandler.registeredHubs)
         {   liststr += (liststr!=""?" , ":"") + key;
         }
         console.log("Current hub list = "+liststr)
    */
     }

    public static async AddAddress(address:string)
    {  /*
        if (address=="") return;
        console.log("adding adress "+address)
        let errmsg: YErrorMsg = new YErrorMsg();
        if (await YAPI.PreregisterHub(address, errmsg)!=YAPI.SUCCESS)
            console.log("PreregisterHub("+address+") failed ("+errmsg.msg+")");
      */
    }
}

class LocalDataStorage
{ public UUID :string;
  constructor(context:any)
   { this.UUID = context.toString()}

}
