/**
 * The plugin system is based
 * on registered services (iframes) from
 * foreign services.
 * The server component spawns new iframes and
 * listens to them.
 *
 * @author Nils Diewald
 */
"use strict";

define(['plugin/widget', 'plugin/service', 'state', 'state/manager', 'pageInfo', 'util'], function (widgetClass, serviceClass, stateClass, stateManagerClass, pageInfoClass) {

  KorAP.Panel = KorAP.Panel || {};

  const d = document;
 
  // Contains all servicess to address with
  // messages to them
  var services = {};
  var plugins  = {};

  // TODO:
  //   These should better be panels and every panel
  //   has a buttonGroup

  // List of panels with dynamic buttons, i.e.
  // panels that may occur multiple times.
  var buttons = {
    match : []
  };

  // List of panels with static buttons, i.e.
  // panels that occur only once.
  var buttonsSingle = {
    query : [],
    result : []
  } 
  
  // This is a counter to limit acceptable incoming messages
  // to a certain amount. For every message, this counter will
  // be decreased (down to 0), for every second this will be
  // increased (up to 100).
  // Once a widget surpasses the limit, it will be killed
  // and called suspicious.
  var maxMessages = 100;
  var limits = {};

  // TODO:
  //   It may be useful to establish a watcher that pings
  //   all widgets every second to see if it is still alive,
  //   otherwise kill
  
  // Load Plugin server
  return {

    /**
     * Create new plugin management system
     */
    create : function () {
      return Object.create(this)._init();
    },

    /*
     * Initialize the plugin manager
     */
    _init : function () {
      this._ql     = d.getElementById("ql-field");
      this._q      = d.getElementById("q-field")
      this._cutoff = d.getElementById("q-cutoff-field");

       // State manager undefined
      this._states =  KorAP.States ? KorAP.States :
        // Not serialized state manager
        stateManagerClass.create(document.createElement('input'));

      return this;
    },

    /**
     * Register a plugin described as a JSON object.
     *
     * This is work in progress.
     *
     * Example:
     *
     *   KorAP.Plugin.register({
     *     'name' : 'CatContent',
     *     'desc' : 'Some content about cats',
     *     'about' : 'https://localhost:5678/',
     *     'embed' : [{
     *       'panel' : 'match',
     *       'title' : loc.TRANSLATE,
     *       'classes' : ['translate']
     *       'onClick' : {
     *         'action' : 'addWidget',
     *         'template' : 'https://localhost:5678/?match={matchid}',
     *       }
     *     },{
     *       'title' : 'glemm',
     *       'panel' : 'query',
     *       'onClick' : {
     *         'action' : 'toggle',
     *         'state' : 'glemm',
     *         'service' : {
     *           'id' : 'glemm',
     *           'template' : 'https://localhost:5678/'
     *         }
     *       }
     *     }]
     *   });
     *
     */
    register : function (obj) {
      // TODO:
      //   These fields need to be localized for display by a structure like
      //   { de : { name : '..' }, en : { .. } }
      const name = obj["name"];

      if (!name)
        throw new Error("Missing name of plugin");

      var desc = obj["desc"];
      
      // Register plugin by name
      var plugin = plugins[name] = {
        name : name,
        desc : desc,
        about : obj["about"],
        widgets : [],
        services : []
      };

      if (typeof obj["embed"] !== 'object')
        throw new Error("Embedding of plugin is no list");
 
      // Embed all embeddings of the plugin
      var that = this;
      obj["embed"].forEach(function(embed) {

        if (typeof embed !== 'object')
          throw new Error("Embedding of plugin is no object");

        // Needs to be localized as well
        let title = embed["title"];        
        let panel = embed["panel"];
        let onClick = embed["onClick"];
        let icon = embed["icon"];
        
        if (!panel || !(buttons[panel] || buttonsSingle[panel]))
          throw new Error("Panel for plugin is invalid"); 

        // The embedding will open a widget
        if (!onClick["action"] ||
            onClick["action"] == "addWidget" ||
            onClick["action"] == "setWidget") {
          
          let cb = function (e) {

            // "this" is bind to the panel
            // "this".button is the button
            // "that" is the server object

            // The button has a state and the state is associated to the
            // a intermediate object to toggle the view
            if ('state' in this.button && this.button.state.associates() > 0) {

              const s = this.button.state;

              // The associated service is existent
              if (services[this.button['widgetID']]) {
                s.roll();
                return;
              }

              // The service is not existent
              else {

                // Remove broken state associations
                s.clear();
                s.set(true);
              }
            };

            // Add the widget to the panel
            let id = that.addWidget(this, {
              "name": name,
              "src": onClick["template"], // that._interpolateURI(onClick["template"], this.match);
              "permissions": onClick["permissions"],
              "desc":desc
            });
            plugin["widgets"].push(id);
            
            // If a state exists, associate with a mediator object
            if ('state' in this.button) {
              this.button['widgetID'] = id;
              this.button.state.associate({
                setState : function (value) {
                  // Minimize the widget
                  if (value == false) {
                    services[id].minimize();
                  }
                  else {
                    services[id].show();                    
                  };
                }
              });
            }
          };


          // Button object
          let obj = {'cls':embed["classes"], 'icon': icon };

          if (embed['desc'] != undefined)
            obj['desc'] = embed['desc'];

          if (onClick["action"] && onClick["action"] == "setWidget") {

            // Create a boolean state value,
            // that initializes to true == opened
            obj['state'] = stateClass.create([true, false]);
            obj['state'].setIfNotYet(true);
          };
          
          // Add to dynamic button list (e.g. for matches)
          if (buttons[panel]) {
            buttons[panel].push([title, obj, cb]);
          }

          // Add to static button list (e.g. for query) already loaded
          else if (KorAP.Panel[panel]) {
            KorAP.Panel[panel].actions().add(title, obj, cb);
          }

          // Add to static button list (e.g. for query) not yet loaded
          else {
            buttonsSingle[panel].push([title, obj, cb]);
          }
        }

        else if (onClick["action"] == "toggle") {

          // TODO:
          //   Accept a "value" list here for toggling, which should
          //   also allow for "rolling" through states via CSS classes
          //   as 'toggle-true', 'toggle-false' etc.
          let state = that._states.newState(
            (onClick["state"] ? onClick["state"] : name),
            [true, false],
            onClick["default"]
          );

          let obj = {'cls':["title"], 'icon': icon};

          if (embed['desc'] != undefined)
            obj['desc'] = embed['desc'];          
          
          // TODO:
          //   Lazy registration (see above!)
          KorAP.Panel[panel].actions().addToggle(title, obj, state);

          // Add the service
          let id = this.addService({
            "name" : name,
            // TODO:
            //   Use the "service" keyword
            "src" : onClick["template"],
            "permissions" : onClick["permissions"]
          });

          // TODO:
          //   This is a bit stupid to get the service window
          let service = services[id];
          let iframe = service.load();

          // Create object to communicate the toggle state
          // once the iframe is loaded.
          iframe.onload = function () {
            let sendToggle = {
              setState : function (val) {
                service.sendMsg({
                  action: 'state',
                  key : onClick['state'],
                  value : val
                });
              }
            };

            // Associate object with the state
            state.associate(sendToggle);          
          };

          plugin["services"].push(id);
        };
      }, this);
    },

    // TODO:
    //   Interpolate URIs similar to https://tools.ietf.org/html/rfc6570
    //   but as simple as possible
    _interpolateURI : function (uri, obj) {
      // ...
    },


    /**
     * Get named button group - better rename to "action"
     */
    buttonGroup : function (name) {
      if (buttons[name] != undefined) {
        return buttons[name];
      } else if (buttonsSingle[name] != undefined) {
        return buttonsSingle[name];
      };
      return [];
    },

    /**
     * Clear named button group - better rename to "action"
     */
    clearButtonGroup : function (name) {
      if (buttons[name] != undefined) {
        buttons[name] = [];
      } else if (buttonsSingle[name] != undefined) {
        buttonsSingle[name] = [];
      }
    },

    // Optionally initialize the service mechanism and get an ID
    _getServiceID : function () {

      // Is it the first service?
      if (!this._listener) {

        /*
         * Establish the global 'message' hook.
         */
        this._listener = this._receiveMsg.bind(this);
        window.addEventListener("message", this._listener);
        
        // Every second increase the limits of all registered services
        this._timer = window.setInterval(function () {
          for (let i = 0; i < limits.length; i++) {
            if (limits[i]++ >= maxMessages) {
              limits[i] = maxMessages;
            }
          }
        }, 1000);
      };

      // Create a unique random ID per service
      return 'id-' + this._randomID();
    },
    
    /**
     * Add a service in a certain panel and return the id.
     */
    addService : function (data) {
      if (!data["src"])
        return;

      let id = this._getServiceID();

      data["id"] = id;

      // Create a new service
      let service = serviceClass.create(data);
      
      services[id] = service;
      limits[id] = maxMessages;

      // Add service to panel
      this.element().appendChild(
        service.load()
      );
      
      return id;
    },

    
    /**
     * Open a new widget view in a certain panel and return
     * the id.
     */
    addWidget : function (panel, data) {
      // panel, name, src, permissions

      let id = this._getServiceID();

      data["id"] = id;
      
      // Create a new widget
      var widget = widgetClass.create(data);

      // Store the widget based on the identifier
      services[id] = widget;
      limits[id] = maxMessages;

      widget._mgr = this;

      // Add widget to panel
      panel.add(widget);

      return id;
    },


    /**
     * Get service by identifier
     */
    service : function (id) {
      return services[id];
    },

    
    // Receive a call from an embedded service.
    // The handling needs to be very careful,
    // as this can easily become a security nightmare.
    _receiveMsg : function (e) {

      // Get event data
      var d = e.data;

      // If no data given - fail
      // (probably check that it's an assoc array)
      if (!d)
        return;

      // e.origin is probably set and okay - CHECK!
      // TODO: Check e.origin is in the list of registered participants
      // if (e.origin !== "http://example.com:8080")
      //   return;

      
      // Get origin ID
      var id = d["originID"];

      // If no origin ID given - fail
      if (!id)
        return;

      // Get the service
      let service = services[id];

      // If the addressed service does not exist - fail
      if (!service)
        return;

      // Check for message limits
      if (limits[id]-- < 0) {

        // Kill service
        KorAP.log(0, 'Suspicious action by service', service.src);

        // TODO:
        //   Potentially kill the whole plugin!

        // This removes all connections before closing the service 
        this._closeService(service.id);

        // if (service.isWidget)
        service.close();
     
        return;
      };

      // Resize the iframe
      switch (d.action) {
      case 'resize':
        if (service.isWidget)
          service.resize(d);
        break;

      // Log message from iframe
      case 'log':
        KorAP.log(d.code, d.msg,  service.src);
        break;

      // Modify pipes
      case 'pipe':
	let j = d.job;
        if (
	  ((j == 'del-after' || j == 'add-after') &&
	   KorAP.ResponsePipe == undefined) ||
	    KorAP.Pipe == undefined) {
	  
	  KorAP.log(0,"No Pipe established");
	  break;
	};

	if (j == 'del') {
          KorAP.Pipe.remove(d.service);
	} else if (j == 'del-after') {
	  KorAP.ResponsePipe.remove(d.service);
        } else if (j == 'add-after') {
	  KorAP.ResponsePipe.append(d.service);
	} else {
          KorAP.Pipe.append(d.service);
        };
        break;

      // Get information from the embedding platform
      case 'get':

        // Get KoralQuery
        if (d.key == 'KQ') {
          if (KorAP.koralQuery !== undefined) {    
            d["value"] = KorAP.koralQuery;
          };
        }

        // Get Query information from form
        else if (d.key == 'QueryForm') {
          let doc = document;
          let v = d["value"] = {};

          var el;
          if (el = doc.getElementById('q-field')) {
            v["q"] = el.value;
          };
          if (el = doc.getElementById('ql-field')) {
            v["ql"] = el.value;
          };
          if (el = KorAP.vc) {
            v["cq"] = el.toQuery();
          };
          if (el = KorAP.Hint) {
            v["selection"] = KorAP.Hint.selectionRange();
          };
        }

        // Get text sigle from match
        else if (d.key == 'textSigle') {
          if (service.panel.type != "match") {
            KorAP.log(0, "Service can only be called on matches",  service.src);
            return;
          };
          let v = d["value"] = {};
          v["value"] = service.panel._match.textSigle;
        }
        
        // Get Query information from parameters
        else if (d.key == 'QueryParam') {

          // Supported in all modern browsers
          var p = new URLSearchParams(window.location.search);
          let v = d["value"] = {};
          v["search"] = window.location.search; // readonly
          v["q"] = p.get('q');
          v["ql"] = p.get('ql');
          v["cq"] = p.get('cq');
        }

        // Get pagination information
        else if (d.key == 'Pagination') {
          const pi = pageInfoClass.create();
          let v = d["value"] = {};
          v["page"] = pi.page();
          v["total"] = pi.total();
          v["count"] = pi.count();
        };

        // data needs to be mirrored
        if (d._id) {
          service.sendMsg(d);
        };

        break;

      case 'set':
        
        // Get Query information from data
        if (d.key == 'QueryForm') {
          let v = d["value"];

          if (v["q"] != undefined && this._q) {
            this._q.value = v["q"];
          };

          // Set query language field
          // Identical to tutorial.js
          if (v["ql"] != undefined) {
            if (KorAP.QLmenu) {
              KorAP.QLmenu.selectValue(v["ql"]);
            }

            else if (this._ql) {
              let found = Array.from(this._ql.options).find(o => o.value === v["ql"]);
              if (found)
                found.selected = true;
            };
          };

          window.scrollTo(0, 0);
          // if (v["cq"] != undefined) {};
        }

        break;
        
        // Redirect to a different page relative to the current
      case 'redirect':
        const url = new URL(window.location);

        // Currently this only accepts search parameters
        if (d["queryParam"]) {
          url.search = new URLSearchParams(d["queryParam"]);
        };

        window.location = url.toString();
        break;
      };

      // TODO:
      //   Close
    },

    // Close the service
    _closeService : function (id) {
      delete limits[id];

      // Close the iframe
      if (services[id] && services[id]._closeIframe) {

        services[id]._closeIframe();

        // Remove from list
        delete services[id];
      };


      // Remove listeners in case no widget
      // is available any longer
      if (Object.keys(limits).length == 0)
        this._removeListener();
    },

    // Get a random identifier
    _randomID : function () {
      return randomID(20);
    },

    // Remove the listener
    _removeListener : function () {
      window.clearInterval(this._timer);
      this._timer = undefined;
      window.removeEventListener("message", this._listener);
      this._listener = undefined;
    },

    /**
     * Return the service element.
     */
    element : function () {
      if (!this._el) {
        this._el = document.createElement('div');
        this._el.setAttribute("id", "services");
      }
      return this._el;
    },


    // Return states object
    states : function () {
      return this._states;
    },

    // Destructor, just for testing scenarios
    destroy : function () {
      limits = {};
      Object.keys(services).forEach(
        s => services[s].close()
      );
      services = {};
      Object.keys(buttons).forEach(
        b => buttons[b] = []
      );
      Object.keys(buttonsSingle).forEach(
        b => buttonsSingle[b] = []
      );

      if (this._el) {
        let e = this._el;
        if (e.parentNode) {
          e.parentNode.removeChild(e);
        };
        this._el = null;
      };
      this._removeListener();
    }
  };
});
