// javascript to support behavioural targeted ad serving. spec on s:\
// Jonathan Hedley, 2005-01-31
// $Id: behave.js,v 1.3 2005/02/16 04:17:48 jhedley Exp $
// state cookies: t_<section>=yyyymmdd-hhmm:n|n|n|n... ; e.g. t_business=20050131-1454:5|3|4|0|0|0
// time is user's time of last hit to section. count is number of hits to section on that day. index 0 is today.

// ad cookies: s_<section>=[1|0]  ; e.g s_business=1  . Assumes false if not present.

// namespace: bhv_

// -------------- lib objects
// a tracked section. constructor takes _section_ name and cookie val as args, splits as appropriate.
// may also be called with just sectName
function TrackedSection(sectName, cookieVal) {
    this.sectName  = sectName;
    this.cookieVal = cookieVal;

    var match;
    if (cookieVal && (match = cookieVal.match(/^(\d{4,})(\d{2,})(\d{2,})-(\d{2,})(\d{2,}):(.*)$/))) {
        // parse the val into date / time / count string components
        // all date handling is user localtime, 'cept when gmting to cookie expiries
        this.lastHit = new Date(match[1], Number(match[2]) - 1, match[3], match[4], match[5]);

        // split the count string and cast to primitives
        var counts = match[6].split(/\|/);
        var countArray = new Array(counts.length);
        for (var i in counts) {
            countArray[i] = Number(counts[i]);
        }
        this.dayCounts = countArray;
    }
    else {
        this.lastHit    = new Date();
        this.dayCounts  = new Array();
    }

    // trim day counts to correct size and zero undefined fields
    if (bhv_sectionConfigs[sectName]) {
        this.dayCounts.length = bhv_sectionConfigs[sectName].numDays;
        for (var i = 0; i < this.dayCounts.length; i++) { // in does not enumerate undefined values
            if (this.dayCounts[i] === undefined) { this.dayCounts[i] = 0; }
         }
    }
}

// Sums the contents of the dayCounts array
TrackedSection.prototype.sumCounts = function() {
    var sum = 0;
    for (var i in this.dayCounts) {
        sum += this.dayCounts[i];
    }
    return sum;
}

// given an input date, calculate and return the day counts appropriately. object method
// expects newDate > lastHit
TrackedSection.prototype.calculateDayCounts = function(newDate) {
    // get the num of days delta, left pad array with that num zeroes, then right trim
    var oldDays = new Date(this.lastHit.getFullYear(), this.lastHit.getMonth(), this.lastHit.getDate());
    var newDays = new Date(newDate.getFullYear(), newDate.getMonth(), newDate.getDate());
    var deltaDays = Math.floor((newDays.getTime() - oldDays.getTime()) / (1000 * 60 * 60 * 24));

    var newDayCounts = new ArrayClone(this.dayCounts);
    for (var i = 0; i < deltaDays; i++) {
        newDayCounts.unshift(0);
        newDayCounts.pop();
    }

    return newDayCounts;
}

// increments the tracked section:- sets the array (via calculateDayCounts), sets last hits, and increments today
TrackedSection.prototype.increment = function() {
    var now = new Date();
    this.dayCounts = this.calculateDayCounts(now);
    this.lastHit = now;
    this.dayCounts[0]++;
}

// serialises the lastHit and dayCounts to match the expected constructor cookieVal input
TrackedSection.prototype.getStateCookieString = function() {
    return formatDate(this.lastHit) + ":" + this.dayCounts.join("|");
}

// saves a tracked section cookie
TrackedSection.prototype.save = function() {
    // we (arbitrarily) set the expiry date to the number of days we want to track over + 1
    var expiryDate = getDateDaysDelta(this.lastHit, bhv_sectionConfigs[this.sectName].numDays);
    var cookieString = "t_" + this.sectName + "=" + this.getStateCookieString() + "; expires=" + expiryDate.toGMTString() + "; path=/; domain = ." + document.domain;

    document.cookie = cookieString;
}

// configure a section to track. constructor also adds to trackSections hash.
function SectionConfig(sectName, minHits, numDays) {
    this.sectName = sectName; // name of the section
    this.minHits  = minHits;  // min num of hits before setting targeting cookie
    this.numDays  = numDays;  // number of days to watch over

    bhv_sectionConfigs[sectName] = this;
}

// constructs a clone of an array
function ArrayClone(input) {
    var clone = new Array(input.length);
    for (var i in input) {
        clone[i] = input[i];
    }
    return clone;
}

// -----------


// configuration.
var bhv_sectionConfigs = new Object(); // hash
new SectionConfig("technology", 3, 13);
new SectionConfig("business", 1, 13);
new SectionConfig("travel", 1, 13);

var bhv_currentSection = (typeof s_channel == "undefined" ? "undefined" : s_channel).toLowerCase(); // site catalyst value.

var bhv_cookies = parseCookies(document.cookie);

if (bhv_sectionConfigs[bhv_currentSection]) { applyHitCount(bhv_currentSection); }
setAdCookies();


// given client's cookies, returns a struct: a hash of TrackedSection objects, keyed by sectName, and a hash of ad cookies
function parseCookies(allCookies) {
    var states  = new Object();
    var ads     = new Object();
    var struct  = new Object();

    var cookies = allCookies.split(/\s*;\s*/);
    for (var i in cookies) {
        var cookie = cookies[i].split(/=/);
        var match = cookie[0].match(/^(t|s)_(\w+)/);
        if (match != null) {
            var type = match[1];
            var sectName = match[2];
            if (type == "t") {
                states[sectName] = new TrackedSection(sectName, cookie[1]);
            }
            else {
                ads[sectName] = cookie[1];
            }
        }
    }
    struct.states = states;
    struct.ads = ads;
    return struct;
}

// increment the hit count if applicable, and save the new state. Will create a new TrackedSection if required.
function applyHitCount(currentSection) {
    if (currentSection === undefined || bhv_sectionConfigs[currentSection] === undefined) { return false; } // don't track this section
    var track;
    if (bhv_cookies.states[currentSection] === undefined) { // we need to create it
        track = new TrackedSection(currentSection);
        bhv_cookies.states[currentSection] = track;
    }
    else { // came from cookie
        track = bhv_cookies.states[currentSection];
    }
    track.increment();
    track.save();
}

// go through the section configs. flip the ad cookies on or off when required.
function setAdCookies() {
    for (var sectName in bhv_sectionConfigs) {
        var config = bhv_sectionConfigs[sectName];
        var track = bhv_cookies.states[sectName];
        var adCookie = bhv_cookies.ads[sectName];
        var now = new Date();

        if (track) { // user has counts for this section
            // calculate the day counts. we don't bother persisting this.
            track.dayCounts = track.calculateDayCounts(now);
            if (track.sumCounts() >= bhv_sectionConfigs[sectName].minHits) { // enough
                if (!adCookie || adCookie != "1") { enableAdCookie(sectName) }
            }
            else if (adCookie && adCookie == "1") { disableAdCookie(sectName) } // disable as not enough
        }
        else if (adCookie && adCookie == "1") { disableAdCookie(sectName) } // disable as not tracked
    }
}

function enableAdCookie(sectName) {
    var expiryDate = getDateDaysDelta(new Date(), bhv_sectionConfigs[sectName].numDays + 1);
    var cookieString = "s_" + sectName + "=1; expires=" + expiryDate.toGMTString() + "; path=/; domain = ." + document.domain;

    document.cookie = cookieString;
}

function disableAdCookie(sectName) {
    var expiryDate = new Date(0);
    var cookieString = "s_" + sectName + "=0; expires=" + expiryDate.toGMTString() + "; path=/; domain = ." + document.domain;

    document.cookie = cookieString;
}


// formats a datetime to yyyymmdd-hhmm
function formatDate(date) {
    var dateString = "" + date.getFullYear() + zeroPad(date.getMonth() + 1) + zeroPad(date.getDate());
    var timeString = "" + zeroPad(date.getHours()) + zeroPad(date.getMinutes());
    return dateString + "-" + timeString;
}

// zero pads a number to 2 sigfigs. always returns a string.
function zeroPad(num) {
    return (num < 10) ? "0" + num : "" + num;
}

// returns a new date that is daysDelta greater then the input data
function getDateDaysDelta(oldDate, daysDelta) {
    return new Date( oldDate.getTime() + (1000 * 60 * 60 * 24 * daysDelta) );
}


