// A cache for potentiallyApplicableRulesets
// Size chosen /completely/ arbitrarily.
var ruleCache = new LRUCache(1000);

// A cache for cookie hostnames.
var cookieHostCache = new LRUCache(100);

function Rule(from, to) {
  //this.from = from;
  this.to = to;
  this.from_c = new RegExp(from);
}

function Exclusion(pattern) {
  //this.pattern = pattern;
  this.pattern_c = new RegExp(pattern);
}

function CookieRule(host, cookiename) {
  this.host = host;
  this.host_c = new RegExp(host);
  this.name = cookiename;
  this.name_c = new RegExp(cookiename);
}

function RuleSet(set_name, match_rule, default_state, note) {
  this.name = set_name;
  if (match_rule)
    this.ruleset_match_c = new RegExp(match_rule);
  else
    this.ruleset_match_c = null;
  this.rules = [];
  this.exclusions = [];
  this.targets = [];
  this.cookierules = [];
  this.active = default_state;
  this.default_state = default_state;
  this.note = note;
}

RuleSet.prototype = {
  apply: function(urispec) {
    var returl = null;
    // If we're covered by an exclusion, go home
    for(var i = 0; i < this.exclusions.length; ++i) {
      if (this.exclusions[i].pattern_c.test(urispec)) {
        log(DBUG,"excluded uri " + urispec);
        return null;
      }
    }
    // If a rulset has a match_rule and it fails, go no further
    if (this.ruleset_match_c && !this.ruleset_match_c.test(urispec)) {
      log(VERB, "ruleset_match_c excluded " + urispec);
      return null;
    }

    // Okay, now find the first rule that triggers
    for(var i = 0; i < this.rules.length; ++i) {
      returl = urispec.replace(this.rules[i].from_c,
                               this.rules[i].to);
      if (returl != urispec) {
        return returl;
      }
    }
    if (this.ruleset_match_c) {
      // This is not an error, because we do not insist the matchrule
      // precisely describes to target space of URLs ot redirected
      log(DBUG,"Ruleset "+this.name
              +" had an applicable match-rule but no matching rules");
    }
    return null;
  }

};


function RuleSets() {
  // Load rules into structure
  var t1 = new Date().getTime();
  this.targets = {};

  for(var i = 0; i < rule_list.length; i++) {
    var xhr = new XMLHttpRequest();
    // Use blocking XHR to ensure everything is loaded by the time
    // we return.
    //var that = this;
    //xhr.onreadystatechange = function() { that.loadRuleSet(xhr); }
    xhr.open("GET", chrome.extension.getURL(rule_list[i]), false);
    //xhr.open("GET", chrome.extension.getURL(rule_list[i]), true);
    xhr.send(null);
    this.loadRuleSet(xhr);
  }
  var t2 =  new Date().getTime();
  log(NOTE,"Loading rulesets took " + (t2 - t1) / 1000.0 + " seconds");
}

RuleSets.prototype = {

  localPlatformRegexp: (function() {
    if (/(OPR|Opera)[\/\s](\d+\.\d+)/.test(navigator.userAgent)) {
      log(DBUG, 'Detected that we are running Opera');
      return new RegExp("chromium|mixedcontent");
    } else {
      log(DBUG, 'Detected that we are running Chrome/Chromium');
      return new RegExp("chromium");
    }
  })(),

  loadRuleSet: function(xhr) {
    // Get file contents
    if (xhr.readyState != 4) {
      return;
    }

    // XXX: Validation + error checking
    var sets = xhr.responseXML.getElementsByTagName("ruleset");
    for (var i = 0; i < sets.length; ++i) {
      this.parseOneRuleset(sets[i]);
    }
  },

  addUserRule : function(params) {
    log(INFO, 'adding new user rule for ' + JSON.stringify(params));
    var new_rule_set = new RuleSet(params.host, null, true, "user rule");
    var new_rule = new Rule(params.urlMatcher, params.redirectTo);
    new_rule_set.rules.push(new_rule);
    if (!(params.host in this.targets)) {
      this.targets[params.host] = [];
    }
    ruleCache.remove(params.host);
    // TODO: maybe promote this rule?
    this.targets[params.host].push(new_rule_set);
    log(INFO, 'done adding rule');
    return true;
  },

  parseOneRuleset: function(ruletag) {
    var default_state = true;
    var note = "";
    if (ruletag.attributes.default_off) {
      default_state = false;
      note += ruletag.attributes.default_off.value + "\n";
    }

    // If a ruleset declares a platform, and we don't match it, treat it as
    // off-by-default
    var platform = ruletag.getAttribute("platform");
    if (platform) {
      if (platform.search(this.localPlatformRegexp) == -1) {
        default_state = false;
      }
      note += "Platform(s): " + platform + "\n";
    }

    var rule_set = new RuleSet(ruletag.getAttribute("name"),
                               ruletag.getAttribute("match_rule"),
                               default_state,
                               note.trim());

    // Read user prefs
    if (rule_set.name in localStorage) {
      rule_set.active = (localStorage[rule_set.name] == "true");
    }

    var rules = ruletag.getElementsByTagName("rule");
    for(var j = 0; j < rules.length; j++) {
      rule_set.rules.push(new Rule(rules[j].getAttribute("from"),
                                    rules[j].getAttribute("to")));
    }

    var exclusions = ruletag.getElementsByTagName("exclusion");
    for(var j = 0; j < exclusions.length; j++) {
      rule_set.exclusions.push(
            new Exclusion(exclusions[j].getAttribute("pattern")));
    }

    var cookierules = ruletag.getElementsByTagName("securecookie");
    for(var j = 0; j < cookierules.length; j++) {
      rule_set.cookierules.push(new CookieRule(cookierules[j].getAttribute("host"),
                                           cookierules[j].getAttribute("name")));
    }

    var targets = ruletag.getElementsByTagName("target");
    for(var j = 0; j < targets.length; j++) {
       var host = targets[j].getAttribute("host");
       if (!(host in this.targets)) {
         this.targets[host] = [];
       }
       this.targets[host].push(rule_set);
    }
  },

  setInsert: function(intoList, fromList) {
    // Insert any elements from fromList into intoList, if they are not
    // already there.  fromList may be null.
    if (!fromList) return;
    for (var i = 0; i < fromList.length; i++)
      if (intoList.indexOf(fromList[i]) == -1)
        intoList.push(fromList[i]);
  },
  
  potentiallyApplicableRulesets: function(host) {
    // Return a list of rulesets that apply to this host

    // Have we cached this result? If so, return it!
    var cached_item = ruleCache.get(host);
    if (cached_item !== undefined) {
        log(DBUG, "Ruleset cache hit for " + host);
        return cached_item;
    }
    log(DBUG, "Ruleset cache miss for " + host);

    var tmp;
    var results = [];
    if (this.targets[host]) {
      // Copy the host targets so we don't modify them.
      results = this.targets[host].slice();
    }

    // Replace each portion of the domain with a * in turn
    var segmented = host.split(".");
    for (var i = 0; i < segmented.length; ++i) {
      tmp = segmented[i];
      segmented[i] = "*";
      this.setInsert(results, this.targets[segmented.join(".")]);
      segmented[i] = tmp;
    }
    // now eat away from the left, with *, so that for x.y.z.google.com we
    // check *.z.google.com and *.google.com (we did *.y.z.google.com above)
    for (var i = 2; i <= segmented.length - 2; ++i) {
      t = "*." + segmented.slice(i,segmented.length).join(".");
      this.setInsert(results, this.targets[t]);
    }
    log(DBUG,"Applicable rules for " + host + ":");
    if (results.length == 0)
      log(DBUG, "  None");
    else
      for (var i = 0; i < results.length; ++i)
        log(DBUG, "  " + results[i].name);

    // Insert results into the ruleset cache
    ruleCache.set(host, results);
    return results;
  },

  shouldSecureCookie: function(cookie, knownHttps) {
    // Check to see if the Cookie object c meets any of our cookierule citeria
    // for being marked as secure.  knownHttps is true if the context for this
    // cookie being set is known to be https.
    //log(DBUG, "Testing cookie:");
    //log(DBUG, "  name: " + cookie.name);
    //log(DBUG, "  host: " + cookie.host);
    //log(DBUG, "  domain: " + cookie.domain);
    //log(DBUG, "  rawhost: " + cookie.rawHost);
    var hostname = cookie.domain;
    // cookie domain scopes can start with .
    while (hostname.charAt(0) == ".")
      hostname = hostname.slice(1);

    if (!knownHttps && !this.safeToSecureCookie(hostname)) {
        return null;
    }

    var rs = this.potentiallyApplicableRulesets(hostname);
    for (var i = 0; i < rs.length; ++i) {
      var ruleset = rs[i];
      if (ruleset.active) {
        for (var j = 0; j < ruleset.cookierules.length; j++) {
          var cr = ruleset.cookierules[j];
          if (cr.host_c.test(cookie.domain) && cr.name_c.test(cookie.name)) {
            return ruleset;
          }
          //log(WARN, "no match domain " + cr.host_c.test(cookie.domain) +
          //          " name " + cr.name_c.test(cookie.name));
          //log(WARN, "with " + cookie.domain + " " + cookie.name);
          //log(WARN, "and " + cr.host + " " + cr.name);
        }
      }
    }
    return null;
  },

  safeToSecureCookie: function(domain) {
    // Check if the domain might be being served over HTTP.  If so, it isn't
    // safe to secure a cookie!  We can't always know this for sure because
    // observing cookie-changed doesn't give us enough context to know the
    // full origin URI.

    // First, if there are any redirect loops on this domain, don't secure
    // cookies.  XXX This is not a very satisfactory heuristic.  Sometimes we
    // would want to secure the cookie anyway, because the URLs that loop are
    // not authenticated or not important.  Also by the time the loop has been
    // observed and the domain blacklisted, a cookie might already have been
    // flagged as secure.

    if (domain in domainBlacklist) {
      log(INFO, "cookies for " + domain + "blacklisted");
      return false;
    }
    var cached_item = cookieHostCache.get(domain);
    if (cached_item !== undefined) {
        log(DBUG, "Cookie host cache hit for " + domain);
        return cached_item;
    }
    log(DBUG, "Cookie host cache miss for " + domain);

    // If we passed that test, make up a random URL on the domain, and see if
    // we would HTTPSify that.

    try {
      var nonce_path = "/" + Math.random().toString();
      nonce_path = nonce_path + nonce_path;
      var test_uri = "http://" + domain + nonce_path;
    } catch (e) {
      log(WARN, "explosion in safeToSecureCookie for " + domain + "\n"
                      + "(" + e + ")");
      cookieHostCache.set(domain, false);
      return false;
    }

    log(INFO, "Testing securecookie applicability with " + test_uri);
    var rs = this.potentiallyApplicableRulesets(domain);
    for (var i = 0; i < rs.length; ++i) {
      if (!rs[i].active) continue;
      var rewrite = rs[i].apply(test_uri);
      if (rewrite) {
        log(INFO, "Cookie domain could be secured: " + rewrite);
        cookieHostCache.set(domain, true);
        return true;
      }
    }
    log(INFO, "Cookie domain could NOT be secured.");
    cookieHostCache.set(domain, false);
    return false;
  },

  rewriteURI: function(urispec, host) {
    var newuri = null;
    var rs = this.potentiallyApplicableRulesets(host);
    for(var i = 0; i < rs.length; ++i) {
      if (rs[i].active && (newuri = rs[i].apply(urispec)))
        return newuri;
    }
    return null;
  }
};
