
var uuid = require("uuid");
var querystring = require("querystring");
var url = require("url");

var utils = require("./utils");
var config = require("./config");
var request = require('./request');

var debug = require("debug")("universal-analytics");


module.exports = init;


function init (tid, cid, options) {
  return new Visitor(tid, cid, options);
}

var Visitor = module.exports.Visitor = function (tid, cid, options, context, persistentParams) {

  if (typeof tid === 'object') {
    options = tid;
    tid = cid = null;
  } else if (typeof cid === 'object') {
    options = cid;
    cid = null;
  }

  this._queue = [];

  this.options = options || {};

  if(this.options.hostname) {
    config.hostname = this.options.hostname;
  }
  if(this.options.path) {
    config.path = this.options.path;
  }

  if (this.options.http) {
    var parsedHostname = url.parse(config.hostname);
    config.hostname = 'http://' + parsedHostname.host;
  }

  if(this.options.enableBatching !== undefined) {
    config.batching = options.enableBatching;
  }

  if(this.options.batchSize) {
    config.batchSize = this.options.batchSize;
  }

  this._context = context || {};
  this._persistentParams = persistentParams || {};

  this.tid = tid || this.options.tid;
  this.cid = this._determineCid(cid, this.options.cid, (this.options.strictCidFormat !== false));
  if(this.options.uid) {
    this.uid = this.options.uid;
  }
}




module.exports.middleware = function (tid, options) {

  this.tid = tid;
  this.options = options || {};

  var cookieName = this.options.cookieName || "_ga";
  var instanceName = this.options.instanceName || "visitor";
  delete this.options.instanceName;

  return function (req, res, next) {

    req[instanceName] = module.exports.createFromSession(req.session);

    if (req[instanceName]) return next();

    var cid;
    if (req.cookies && req.cookies[cookieName]) {
      var gaSplit = req.cookies[cookieName].split('.');
      cid = gaSplit[2] + "." + gaSplit[3];
    }

    req[instanceName] = init(tid, cid, options);

    if (req.session) {
      req.session.cid = req[instanceName].cid;
    }

    next();
  }
}



module.exports.createFromSession = function (session) {
  if (session && session.cid) {
    return init(this.tid, session.cid, this.options);
  }
}



Visitor.prototype = {

  debug: function (d) {
    debug.enabled = arguments.length === 0 ? true : d;
    debug("visitor.debug() is deprecated: set DEBUG=universal-analytics to enable logging")
    return this;
  },


  reset: function () {
    this._context = null;
    return this;
  },

  set: function (key, value) {
    this._persistentParams = this._persistentParams || {};
    this._persistentParams[key] = value;
  },

  pageview: function (path, hostname, title, params, fn) {

    if (typeof path === 'object' && path != null) {
      params = path;
      if (typeof hostname === 'function') {
        fn = hostname
      }
      path = hostname = title  = null;
    } else if (typeof hostname === 'function') {
      fn = hostname
      hostname = title = null;
    } else if (typeof title === 'function') {
      fn = title;
      title = null;
    } else if (typeof params === 'function') {
      fn = params;
      params = null;
    }

    params = this._translateParams(params);

    params = Object.assign({}, this._persistentParams || {}, params);

    params.dp = path || params.dp || this._context.dp;
    params.dh = hostname || params.dh || this._context.dh;
    params.dt = title || params.dt || this._context.dt;

    this._tidyParameters(params);

    if (!params.dp && !params.dl) {
      return this._handleError("Please provide either a page path (dp) or a document location (dl)", fn);
    }

    return this._withContext(params)._enqueue("pageview", params, fn);
  },


  screenview: function (screenName, appName, appVersion, appId, appInstallerId, params, fn) {

      if (typeof screenName === 'object' && screenName != null) {
          params = screenName;
          if (typeof appName === 'function') {
              fn = appName
          }
          screenName = appName = appVersion = appId = appInstallerId = null;
      } else if (typeof appName === 'function') {
          fn = appName
          appName = appVersion = appId = appInstallerId = null;
      } else if (typeof appVersion === 'function') {
          fn = appVersion;
          appVersion = appId = appInstallerId = null;
      } else if (typeof appId === 'function') {
          fn = appId;
          appId = appInstallerId = null;
      } else if (typeof appInstallerId === 'function') {
          fn = appInstallerId;
          appInstallerId = null;
      } else if (typeof params === 'function') {
          fn = params;
          params = null;
      }

      params = this._translateParams(params);

      params = Object.assign({}, this._persistentParams || {}, params);

      params.cd = screenName || params.cd || this._context.cd;
      params.an = appName || params.an || this._context.an;
      params.av = appVersion || params.av || this._context.av;
      params.aid = appId || params.aid || this._context.aid;
      params.aiid = appInstallerId || params.aiid || this._context.aiid;

      this._tidyParameters(params);

      if (!params.cd || !params.an) {
          return this._handleError("Please provide at least a screen name (cd) and an app name (an)", fn);
      }

      return this._withContext(params)._enqueue("screenview", params, fn);
  },


  event: function (category, action, label, value, params, fn) {

    if (typeof category === 'object' && category != null) {
      params = category;
      if (typeof action === 'function') {
        fn = action
      }
      category = action = label = value = null;
    } else if (typeof label === 'function') {
      fn = label;
      label = value = null;
    } else if (typeof value === 'function') {
      fn = value;
      value = null;
    } else if (typeof params === 'function') {
      fn = params;
      params = null;
    }

    params = this._translateParams(params);

    params = Object.assign({}, this._persistentParams || {}, params);

    params.ec = category || params.ec || this._context.ec;
    params.ea = action || params.ea || this._context.ea;
    params.el = label || params.el || this._context.el;
    params.ev = value || params.ev || this._context.ev;
    params.p = params.p || params.dp || this._context.p || this._context.dp;

    delete params.dp;
    this._tidyParameters(params);

    if (!params.ec || !params.ea) {
      return this._handleError("Please provide at least an event category (ec) and an event action (ea)", fn);
    }

    return this._withContext(params)._enqueue("event", params, fn);
  },


  transaction: function (transaction, revenue, shipping, tax, affiliation, params, fn) {
    if (typeof transaction === 'object') {
      params = transaction;
      if (typeof revenue === 'function') {
        fn = revenue
      }
      transaction = revenue = shipping = tax = affiliation = null;
    } else if (typeof revenue === 'function') {
      fn = revenue;
      revenue = shipping = tax = affiliation = null;
    } else if (typeof shipping === 'function') {
      fn = shipping;
      shipping = tax = affiliation = null;
    } else if (typeof tax === 'function') {
      fn = tax;
      tax = affiliation = null;
    } else if (typeof affiliation === 'function') {
      fn = affiliation;
      affiliation = null;
    } else if (typeof params === 'function') {
      fn = params;
      params = null;
    }

    params = this._translateParams(params);

    params = Object.assign({}, this._persistentParams || {}, params);

    params.ti = transaction || params.ti || this._context.ti;
    params.tr = revenue || params.tr || this._context.tr;
    params.ts = shipping || params.ts || this._context.ts;
    params.tt = tax || params.tt || this._context.tt;
    params.ta = affiliation || params.ta || this._context.ta;
    params.p = params.p || this._context.p || this._context.dp;

    this._tidyParameters(params);

    if (!params.ti) {
      return this._handleError("Please provide at least a transaction ID (ti)", fn);
    }

    return this._withContext(params)._enqueue("transaction", params, fn);
  },


  item: function (price, quantity, sku, name, variation, params, fn) {
    if (typeof price === 'object') {
      params = price;
      if (typeof quantity === 'function') {
        fn = quantity
      }
      price = quantity = sku = name = variation = null;
    } else if (typeof quantity === 'function') {
      fn = quantity;
      quantity = sku = name = variation = null;
    } else if (typeof sku === 'function') {
      fn = sku;
      sku = name = variation = null;
    } else if (typeof name === 'function') {
      fn = name;
      name = variation = null;
    } else if (typeof variation === 'function') {
      fn = variation;
      variation = null;
    } else if (typeof params === 'function') {
      fn = params;
      params = null;
    }

    params = this._translateParams(params);

    params = Object.assign({}, this._persistentParams || {}, params);

    params.ip = price || params.ip || this._context.ip;
    params.iq = quantity || params.iq || this._context.iq;
    params.ic = sku || params.ic || this._context.ic;
    params.in = name || params.in || this._context.in;
    params.iv = variation || params.iv || this._context.iv;
    params.p = params.p || this._context.p || this._context.dp;
    params.ti = params.ti || this._context.ti;

    this._tidyParameters(params);

    if (!params.ti) {
      return this._handleError("Please provide at least an item transaction ID (ti)", fn);
    }

    return this._withContext(params)._enqueue("item", params, fn);

  },

  exception: function (description, fatal, params, fn) {

    if (typeof description === 'object') {
      params = description;
      if (typeof fatal === 'function') {
        fn = fatal;
      }
      description = fatal = null;
    } else if (typeof fatal === 'function') {
      fn = fatal;
      fatal = 0;
    } else if (typeof params === 'function') {
      fn = params;
      params = null;
    }

    params = this._translateParams(params);

    params = Object.assign({}, this._persistentParams || {}, params);

    params.exd = description || params.exd || this._context.exd;
    params.exf = +!!(fatal || params.exf || this._context.exf);

    if (params.exf === 0) {
      delete params.exf;
    }

    this._tidyParameters(params);

    return this._withContext(params)._enqueue("exception", params, fn);
  },

  timing: function (category, variable, time, label, params, fn) {

    if (typeof category === 'object') {
      params = category;
      if (typeof variable === 'function') {
        fn = variable;
      }
      category = variable = time = label = null;
    } else if (typeof variable === 'function') {
      fn = variable;
      variable = time = label = null;
    } else if (typeof time === 'function') {
      fn = time;
      time = label = null;
    } else if (typeof label === 'function') {
      fn = label;
      label = null;
    } else if (typeof params === 'function') {
      fn = params;
      params = null;
    }

    params = this._translateParams(params);

    params = Object.assign({}, this._persistentParams || {}, params);

    params.utc = category || params.utc || this._context.utc;
    params.utv = variable || params.utv || this._context.utv;
    params.utt = time || params.utt || this._context.utt;
    params.utl = label || params.utl || this._context.utl;

    this._tidyParameters(params);

    return this._withContext(params)._enqueue("timing", params, fn);
  },


  send: function (fn) {
    var self = this;
    var count = 1;
    var fn = fn || function () {};
    debug("Sending %d tracking call(s)", self._queue.length);

    var getBody = function(params) {
      return params.map(function(x) { return querystring.stringify(x); }).join("\n");
    }

    var onFinish = function (err) {
      debug("Finished sending tracking calls")
      fn.call(self, err || null, count - 1);
    }

    var iterator = function () {
      if (!self._queue.length) {
        return onFinish(null);
      }
      var params = [];

      if(config.batching) {
        params = self._queue.splice(0, Math.min(self._queue.length, config.batchSize));
      } else {
        params.push(self._queue.shift());
      }

      var useBatchPath = params.length > 1;

      var path = config.hostname + (useBatchPath ? config.batchPath : config.path);

      debug("%d: %o", count++, params);

      var body = getBody(params);

      request.post(path, body, self.options.headers, nextIteration);
    }

    function nextIteration(err) {
      if (err) return onFinish(err);
      iterator();
    }

    iterator();

  },

  _enqueue: function (type, params, fn) {

    if (typeof params === 'function') {
      fn = params;
      params = {};
    }

    params = this._translateParams(params) || {};

    Object.assign(params, {
      v: config.protocolVersion,
      tid: this.tid,
      cid: this.cid,
      t: type
    });
    if(this.uid) {
      params.uid = this.uid;
    }

    this._queue.push(params);

    if (debug.enabled) {
      this._checkParameters(params);
    }

    debug("Enqueued %s (%o)", type, params);

    if (fn) {
      this.send(fn);
    }

    return this;
  },


  _handleError: function (message, fn) {
      debug("Error: %s", message)
      fn && fn.call(this, new Error(message))
      return this;
  },



  _determineCid: function () {
    var args = Array.prototype.splice.call(arguments, 0);
    var id;
    var lastItem = args.length-1;
    var strict = args[lastItem];
    if (strict) {
      for (var i = 0; i < lastItem; i++) {
        id = utils.ensureValidCid(args[i]);
        if (id !== false) return id;
        if (id != null) debug("Warning! Invalid UUID format '%s'", args[i]);
      }
    } else {
      for (var i = 0; i < lastItem; i++) {
        if (args[i]) return args[i];
      }
    }
    return uuid.v4();
  },


  _checkParameters: function (params) {
    for (var param in params) {
      if (config.acceptedParameters.indexOf(param) !== -1 || config.acceptedParametersRegex.filter(function (r) {
          return r.test(param);
        }).length) {
        continue;
      }
      debug("Warning! Unsupported tracking parameter %s (%s)", param, params[param]);
    }
  },

  _translateParams: function (params) {
        var translated = {};
        for (var key in params) {
            if (config.parametersMap.hasOwnProperty(key)) {
                translated[config.parametersMap[key]] = params[key];
            } else {
                translated[key] = params[key];
            }
        }
        return translated;
    },

  _tidyParameters: function (params) {
    for (var param in params) {
      if (params[param] === null || params[param] === undefined) {
        delete params[param];
      }
    }
    return params;
  },

  _withContext: function (context) {
    var visitor = new Visitor(this.tid, this.cid, this.options, context, this._persistentParams);
    visitor._queue = this._queue;
    return visitor;
  }


}

Visitor.prototype.pv = Visitor.prototype.pageview
Visitor.prototype.e = Visitor.prototype.event
Visitor.prototype.t = Visitor.prototype.transaction
Visitor.prototype.i = Visitor.prototype.item
