/* -*- coding:utf-8 -*- */
/*
Copyright © 2006 Daniel C. Silverstein (dans@csua.berkeley.edu)

This program is free software; you can redistribute it and/or modify
it under the terms of the GNU Lesser General Public License as
published by the Free Software Foundation; either version 2.1 of the
License, or (at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public
License along with this program; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor,
Boston, MA  02110-1301  USA

When used as a component in Google Homepage API modules, this program
is also subject to the Google Homepage API Terms and Conditions located
at http://www.google.com/apis/homepage/terms.html.
*//*
<rdf:RDF xmlns="http://web.resource.org/cc/"
    xmlns:dc="http://purl.org/dc/elements/1.1/"
    xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
<Work rdf:about="">
   <license rdf:resource="http://creativecommons.org/licenses/LGPL/2.1/" />
   <dc:type rdf:resource="http://purl.org/dc/dcmitype/Software" />
</Work>

<License rdf:about="http://creativecommons.org/licenses/LGPL/2.1/">
<permits rdf:resource="http://web.resource.org/cc/Reproduction" />
   <permits rdf:resource="http://web.resource.org/cc/Distribution" />
   <requires rdf:resource="http://web.resource.org/cc/Notice" />
   <permits rdf:resource="http://web.resource.org/cc/DerivativeWorks" />
   <requires rdf:resource="http://web.resource.org/cc/ShareAlike" />
   <requires rdf:resource="http://web.resource.org/cc/SourceCode" />
</License>

</rdf:RDF>
*/

// Javascript support functions, these eventually will live in js_lang.js
function map(f, array0) {
    // Question: Do I need defensive code in case this is null?
    var result = [], args = [];

    // alert("map: this: " + this);

    if (arguments.length == 2) { // Make the common case fast
        for (var i = 0; i < array0.length; i++) {
            result.push(f.call(this, array0[i]));
        }
    } else {
        for (var j = 0; j < array0.length; j++) {
            for (var i = 1; i < arguments.length; i++) {
                args.push(arguments[i][j]);
            }
            result.push(f.apply(this, args));
            args = [];
        }
    }

    return result;
}

function foreach(array, f) {
    // alert("foreach: this: " + this);
    for (var i = 0; i < array.length; i++) { f(array[i]); }
}

function toArray (maybeArray) {
    if (typeof maybeArray == "object" && maybeArray.constructor == Array) {
        return maybeArray;
    }

    if (null == maybeArray) { return []; }

    return [maybeArray];
};
////////////////////////////////////////////////////////////////////////
// Regex builder functions and vars, these will eventually live in igregex.js
// Match an open tag with no attributes
function openTagNoAttrs (tag) { return '<\\s*' + tag + '\\s*/?>'; }

// Match an open tag with exactly the specified attributes
function openTagAllAttrsEx (tag, attrs) {
    var regex = '<\\s*' + tag + '\\s*';
    attrs = toArray(attrs);
    // Build the expression: (?:(?:attr0="?val0"?|...|attrN="?valN"?))
    regex += '(?:' + altAttrs(attrs) + '\\s*){' + attrs.length + '}';
    regex += '/?>'; 
    return regex;
}

// Match an open tag with all the specified attributes, and, optionally others
function openTagAllAttrsInc (tag, attrs) {
    var optAttrs = '(?:.|\\s)*?', altClause = '', regex = '<\\s*' + tag;
    attrs = toArray(attrs);
    /* Build an expression that contains one alternation clause for
       each possible value, i.e.
       optAttrs + (?:attr0="?val0"?|attr1="?val1"?|attr2="?val2") +
       optAttrs + (?:attr0="?val0"?|attr1="?val1"?|attr2="?val2") +
       optAttrs + (?:attr0="?val0"?|attr1="?val1"?|attr2="?val2")
    */
    altClause = altAttrs(attrs);
    for (var i = 0; i < attrs.length; i++) { regex += optAttrs + altClause; }
    regex += optAttrs + '/?>';
    return regex;
}

// Match an open tag with any of the specified attrs, but no others
function openTagAnyAttrsEx (tag, attrs) {
    attrs = toArray(attrs);
    return '<\\s*' + tag + '\\s*' +
           '(?:' + altAttrs(attrs) + '\\s*){0,' + attrs.length + '}' +
           '/?>';
}

// Match an open tag with at least one of the specified attrs, and optionally others
function openTagAnyAttrsInc (tag, attrs) {
    attrs = toArray(attrs);
    return '<\\s*' + tag + '(?:.|\\s)*?' + altAttrs(attrs) + '(?:.|\\s)*?' + '/?>';
}

function altAttrs (attrs) {
    var regex = '', attr = null, name = 0, value = 1;
    /*
    for (var i = 0; i < attrs.length; i++) {
        attr = attrs[i];
        regex += attr[name] +
                 (attr.length > 1 ? '="?' + attr[value] + '"?' : '') +
                 '|';
        }
    */

    foreach(attrs, function (attr) {
        regex += attr[name] +
                 (attr.length > 1 ? '="?' + attr[value] + '"?' : '') +
                 '|';
    });

    regex = regex.substring(0, regex.length - 1); // trim last pipe (|)
    return '(?:' + regex + ')';
}

function closeTag (tag) { return '<\\s*/' + tag + '\\s*>'; }
function optCloseTag (tag) { return '(?:<\\s*/' + tag + '\\s*>)?'; }

var anyText = '(?:.|\\s)+?';
var optAnyText = '(?:.|\\s)*?';

function makeSubExp (regex) { return '(' + regex + ')'; }

////////////////////////////////////////////////////////////////////////
// DOM functions and vars, these will eventually live in igdom.js
function hasAllAttrsEx (element, attrs) {
    var matchedAttrs = 0, name = 0, value = 1;
    foreach(attrs, function (attr) {
        var eAttr;
        if (eAttr = element.attributes.getNamedItem(attr[name])) {
            if (eAttr.nodeValue == attr[value]) { matchedAttrs++; }
            else { /* Break to labeled statement, if I can get labels working */ }
        }
    });
    // Labeled statement goes here
    
    if (element.attributes.length == matchedAttrs) { return true; }
    return false;
}

function hasAllAttrsInc (element, attrs) {
    var matchedAttrs = 0, name = 0, value = 1;
    foreach(attrs, function (attr) {
        var eAttr;
        if (eAttr = element.attributes.getNamedItem(attr[name])) {
            if (eAttr.nodeValue == attr[value]) { matchedAttrs++; }
            else { /* Break to labeled statement, if I can get labels working */ }
        }
    });
    // Labeled statement goes here
    
    if (attrs.length == matchedAttrs) { return true; }
    return false;
}

////////////////////////////////////////////////////////////////////////
// Utility filters, don't know if these stay in igmonkey.js or not
function sFltrViewHTML (canvas) {
    var html = canvas.innerHTML.replace(/&/g, "&amp;");
    html = html.replace(/</g, "&lt;");
    html = html.replace(/>/g, "&gt;");
    canvas.innerHTML = "<code>" + html + "</code>";
    return canvas;
}

// TODO: Implement me!
function sFltrEvalHTML (canvas) {
    alert("sFltrEvalHTML is an unimplemented stub function");
}

function sFltrHideCanvas (canvas) {
    // nb, verify this works when position is set/defaults to static
    canvas.style.visibility = "hidden";
    return canvas;
}

function sFltrShowCanvas (canvas) {
    // nb, verify this works when position is set/defaults to static
    canvas.style.visibility = "visible";
    return canvas;
}

function qFltrBustCacheDay (request) {
    var now = new Date();
    return request + "&" + "igbuster=" + (now.getUTCMonth() + 1) +
        now.getUTCDate() + now.getUTCFullYear();
}

function qFltrBustCacheHour (request) {
    var now = new Date();
    return request + "&" + "igbuster=" + (now.getUTCMonth() + 1) +
        now.getUTCDate() + now.getUTCFullYear() + now.getUTCHours() + "00";
}

/* Using a new cache buster every minute hammers the Google Content
 * Proxy cache.  Do not use this unless the content you are retrieving
 * updates repeatedly every hour.
 */
function qFltrBustCacheMinute (request) {
    var now = new Date();
    return request + "&" + "igbuster=" + (now.getUTCMonth() + 1) +
        now.getUTCDate() + now.getUTCFullYear() + now.getUTCHours() +
        now.getUTCMinutes();
}

/* Using the real time cache buster hammers the Google Content Proxy
 * cache.  Do not use this unless the content you are retrieving
 * really updates in real time.
 */
function qFltrBustCacheRealTime (request) {
    var now = new Date();
    return request + "&" + "igbuster=" + (now.getUTCMonth() + 1) +
        now.getUTCDate() + now.getUTCFullYear() + now.getUTCHours() +
        now.getUTCMinutes() + now.getUTCSeconds() + now.getUTCMilliseconds();
}

////////////////////////////////////////////////////////////////////////
// Core IGMonkey code
function IGMonkey(requestURLs, sourceNames, canvases) {
    this.reqURLs = toArray(requestURLs);
    this.canvases = toArray(canvases);
    this.srcNames = toArray(sourceNames);

    this.requestFilters = toArray(arguments[3]);
    this.resultFilters = toArray(arguments[4]);

    if (arguments[5]) {
        this.errMsgs = toArray(arguments[5]);
    } else {
        var errMsgs = this.errMsgs = []; // this isn't *this* in foreach loops
        foreach(this.srcNames,
                function (src) {
                    errMsgs.push("error querying " + src +
                                 "<br />please try again later");
                });
    }
}

IGMonkey.prototype.toString = function () { return "[object IGMonkey]" };

IGMonkey.prototype.exec = function () {
    // alert("IGMonkey.exec: this: " + this);
    this.reqURLs = map.call(this, this.filterRequest, this.reqURLs);

    for (var i = 0; i < this.canvases.length; i++) {
        this.canvases[i].innerHTML = "<p>querying " + this.srcNames[i] + "</p>";
    }
    /* Alternatively:
    map(function (canvas, src) { canvas.innerHTML = "<p>querying " + src + "</p>"; },
        this.canvases, this.srcNames);
    */

    this.canvases = map.call(this, function (request, canvas, errMsg) {
            var filteredCanvas = null, callObj = this;
            // alert ("anon: about to call _IG_FetchContent this: " + this);
            _IG_FetchContent(request, function (response) {
                                 if (response == "") {
                                     canvas.innerHTML = errMsg; return;
                                 }
                                 canvas.innerHTML = response;
                                 filteredCanvas = callObj.filterResult(canvas);
                                 
                                 /*
                                 response = "<p>continuation: this: " + this +
                                     "<br />filteredCanvas: " + filteredCanvas +
                                     "<br />callObj: " + callObj +
                                     "</p>" + "<hr />" + response;
                                 var f = function (r) {
                                     r = "<p>f: this: " + this + "</p>" + 
                                     "<br />f: filteredCanvas: " + filteredCanvas +
                                     "<br />f: callObj: " + callObj +
                                     "</p>" + "<hr />" + r;
                                     if (r == "") {
                                         canvas.innerHTML = errMsg; return;
                                     }
                                     canvas.innerHTML = r;
                                     filteredCanvas = this.filterResult(canvas);
                                 };
                                 f.call(this, response);
                                 */
                             });
            return filteredCanvas;
        },
        this.reqURLs, this.canvases, this.errMsgs);
    /*
    this.canvases = map.call(this, function (request, canvas, errMsg) {
            var filteredCanvas = null;
            _IG_FetchContent(request, function (response) {
                                 if (response == "") {
                                     canvas.innerHTML = errMsg; return;
                                 }
                                 canvas.innerHTML = response;
                                 filteredCanvas = this.filterResult(canvas);
                             });
            return filteredCanvas;
        },
        this.reqURLs, this.canvases, this.errMsgs);
    */
    /* The following looks like an alternative to the above, but it won't
       work.  The this pointer won't have the right value in the
       continuation function.  One solution would be to assign
       this.canvases to a local var, e.g. var c = this.canvases, push a
       new scope onto the call chain, and invoke _IG_FetchContent from
       inside there, but this is more hopes than I care to jump through.
    for (i = 0; i < this.reqURLs.length; i++) {
        _IG_FetchContent(this.reqURLs[i], function (response) {
                             if (response == "") {
                                 this.canvases[i].innerHTML = this.errMsgs[i];
                                 return;
                             }
                             this.canvases[i].innerHTML = response;
                             alert("Calling filterResult");
                             this.canvases[i] = filterResult(canvases[i]);
                         });
    }
    */
};

// Check that this actually returns what you think it does.  i.e. does
// the request = filter(request) actually modify the request we return?

IGMonkey.prototype.filterRequest = function (request) {
    // alert("IGMonkey.filterRequest: this: " + this);
    foreach(this.requestFilters,
            function(filter) { request = filter(request); });
    // alert("Returning from filterRequest");
    return request;
};

IGMonkey.prototype.filterResult = function (canvas) {
    // alert("IGMonkey.filterResult:this " + this);
    foreach(this.resultFilters,
            function(filter) { canvas = filter(canvas); });
    // alert("Returning from filterResult");
    return canvas;
};
