/**
 * @fileoverview DOM manipulation library
 *
 * @author      WebDialogs
 * @version     1.3
 */

/**
 * @class Encapsulates functions for DOM manipulation.
 *
 * @constructor
 * @return A new instance of WdDom class
 */
function WdDom() {
    /**
     * Call this function when unsure if an argument is a DOM element, or its id.
     * <br>
     * If the type of <code>elem</code> is object, it returns <code>elem</code> itself,
     * otherwise, the result of <code>document.getElementById(elem)</code>.
     *
     * @param   elem    either DOM element (Object), or its id (String)
     * @type    Object
     * @return          DOM element -- either <code>elem</code> itself,
     *                  or the element identified by it
     */
    this.getElement = function(elem) {
        return typeof elem == "object" ? elem : document.getElementById(elem);
    };

    this.getElements = function(elements) {
        if (typeof elements == "string") {
            // a single string; must be element id
            return new Array(this.getElement(elements));
        } else if (!elements.push || !elements.pop || !elements.shift || !elements.unshift) {
            // an object, but not an array
            return new Array(elements);
        } else {
            // an array; could be of either elements, ids, or mixed; traverse the array and eliminate bad references
            var result = new Array();
            for (var e in elements) {
                var element = (typeof elements[e] == "object" ? elements[e] : this.getElement(elements[e]));
                if (element)
                    result.push(element);
            }
            return result;
        }
    };

    this.getBody = function() {
        return document.body ? document.body : document.getElementsByTagName("body")[0];
    };

    /**
     * Creates a DOM element.
     * <br>
     * If provided, assigns the attributes to the newly created element
     * by calling {@link #setAttributes}.
     *
     * @param   {String} tagname    HTML tag name
     * @param   {Object} attributes (optional) Attribute-value pairs object
     * @type    Object
     * @return                      Newly created DOM element
     * @see     #setAttributes
     */
    this.createElement = function(tagname, attributes) {
        var element = document.createElement(tagname);
        if (attributes)
            this.setAttributes(element, attributes);
        return element;
    };

    this.getAttribute = function (element, attribute) {
        if (typeof element == "string")
            element = this.getElement(element);
        if (!element || typeof element != "object")
            return undefined;
        else if (attribute in element)
            return element[attribute];
        else if (attribute == "class" && "className" in element)
            return element.className;
        else if (element.getAttribute)
            return element.getAttribute(attribute);
        else
            return undefined;
    };

    this.getElementsByAttributes = function(attributes, parent, direct) {
        // if an attribute is not specified, depending on the implementation, it may be "" or undefined
        // hence, we count "" equal to either "" or undefined, and vice versa
        function nvl(v, nn) {
            return v == undefined ? nn : v;
        }
        function isRE(what) {
            return (typeof what == "function" || typeof what == "object") && (typeof what.exec == "function" && typeof what.test == "function");
        }
        function toRE(s) {
            return isRE(s) ? s : new RegExp("^" + nvl(s, "") + "$");
        }
        if (parent == undefined)
            parent = this.getBody();
        else if (typeof parent == "string")
            parent = this.getElement(parent);
        var result = new Array();
        if (parent) {
            var elements = direct ? parent.childNodes : parent.getElementsByTagName("*");
            // on Safari, NodeList doesn't implement "in" operator correctly
            // for (var e in elements) {
            for (var e=0; e<elements.length; ++e) {
                var matches = true;
                for (var a in attributes)
                    matches = matches && toRE(attributes[a]).test(nvl(this.getAttribute(elements[e], a), ""));
                if (matches)
                    result.push(elements[e]);
            }
        }
        return result;
    };

    /**
     * Appends one or more children to a parent node.
     * <br><br>
     * <strong>Suggested usage:</strong><ul>
     * <code>
     *   wdDom.appendChildren("div1", "div2");<br>
     *   wdDom.appendChildren("div1", ["div2", "div3"]);<br>
     *   d1 = document.getElementById("div1");<br>
     *   d2 = document.getElementById("div2");<br>
     *   d3 = document.getElementById("div3");<br>
     *   d4 = document.getElementById("div4");<br>
     *   wdDom.appendChildren(d1, d2);<br>
     *   wdDom.appendChildren(d1, [d3, d4]);
     * </code></ul>
     *
     * @type    Object
     * @param   parent      DOM element to append to, or its id
     * @param   children    DOM element, its id, or array of either
     *                      elements or their ids to append
     * @return  {Object}    The parent node with newly appended children
     */
    this.appendChildren = function(parent, children) {
        if (typeof parent == "string")
            parent = this.getElement(parent);
        children = this.getElements(children);
        if (!parent || !children)
            return;
        for (var c in children) {
            if (children[c])
                parent.appendChild(children[c]);
        }
        return parent;
    };

    /**
     * Sets one or more attributes of one or more DOM elements in a single call.
     * If an attribute is an object (eg "style"), it is traversed recursively.
     * <br><br>
     * <strong>Suggested usage:</strong><ul>
     * <code>
     *   wdDom.setAttributes("image1", {width:"20px", height:"40px"});<br>
     *   wdDom.setAttributes(["image1", "image2"], {width:"20px", height:"40px"});<br>
     *   wdDom.setAttributes(myCell, {align:"left", style{fontSize:"12pt"}});
     * </code></ul>
     *
     * @param   elements    DOM element, its id, or array of either
     *                      elements or their ids
     * @param   attributes  atrribute-value pairs object; if a value is an object,
     *                      it is traversed recursively
     * @return  {Array}     array of elements referenced by the input argument <code>elements</code>
     */
    this.setAttributes = function(elements, attributes) {
        elements = this.getElements(elements);
        for (var e in elements) {
            var element = elements[e];
            for (var a in attributes) {
                if (typeof attributes[a] == "object" && a in element && typeof element[a] == "object") {
                    // attribute is an object (eg style)
                    for (var aa in attributes[a]) {
                        if (typeof element[a][aa] != "function" && typeof element[a][aa] != "object") {
                            try {
                                element[a][aa] = attributes[a][aa];
                            } catch (err) {
                                // possibly trying to set a property that has only a getter
                            }
                        }
                    }
                } else {
                    element[a] = attributes[a];
                }
            }
        }
        return elements;
    };

    /**
     * Portable way to set element's maximum height
     * <br>
     * IE doesn't recognize <code>max-height</code> style attribute, but provides workarounds...
     *
     * @param   elements    DOM element, its id, or array of either
     *                      elements or their ids
     * @param   maxH        value for maxHeight
     * @return  {Array}     array of elements referenced by the input argument <code>elements</code>
     */
    this.setMaxHeight = function(elements, maxH) {
    };


    /**
     * Sets one or more style attributes of one or more DOM elements in a single call.
     * A shortcut to <code>setAttributes(element, {style:attributes});</code>.
     * <br><br>
     * <strong>Suggested usage:</strong><ul>
     * <code>
     *   wdDom.setStyle(["image1", "image2"], {width:"20px", height:"40px"});<br>
     *   wdDom.setAttributes(myCell, {fontWeight:"bold", fontSize:"12pt"});
     * </code></ul>
     *
     * @param   elements    DOM element, its id, or array of either
     *                      elements or their ids
     * @param   attributes  atrribute-value pairs object
     * @return  {Array}     array of elements referenced by the input argument <code>elements</code>
     */
    this.setStyle = function(elements, attributes) {
        return this.setAttributes(elements, {style:attributes});
    };

    /**
     * Portable way to set opacity of HTML element(s).
     *
     * @param   elements    DOM element, its id, or array of either
     *                      elements or their ids
     * @param   opacity     a number between 0 (transparent) and 1 (opaque)
     */
    this.setOpacity = function(elements, opacity) {
        this.setStyle(elements, window.ActiveXObject ? {filter:"alpha(opacity="+(opacity*100)+")"} : {opacity:opacity});
    };

    /**
     * Portable way to get opacity of an HTML element. If opacity is not
     * sppecified in element's style, function returns <code>undefined</code>.
     *
     * @param   element     DOM element or id
     * @return  {Number}    a number between 0 (transparent) and 1 (opaque), or
     *                      <code>undefined</code> if opacity is not set or cannot be retrieved
     */
    this.getOpacity = function(element) {
        if (typeof element == "string")
            element = this.getElement(element);
        if (window.ActiveXObject && element.style.filter && element.style.filter.match(/opacity=\d+/))
            return (element.style.filter.match(/opacity=(\d+)/)[1]-0)/100;
        else if (element.style.opacity)
            return element.style.opacity-0;
        else
            return undefined;
    };
}

/**
 * @class General purpose object repository.
 *
 * Maintains an array for storing objects of arbitrary type.
 * The array is created as a property of <code>window</code> object.
 *
 * @constructor
 * @return Creates the repository, and returns new instance of WdRepository class
 */
function WdRepository() {
    // "private" method -- allocates the storage
    this._allocate = function() {
        if (!window[this._rId])
            window[this._rId] = new Array();
    };

    // "private" method -- compacts the storage by removing the empty trailing elements
    this._compact = function() {
        var rep = window[this._rId];
        var len = rep.length;
        for (var i = len-1; i >= 0; --i)
            if (rep[i] == undefined)
                rep.length = i;
            else
                break;
    };

    /**
     * Adds an object to the repository.
     *
     * @type    Number
     * @param   what        An object to add
     * @return  {Number}    The added object's index which can be used to retrieve it
     */
    this.add = function(what) {
        var rep = window[this._rId];
        rep.push(what);
        return rep.length - 1;
    };

    /**
     * Gets an object, residing at the <code>index</code> position, from the repository.
     *
     * @param   index   The index in the repository
     * @return          The object residing at the <code>index</code> position;
     *                  <code>null</code> if <code>index</code> is out of bounds.
     */
    this.get = function(index) {
        var rep = window[this._rId];
        return rep[index];
    };

    /**
     * Removes an object from the repository.
     * It then tries to compact the storage, which means that the index can be reused
     * in the next call to <code>add()</code>.
     *
     * @param   index   The index of the element to be removed
     */
    this.remove = function(index) {
        var rep = window[this._rId];
        if (index > rep.length-1)
            return;
        rep[index] = undefined;
        this._compact();
    };

    this._rId = "_wd_repository";
    this._allocate();
}

/**
 * @class Visual effects.
 *
 * @constructor
 * @return A new instance of WdEffects class
 */
function WdEffects() {
    // "private" method, which is scheduled by fadeIn()
    this._fadein = function(index) {
        var step = 0.25;
        var delay = 0;
        var eco = wdRepository.get(index);
        var elements = eco[0];
        var callback = eco[1];
        var opacity = Math.min(eco[2]+step, 1);
        wdDom.setOpacity(elements, opacity);
        if (opacity == 1) {
            wdRepository.remove(index);
            if (callback)
                callback();
        } else {
            eco[2] = opacity;
            window.setTimeout("wdEffects._fadein(" + index + ");", delay);
        }
    };

    // "private" method, which is scheduled by fadeOut()
    this._fadeout = function(index) {
        var step = 0.25;
        var delay = 0;
        var eco = wdRepository.get(index);
        var elements = eco[0];
        var callback = eco[1];
        var opacity = Math.max(eco[2]-step, 0);
        wdDom.setOpacity(elements, opacity);
        if (opacity == 0) {
            wdRepository.remove(index);
            if (callback)
                callback();
        } else {
            eco[2] = opacity;
            window.setTimeout("wdEffects._fadeout(" + index + ");", delay);
        }
    };

    /** Makes an element visible by gradually increasing its opacity.
     * The recommended usage is to define the element as invisible by setting the style's
     * <code>visibility</code> attribute to <code>hidden</code>, instead of setting the
     * <code>display</code> attribute to <code>none</code>, making its coordinates
     * known at the beggining of the effect.
     *
     * @param   elements    HTML element, its id, or array of either elements or ids.
     * @param   callback    (optional) {function} A function to call after the transition is complete
     *                      (for example, to hide a progress indicator)
     */
    this.fadeIn = function(elements, callback) {
        elements = wdDom.getElements(elements);
        wdDom.setOpacity(elements, 0);
        for (var e in elements) {
            var element = elements[e];
            if (element.style.visibility == "hidden")
                element.style.visibility = "visible";
            if (element.style.display == "none")
                element.style.display = "";
        }
        var index = wdRepository.add([elements, callback, 0]);
        this._fadein(index);
    };

    this.fadeOut = function(elements, callback) {
        elements = wdDom.getElements(elements);
        wdDom.setOpacity(elements, 1);
        var index = wdRepository.add([elements, callback, 1]);
        this._fadeout(index);
    };
}

/**
 * @class Portable functions for querying window and document geometry
 *
 * @constructor
 * @return A new instance of WdGeometry class
 */
function WdGeometry() {
    /**
     * Get the position of the window on the screen.
     * <br>
     *
     * @return  The horizontal position of the window on the screen
     * @see     #getWindowY
     */
    this.getWindowX = function() {
        if (window.screenLeft != undefined) {
            // IE & others
            return window.screenLeft;
        } else {
            // FF & others
            return window.screenX;
        }
    };

    /**
     * Get the position of the window on the screen.
     *
     * @return  The vertical position of the window on the screen
     * @see     #getWindowX
     */
    this.getWindowY = function() {
        if (window.screenTop != undefined) {
            // IE & others
            return window.screenTop;
        } else {
            // FF & others
            return window.screenY;
        }
    };

    /**
     * Get the size of the browser viewport area.
     *
     * @return  The width of the browser's viewport area
     * @see     #getViewportHeight
     */
    this.getViewportWidth = function() {
        if (window.innerWidth) {
            // all but IE
            return window.innerWidth;
        } else if (document.documentElement && (document.documentElement.clientWidth)) {
            // IE6 with DOCTYPE
            return document.documentElement.clientWidth;
        } else {
            // IE4, IE5, and IE6 without DOCTYPE
            return document.body.clientWidth;
        }
    };

    /**
     * Get the size of the browser viewport area.
     *
     * @return  The height of the browser's viewport area
     * @see     #getViewportWidth
     */
    this.getViewportHeight = function() {
        if (window.innerHeight) {
            // all but IE
            return window.innerHeight;
        } else if (document.documentElement && (document.documentElement.clientHeight)) {
            // IE6 with DOCTYPE
            return document.documentElement.clientHeight;
        } else {
            // IE4, IE5, and IE6 without DOCTYPE
            return document.body.clientHeight;
        }
    };

    /**
     * Get the position of the scrollbars.
     *
     * @return  The position of the horizontal scrollbar
     * @see     #getVerticalScroll
     */
    this.getHorizontalScroll = function() {
        if (window.pageXOffset != undefined) {
            // all but IE
            return window.pageXOffset;
        } else if (document.documentElement && (document.documentElement.scrollLeft)) {
            // IE6 with DOCTYPE
            return document.documentElement.scrollLeft;
        } else {
            // IE4, IE5, and IE6 without DOCTYPE
            return document.body.scrollLeft;
        }
    };

    /**
     * Get the position of the scrollbars
     *
     * @return  The position of the vertical scrollbar
     * @see     #getHorizontalScroll
     */
    this.getVerticalScroll = function() {
        if (window.pageYOffset != undefined) {
            // all but IE
            return window.pageYOffset;
        } else if (document.documentElement && (document.documentElement.scrollTop)) {
            // IE6 with DOCTYPE
            return document.documentElement.scrollTop;
        } else {
            // IE4, IE5, and IE6 without DOCTYPE
            return document.body.scrollTop;
        }
    };

    /**
     * Get the size of the document
     *
     * @return  The width of the document
     * @see     #getDocumentHeight
     */
    this.getDocumentWidth = function() {
        if (document.documentElement && (document.documentElement.scrollWidth)) {
            // IE6 with DOCTYPE
            return document.documentElement.scrollWidth;
        } else {
            // all others
            return document.body.scrollWidth;
        }
    };
    /**
     * Get the size of the document
     *
     * @return  The height of the document
     * @see     #getDocumentWidth
     */
    this.getDocumentHeight = function() {
        if (document.documentElement && (document.documentElement.scrollHeight)) {
            // IE6 with DOCTYPE
            return document.documentElement.scrollHeight;
        } else {
            // all others
            return document.body.scrollHeight;
        }
    };
}


/**
 * ready-to-use instances
 */
var wdDom = new WdDom();
var wdRepository = new WdRepository();
var wdEffects = new WdEffects();
var wdGeometry = new WdGeometry();

