angular
.module('webui.services.rpc', [
  'webui.services.rpc.syscall', 'webui.services.configuration', 'webui.services.alerts',
  'webui.services.utils'
])
.factory('$rpc', [
  '$syscall', '$globalTimeout', '$alerts', '$utils',
  '$rootScope', '$location', '$authconf', '$filter',
function(syscall, globalTimeout, alerts, utils, rootScope, uri, authconf, filter) {

  var subscriptions = []
    , configurations = [authconf]
    , currentConf = {}
    , currentToken
    , timeout = null
    , forceNextUpdate = false;

  var cookieConf = utils.getCookie('aria2conf');
  // try at the start, so that it is presistant even when default authconf works
  if(cookieConf) configurations.unshift(cookieConf);

  if (uri.search().host) {
    configurations.unshift(uri.search());
    configurations[0].auth = {
      token: configurations[0].token,
      user: configurations[0].username,
      pass: configurations[0].password
    };
  }

  if (['http', 'https'].indexOf(uri.protocol()) != -1 && uri.host() != 'localhost') {
    configurations.push({
      host: uri.host(),
      path: '/jsonrpc',
      port: 6800,
      encrypt: false
    },
    {
      host: uri.host(),
      port: uri.port(),
      path: '/jsonrpc',
      encrypt: ( uri.protocol() == 'https' )
    },
    {
      host: uri.host(),
      port: uri.port(),
      path: authconf.path,
      encrypt: ( uri.protocol() == 'https' )
    });
  }


  // set if we got error on connection. This will cause another connection attempt.
  var needNewConnection = true;

  // update is implemented such that
  // only one syscall at max is ongoing
  // (i.e. serially) so should be private
  // to maintain that invariant
  var update = function() {

    clearTimeout(timeout);
    timeout = null;

    subscriptions = _.filter(subscriptions, function(e) {
      return !!e && e.once !== 2;
    });
    var subs = subscriptions.slice();
    if (!subs.length) {
      timeout = setTimeout(update, globalTimeout);
      return;
    }

    if (syscall.state == 'initializing') {
      console.log("Syscall is initializing, waiting");
      timeout = setTimeout(update, globalTimeout);
      return;
    }

    if (needNewConnection && configurations.length) {
      needNewConnection = false;
      currentConf = configurations[0];
      if (currentConf && currentConf.auth && currentConf.auth.token) {
        currentToken = currentConf.auth.token;
      }
      else {
        currentToken = null;
      }
      syscall.init(currentConf);
      timeout = setTimeout(update, globalTimeout);
      return;
    }

    var params = _.map(subs, function(s) {
      var p = s.params;
      if (currentToken) {
        p = ["token:" + currentToken].concat(p || []);
      }
      return {
        methodName: s.name,
        params: p && p.length ? p : undefined
      };
    });

    var error = function() {
      needNewConnection = true;
      var ind = configurations.indexOf(currentConf);
      if (ind != -1) configurations.splice(ind, 1);

      // If some proposed configurations are still in the pipeline then retry
      if (configurations.length) {
        alerts.log(filter('translate')("The last connection attempt was unsuccessful. Trying another configuration"));
        timeout = setTimeout(update, 0);
      }
      else {
        alerts.addAlert('<strong>' + filter('translate')('Oh Snap!') + '</strong> ' +
          filter('translate')('Could not connect to the aria2 RPC server. Will retry in 10 secs. You might want to check the connection settings by going to Settings > Connection Settings')
        , 'error');
        timeout = setTimeout(update, globalTimeout);
      }
    };

    syscall.invoke({
      name: 'system.multicall',
      params: [params],
      success: function(data) {
        var failed = _.some(data.result, function(d) {
          return d.code && d.message === "Unauthorized";
        });

        if (failed) {
          needNewConnection = true;
          alerts.addAlert('<strong>' + filter('translate')('Oh Snap!') + '</strong> ' +
            filter('translate')('Authentication failed while connecting to Aria2 RPC server. Will retry in 10 secs. You might want to confirm your authentication details by going to Settings > Connection Settings', 'error'));
          timeout = setTimeout(update, globalTimeout);
          return;
        }

        if (configurations.length) {
          // configuration worked, save it in cookie for next time and
          // delete the pipelined configurations!!
          if (currentToken)
            alerts.addAlert(filter('translate')('Successfully connected to Aria2 through its remote RPC …'), 'success');
          else
            alerts.addAlert(filter('translate')('Successfully connected to Aria2 through remote RPC, however the connection is still insecure. For complete security try adding an authorization secret token while starting Aria2 (through the flag --rpc-secret)'));
          configurations = [];
        }

        utils.setCookie('aria2conf', currentConf);

        var cbs = [];
        _.each(data.result, function(d, i) {
          var handle = subs[i];
          if (handle) {
            if (d.code) {
              console.error(handle, d);
              alerts.addAlert(d.message, 'error');
            }
            // run them later as the cb itself can mutate the subscriptions
            cbs.push({cb: handle.cb, data: d});
            if (handle.once) {
              handle.once = 2;
            }
          }
        });


        _.each(cbs, function(hnd) {
          hnd.cb(hnd.data);
        });

        rootScope.$digest();

        if (forceNextUpdate) {
          forceNextUpdate = false;
          timeout = setTimeout(update, 0);
        }
        else {
          timeout = setTimeout(update, globalTimeout);
        }
      },
      error: error
    });
  };

  // initiate the update loop
  timeout = setTimeout(update, globalTimeout);

  return {
    // conf can be configuration or array of configurations,
    // each one will be tried one after the other till success,
    // for all options for one conf read rpc/syscall.js
    configure: function(conf) {
      alerts.addAlert(filter('translate')('Trying to connect to aria2 using the new connection configuration'), 'info');

      if (conf instanceof Array)
        configurations = conf;
      else
        configurations = [conf];

      if (timeout) {
        clearTimeout(timeout);
        timeout = setTimeout(update, 0);
      }
    },

    // get current configuration being used
    getConfiguration: function() { return currentConf },

    // get currently configured directURL
    getDirectURL : function() { return currentConf.directURL },

    // syscall is done only once, delay is optional
    // and pass true to only dispatch it in the global timeout
    // which can be used to batch up once calls
    once: function(name, params, cb, delay) {
      cb = cb || angular.noop;
      params = params || [];

      subscriptions.push({
        once: true,
        name: 'aria2.' + name,
        params: params,
        cb: cb
      });

      if (!delay) {
        this.forceUpdate();
      }
    },

    // callback is called each time with updated syscall data
    // after the global timeout, delay is optional and pass it
    // true to dispatch the first syscall also on global timeout
    // which can be used to batch the subscribe calls
    subscribe: function(name, params, cb, delay) {
      cb = cb || angular.noop;
      params = params || [];

      var handle = {
        once: false,
        name: 'aria2.' + name,
        params: params,
        cb: cb
      };
      subscriptions.push(handle);

      if (!delay) this.forceUpdate();
      return handle;
    },

    // remove the subscribed callback by passing
    // the returned handle bysubscribe
    unsubscribe: function(handle) {
      var ind = subscriptions.indexOf(handle);
      subscriptions[ind] = null;
    },

    // force the global syscall update
    forceUpdate: function() {
      if (timeout) {
        clearTimeout(timeout);
        timeout = setTimeout(update, 0);
      }
      else {
        // a batch call is already in progress,
        // wait till it returns and force the next one
        forceNextUpdate = true;
      }
    }
  };
}]);
