/*
 * RAD - ReferralAds.com Client
 * Copyright (c) 2007 Brian Adkins (lojic.com)
 * All rights reserved.
 */

var RAD = (function () {
  //------------------------------------------------------------
  // Cookie class from "JavaScript The Definitive Guide" p. 271
  // Allows easily storing multiple items in one cookie
  //------------------------------------------------------------

  function Cookie(document, name, days, path, domain, secure) {
    var MS_PER_DAY = 86400000;
    
    this.$document    = document;
    this.$name        = name;
    this.$expiration  = days ? new Date((new Date()).getTime() + days * MS_PER_DAY) : null;
    this.$path        = path ? path : null;
    this.$domain      = domain ? domain : null;
    this.$secure      = secure ? secure : false;
  }

  Cookie.prototype.store = function () {
    var cookieval = "";

    for (var prop in this) {
      if ((prop.charAt(0) === '$') || ((typeof this[prop]) === 'function')) { continue; }
      if (cookieval !== "") { cookieval += '&'; }
      cookieval += prop + ':' + escape(this[prop]);
    }

    var cookie = this.$name + '=' + cookieval;

    if (this.$expiration) { cookie += '; expires=' + this.$expiration.toUTCString();  }
    if (this.$path)       { cookie += '; path=' + this.$path;                         }
    if (this.$domain)     { cookie += '; domain=' + this.$domain;                     }
    if (this.$secure)     { cookie += '; secure';                                     }

    this.$document.cookie = cookie;
  };

  Cookie.prototype.load = function () {
    var allcookies = this.$document.cookie; 
    if (allcookies === "") { return false; }

    var start = allcookies.indexOf(this.$name + '=');
    if (start === -1) { return false; }
    start += this.$name.length + 1;
    var end = allcookies.indexOf(';', start);
    if (end === -1) { end = allcookies.length; }
    var cookieval = allcookies.substring(start, end);
    var a = cookieval.split('&');

    var i;
    for (i = 0; i < a.length; ++i) { a[i] = a[i].split(':'); }
    for (i = 0; i < a.length; ++i) { this[a[i][0]] = unescape(a[i][1]); }

    return true;
  };

  Cookie.prototype.remove = function () {
    var cookie = this.$name + '=';
    if (this.$path) { cookie += '; path=' + this.$path; }
    if (this.$domain) { cookie += '; domain=' + this.$domain; }
    cookie += '; expires=Fri, 02-Jan-1970 00:00:00 GMT';
    this.$document.cookie = cookie;
  };

  //------------------------------------------------------------
  // ISO 8601 Date code (derived from Dojo toolkit)
  //------------------------------------------------------------

  function fromISOString(str) {
    var regex = /^(?:(\d{4})(?:-(\d{2})(?:-(\d{2}))?)?)?(?:T(\d{2}):(\d{2})(?::(\d{2})(.\d+)?)?((?:[+-](\d{2}):(\d{2}))|Z)?)?$/;

    var match = regex.exec(str);
    var result = null;

    if (match) {
      match.shift();
      if (match[1]) { match[1]--; }         // Javascript Date months are 0-based
      if (match[6]) { (match[6] *= 1000); } // Javascript Date expects fractional seconds as milliseconds

      result = new Date(match[0] || 1970, match[1] || 0, match[2] || 0, 
        match[3] || 0, match[4] || 0, match[5] || 0, match[6] || 0);

      var offset = 0;
      var zoneSign = match[7] && match[7].charAt(0);

      if (zoneSign !== 'Z') {
        offset = ((match[8] || 0) * 60) + (Number(match[9]) || 0);
        if (zoneSign !== '-') { offset *= -1; }
      }

      if (zoneSign) { offset -= result.getTimezoneOffset(); }
      if (offset) { result.setTime(result.getTime() + offset * 60000); }
    }

    return result; // Date or null
  }

  function toISOString(dateObject) {
    var _ = function (n) { 
      return (n < 10) ? "0" + n : n; 
    };
    return [dateObject.getUTCFullYear(), _(dateObject.getUTCMonth() + 1), _(dateObject.getUTCDate())].join('-') +
      "T" +
      [_(dateObject.getUTCHours()), _(dateObject.getUTCMinutes()), _(dateObject.getUTCSeconds())].join(':') +
      "Z";
  }

  //------------------------------------------------------------
  // Namespace variables 
  //------------------------------------------------------------

  // The properties of config could probably be stored as local variables
  // to the function, but having them encapsulated in an object allows
  // exposing config to the unit tests.
  var config = {
    // Overridable
    aid:                  'aid',  // URL param key for affiliate id
    atag:                 'atag', // URL param key for affiliate data
    cookieDays:           30,
    cookieName:           'referralads',
    merchantAsAffiliate:  true,   // true => merchant acquires click of no affiliate id
    processAlienClick:    true,   // true => transmit info to referralads for alien click
    replaceOldAffiliate:  true,   // true => new affiliate overwrites old affiliate cookie
    onError:              null,
    onSuccess:            null,
    // Non-overridable
    maxCookieDays:        366,  // See Merchant::MAX_COOKIE_DAYS
    maxKeyLength:         15,   // See Merchant::MAX_MERCHANT_TAG_LENGTH
    serverTimeoutSeconds: 10,   // TODO
    radwsServers: [ 'http://radsws.referralads.com/api/' ]
  };

  var query_params = null;
  var radCookie = null;
  var server_called = false;

  //------------------------------------------------------------
  // Namespace private functions 
  //------------------------------------------------------------

  // Indicate whether click is from an affiliate other than the one in the cookie
  function isAlien(aid, cookie) {
    if (cookie[config.aid] && cookie[config.aid] !== aid && cookie.timestamp) {
      return fromISOString(cookie.timestamp).getTime() > (new Date()).getTime();
    }
    return false;
  }

  function transmitToReferralads(method, parameters) {
    var s = document.createElement('script');
    s.type = 'text/javascript';
    s.charset = 'utf-8';
    // grab the first server for now
    s.src = config.radwsServers[0] + method;
    var sep = '?';
    // Add a random parameter to prevent caching
    parameters.nocache = (new Date()).getTime();
    for (var prop in parameters) {
      if (parameters[prop] != null) { // Compare with null to allow 0
        s.src += sep + prop + '=' + escape(parameters[prop]);
        if (sep === '?') { sep = '&'; }
      }
    }
    document.body.appendChild(s);
  }

  function updateCookie(cookie, aid, atag, alien) {
    if (!alien || config.replaceOldAffiliate) {
      cookie[config.aid] = aid;
      cookie[config.atag] = atag == null ? '' : atag;
      // TODO the timestamp should reflect the cookie expire days
      // let's see if unit testing catches this :)
      var t = (new Date()).getTime() + 
        config.cookieDays * 24 * 60 * 60 * 1000;
      cookie.timestamp = toISOString(new Date(t));
      cookie.store();
    }
  }

  // Returns true if click was processed; otherwise, false
  function _click(args) {
    var aid = query_params[config.aid];

    // If there is no affiliate id and merchantAsAffiliate has been specified,
    // set the affiliate id to '' to indicate the merchant is the
    // affiliate.
    if (!aid && config.merchantAsAffiliate) { aid = ''; }

    // Return if either of the following is true:
    // 1) the affiliate id doesn't exist
    // 2) the affiliate specified in the URL is different than the one in the cookie
    //    AND
    //    processAlienClick wasn't specified
    var alien;
    if (!aid || ((alien = isAlien(aid, radCookie)) && !config.processAlienClick)) { return false; }
    
    var atag = query_params[config.atag];
    updateCookie(radCookie, aid, atag, alien);

    transmitToReferralads('click_js', 
      { aid:  aid, 
        atag: atag,
        mid:  args.merchantId,
        mtag: args.merchantTag });
    return true;
  }

  // Returns true if purchase was processed; otherwise, false
  function _purchase(args) {
    var aid = args.aid ?
      args.aid :
      radCookie[config.aid];

    // If no affiliate id, nothing to process
    if (!aid) { return false; }

    var atag = args.atag ?
      args.atag :
      radCookie[config.atag];

    transmitToReferralads('purchase_js', 
      { atag:   atag,
        aid:    aid, 
        mtag:   args.merchantTag,
        mid:    args.merchantId,
        mvalue: args.referralFee });
    return true;
  }

  function parseQueryParams(str) {
    var result = {};
    // Minimum query string for our purposes is '?a=b' so length must be > 3
    if (str.length > 3) {
      var pairs = (str.slice(1)).split('&');
      for (var idx = 0; idx < pairs.length; ++idx) {
        // split on '=' before unescaping in case an '=' has been escaped
        var keyValue = pairs[idx].split('=');
        if (keyValue.length === 2) { 
          result[unescape(keyValue[0])] = unescape(keyValue[1]); 
        }
      }
    }
    return result;
  }

  function validCookieDays(days) {
    return ((typeof days === "number") && days >= 0 && days <= config.maxCookieDays);
  }

  function validKey(str) {
    return (str && 
            str.length > 0 && 
            str.length <= config.maxKeyLength &&
            str.search(/^[A-Za-z0-9]+$/) > -1);
  }

  function overrideConfig(args) {
    if (validKey(args.affiliateIdKey))    { config.aid = args.affiliateIdKey;     }
    if (validKey(args.affiliateTagKey))   { config.atag = args.affiliateTagKey;   }
    if (validCookieDays(args.cookieDays)) { config.cookieDays = args.cookieDays;  }
    if (validKey(args.cookieName))        { config.cookieName = args.cookieName;  }

    if (args.onError && typeof args.onError === "function") {
      config.onError = args.onError;
    }

    if (args.onSuccess && typeof args.onSuccess === "function") {
      config.onSuccess = args.onSuccess;
    }
  }

  // Common 'before' functionality
  function before(args) {
    overrideConfig(args);
    query_params = parseQueryParams(location.search);
    radCookie = new Cookie(document, config.cookieName, config.cookieDays);
    radCookie.load();
    server_called = false;
  }

  function after() {
    setTimeout(function () {
      if (!server_called && config.onError) {
        config.onError('server timeout');
      }
    }, config.serverTimeoutSeconds * 1000);
  }

  // The closure
  var result = {
    click : function (args) {
      try {
        before(args);
        if (_click(args)) { after(); }
      } catch (e) {
        if (args.onError) { args.onError(e); }
      }
    },
    purchase : function (args) {
      try {
        before(args);
        if (_purchase(args)) { after(); }
      } catch (e) {
        if (args.onError) { args.onError(e); }
      }
    },
    onError : function (e) {
      server_called = true;
      if (config.onError) {
        config.onError(e);
      }
    },
    onSuccess : function () {
      server_called = true;
      if (config.onSuccess) {
        config.onSuccess();
      }
    }
  };
  
  if (this.JSUNIT) {
    result.after = after;
    result.before = before;
    result.config = config;
    result.Cookie = Cookie;
    result.fromISOString = fromISOString;
    result.isAlien = isAlien;
    result.overrideConfig = overrideConfig;
    result.toISOString = toISOString;
    result.parseQueryParams = parseQueryParams;
    result.updateCookie = updateCookie;
  }

  return result;
})();
