// $Id: obdefs.js,v 1.8.2.4 2008-06-10 11:17:00 lbrouwer Exp $
// generate documentation with the "jsdoc-update" command 
/** M_Url 
* Class to manipulate URL components, loosely based on <code>Location</code> object.
* Note: the "mailto:" scheme is not supported.
* @constructor
*/
function M_Url(strUrl) {
    if (! strUrl) strUrl = "";
    // initialisation
    this.parse(strUrl);
};

/**
* Parse argument (url) and update all fields accordingly.
* The URL is interpreted as fully qualified if the
* literal string <code>://</code> is found somewhere,
* otherwise it is supposed to be a partial URL 
* consisting of path, file and query/hash components.
*/
M_Url.prototype.parse = function(strUrl) {
    this.oQuery = new M_Query();
    this.protocol = "";
    this.port = "";
    this.hostname = "";
    this.pathname = "";
    this.hash = "";
    var idx = strUrl.indexOf('://');
    // parse {protocol, host, port} only if protocol is specified
    if (idx >= 0) {
        this.protocol = strUrl.substring(0, idx+1);
        strUrl = strUrl.substring(idx+3);
        idx = strUrl.indexOf('/');
        if (idx == -1) idx = strUrl.indexOf('?'); // look for search (MSIE)
        if (idx == -1) idx = strUrl.length; // no path after host[:port]
        var host = strUrl.substring(0, idx);
        strUrl = strUrl.substring(idx);
        if ((idx = host.indexOf(':')) > -1) {
            this.hostname = host.substring(0, idx);
            this.port = host.substring(idx+1);
        } else this.hostname = host;
    }
    // oherwise treat it as a (absolute/relative) path only
    this.setPathname(strUrl);
};

// getters
/**
 * Returns the full URL as string.
 * Exception: if the <code>hostname</code> component
 * is empty, the returned value is to be interpreted as a partial
 * URL, consisting of path, file, query and/or hash components. 
 * @return String url as in <code>Location.href</code>
 * @see http://www.w3schools.com/htmldom/dom_obj_location.asp the Location object.
 */
M_Url.prototype.getHref = function() {
    // absolute path
    var s = this.getPathname() + this.getSearch() + this.getHash();
    return (this.hostname) ? this.getProtocol() + "//" + this.getHost() + s : s;
};

/**
 * @return String protocol as in <code>Location.protocol</code>
 * @see http://www.w3schools.com/htmldom/dom_obj_location.asp the Location object.
 */
M_Url.prototype.getProtocol = function() {
    return this.protocol;
};

/**
 * Host name, including optional port specifier.
 * @return String host as in <code>Location.host</code>
 * @see http://www.w3schools.com/htmldom/dom_obj_location.asp the Location object.
 */
M_Url.prototype.getHost = function() {
    return this.hostname + ((this.port)?":"+this.port:"");
};

/**
 * @return String port as in <code>Location.port</code>
 * @see http://www.w3schools.com/htmldom/dom_obj_location.asp the Location object.
 */
M_Url.prototype.getPort = function() {
    return this.port;
};

/**
 * @return String hostname as in <code>Location.hostname</code>
 * @see http://www.w3schools.com/htmldom/dom_obj_location.asp the Location object.
 */
M_Url.prototype.getHostname = function() {
    return this.hostname;
};

/**
 * @return String pathname as in <code>Location.pathname</code>
 * @see http://www.w3schools.com/htmldom/dom_obj_location.asp the Location object.
 */
M_Url.prototype.getPathname = function() {
    return this.pathname;
};

/**
 * @return String hash (named anchor or fragment identifier) as in <code>Location.hash</code>
 * Note: this includes the '#' character
 * @see http://www.w3schools.com/htmldom/dom_obj_location.asp the Location object.
 */
M_Url.prototype.getHash = function() {
    return this.hash;
};

/**
 * @return String search (or query string) as in <code>Location.search</code>
 * Note: this includes the '?' character
 * @see http://www.w3schools.com/htmldom/dom_obj_location.asp the Location object.
 */
M_Url.prototype.getSearch = function() {
    var search = this.oQuery.toString();
    return (search) ? "?" + search : "";
};

// setters
/**
 * Set full url as in <code>Location.href</code>
 * Note: this has the same effect as calling method <code>parse(String)</code>
 * @see http://www.w3schools.com/htmldom/dom_obj_location.asp the Location object.
 */
M_Url.prototype.setHref = function(str) {
    // replace all fields
    this.parse(str);
};

/**
 * Set protocol as in <code>Location.protocol</code>
 * @see http://www.w3schools.com/htmldom/dom_obj_location.asp the Location object.
 */
M_Url.prototype.setProtocol = function(str) {
    this.protocol = str;
};

/**
 * Set host as in <code>Location.host</code>
 * @see http://www.w3schools.com/htmldom/dom_obj_location.asp the Location object.
 */
M_Url.prototype.setHost = function(str) {
    var i = str.indexOf(":");
    if (i > -1) {
        this.setHostname(str.substring(0, i));
        this.setPort(str.substring(i+1));
    } else this.setHostname(str);
};

/**
 * Set hostname as in <code>Location.hostname</code>
 * @see http://www.w3schools.com/htmldom/dom_obj_location.asp the Location object.
 */
M_Url.prototype.setHostname = function(str) {
    this.hostname = str;
};

/**
 * Set port as in <code>Location.port</code>
 * @see http://www.w3schools.com/htmldom/dom_obj_location.asp the Location object.
 */
M_Url.prototype.setPort = function(str) {
    this.port = str;
};

/**
 * Update current URL with (relative) url.
 * The String argument is either interpreted as
 * <ul>
 * <li>Fully qualified URL</li>
 * <li>Absolute path</li>
 * <li>Relative path</li>
 * </ul>
 * Note: an empty argument selects the current directory. 
 * Both query and hash are updated as well (or cleared if not part of the new pathname).
 * Relative paths "up" (../ etc) are not supported.
 */
M_Url.prototype.update = function(str) {
    if (/^\w+:\/\/\w/.test(str)) {
        this.parse(str);
        return;
    } else if (str.charAt(0) == "/") { // abs. path
        this.setHash(); // clear hash and query string
        this.setSearch();
        this.setPathname(str); // handles both hash and query
    } else { // rel. path
        var path = this.getPathname();
        var i = path.lastIndexOf('/');
        if (i > -1) {
            path = path.substring(0, i + 1);
        }
        this.setHash();
        this.setSearch();
        this.setPathname(path + str);
    }
};

/**
 * Set pathname as in <code>Location.pathname</code>
 * @see http://www.w3schools.com/htmldom/dom_obj_location.asp the Location object.
 * Note: if a query string or hash are part of the pathname, they will be set as well
 */
M_Url.prototype.setPathname = function(str) {
    var h = str.indexOf("#");
    var q = str.indexOf("?");
    if (h > -1) {
        this.setHash(str);
        str = str.substring(0, h);
    }
    if (q > -1) {
        this.setSearch(str);
        str = str.substring(0, q);
    }
    this.pathname = str;
};

/**
 * Set hash (named anchor, fragment identifier) as in <code>Location.hash</code>.
 * Note: if the String argument does not start with a "#" character, everything before
 * the "#" character is ignored, so you could pass in a full URL as well.
 * @see http://www.w3schools.com/htmldom/dom_obj_location.asp the Location object.
 */
M_Url.prototype.setHash = function(str) {
    if (typeof(str) ==  "undefined") str = "";
    var i = str.indexOf("#");
    switch (i) {
        case -1:
            this.hash = "";
            break;
        case 0:
            this.hash = str;
            break;
        default:
            this.hash = str.substring(i);
    }
};

/**
 * Set search (query string) as in <code>Location.search</code>
 * Note: the entire query is replaced, otherwise see <code>setSearchParam()</code>
 * @see http://www.w3schools.com/htmldom/dom_obj_location.asp the Location object.
 */
// replaces current Query object
M_Url.prototype.setSearch = function(par) {
    this.oQuery = new M_Query(par);
};

/**
 * Set a single query parameter as <code>name, [value]</code> pair.
 * Note: empty values are followed by an "=" sign only if suppressEmptyValues(true) is called
 * on the M_Query object. 
 * @see M_Query()
 */
M_Url.prototype.setSearchParam = function(key, val) {
    this.oQuery.setParam(key, val);
};

/**
 * Delete a single query parameter as <code>name, [value]</code> pair.
 */
M_Url.prototype.deleteSearchParam = function(key) {
    this.oQuery.deleteParam(key);
};

/**
 * Retrieve query object as instance of M_Query().
 * @see M_Query()
 */
M_Url.prototype.getQuery = function() {
    return this.oQuery;
};

/**
 * Retrieve the value for parameter <code>name</code> from the search parameters.
 */
M_Url.prototype.getSearchParam = function(key) {
    return this.oQuery.getParam(key);
};

/**
 * Delete all query parameters.
 */
M_Url.prototype.resetSearch = function() {
    this.oQuery.clear();
};

/**
 * <code>toString()</code> returns the same result as <code>getHref()</code> 
 * @returns String full url
 */
M_Url.prototype.toString = function() {
    return this.getHref();
};


// =========================================== M_Query =====================
/**
 * object to manipulate individual query string parameters.
 * Passing in a query string to the constructor has the same
 * effect as calling the <code>parse(String)</code> method.
 * @constructor
 */
function M_Query(qs) {
    this.suppressEmptyValues = false;
    this.parse(qs || "");
};

/**
 * Resets the query object and clears the string representation.
 */
M_Query.prototype.clear = function() {
    this.Q = new M_OrderedHash();
};

/**
 * Initializes the query object from a query string (starting with the "?" character).
 * Note: the query string may be a full URL, everything beside the 
 * query parameters is ignored.
 * Previous query elements are cleared.
 */
M_Query.prototype.parse = function(qs) {
    this.clear();
    var pair;
    var j;
    var i = qs.indexOf("#"); // strip off hash part
    if (i > -1) qs = qs.substring(0, i);
    i = qs.indexOf("?");
    var rhs = (i >= 0) ? qs.substring(i+1) : qs;
    while(rhs.indexOf("&") != -1) {
        j = rhs.indexOf("&");
        this._parsePair(rhs.substring(0,j));
        rhs = rhs.substring(j+1);
    }
    this._parsePair(rhs);
};

/**
 * @private helper for <code>parse()</code>
 */
M_Query.prototype._parsePair = function(pair) {
    var key;
    var i = pair.indexOf("=");
    if (i == -1) {
        if (pair.length) this.Q.put(unescape(pair), "");
    } else {
        key = unescape(pair.substring(0,i));
        this.Q.put(key, unescape(pair.substring(i+1)));
    }
};

/**
 * @return value for key
 */
M_Query.prototype.getParam = function(key) {
    return this.Q.get(key);
};

/**
 * Set <code>value</code> for <code>key</code>,
 * if <code>key</code> exists its value is overwritten
 */
M_Query.prototype.setParam = function(key, val) {
    this.Q.put(key, val);
};

/**
 * Removes the parameter and its value from the query object
 */
M_Query.prototype.deleteParam = function(key) {
    this.Q.remove(key);
};

/**
 * @return String query as http escaped sequence
 * Note: if a <code>value</code> is empty or undefined, the <code>key</code>
 * is followed by an equal sign "=" unless the property <code>suppressEmptyValues</code>
 * is set to boolean <code>true</code>
 */
M_Query.prototype.toString = function() {
    var str = "";
    var k;
    for (var i = this.Q.iterator(); i.hasNext(); ) {
        k = i.next();
        str += escape(k);
        if ((this.Q.get(k) != "") || (! this.suppressEmptyValues)) str += "=";
        str += escape(this.Q.get(k)) + "&";
    } 
    str = str.substring(0, str.length - 1); // strip trailing &
    return str;
};



// ======================================== M_PageView ==================
/**
 * The M_PageView object encapsulates anything necessary to
 * register a page view with a logging service.
 * Typical usage looks something like:
 * <pre>
 * var PV = new M_PageView();
 * PV.setPrefix("openweb"); // optional, default nothing
 * PV.setPath("/hlp.html"); // optional, default window.location.href
 * PV.setPartner("party");  // optional, default empty
 * PV.setReferrer("/");     // optional, default document.referrer
 * PV.pageView();           // Do it!
 * </pre>
 * @constructor
 */
function M_PageView() {
    this.img = new Image(1,1);

    this.img.owner = this;
    this.img.onload = function() {
        this.owner.random = (new Date()).getTime();
    };

    this.url = new M_Url(window.location.href);
    this.prefix = "ziggo";
    this.partner = "";
    var ref = document.referrer;
    if (ref.charAt(ref.length - 1) == "/") ref = ref.substring(0, ref.length - 1);
    this.referrer = ref;
    this.random = (new Date()).getTime();
    this.statUrl = new M_Url(window.location.protocol + "//nl.sitestat.com/zesko/ziggo/s");
    // first element - counter name must not be followed by an equals character
    this.statUrl.getQuery().suppressEmptyValues = true;
    /**
    * debugging behaviour, default false
    */
    this.debug = false;
    /**
    * allow to override <code>referrer</code> property, based on certain search parameters;
    * default true
    */
    this.allowOverride = true;
};

/* 2007-10-09 BN temporary null function to prevent script errors in WBCS */
M_PageView.prototype.addLabel = function() { /* NOP */ };

/**
 * Commit page view, call page logging service.
 * Registers a hit with current attributes
 */
M_PageView.prototype.pageView = function() {
	if (this.debug) {
		window.alert(this.getCounterUrl().toString().replace(/([?&])/g, "\n$1"));
	}
	this.img.src = this.getCounterUrl();
};

/**
 * Specify a prefix which is prepended before the
 * counter name, regardless of path.
 * Example:
 * <pre>prefix.counter.name</pre>
 */
M_PageView.prototype.setPrefix = function(str) {
      this.prefix += "." + str;
};

/**
 * Page to log a hit for, full url (href property)
 * @see setPath() method
 */
M_PageView.prototype.setHref = function(href) {
    this.url.setHref(href);
};

/**
 * Page to log a hit for, path name
 * @see setHref() method
 */
// page to log, pathname only
M_PageView.prototype.setPath = function(path) {
    this.url.setPathname(path);
};

/**
 * Page to log a hit for, relative path name
 * @see setHref() method
 */
// page to log, pathname only
M_PageView.prototype.updatePath = function(path) {
    this.url.update(path);
};

/**
 * Set a value for the "partner" attribute (optional)
 */
M_PageView.prototype.setPartner = function(str) {
    this.partner = str;
};

/**
 * Specify a referrer, overriding the default, which
 * is initialized with the value of <code>document.referrer</code>
 */
M_PageView.prototype.setReferrer = function(str) {
    this.referrer = str;
};

/**
 * Optional error handler if logging mechanism fails
 */
M_PageView.prototype.setErrorHandler = function(str) {
    if (typeof(str) == "function") {
        this.img.onerror = str;
        this.img.onabort = str;
    }
};

/**
 * Optional handler after logging is complete
 */
M_PageView.prototype.setSuccessHandler = function(str) {
    if (typeof(str) == "function") this.img.onload = str;
};

// getters
/**
 * @return String path component of current logical page hit.
 * Default: the pathname property of the current web page's Location.
 */
M_PageView.prototype.getPath = function() {
    return this.url.getPathname();
};

/**
 * @return String full request through which the page hit is logged.
 */
M_PageView.prototype.getCounterUrl = function() {
    this.statUrl.resetSearch();
    this.statUrl.setSearchParam(this.getPageViewCounterName(), "");
    if (this.partner) this.statUrl.setSearchParam("category", this.partner);
    this.statUrl.setSearchParam("ns__t", this.random);
    var ref = this.getReferrer();
    if (ref) this.statUrl.setSearchParam("ns_referrer", ref);
    if(this.debug) {
      window.alert(this.statUrl);
    }
    return this.statUrl; 
};

/**
 * @return String referring URL.
 * Default: document.referrer property
 * will be overridden when certain query strings are present,
 * depending on the value of the <code>allowOverride</code> property. 
 */
M_PageView.prototype.getReferrer = function() {
    if (this.allowOverride) {
        var Q = this.url.getQuery();
        if (Q.getParam("di") != null) {
            return "athome_from.desktop_icon";
        } else if (Q.getParam("from") != null) {
            return "athome_from." + Q.getParam("from").replace(/\./g, "_");
/* @TODO: remove, no longer supported since 23 Oct 2004
        } else if (Q.getParam("ref") != null) {
            return "athome_ref." + Q.getParam("ref").replace(/\./g, "_");
remove up to here */
        }
    }
    return this.referrer;
};

/**
 * @return String counter name
 */
M_PageView.prototype.getPageViewCounterName = function() {
    var path = this._counterName(this.url.pathname);
    if (! this.prefix) path = path.substring(1);
    return this.prefix + path;
};

/**
 * @private
 * Note: directory index is indicated as ".index" in counter name
 */
M_PageView.prototype._counterName = function(path) {
    path = path
    	.replace(/(\.|\s)/g, "_")
    	.replace(/[\/]/g, ".")
    	.replace(/[^\w\._]/g, "");
    if (path.lastIndexOf(".") == path.length - 1) path += "index";
    return path;
};

// ============================== Click extends PageView ======================
/**
 * The M_Click object encapsulates anything necessary to
 * register a click in or click out (default) hit.
 * Typical usage looks something like:
 * <pre>
 * var C = new M_Click();
 * C.setClickType("clickin");  // optional, default <code>clickout</code>
 * C.setClickTo("http://..."); // destination
 * C.redirect();               // jump to destination
 * ...or...
 * C.setTargetWin("my_win");   // optional, default <code>"nl_homegateway"</code>
 * C.openWin();                // open window
 * </pre>
 * @constructor
 */
M_Click = function(base) {
    this.prefix = "clickout"; // default
    this.targetWin = "nl_homegateway";
    this.timeout = 2500;
    this.dest = "_uninitialized"; // real destination URL
    this.logUrl = "";    // destination URL for logging purposes
    this.tag = "";       // override origin tag
    this.logging = true; // wether we do logging

    // kludge, should be improved...
    if (typeof(base) == "object") this.setPath(base.getPath());
};
// Inheritance, kind of...
M_Click.prototype = new M_PageView();

/** Set the click type to <code>clickout</code> (default) or <code>clickin</code>
* @param type String, should be one of {"clickout", "clickin"}
*/
M_Click.prototype.setClickType = function(type) {
    this.prefix = type;
};
/** Set the target window name, default <code>nl_homegateway</code>
* @param wname String
*/
M_Click.prototype.setTargetWin = function(wname) {
    this.targetWin = wname;
};
/** Set the destination URL for the click
* @param dest String
*/
M_Click.prototype.setClickTo = function(dest) {
    this.dest = dest;
};
/** Specify a tag name for the origin part of the click log.
* <code>cickout.<b>tagName</b>.clickTo.<b>destination.url</b></code>
* @param tag String, e.g. "tagName"
* Note: reserved characters are replaced by private function _counterName(tag).
*/
M_Click.prototype.setClickTag = function(tag) {
    this.tag = this._counterName(tag);
};
/** Specify an alternative URL for logging the destination (replaces
* the destination URL only for logging purposes).
* @param dest String
*/
M_Click.prototype.setLogUrl = function(dest) {
    this.logUrl = dest;
};
/** Specify whether the current action will be logged (default true)
* @param lg Boolean
*/
M_Click.prototype.setLogging = function(lg) {
    this.logging = !!lg; // convert to boolean
};
// getters
/** Get the tag name for the origin part of the click log.
* <code>cickout.<b>tagName</b>.clickTo.<b>destination.url</b></code>
* @return String, e.g. "tagName" (default: pageViewCounterName)
*/
M_Click.prototype.getClickTag = function() {
    return (this.tag) ? this.prefix + "." + this.tag : this.getPageViewCounterName();
};
M_Click.prototype.getClickCounterName = function() {
    var dUrl = new M_Url((this.logUrl) ? this.logUrl : this.dest);
    var host = dUrl.getHostname();
    var path = dUrl.getPathname();
    return this.getClickTag() +                    // prefix plus local counter name
        ".clickTo" +                               // literal
        (host && this._counterName("/" + host)) +  // host, preceded with a slash -> period
        (path && this._counterName(path));         // path, always starts with a slash
};
M_Click.prototype.getCounterUrl = function() {
    this.statUrl.resetSearch();
    this.statUrl.setSearchParam(this.getClickCounterName(), "");
    this.statUrl.setSearchParam("ns_type", this.prefix);
    this.statUrl.setSearchParam("ns_url", "[http://www.home.nl/images/blank.gif]");
    this.statUrl.setSearchParam("ns__t", this.random);
    return this.statUrl; 
};
/** Will the current action be logged (depends on setting and internal logic)
* @return Boolean
*/
M_Click.prototype.getLogging = function() {
    // don't apply logging on "clickin/clickout" links (via statUrl service)
    if (this.dest.toLowerCase().indexOf(this.statUrl.getHostname() + this.statUrl.getPathname()) >= 0) {
        return false;
    }
    return this.logging;
};

/** 
* redirect current window to destination URL
*/
M_Click.prototype.redirect = function() {
	if (this.timer) clearTimeout(this.timer);
    if (this.getLogging()) {
        this.setSuccessHandler(new Function("", "window.location.href=\"" + this.dest + "\";")); 
        this.pageView();
        if (! this.debug) {
            this.timer = setTimeout("window.location.href=\"" + this.dest + "\";", this.timeout);
        }
    } else {
        window.location.href = this.dest;
    }
};
/** 
* open destination URL in a (named) window
*/
M_Click.prototype.openWin = function() {
    var widgets = (/MSIE 5\.\d+; Mac/.test(navigator.userAgent))
        // specify reasonable widges (default: none) for Mac MSIE 5.x
        ? 'location=yes,menubar=yes,resizable=yes,scrollbars=yes,status=yes,toolbar=yes'
        : '';
    try {
        this.getLogging() && this.pageView();
        if (this.debug) {
            if (! window.confirm("Open link:\n" + this.dest)) return;
        }
        window.open(this.dest, this.targetWin, widgets).focus();
    }
    catch (e) {
        if (this.debug) window.alert("Exception: " + ((ex.message) ? ex.message : ex.toString()));
    };
};


// ========================================= M_OrderedHash =================
/**
 * Hash list, which behaves like a fifo (first in, first out).
 * This is <em>not guaranteed</em> with regular JavaScript objects; 
 * methods look a lot like Java's Collections.
 * @constructor
 */
function M_OrderedHash() {
    this.idx = 0;
    this.idxAr = new Array();
    this.hash = new Object();

/** 
 * Store or replace a value <code>val</code> for key <code>key</code>.
 * @param key String
 * @param val Object
 */
M_OrderedHash.prototype.put = function(key, val) {
    if (typeof(this.hash[key]) == 'undefined') {
        this.idxAr[this.idxAr.length] = key;
    }
    this.hash[key] = val;
};

/** 
 * Retrieve the value for key <code>key</code>.
 * @return Object
 */
M_OrderedHash.prototype.get = function(key) {
    return this.hash[key];
};

/** 
 * halfway mimicked Java iterator, initializes <code>hasNext()</code> and <code>next()</code>
 * <em>NOTE:</em> there can be only one iterator active at a time!
 * @return Object <code>this</code>
 */
M_OrderedHash.prototype.iterator = function() {
    this.idx = 0;
    return this;
};

/** 
 * Returns true if there are elements left on the iterator
 * @return boolean
 * @see <code>iterator()</code>
 */
M_OrderedHash.prototype.hasNext = function() {
    return (this.idx < this.idxAr.length);
};

/** 
 * Returns next object from the iterator
 * @return Object
 * @see <code>iterator()</code>
 */
M_OrderedHash.prototype.next = function() {
    var key = this.idxAr[this.idx];
    this.idx++;
    return key;
};

/** 
 * Removes element from the hash; ignored if <code>key</code> doesn't exist.
 */
M_OrderedHash.prototype.remove = function(key) {
    delete this.hash[key];
    for (var i=0; i<this.idxAr.length; i++) {
        if (key == this.idxAr[i]) {
            this._splice(i, 1);
            break;
        }
    }
};

/**
 * Implements <code>Array.splice()</code> for MSIE 5.0 compatibility
 * @private
 */
M_OrderedHash.prototype._splice = function(start, len) {
    if (typeof(Array.prototype.splice) == 'function') {
        this.idxAr.splice(start, len);
    } else {
        // NOTE: no sanity checking on bounds
        for (var j = start; j<(this.idxAr.length - len); j++) {
            this.idxAr[j] = this.idxAr[j + len];
        }
        this.idxAr.length -= len;
    }
};
}; // M_OrderedHash()
