
(function(){

var IS_MSIE = /msie/.test(navigator.userAgent.toLowerCase()) && !/opera/.test(navigator.userAgent.toLowerCase());
var STATE_ZIP = {
  "AK": /^(99)/,
  "AL": /^(35|36|38)/,
  "AR": /^(38|65|71|72|78)/,
  "AS": /^(96)/,
  "AZ": /^(84|85|86|87|89)/,
  "CA": /^(89|90|91|92|93|94|95|96|97)/,
  "CO": /^(80|81|82)/,
  "CT": /^(06)/,
  "DC": /^(20)/,
  "DE": /^(19)/,
  "FL": /^(32|33|34)/,
  "GA": /^(30|31|39)/,
  "HI": /^(96)/,
  "IA": /^(50|51|52|55|56)/,
  "ID": /^(83|97|99)/,
  "IL": /^(52|60|61|62)/,
  "IN": /^(46|47)/,
  "KS": /^(66|67)/,
  "KY": /^(38|40|41|42)/,
  "LA": /^(70|71)/,
  "MA": /^(01|02|05)/,
  "MD": /^(20|21)/,
  "ME": /^(03|04)/,
  "MI": /^(48|49)/,
  "MN": /^(55|56)/,
  "MO": /^(63|64|65)/,
  "MS": /^(38|39)/,
  "MT": /^(59)/,
  "NC": /^(27|28)/,
  "ND": /^(57|58)/,
  "NE": /^(68|69|82)/,
  "NH": /^(00|03)/,
  "NJ": /^(07|08|10)/,
  "NM": /^(79|87|88)/,
  "NV": /^(88|89)/,
  "NY": /^(00|06|10|11|12|13|14)/,
  "OH": /^(43|44|45)/,
  "OK": /^(73|74|76|79)/,
  "OR": /^(97|99)/,
  "PA": /^(15|16|17|18|19)/,
  "PR": /^(00)/,
  "RI": /^(02)/,
  "SC": /^(29)/,
  "SD": /^(57|58)/,
  "TN": /^(31|37|38)/,
  "TX": /^(73|75|76|77|78|79|88)/,
  "UT": /^(84)/,
  "VA": /^(20|22|23|24)/,
  "VI": /^(00)/,
  "VT": /^(05)/,
  "WA": /^(98|99)/,
  "WI": /^(52|53|54)/,
  "WV": /^(24|25|26)/,
  "WY": /^(82|83)/,
  /* CAN */
  "AB": /^[A-z]\d[A-z]\s*(?:\d[A-z]\d)?$/,
  "BC": /^[A-z]\d[A-z]\s*(?:\d[A-z]\d)?$/,
  "MB": /^[A-z]\d[A-z]\s*(?:\d[A-z]\d)?$/,
  "NB": /^[A-z]\d[A-z]\s*(?:\d[A-z]\d)?$/,
  "NL": /^[A-z]\d[A-z]\s*(?:\d[A-z]\d)?$/,
  "NT": /^[A-z]\d[A-z]\s*(?:\d[A-z]\d)?$/,
  "NU": /^[A-z]\d[A-z]\s*(?:\d[A-z]\d)?$/,
  "ON": /^[A-z]\d[A-z]\s*(?:\d[A-z]\d)?$/,
  "PE": /^[A-z]\d[A-z]\s*(?:\d[A-z]\d)?$/,
  "QC": /^[A-z]\d[A-z]\s*(?:\d[A-z]\d)?$/,
  "SK": /^[A-z]\d[A-z]\s*(?:\d[A-z]\d)?$/,
  "YT": /^[A-z]\d[A-z]\s*(?:\d[A-z]\d)?$/
};



var  _ownerDocument = function(e){
  if(typeof e == "undefined")
    return document;
  return e.ownerDocument || document;
};


var _trim =  function(text){
  return (text || "").replace( /^\s+|\s+$/g, "" );
};

var fieldValue = function(el, successful) {
    var n = el.name, t = el.type, tag = el.tagName.toLowerCase();
    if (typeof successful == 'undefined') successful = true;

    if (successful && (!n || el.disabled || t == 'reset' || t == 'button' ||
        (t == 'checkbox' || t == 'radio') && !el.checked ||
        (t == 'submit' || t == 'image') && el.form && el.form.clk != el ||
        tag == 'select' && el.selectedIndex == -1))
            return null;

    if (tag == 'select') {
        var index = el.selectedIndex;
        if (index < 0) return null;
        var a = [], ops = el.options;
        var one = (t == 'select-one');
        var max = (one ? index+1 : ops.length);
        for(var i=(one ? index : 0); i < max; i++) {
            var op = ops[i];
            if (op.selected) {
                // extra pain for IE...
                var v = IS_MSIE && !(op.attributes['value'].specified) ? op.text : op.value;
                if (one) return v;
                a.push(v);
            }
        }
        return a;
    }
    return el.value;
};

/**
 * Get values from "referenced" form fields.
 * 
 * Referenced form fields are fields that are not
 * created by the ad's template.  This is primarily
 * of use for coreg offers which might require
 * registration-type fields (first name, etc...).
 * 
 * Rather than duplicating these fields, if used
 * on that same page, the ad can reference the
 * registration fields.
 */
var getReferencedValues = function(form, impressionKey, references){
  var suffix = new RegExp("\\|"+impressionKey+"\\|$");
  var forms = _ownerDocument(form).forms;
  for(var i=0; i < form.elements.length; i++){
    var e = form.elements[i];
    if(!e.name || (e.name && e.name.search(suffix) == -1))  // only find field ending with "|key|"
      continue;
    var name = e.name.substring(0, e.name.indexOf("|"));
    var ref = references[name];
    if(!ref)
      continue;
    // 'reference' is expected to be formKey.fieldKey
    var dot = ref.indexOf(".");
    var rFormName = ref.substring(0,dot);
    var rFieldName = ref.substring(dot+1);
    var rField = forms[rFormName][rFieldName];
    var v;
    // might get a nodelist back (in case of radios, for example)
    if(typeof rField.tagName == 'undefined' && rField.length){
      for(var j=0; j < rField.length; j++){
        v = fieldValue(rField[j]);
        if(v){
          e.value = v;
          break;
        }
      }
    }
    else{
      v = fieldValue(rField);
      if(v)
        e.value = v;
    }
  }
};

var serializeForm = function(form, impressionKey){
  var s = [];
  var suffix = new RegExp("\\|"+impressionKey+"\\|$");
  for(var i=0; i < form.elements.length; i++){
    var e = form.elements[i];
    if(e.name && e.name.search(suffix) == -1)  // only find field ending with "|key|"
      continue;
    var v = fieldValue(e);
    if(v != null){
      var attrName = e.name.replace("|"+impressionKey+"|", "|s|");
      // a=1,a=2,a=3,etc... if v is array
      if(v instanceof Array){
        for(var j=0; j < v.length; j++)
          s.push(encodeURIComponent(attrName) + "=" + encodeURIComponent(v[j]));
      }
      else
        s.push(encodeURIComponent(attrName) + "=" + encodeURIComponent(v));
    }
  }
  
  return s.join("&").replace(/%20/g, "+");
};

var insideForm = function(container){
  return (AdSyndicate.getContainingForm(container) != null);
}


function _parseGACookie(cookie){
  var gat = {};
  if(cookie && cookie != ""){
    var cookies = cookie.split(";");
    for(var i=0; i < cookies.length; i++){
      var cookie = _trim(cookies[i]);
      if(cookie.substring(0, 7) == "__utmz="){
        var value = cookie.substring(7);
        // save original value (for debugging, etc...)
        gat["gaCookie"] = value;
        // strip what's leading name=value pairs
        // e.g.: 1231231231.12312312312.12.1.utmcsr=google...
        value = value.split(".");
        value.shift();  // site hash
        gat["gaCreatedDate"] = value.shift();  // time created (seconds since unix epoch)
        value.shift();value.shift();  // # visitor sessions, # sources
        value = value.join(".");
        var values = value.split("|");
        while(values.length > 0){
          // split "by-hand" in case "=" in value
          var nv = values.pop();
          var n = decodeURIComponent(nv.split("=",1)[0]);
          var v = _trim(decodeURIComponent(nv.substr(n.length+1)));
          gat[n] = v;
        }
      }
    }
  }
  return gat;
};

/**
 * @param attrs: dict of config parameters for the slot.
 */
var _Slot = function(attrs){
  this.attrs = attrs;
  /**
   * function that takes form as an argument.
   * 
   * returns map of {field:[errors,...]}
   */
  this.validate = function(form){
    return {};
  };
  /**
   * function called just prior to submitting data.
   * 
   * This exists to be overridden by ads.
   *
   * @param form: form instance
   * @param response: "Y" or "N"
   */
  this.beforeSubmit = function(form, response){
    return;
  };
  this.toString = function(){
    return "{Slot container:" + this.attrs.container + ", type:"
              + this.attrs.type + ", adKey:" + this.attrs.adKey
              + ", placementKey:" + this.attrs.placementKey
              + ", affiliateCode:" +  this.attrs.affiliateCode + "}";
  };
  return this;
};

var _AdSyndicate = function(){
  
  this.CO_REG = 'C';
  this.INTERSTITIAL = 'I';
  
  this._GAT = _parseGACookie(document.cookie);
  this.slots = {};
  this.profile = {};
  
  this._ignore = ["career-interest"]; // CNC & FT can clear
  this._referencedFields = {};
  this._labels = {};
  this._billing = {};
  this.siteKey = "de95a6c96c50fc0723230623cd4e88a7";
  this.baseURL = "http://syndicate.alloyedu.com/syndicate/api/de95a6c96c50fc0723230623cd4e88a7/";
  this.defaultType = this.CO_REG;  // default is CO_REG
  this.strategy;
  
  /**
   * Create a new _Slot.
   * 
   * @returns: _Slot instance
   */
  this.createSlot = function(attrs){
    if (!attrs['type']) 
       attrs['type'] = this.defaultType;
    if(!attrs['needsform'])
      attrs['needsform'] = !insideForm(attrs['container']);
    if(!attrs['placementKey']) // default to empty string, so always exists as attr
      attrs['placementKey'] = "";
    if(!attrs['affiliateCode']) // default to empty string, so always exists as attr
      attrs['affiliateCode'] = "";
    if(!attrs['fillerUrl']) // default to empty string, so always exists as attr
      attrs['fillerUrl'] = "";
    var slot = new _Slot(attrs);
    this.slots[attrs['container']] = slot;
    return slot;
  };
  /**
   * @arg strategy: int constant for selector strategy
   */
  this.setStrategy = function(strategy){
    this.strategy = strategy;
    return this;
  };
  /**
   * @arg profile: object containing demographic and targeting data.
   */
  this.setProfile = function(profile){
    this.profile = profile;
    return this;
  };
  /**
   * @arg billing: object containing billing information
   *
   * attributes are copied from given object and
   * prefixed with "x_billing_" (e.g.: address1 -> x_billing_address1).
   *
   * - cc_name
   * - cc_num (encrypted)
   * - cc_exp (MM/YYYY)
   * - address1
   * - address2
   * - city
   * - state
   * - zip
   */
  this.setBilling = function(billing){
    this._billing = {};
    for(var attrname in billing){
      this._billing["x_billing_"+attrname] = billing[attrname];
    }
    return this;
  };
  /**
   * @arg referencedFields: object containing ad field names
   * and dotted form.field name where to find.  For example,
   * with {email: 'Login.Email'}, document.forms['Login']['Email']
   * would be where to find a value for "email".
   *
   * ex: setReferencedFields({email: 'reg.email,
   *                          state: {field:'reg.state',
   *                                  label:'State/Province'}})
   */
  this.setReferencedFields = function(referencedFields){
    this._referencedFields = {};
    for(var localname in referencedFields){
      var reference = referencedFields[localname];
      var label = null;
      // XXX split form.field here?
      if(typeof reference == "string"){
        this._referencedFields[localname] = reference;
        label = localname;
        if(/^x_/.test(label)) // strip leading 'x_' (extras)
          label = label.substring(2);
        label = label.replace(/([A-Z])/g, " $1");
        label = label.charAt(0).toUpperCase() + label.substring(1);
      }
      else{
        this._referencedFields[localname] = reference.field;
        label = reference.label;
      }
      this._labels[localname] = label;
    }
    return this;
  }
  
  /**
   * query string fragment for global parameters.
   * 
   * @arg strategy: default strategy if none explicitly set
   */
  this.qs = function(strategy){
    var a = [];
    if(this.strategy)
      a.push("strategy="+encodeURIComponent(this.strategy));
    else if(strategy)
      a.push("strategy="+encodeURIComponent(strategy));
    if(this._ignore){
      for(var n=0; n < this._ignore.length; n++)
        a.push("ignore="+encodeURIComponent(this._ignore[n]));
    }
    return a.join("&");
  };
  
  /**
   * query string fragment for ad keys to exclude.
   * 
   * @arg slotId: optional slot to ignore
   */
  this.qs4exclude = function(slotId){
    var a = [];
    for(var i in this.slots){
      var slot = this.slots[i];
      if(slot.filledWith && (!slotId || slot.container != slotId))
          a.push("exclude="+slot.filledWith);
    }
    return a.join("&");
  };
  
  
  this.qs4tracking = function(gat){
    var a = [];
    for(var attr in gat)
      a.push(encodeURIComponent(attr)+"="+encodeURIComponent(gat[attr]));
    return a.join("&");
  };
  
  /**
   * query string fragment for profile-specific parameters.
   */
  this.qs4profile = function(){
    var a = [];
    for(var attr in this.profile){
      if(this.profile[attr]) 
        a.push(encodeURIComponent(attr)+"="+encodeURIComponent(this.profile[attr]));
    }
    return a.join("&");
  };
  /**
   * query string fragment for billing-specific parameters.
   */
  this.qs4billing = function(){
    var a = [];
    for(var attr in this._billing){
      if(this._billing[attr])
        a.push(encodeURIComponent(attr)+"="+encodeURIComponent(this._billing[attr]));
    }
    return a.join("&");
  }
  
  /**
   * query string fragment for referenced form fields.
   */
  this.qs4referencedFields = function(){
    var a = [];
    for(var attr in this._referencedFields){
      if(this._referencedFields[attr])
        a.push(encodeURIComponent("rFields("+attr+")")+"="+encodeURIComponent(this._referencedFields[attr]));
    }
    return a.join("&");
  };
  
  /**
   * query string fragment for slot-specific parameters.
   * 
   * @arg includeType: whether to include 'type' (see inline comment)
   */
  this.qs4slot = function(slot,includeType){
    var a = [
      "container="+encodeURIComponent(slot.attrs.container),
      "needsform="+encodeURIComponent(slot.attrs.needsform),
      "placementKey="+encodeURIComponent(slot.attrs.placementKey),
      "affiliateCode="+encodeURIComponent(slot.attrs.affiliateCode),
      "fillerUrl="+encodeURIComponent(slot.attrs.fillerUrl)
    ];
    if(slot.attrs.nexturl)  // optional
      a.push("nexturl="+encodeURIComponent(slot.attrs.nexturl));
    if(slot.attrs.y_url)    // optional
        a.push("y_url="+encodeURIComponent(slot.attrs.y_url));
    if(slot.attrs.n_url)    // optional
        a.push("n_url="+encodeURIComponent(slot.attrs.n_url));
    // since multiple slots are possible w/different types
    // but display can only do 1/time, need to be able to
    // to *not* send type (since have have to separate them)
    // However, when doing a single slot (eg submit),
    // it's easiest to include it here.
    if(includeType){
      a.push("type="+encodeURIComponent(slot.attrs.type));
    }
    return a.join("&");
  };
  
  /**
   * args is query string arguments, *needed* for type,
   * adKey, etc...  *only* container is automatically passed.
   * 
   * @arg slots: list of _Slot instances
   * @arg args: object containing query string arguments
   */
  this.displaySlots = function(slots, args){
    var qs = [];
    var strategy;
    for(var name in args)
      qs.push(encodeURIComponent(name) + "=" + encodeURIComponent(args[name]));
    
    if(args['adKey'])
      strategy = 3;
    qs.push(this.qs(strategy));
    for(var i=0; i < slots.length; i++)
      qs.push(this.qs4slot(slots[i], false));
    qs.push(this.qs4tracking(this._GAT));
    qs.push(this.qs4profile());
    qs.push(this.qs4billing());
    qs.push(this.qs4referencedFields());
    qs.push(this.qs4exclude());
    var src = this.baseURL + "ad/get.do?" + qs.join("&");
    // handle embedded single quotes in values (which are valid
    // as uri components, but will result in a truncated src
    // attribute value).  Creating a script element via the
    // DOM didn't work in IE.
    src = src.replace(/\'/g, "&#39;");
    var js = "<" + "script src='" + src + "' type='text/javascript'><" + "/script>";
    document.write(js);
  };
  
  /**
   * Attach a custom ad validator function to a slot.  Overrides
   * any existing, and will be removed when impression is submitted.
   *
   * @arg slotId: DOM element id of slot to attach validator to
   * @arg validate: function that takes form instance and returns
   *                an object with field names containing errors
   *                as keys, and a list of error message strings
   *                for each field with errors.  If no errors,
   *                '{}' should be returned.
   */
  this.setValidator = function(slotId, validate){
    this.slots[slotId].validate = validate;
  }

  /**
   * Attach a pre-submit hook to the slot.
   * 
   * @arg slotId: DOM element id of slot.
   * @arg callback: function that gets called with (form, response)
   *                after getReferencedValues is called but before
   *                the "form data" is submitted.  Return code is
   *                currently ignored, just a hook for e.g. scrubbing.
   */
  this.setBeforeSubmit = function(slotId, beforeSubmit){
    this.slots[slotId].beforeSubmit = beforeSubmit;
  }
  
  /**
   * Fetch and display Ads in designated slots.
   * 
   * For ease of implementation fetching may be
   * done in multiple iterations to avoid conflicts
   * between ad types, requests for specific ads, etc...
   *
   * 1. Ads requested by key are fetched.
   * 2. Ads grouped by type are fetched.
   */
  this.display = function(){
    var keyed = [];
    var typed = {}; // typed[type] -> types
    var types;
    var type;
    var slot;
    var slotId;
    
    // site doesn't exist, db unavailable, etc...
    if(!this.siteKey){
        for(slotId in this.slots){
            slot = this.slots[slotId];
            // blank-out if coreg
            if(slot.attrs.type == "C"){
                this.fill(document, slotId, "-", "<!-- nothing available -->");
            }
            // on to the next if interstitial
            else{
                document.location.href = slot.attrs.nexturl;
                break;
            }
        }
        return;
    }

    // split into either "keyed" or "typed"
    for(slotId in this.slots){
      slot = this.slots[slotId];
      if("adKey" in slot.attrs){
        keyed.push(slot);
      }
      else{
        // split into lists keyed by type
        type = slot.attrs.type;
        if(!(type in typed)){
          types = [];
          typed[type] = types;
        }
        else
          types = typed[type];
        types.push(slot);
      }
    }
    if(keyed.length){
      var adKeys = [];
      for(var k in keyed){
        adKeys.push(keyed[k].attrs.adKey);
      }
      this.displaySlots(keyed, {
          adKey:adKeys.join(),
          type:keyed[0].attrs.type  // XXX just picking first, so mixing/matching won't work
        });
    }
    if(typed){
      for(type in typed){
        this.displaySlots(typed[type], {type:type});
      }
    }
    
  };

  this.fill = function(doc, slotId, adKey, html){
    var slot = this.slots[slotId];
    // reset ad-specific slot attributes
    slot.validate = function(){;};  // blank out existing custom validator
    slot.beforeSubmit = function(){;}; // blank out existing callback
    slot._errorContainer = null;
    doc.getElementById(slotId).innerHTML = html;
    slot.filledWith = adKey;  // can't use adKey -- is already taken
    if(slot.attrs.on_fill){
      slot.attrs.on_fill(slotId, adKey == "-");
    }
  };
  
  /**
   * @arg form: form field instance
   * @arg impressionKey: Impression.Key
   * @arg container: DOM element id of slot
   * 
   * @returns boolean, always false
   */
  this.submit = function(form, impressionKey, container, response){
    response = response || "Y"; // in case of enter in form
    getReferencedValues(form, impressionKey, this._referencedFields);
    var slot = this.slots[container];
    if(slot.beforeSubmit)
      slot.beforeSubmit(form, response);
    if(response == "Y" && !this.validate(form, slot, impressionKey)) {
         return false;
    }
    if(slot.on_submit)
      slot.on_submit(form, impressionKey, response);
    document.getElementById(impressionKey).className = document.getElementById(impressionKey).className + " syndicatedisabled";
    var iqs = serializeForm(form, impressionKey) + "&response|"+impressionKey+"|="+response;
    var type = slot.attrs['type'];
    var src = "http://syndicate.alloyedu.com/syndicate/api/"+impressionKey+"/impression/update.do?";
    var args = [
      this.qs(),
      this.qs4tracking(this._GAT),
      this.qs4profile(),
      this.qs4billing(),
      this.qs4referencedFields(),
      this.qs4slot(slot,true),
      iqs,
      this.qs4exclude(slot.container)
    ];
    src += args.join("&");
    var script = document.createElement('script');
    script.setAttribute('type', 'text/javascript');
    script.setAttribute('src', src);
    document.body.appendChild(script);
    return false;
  };
  
  
  
  /**
   * Builtin validators.
   * 
   * Each validator must have a 'msg' property, the error message,
   * and a 'test' function which takes a form field instance as
   * a parameter and returns a boolean indicating whether the 
   * test passed.
   */
  this.validators = {
    required: {
      msg: "required",
      test: function(obj){
        if (obj.type != "radio" && 
           !(obj.type == "checkbox" && obj.form[obj.name].length))
          return fieldValue(obj);
        else {
          var group = obj.form[obj.name];
          for (var i=0; i<group.length;i++) {
            if (group[i].checked)
              return true;
          }
          return false;
        }
      }
    },
    agerange: {
      msg: "age invalid", /* replaced by one of below */
      minMsg: "must be %{MINAGE} years old",
      maxMsg: "must not be over %{MAXAGE} years old",
      // e.g.: agerange:[min:13],[max:100]
      test: function(obj, args){
        if(!obj.value)
          return true;
        var minAge = parseInt(args[0] || "-1");
        var maxAge = parseInt(args[1] || "-1");
        // no range given
        if(minAge == -1 && maxAge == -1)
          return true;
        var dob = obj.value;
        // Date.parse doesn't like MM-DD-YYY
        if(/^\d{2}-\d{2}-\d{4}$/.test(dob))
          dob = dob.replace("-","/");
        dob = new Date(Date.parse(dob));
        var today = new Date();
        var age = today.getFullYear()-dob.getFullYear();
        if(dob.getMonth() > today.getMonth())
          age--;
        else if(dob.getMonth() == today.getMonth() && dob.getDate() > today.getDate())
          age--;
        // no need to give range, just tell at which end they overshoot
        if(minAge > age && minAge > -1){
          this.msg = this.minMsg.replace('%{MINAGE}', ''+minAge);
          return false;
        }
        else if(age > maxAge && maxAge > -1){
          this.msg = this.maxMsg.replace('%{MAXAGE}', ''+maxAge);
          return false;
        }
        return true;
      }
    },
    cc_exp: {
      msg: "MM/YY expected",
      test: function(obj){
        return !obj.value ||
          /^[01][0-9]\/\d{2}$/.test(obj.value);
      }
    },
    email: {
      msg: "invalid e-mail address",
      test: function(obj){
        return !obj.value ||
        /^[a-z0-9_+.-]+\@([a-z0-9-]+\.)+[a-z0-9]{2,4}$/i.test( obj.value );
      }
    },
    endswith: {
      msg: "must end with \"%{SUFFIX}\"",
      defaultMsg: "must end with \"%{SUFFIX}\"",
      test: function(obj, args){
        var suffix = args[0];
        var valid = false;
        if(!obj.value)
          return true;  // skip if empty
        if(obj.value.length < suffix.length)
          valid = false;
        else
          valid = (obj.value.substring(obj.value.length-suffix.length) == suffix);
        if(!valid)
            this.msg = this.defaultMsg.replace('%{SUFFIX}',suffix);
        return valid;
      }
    },
    equals: {
      msg: "fields don't match",
      test: function(obj, args){
        var otherName = args[0];
        // a bit hacky, but since fields are scoped to impression
        // we need to know this one's key to get to the other
        var impressionKey = obj.name.match(/\|(\w+)\|$/)[1];
        var other = AdSyndicate.fieldFor(obj.form, otherName, impressionKey);
        return !obj.value || obj.value == other.value;
      }
    },
    date: {
      msg: "invalid date",
      test: function(obj){
        return !obj.value ||
        /^\d{1,2}[\/-]\d{1,2}[\/-]\d{4}$/.test(obj.value) ||
        /^\d{4}-\d{1,2}-\d{1,2}$/.test(obj.value);
      }
    },
    minlength: {
      msg: "must be at least %{LENGTH} characters",
      defaultMsg: "must be at least %{LENGTH} characters",
      test: function(obj, args){
        var length = args[0];
        if(!obj.value)
          return true;    // skip empty
        if(obj.value.length < length){
          this.msg = this.defaultMsg.replace('%{LENGTH}',length);
          return false;
        }
        return true;
      }
    },
    phone: {
      msg: "invalid phone number",
      test: function(obj){
        if(!obj.value)
          return true;  // skip if empty
        // regexes from http://en.wikipedia.org/wiki/North_American_Numbering_Plan
        var match = /^([2-9][0-8][0-9])[-. ]?([2-9][0-9][0-9])[-. ]?(\d{4})$/.exec(obj.value)
                 || /^\(([2-9][0-8][0-9])\)\s*([2-9][0-9][0-9])[-. ](\d{4})$/.exec(obj.value);
        if(!match)
          return false;
        // UoP doesn't accept
        if(match[0] == "800" || match[1] == "888" || match[0] == "900")
          return false;
        if(match[2] == "555")
          return false;
        if(match[2].substr(1) == "11")  // X11 is invalid
          return false;
        return true;  // didn't find a problem
      }
    },
    zip: {
      msg: "invalid US zip code",
      test: function(obj){
        return !obj.value ||
        /^\d{5}(?:-\d{4})?$/.test(obj.value);
      }
    },
    zipstate: {
      defaultMsg: "invalid for %{STATE}",
      msg: "invalid for %{STATE}",
      test: function(obj, args){
        var stateField = args[0];
        // a bit hacky, but since fields are scoped to impression
        // we need to know this one's key to get to the other
        var impressionKey = obj.name.match(/\|(\w+)\|$/)[1];
        var state = AdSyndicate.fieldFor(obj.form, stateField, impressionKey);
        if(!obj.value || !fieldValue(state))
          return true;
        var p = STATE_ZIP[fieldValue(state).toUpperCase()];
        if(!p)
          return true;
        if(!p.test(obj.value)){
          this.msg = this.defaultMsg.replace('%{STATE}',fieldValue(state));
          return false;
        }
        return true;  // all is well
      }
    },
    int: {
      msg: "invalid number",
      test: function(obj){
        return !obj.value ||
        /^[0-9]+$/i.test( obj.value );
      }
    },
    float: {
      msg: "invalid number",
      test: function(obj){
        return !obj.value ||
        /^[0-9]+\.*[0-9]*$/i.test( obj.value );
      }
    }
  };

  /**
   * Register a new validator type, addition to builtin.
   *
   * @arg name: validator name (ex: 'email')
   * @arg validator: object with 'msg' property and a 'test' function.
   */
  this.addValidator = function(name, validator){
    this.validators[name] = validator;
  };
  
  /**
   * @arg form: form instance
   * @arg slot: _Slot instance
   * @arg impressionKey: Impression.Key
   * 
   * @returns: boolean, true if valid, otherwise false
   */
  this.validate = function(form, slot, impressionKey){
    var suffix = new RegExp("\\|"+impressionKey+"\\|$");
    var legit = true;
    var errors;
    var field;
    var e;
    // First clear all errors, in case of custom validation
    // exists for any fields with regular validation.
    // This is to avoid having errors from custom
    // validation cleared when regular is run.
    for(var i=0; i < form.elements.length; i++){
      e = form.elements[i];
      if(e.name && e.name.search(suffix) == -1)
        continue; // not associated with given slot
      this.hideErrors(slot, e);
    }
    if(slot.validate){
      errors = slot.validate(form);
      for(field in errors){
        if(errors[field] && errors[field].length){
          e = form[field];
          // field may not actually exist or multiple may be returned
          // in the case of a series of checkboxes, so check that we're
          // passing something that can either be resolved by name
          // or pass the name
          this.showErrors(slot, e.name ? e : field, errors[field]);
          legit = false;  // not legit if any errors
        }
      }
    }
    for(var i=0; i < form.elements.length; i++){
      e = form.elements[i];
      if(e.name && e.name.search(suffix) == -1)
        continue; // not associated with given slot
      errors = this.validateField(e);
      if(errors.length){
        this.showErrors(slot, e, errors);
        legit = false;
      }
    }
    return legit;
  };
  
  /**
   * @arg field: form element instance
   */
  this.hideErrors = function(slot, field){
    var doc = _ownerDocument(field);
    var errorlist = this._errorListFor(slot, field);
    if(!errorlist)
      return;
    var errors = errorlist.childNodes;
    for(var i=0; i < errors.length; i++){
      errorlist.removeChild(errors[i]);
    }
  };
  
  /**
   * @arg field: form element instance
   * 
   * @returns: array of error messages (empty if none)
   */
  this.validateField = function(field){
    var errors = [];
    for(var name in this.validators){
      var re = new RegExp("(^|\\s)" + name + "(\\s|:[A-z0-9_.,-]+|$)");
      var m = re.exec(field.className);
      if(m && m.length){
        var arg = m[2].substring(1);  // name:arg[,arg] drop ':'
        if(!this.validators[name].test(field, arg.split(","))){
          errors.push(this.validators[name].msg);
        }
      }
    }
    return errors;
  };
  
  /**
   * Returns appropriate error list dom element, creating
   * it (for referenced fields) in needed.
   *
   * @arg slot: _Slot instance
   * @arg field: form element instance
   */
  this._errorListFor = function(slot, field){
    var container = null;
    var doc = _ownerDocument(field);
    // in case "field" isn't one -- this allows
    // showing errors that aren't tied to a field
    if(typeof field == "string"){
      container = doc.getElementById(field+'-errors');
    }
    // no reason to create an error list for "real" hidden fields
    else if(field.type == 'hidden'
      && this._referencedFields[field.name.split('|')[0]]){
      container = slot['_errorContainer'];
      if(!container){
        container = doc.createElement('ul');
        container.className = "errors";
        doc.getElementById(slot.attrs.container).appendChild(container);
        slot['_errorContainer'] = container;
      }
    }
    else{
      container = doc.getElementById(field.name+'-errors');
    }
    return container;
  }
  
  /**
   * @arg slot: _Slot instance
   * @arg field: form element instance or name for resolving error target
   * @arg errors: list of error messages
   */
  this.showErrors = function(slot, field, errors){
    var doc = _ownerDocument(field);
    var errorlist = this._errorListFor(slot, field);
    if(!errorlist)
      return;
    var itemType = errorlist.nodeName == "UL" ? "li" : "span";
    var name = typeof field == "string" ? field : field.name;
    var base = name.substring(0,name.indexOf('|'));
    var label = this._labels[base];
    var item = doc.createElement(itemType);
    if(label) // a bit tenuous -- relies only references needing labels
      item.innerHTML = label + ": " + errors.join(", ");
    else
      item.innerHTML = errors.join(", ");
    item.className = "error";
    errorlist.appendChild(item);
    return;
  };

  /**
   * @arg form: form instance
   * @arg fieldName: base name of form field (aka, 'email')
   * @arg impressionKey: Impression.Key
   * 
   * @returns: form field instance
   */
  this.fieldFor = function(form, fieldName, impressionKey){
      return form[fieldName+"|"+impressionKey+"|"];
  };
  
  /**
   * @arg array: array to search
   * @arg find: what to find in array
   *
   * @returns: index or -1, if not found
   */
  this.binarySearch = function(array, find){
    var first = 0;
    var last = array.length-1;
    var mid = Math.floor((first+last)/2);
    while(( first <= last) && array[mid]!=find){
      if(find < array[mid]){
        last = mid-1;
      }
      else{
        first = mid+1;
      }
      mid = Math.floor((first+last)/2);
    }
    return array[mid] == find ? mid : -1;
  }

  /**
   * Returns dom element of containing form or null.
   * 
   * @arg elementId element to get parent form of
   */
  this.getContainingForm = function(elementId){
    var e = document.getElementById(elementId);
    while(e != null){
      if(e.nodeName == 'FORM')
        return e;
      e = e.parentNode;
    }
    return null;
  }

  /**
   * Return form field value or defaultValue, if it doesn't exist.
   *
   * @arg form form instance
   * @arg name field name
   * @arg defaultValue return value if form field doesn't exist
   */
  this.getFormValue = function(form, name, defaultValue){
    // for radios, we'll get an array, not a single element
    if(form[name] && typeof form[name].tagName == "undefined"){
      var v = null;
      for(var n=0; n < form[name].length; n++){
        v = fieldValue(form[name][n]);
        if(v!=null)
          return v;
      }
    }
    else if(form[name])
      return fieldValue(form[name]);
    return defaultValue;
  }
  
};

if(!window.AdSyndicate){
  window.AdSyndicate = new _AdSyndicate();
}

})();

