////////////////////////////////////////////////////////////////////////////////
// Constants
////////////////////////////////////////////////////////////////////////////////

var __CFELEMENT_ERROR_CHILDREN = "element's children are unavailable";
var __CFELEMENT_ERROR_DIMENSIONS = "element dimensions are unavailable";
var __CFELEMENT_ERROR_GET_HTML = "can't get element's inner html";
var __CFELEMENT_ERROR_ID = "invalid element id";
var __CFELEMENT_ERROR_INVALID_CLASS = "invalid class name";
var __CFELEMENT_ERROR_PARENT = "element's parent is unavailable";
var __CFELEMENT_ERROR_POSITION = "element position is unavailable";
var __CFELEMENT_ERROR_SET_ATTRIBUTE = "can't set element's attribute";
var __CFELEMENT_ERROR_SET_HTML = "can't set element's inner html";

var __CFELEMENT_VALID_CSS_CLASS_CHARS = String.ALPHANUMERIC + '-';

var CFELEMENT_NODE_TYPE_ELEMENT = 1;
var CFELEMENT_NODE_TYPE_ATTRIBUTE = 2;
var CFELEMENT_NODE_TYPE_TEXT = 3;
var CFELEMENT_NODE_TYPE_CDATA_SECTION = 4;
var CFELEMENT_NODE_TYPE_ENTITY_REFERENCE = 5;
var CFELEMENT_NODE_TYPE_ENTITY = 6;
var CFELEMENT_NODE_TYPE_PROCESSING_INSTRUCTION = 7;
var CFELEMENT_NODE_TYPE_COMMENT = 8;
var CFELEMENT_NODE_TYPE_DOCUMENT = 9;
var CFELEMENT_NODE_TYPE_DOCUMENT_TYPE = 10;
var CFELEMENT_NODE_TYPE_DOCUMENT_FRAGMENT = 11;
var CFELEMENT_NODE_TYPE_NOTATION = 12;

var CFELEMENT_HIDDEN_CLASS = "cf-hidden-element";
var CFELEMENT_INVISIBLE_CLASS = "cf-invisible-element";
var CFELEMENT_SKELETON_CLASS = "cf-skeleton";

////////////////////////////////////////////////////////////////////////////////
// Private functions
////////////////////////////////////////////////////////////////////////////////

function __cfElementGetChildren(element)
{
    return element.children;
}

function __cfElementGetChildrenChildNodes(element)
{
    return element.childNodes;
}

function __cfElementGetDimensions(element, ignoreError)
{
    if ((typeof(element.offsetWidth) == "number") ||
        (typeof(element.offsetHeight) == "number")) {
        __cfElementGetDimensions = __cfElementGetDimensionsOffset;
    } else {
        return undefined;
    }
    return __cfElementGetDimensions(element, ignoreError);
}

function __cfElementGetDimensionsOffset(element, ignoreError)
{
    return {height: element.offsetHeight || 0, width: element.offsetWidth || 0};
}

function __cfElementGetDimensionsUndefined(element, ignoreError)
{
    if (ignoreError) {
        return undefined;
    }
    return cfErrorTrigger("__cfElementGetDimensionsUndefined: " +
                          __CFELEMENT_ERROR_DIMENSIONS);
}

function __cfElementGetDocumentPosition(element, ignoreError)
{
    if ((typeof(element.offsetLeft) == "number") ||
        (typeof(element.offsetTop) == "number")) {
        if ((typeof(element.clientLeft) == "number") ||
            (typeof(element.clientTop) == "number")) {
            cfElementGetDocumentPosition =
                __cfElementGetDocumentPositionOffsetClient;
        } else {
            cfElementGetDocumentPosition = __cfElementGetDocumentPositionOffset;
        }
    } else if ((typeof(element.x) == "number") ||
               (typeof(element.y) == "number")) {
        cfElementGetDocumentPosition = __cfElementGetDocumentPositionXY;
    } else {
        return undefined;
    }
    return cfElementGetDocumentPosition(element, ignoreError);
}

function __cfElementGetDocumentPositionOffset(element, ignoreError)
{
    var left;
    var top;
    var x = 0;
    var y = 0;
    do {
        left = element.offsetLeft;
        top = element.offsetTop;
        if ((typeof(left) == "undefined") || (typeof(top) == "undefined")) {
            if (ignoreError) {
                return undefined;
            }
            return cfErrorTrigger("__cfElementGetDocumentPositionOffset: " +
                                  __CFELEMENT_ERROR_POSITION);
        }
        x += left;
        y += top;
        element = element.offsetParent;
    } while (element);
    return {x: x, y: y};
}

function __cfElementGetDocumentPositionOffsetClient(element, ignoreError)
{
    var left;
    var leftBorder;
    var top;
    var topBorder;
    var x = 0;
    var y = 0;
    do {
        left = element.offsetLeft;
        leftBorder = element.clientLeft;
        top = element.offsetTop;
        topBorder = element.clientTop;
        if ((typeof(left) == "undefined") || (typeof(top) == "undefined") ||
            (typeof(leftBorder) == "undefined") ||
            (typeof(topBorder) == "undefined")) {
            if (ignoreError) {
                return undefined;
            }
            var func = "__cfElementGetDocumentPositionOffsetClient";
            return cfErrorTrigger(func + ": " + __CFELEMENT_ERROR_POSITION);
        }
        x += left + leftBorder;
        y += top + topBorder;
        element = element.offsetParent;
    } while (element);
    return {x: x, y: y};
}

function __cfElementGetDocumentPositionUndefined(element, ignoreError)
{
    if (ignoreError) {
        return undefined;
    }
    return cfErrorTrigger("__cfElementGetDocumentPositionUndefined: " +
                          __CFELEMENT_ERROR_POSITION);
}

function __cfElementGetDocumentPositionXY(element, ignoreError)
{
    var x = element.x;
    var y = element.y;
    if ((typeof(x) == "undefined") || (typeof(y) == "undefined")) {
        if (ignoreError) {
            return undefined;
        }
        return cfErrorTrigger("__cfElementGetDocumentPositionXY: " +
                              __CFELEMENT_ERROR_POSITION);
    }
    return {x: x, y: y};
}

function __cfElementGetInnerHTML(element)
{
    return element.innerHTML;
}

function __cfElementGetParentNode(element)
{
    return element.parentNode;
}

function __cfElementIsValidCSSClass(cls)
{
    if (! cls) {
        return false;
    }
    var length = cls.length;
    if (! length) {
        return false;
    }
    var c = cls.charAt(0);
    if (! c.isAlphanumeric()) {
        return false;
    }
    return __CFELEMENT_VALID_CSS_CLASS_CHARS.containsAllChars(cls.substring(1));
}

function __cfElementLoadDisplay(element)
{
    var style = element.style;
    var state = {
        display: style.display,
        position: style.position,
        visibility: style.visibility
    };
    style.visibility = "hidden";
    style.position = "absolute";
    style.display = "block";
    return state;
}

function __cfElementRestoreDisplay(element, state)
{
    var style = element.style;
    style.display = state.display;
    style.position = state.position;
    style.visibility = state.visibility;
}

function __cfElementSetAttribute(element, name, value)
{
    element.setAttribute(name, value);
}

function __cfElementSetAttributeCreate(element, name, value)
{
    var node = document.createAttribute(name);

    // Workaround Safari bug
    node.nodeValue = value;

    node.value = value;
    element.setAttributeNode(node);
}

function __cfElementSetInnerHTML(element, html)
{
    // Workaround bugs in Netscape 6 and IE 5.
    element.innerHTML = '';
    element.innerHTML = html.toString();
}

function __cfElementSetInnerHTMLDocument(element, html)
{
    var doc = element.document;
    doc.open();
    doc.write(html);
    doc.close();
}

////////////////////////////////////////////////////////////////////////////////
// Public API
////////////////////////////////////////////////////////////////////////////////

function cfElementAddClass(element, cls)
{
    var classes = cfElementGetClasses(element);
    classes.push(cls);
    cfElementSetClasses(element, classes);
}

function cfElementDiscardClass(element, cls)
{
    var classes = cfElementGetClasses(element);
    var result = classes.discard(cls);
    if (result) {
        cfElementSetClasses(element, classes);
    }
    return result;
}

function cfElementDiscardClasses(element, cls)
{
    while (cfElementDiscardClass(element, cls));
}

function cfElementGet(id)
{
    var element = document.getElementById(id);
    if (! element) {
        return cfErrorTrigger("cfElementGet: '" + id + "': " +
                              __CFELEMENT_ERROR_ID);
    }
    return element;
}

function cfElementGetChildren(element, ignoreError)
{
    if (typeof(element.childNodes) != "undefined") {
        cfElementGetChildren = __cfElementGetChildrenChildNodes;
    } else if (typeof(element.children) != "undefined") {
        cfElementGetChildren = __cfElementGetChildren;
    } else if (ignoreError) {
        return undefined;
    } else {
        return cfErrorTrigger("cfElementGetChildren: " +
                              __CFELEMENT_ERROR_CHILDREN);
    }
    return cfElementGetChildren(element);
}

function cfElementGetClasses(element)
{
    var className = element.className;
    if (! className) {
        return new Array();
    }
    var classes = className.split(/\s+/);
    var replace = false;
    for (var i = classes.length - 1; i >= 0; i--) {
        var cls = classes[i];
        if (! __cfElementIsValidCSSClass(cls)) {
            classes.splice(i, 1);
            replace = true;
        }
    }
    if (replace) {
        cfElementSetClasses(element, classes);
    }
    return classes;
}

function cfElementGetDimensions(element, ignoreError)
{
    var dimensions = __cfElementGetDimensions(element, ignoreError);
    if ((typeof(dimensions) == "undefined") ||
        (! (dimensions.width || dimensions.height))) {
        // Perhaps the element is hidden
        var state = __cfElementLoadDisplay(element);
        dimensions = __cfElementGetDimensions(element);
        __cfElementRestoreDisplay(element, state);
        if (typeof(dimensions) == "undefined") {
            cfElementGetDimensions = __cfElementGetDimensionsUndefined;
            return cfElementGetDimensions(element, ignoreError);
        }
    }
    return dimensions;
}

function cfElementGetDocumentPosition(element, ignoreError)
{
    var position = __cfElementGetDocumentPosition(element, ignoreError);
    if (typeof(position) == "undefined") {
        // Perhaps the element is hidden
        var state = __cfElementLoadDisplay(element);
        position = __cfElementGetDocumentPosition(element);
        __cfElementRestoreDisplay(element, state);
        if (typeof(position) == "undefined") {
            cfElementGetDocumentPosition =
                __cfElementGetDocumentPositionUndefined;
            return cfElementGetDocumentPosition(element, ignoreError);
        }
    }
    return position;
}

function cfElementGetInnerHTML(element)
{
    if (typeof(element.innerHTML) != "undefined") {
        cfElementGetInnerHTML = __cfElementGetInnerHTML;
        cfElementSetInnerHTML = __cfElementSetInnerHTML;
    } else {
        return cfErrorTrigger("cfElementGetInnerHTML: " +
                              __CFELEMENT_ERROR_GET_HTML);
    }
    return cfElementGetInnerHTML(element);
}

function cfElementGetParent(element)
{
    if (typeof(element.parentNode) != "undefined") {
        cfElementGetParent = __cfElementGetParentNode;
    } else {
        return cfErrorTrigger("cfElementGetParent: " +
                              __CFELEMENT_ERROR_PARENT);
    }
    return cfElementGetParent(element);
}

function cfElementIntersectsElements(element, elementList)
{
    var results = new Array();
    var aPosition = cfElementGetDocumentPosition(element, true);
    if (typeof(aPosition) != "undefined") {
        var aDimensions = cfElementGetDimensions(element, true);
        if (typeof(aDimensions) != "undefined") {
            var aHeight = aDimensions.height;
            var aLeft = aPosition.x;
            var aTop = aPosition.y;
            var aWidth = aDimensions.width;
            for (var i = 0; i < elementList.length; i++) {
                var bElement = elementList[i];
                var bPosition = cfElementGetDocumentPosition(bElement, true);
                if (typeof(bPosition) == "undefined") {
                    continue;
                }
                var bDimensions = cfElementGetDimensions(bElement, true);
                if (typeof(bDimensions) == "undefined") {
                    continue;
                }
                var bLeft = bPosition.x;
                var bTop = bPosition.y;
                if (! ((aLeft > (bLeft + bDimensions.width)) ||
                       (bLeft > (aLeft + aWidth)) ||
                       (aTop > (bTop + bDimensions.height)) ||
                       (bTop > (aTop + aHeight)))) {
                    results.push(bElement);
                }
            }
        }
    }
    return results;
}

function cfElementIntersectsElement(elementA, elementB)
{
    return !! cfElementIntersectsElements(elementA, [elementB]).length;
}

function cfElementHasDescendant(element, descendant)
{
    var nodes = cfElementGetChildren(element);
    for (var i = 0; i < nodes.length; i++) {
        var node = nodes[i];
        if (node === descendant) {
            return true;
        }
        if (cfElementHasDescendant(node, descendant)) {
            return true;
        }
    }
    return false;
}

function cfElementRemoveClass(element, cls)
{
    var classes = cfElementGetClasses(element);
    classes.remove(cls);
    cfElementSetClasses(element, classes);
}

function cfElementSetAttribute(element, name, value)
{
    if (typeof(element.setAttribute) != "undefined") {
        cfElementSetAttribute = __cfElementSetAttribute;
    } else if ((typeof(document.createAttribute) != "undefined") &&
               (typeof(element.setAttributeNode) != "undefined")) {
        cfElementSetAttribute = __cfElementSetAttributeCreate;
    } else {
        return cfErrorTrigger("cfElementSetAttribute: " +
                              __CFELEMENT_ERROR_SET_ATTRIBUTE);
    }
    return cfElementSetAttribute(element, name, value);
}

function cfElementSetClasses(element, classes)
{
    for (var i = 0; i < classes.length; i++) {
        var cls = classes[i];
        if (! __cfElementIsValidCSSClass(cls)) {
            return cfErrorTrigger("cfElementSetClasses: '" + cls + "': " +
                                  __CFELEMENT_ERROR_INVALID_CLASS);
        }
    }
    element.className = classes.join(' ');
}

function cfElementSetInnerHTML(element, html)
{
    if (typeof(element.innerHTML) != "undefined") {
        cfElementGetInnerHTML = __cfElementGetInnerHTML;
        cfElementSetInnerHTML = __cfElementSetInnerHTML;
    } else if (element.document) {
        cfElementSetInnerHTML = __cfElementSetInnerHTMLDocument;
    } else {
        return cfErrorTrigger("cfElementSetInnerHTML: " +
                              __CFELEMENT_ERROR_SET_HTML);
    }
    return cfElementSetInnerHTML(element, html);
}

function cfElementSetInnerText(element, text)
{
    cfElementSetInnerHTML(element, text.toString().toHTML());
}
