//#############################################################################
//
// A JavaScript library for generic HTML rendering
//
// ©2007 Peter A. Kemmer
//
// Library assumptions:
//
// - Attributes are stripped from the end of html related functions using
//   the Atrribute constructor and the argument count as a starting index
//
// - A certian amount of attribute name mangling has to occur to make all
//   the possible tag attributes safe for the JavaScript namespace... and
//   you could still totally mess things up using certain attribute names
//
// - 'clazz' is used all over the place as a name replacement for 'class'
//
// - Styles are handled differently from most attributes. They collect as
//   as you add them; old vaues aren't replaced, they have the new values
//   tacked on to them, after a space. It 'cascades' styles just like CSS
//
//   setAtribute("class", "foo");
//   setAtribute("class", "bar");
//   setAtribute("style", "width: 100px;", "style", "height: 100px;");
//
//   toString() == 'class="foo bar" style="width: 100px; height: 100px;"'
//
// - If any normal tag doesn't take 'href' as a parameter but you set the
//   value in the attributes, it will automatically nest in an anchor tag
//
// - Attribute objects can NOT be Arrays... using an array for attributes
//   will cause the array's values, not properties, to be copied at times
//
// - 'Smart' HTML objects always override toString() to render their html
//
//#############################################################################

//-----------------------------------------------------------------------------
// Verify Dependencies
//-----------------------------------------------------------------------------

if (!isDefined(CORE_LIBRARY_LOADED)) {
    alert("Error: The library Core.js needs to be loaded before HTML.js");
}

//-----------------------------------------------------------------------------
// Constants
//-----------------------------------------------------------------------------

var HTML_LIBRARY_LOADED = true;

// A map of tag types that normally use the href attribute. When other tag
// types define an href attribute, it gets removed from the attributes and
// the tag is wrapped in an anchor instead, linked to the value of href!!!

var TAGS_THAT_USE_HREF = new Object();

TAGS_THAT_USE_HREF.a = true;
TAGS_THAT_USE_HREF.link = true;

// A map of properties that Attribures.toString shouldn't render directly!

var HIDDEN_ATTRIBUTES_PROPERTIES = new Object();

HIDDEN_ATTRIBUTES_PROPERTIES.setAttribute = true;
HIDDEN_ATTRIBUTES_PROPERTIES.emptyClass = true;
HIDDEN_ATTRIBUTES_PROPERTIES.emptyStyle = true;
HIDDEN_ATTRIBUTES_PROPERTIES.copyClassAndStyleTo = true;
HIDDEN_ATTRIBUTES_PROPERTIES.copyAttributes = true;
HIDDEN_ATTRIBUTES_PROPERTIES.toString = true;

HIDDEN_ATTRIBUTES_PROPERTIES.clazz = true;
HIDDEN_ATTRIBUTES_PROPERTIES.clazzBuffer = true;
HIDDEN_ATTRIBUTES_PROPERTIES.style = true;
HIDDEN_ATTRIBUTES_PROPERTIES.styleBuffer = true;

//#############################################################################
// Comments
//#############################################################################

/**
 * getConditionalComment
 *
 * Get the html for a conditional comment
 *
 * @param condition - The condition
 * @param comment - The comment
 */
function getConditionalComment(condition, comment) {
    var buffer = new StringBuffer();

    buffer.append("<!--[if ", condition, "]>\n", comment.toString().prependLines(), "\n<![endif]-->");

    return buffer.toString();
}

/**
 * writeConditionalComment
 *
 * Write the html for a conditional comment
 *
 * @param condition - The condition
 * @param comment - The comment
 */
function writeConditionalComment(condition, comment) {
    write(getConditionalComment(condition, comment));
}

//#############################################################################
// Attributes - HTML tag attribute management
//#############################################################################

/**
 * Attributes 'Constructor'
 *
 * A convenience function to 'construct' an attributes object, in two ways
 *
 * If the function detects the first argument is an array, it does this...
 *
 * ARGUMENT PARSING MODE:
 *
 * @param <argument 1> - Array of arguments from another method
 * @param <argument 2> - Optional index to start parsing at
 * @param <argument 3> - Optional object containing default attributes
 *
 * Parse an argument array, starting at an optional index, and convert any
 * objects or data into a new object containing those values as properties
 *
 * If an argument is an object we will copy all of the object's properties
 *
 * If an argument is a primitive we will use it and the next argument as a
 * name/value pair, converting the name to a property containing the value
 *
 * Additionally, you can pass an object containing default attributes that
 * will only be set if the same attributes were not found parsing the args
 *
 * Otherwise it defaults to...
 *
 * RAW ATTRIBUTES MODE:
 *
 * @param <object> || <name, value> ...
 *
 * Get the argument array and stuff it into a new Attributes object, using
 * that one to do the argument parsing mode, then copy all it's attributes
 */
function Attributes(firstArgument) {

    // class variables --------------------------------------------------------

    this.clazzBuffer = new StringBuffer(" ");
    this.styleBuffer = new StringBuffer(" ");

    // construct class --------------------------------------------------------

    if (arguments.length >= 1) {
        if (isArray(firstArgument)) {
            // Assume that we are running argument parsing mode
            // which is used to convert function arguments into
            // attributes using a argument array & start index!

            // You can also pass in a default attributes object
            // in this parsing mode, copying only unset values!

            var myArguments = arguments[0];
            var startIndex = arguments[1] ? arguments[1] : 0;
            var defaultAttributes = arguments[2];

            // Iterate through myArguments, converting to props

            for (var ii = startIndex; ii < myArguments.length; ii++) {
                var argument = myArguments[ii];

                if (typeof argument == "object") {
                    // Copy attributes from object to this one!

                    this.copyAttributes(argument);
                } else {
                    this.setAttribute(argument, myArguments[ii + 1]);

                    // Skip right past the one we just utilized

                    ii++;
                }
            }

            // Only use default values if they haven't been set

            if (defaultAttributes != null) {
                this.copyAttributes(defaultAttributes, false);
            }
        } else {
            // If the first argument ISN'T an array, just shove
            // every last argument into a NEW Attributes object
            // as an argument array (triggering the above) then
            // copy the attributes from the new object to here!

            // It probably means we're parsing name/value pairs

            var attributes = new Attributes(arguments);

            this.copyAttributes(attributes);
        }
    }
}

/**
 * Attributes.setAttribute
 *
 * Set an attribute on this object, overwriting old values by default
 *
 * @param attribute - The attribute to set
 * @param value - The value to set
 * @param overwriteOpt - Replace if attribute already exists, defaults true
 */
Attributes.prototype.setAttribute = function(attribute, value, overwriteOpt) {
    var overwrite = overwriteOpt ? overwriteOpt : true;

    // Can't use this[property] syntax, use a proxy!!!

    var proxy = this;

    // Always CONCATENTATE styles, they're cumulative!
    // Also, trim off the whitespace before adding it!

    attribute = sanitizeAttribute(attribute);

    if (attribute == "clazz") {
        this.clazzBuffer.append(value.trim());
    } else if (attribute == "style") {
        value = value.trim();

        if (!value.endsWith(";")) {
            // Override the default item separator " "

            this.styleBuffer.appendSep("; ", value);
        } else {
            this.styleBuffer.append(value);
        }
    } else if (overwrite || proxy[attribute] == null) {
        proxy[attribute] = value;
    }
}

/**
 * Attributes.emptyClass
 *
 * Removes any classes in the class buffer
 */
Attributes.prototype.emptyClass = function() {
    this.clazzBuffer.empty();
}

/**
 * Attributes.emptyStyle
 *
 * Removes any styles in the style buffer
 */
Attributes.prototype.emptyStyle = function() {
    this.styleBuffer.empty();
}

/**
 * Attributes.copyClassAndStyleTo
 *
 * Copy this object's class and style into the provided buffers
 *
 * @param clazzBuffer - The class buffer to copy into
 * @param styleBuffer - The style buffer to copy into
 */
Attributes.prototype.copyClassAndStyleTo = function(clazzBuffer, styleBuffer) {
    if (!this.clazzBuffer.isEmpty()) {
        clazzBuffer.append(this.clazzBuffer.getActualData());
    }

    if (!this.styleBuffer.isEmpty()) {
        styleBuffer.append(this.styleBuffer.getActualData());
    }
}

/**
 * Attributes.copyAttributes
 *
 * Copy an object's attributes to this one, overwriting old values by default
 *
 * @param source - The source object
 * @param overwriteOpt - Replace if attribute already exists, defaults true
 */
Attributes.prototype.copyAttributes = function(source, overwriteOpt) {
    var overwrite = overwriteOpt ? overwriteOpt : true;

    // Copy the contents of the class and style buffers

    if (isDefined(source.copyClassAndStyleTo)) {
        source.copyClassAndStyleTo(this.clazzBuffer, this.styleBuffer);
    }

    for (var attribute in source) {
        // Wee hacky way to remove function properties!

        if (!isValidAttribute(attribute)) {
            continue;
        }

        this.setAttribute(attribute, source[attribute], overwrite);
    }
}

/**
 * Attributes.toString
 *
 * Gets the HTML used to render an Attributes object
 */
Attributes.prototype.toString = function() {
    var buffer = new StringBuffer(" ");

    // Special case the 'class' and 'style' attributes

    // Copy hand set attributes to the correct buffer!

    if (isDefined(this.clazz)) {
        this.setAttribute("class", this.clazz);

        delete this.clazz;
    }

    if (isDefined(this.style)) {
        this.setAttribute("style", this.style);

        delete this.style;
    }

    // Convert buffers to strings and tack on to start

    if (!this.clazzBuffer.isEmpty()) {
        buffer.append('class="');

        // Need to override the default separator " "!

        buffer.appendSep("", this.clazzBuffer.toString(), '"');
    }

    if (!this.styleBuffer.isEmpty()) {
        buffer.append('style="');

        // Need to override the default separator " "!

        buffer.appendSep("", this.styleBuffer.toString(), '"');
    }

    // Can't use this[property] syntax, so use a proxy

    var proxy = this;

    for (var attribute in proxy) {
        // Wee hacky way to remove function properties

        if (!isValidAttribute(attribute)) {
            continue;
        }

        // De-sanitize the attribute name before using

        buffer.append(restoreAttribute(attribute));

        if (proxy[attribute] != null) {
            // Must override the default separator " "

            buffer.appendSep("", '="', proxy[attribute], '"');
        }
    }

    return buffer.toString();
}

//-----------------------------------------------------------------------------
// Attribute Utilities
//-----------------------------------------------------------------------------

/**
 * isValidAttribute
 *
 * Dumb hack to screen out the Attribute object properties that
 * point to functions instead of attributes we want to display!
 *
 * @param attribute - The name of the attribute
 */
function isValidAttribute(attribute) {
    // Watch me go out of sync when someone edits the functions

    return !isDefined(HIDDEN_ATTRIBUTES_PROPERTIES[attribute]);
}

/**
 * sanitizeAttribute
 *
 * Convenience routine to sanitize attributes, allowing direct access to
 * them as properties off of an object by removing any namespace issues!
 *
 * @param attribute - Attribute to sanitize
 */
function sanitizeAttribute(attribute) {
    attribute = attribute.trim().toLowerCase();

    if (attribute == "class") {
        return "clazz";
    }

    // Exit before a big loop of doom
    // if it shouldn't even bother!!!

    if (attribute.indexOf("-") == -1) {
        return attribute;
    }

    // Convert "foo-bar" to "fooBar"
    // I bet there's regex to do it!

    var buffer = new StringBuffer();

    for (var ii = 0; ii < attribute.length; ii++) {
        if (attribute.charAt(ii) == "-") {
            buffer.append(attribute.charAt(ii + 1).toUpperCase());

            ii++;
        } else {
            buffer.append(attribute.charAt(ii));
        }
    }

    return buffer.toString();
}

/**
 * restoreAttribute
 *
 * A convenience routine to restore attributes allowing direct access to
 * them as properties off of an object by removing any namespace issues!
 *
 * @param attribute - Attribute to sanitize
 */
function restoreAttribute(attribute) {
    if (attribute == "clazz") {
        return "class";
    }

    // Convert "fooBar" to "foo-bar"
    // I bet there's regex to do it!

    var buffer = new StringBuffer();

    for (var ii = 0; ii < attribute.length; ii++) {
        var charAt = attribute.charAt(ii);

        if (charAt >= "A" && charAt <= "Z") {
            buffer.append("-", charAt.toLowerCase());
        } else {
            buffer.append(charAt);
        }
    }

    return buffer.toString();
}

//#############################################################################
// HTML Tags
//#############################################################################

//-----------------------------------------------------------------------------
// Generic HTML Tag Rendering
//-----------------------------------------------------------------------------

/**
 * getTagStart
 *
 * Create the HTML for the start of a tag
 *
 * @param type - The type of tag
 * @param ... - <object> || <name, value>
 */
function getTagStart(type) {
    var attributes = new Attributes(arguments, 1);

    // Do NOT test for null... we only care if it EXISTS

    if (isDefined(attributes.href)) {
        // Remove href for tags that don't use it, since
        // we'll probably be anchoring the tag elsewhere

        if (isAnchorableTag(type)) {
            delete attributes.href;
        }
    }

    var buffer = new StringBuffer();

    buffer.append("<", type.toLowerCase(), " ", attributes.toString(), ">");

    return buffer.toString();
}

/**
 * getTagEnd
 *
 * Create the HTML for the end of a tag
 *
 * @param type - The type of tag
 * @param ... - <object> || <name, value>
 */
function getTagEnd(type) {
    var buffer = new StringBuffer();

    buffer.append("</", type.toLowerCase(), ">");

    return buffer.toString();

}

/**
 * getTag
 *
 * Create the HTML for an entire tag
 *
 * If a tag that doesn't normally use href as an attribute has it set,
 * the output will be conveniently wrapped in an anchor tag for you!!!
 *
 * @param type - The type of tag
 * @param content - The content
 * @param ... - <object> || <name, value>
 */
function getTag(type, content) {
    var attributes = new Attributes(arguments, 2);

    return getTagStart(type, attributes) + content + getTagEnd(type);
}

/**
 * isAnchorableTag
 *
 * Tags that have an href attribute will be wrapped in an anchor by the
 * wrapTag function as long as they are not found in TAGS_THAT_USE_HREF
 *
 * getTagStart also uses this value, to remove hrefs from any tags that
 * don't normally use them. The assumption is that it's there for wrap!
 *
 * @param type - The type of tag
 */
function isAnchorableTag(type) {
    return !isDefined(TAGS_THAT_USE_HREF[type]);
}

/**
 * wrapTag
 *
 * Automatically parse a tag and it's attributes looking for anything
 * out of the ordinary, for example, an href attribute on an img tag.
 *
 * When it finds certain combinations it will perform shortcuts, like
 * wrapping tags that don't normally use an href with an anchor. Woo!
 *
 * @param tag - The html representing the tag
 * @param attributes - The tag's attributes
 */
function wrapTag(tag, attributes) {
    // Do NOT test for null... we only care if it EXISTS

    if (isDefined(attributes.href)) {
        var type = tag.substr(1);

        type = type.split(" ")[0];
        type = type.split(">")[0];

        // Sanity check to prevent infinite recursion on
        // mistakenly wrapped tags that really use href!

        if (isAnchorableTag(type)) {
            return getA(attributes.href, tag);
        }
    }

    return tag;
}

//-----------------------------------------------------------------------------
// Specific HTML Tag Rendering
//-----------------------------------------------------------------------------

/**
 * getA
 *
 * Create the HTML for an anchor tag
 *
 * @param href - The url to visit, only renders the start of the tag if null
 * @param content - The content
 * @param ... - <object> || <name, value>
 */
function getA(href, content) {
    var attributes = new Attributes(arguments, 2);

    if (href != null) {
        attributes.href = href;
    }

    // Only render the start of the anchor if there's no href

    // Dont wrap the tag, it doesn't make sense!

    if (content != null) {
        return getTag("a", content, attributes);
    } else {
        return getTagStart("a", attributes);
    }
}

/**
 * writeA
 *
 * Write the HTML for an anchor tag
 *
 * @param href - The url to visit, only renders the start of the tag if null
 * @param content - The content
 * @param ... - <object> || <name, value>
 */
function writeA(href, content) {
    var attributes = new Attributes(arguments, 2);

    write(getA(href, content, attributes));
}

/**
 * getDiv
 *
 * Create the HTML for a div tag
 *
 * @param clazz - The class, 'clazz' due to JavaScript namespace collision
 * @param content - The content
 * @param ... - <object> || <name, value>
 */
function getDiv(clazz, content) {
    var attributes = new Attributes(arguments, 2);

    // Always CONCATENTATE styles, they're cumulative

    if (clazz != null) {
        attributes.setAttribute("clazz", clazz);
    }

    // Robustness... null should not render "null"!!!

    if (content == null) {
        content = "";
    }

    return wrapTag(getTag("div", content, attributes), attributes);
}

/**
 * writeDiv
 *
 * Write the HTML for a div tag
 *
 * @param clazz - The class, 'clazz' due to JavaScript namespace collision
 * @param content - The content
 * @param ... - <object> || <name, value>
 */
function writeDiv(clazz, content) {
    var attributes = new Attributes(arguments, 2);

    write(getDiv(clazz, content, attributes));
}

/**
 * getImg
 *
 * Create the HTML for an img tag
 *
 * @param src - The url of the image file
 * @param title - The title of the image
 * @param ... - <object> || <name, value>
 */
function getImg(src, title) {
    var attributes = new Attributes(arguments, 2);

    if (src != null) {
        attributes.src = src;
    }

    if (title != null) {
        attributes.title = title;
    }

    return wrapTag(getTagStart("img", attributes), attributes);
}

/**
 * writeImg
 *
 * Write the HTML for an img tag
 *
 * @param src - The url of the image file
 * @param title - The title of the image
 * @param ... - <object> || <name, value>
 */
function writeImg(src, title) {
    var attributes = new Attributes(arguments, 2);

    write(getImg(src, title, attributes));
}

/**
 * getLI
 *
 * Create the HTML for an list item tag
 *
 * @param content - The content
 * @param ... - <object> || <name, value>
 */
function getLI(content) {
    var attributes = new Attributes(arguments, 1);

    // Dont wrap the tag, it doesn't make sense!

    return getTag("li", content, attributes);
}

/**
 * writeLI
 *
 * Write the HTML for an list item tag
 *
 * @param content - The content
 * @param ... - <object> || <name, value>
 */
function writeLI(content) {
    var attributes = new Attributes(arguments, 1);

    write(getLI(content, attributes));
}

/**
 * getOL
 *
 * Create the HTML for an ordered list tag
 *
 * @param content - The content
 * @param ... - <object> || <name, value>
 */
function getOL(content) {
    var attributes = new Attributes(arguments, 1);

    return wrapTag(getTag("ol", content, attributes), attributes);
}

/**
 * writeOL
 *
 * Write the HTML for an ordered list tag
 *
 * @param content - The content
 * @param ... - <object> || <name, value>
 */
function writeOL(content) {
    var attributes = new Attributes(arguments, 1);

    write(getOL(content, attributes));
}

/**
 * getSpan
 *
 * Create the HTML for a span tag
 *
 * @param clazz - The class, 'clazz' due to JavaScript namespace collision
 * @param content - The content
 * @param ... - <object> || <name, value>
 */
function getSpan(clazz, content) {
    var attributes = new Attributes(arguments, 2);

    // Always CONCATENTATE styles, they're cumulative

    if (clazz != null) {
        attributes.setAttribute("clazz", clazz);
    }

    // Robustness... null should not render "null"!!!

    if (content == null) {
        content = "";
    }

    return wrapTag(getTag("span", content, attributes), attributes);
}

/**
 * writeSpan
 *
 * Write the HTML for a span tag
 *
 * @param clazz - The class, 'clazz' due to JavaScript namespace collision
 * @param content - The content
 * @param ... - <object> || <name, value>
 */
function writeSpan(clazz, content) {
    var attributes = new Attributes(arguments, 2);

    write(getSpan(clazz, content, attributes));
}

/**
 * getTitle
 *
 * Create the HTML for a title tag
 *
 * @param title - The title
 * @param ... - <object> || <name, value>
 */
function getTitle(title) {
    var attributes = new Attributes(arguments, 1);

    return wrapTag(getTag("title", title, attributes), attributes);
}

/**
 * writeTitle
 *
 * Write the HTML for a title tag
 *
 * @param title - The title
 * @param ... - <object> || <name, value>
 */
function writeTitle(title) {
    var attributes = new Attributes(arguments, 1);

    write(getTitle(title, attributes));
}

/**
 * getUL
 *
 * Create the HTML for an unordered list tag
 *
 * @param content - The content
 * @param ... - <object> || <name, value>
 */
function getUL(content) {
    var attributes = new Attributes(arguments, 1);

    return wrapTag(getTag("ul", content, attributes), attributes);
}

/**
 * writeUL
 *
 * Write the HTML for an unordered list tag
 *
 * @param content - The content
 * @param ... - <object> || <name, value>
 */
function writeUL(content) {
    var attributes = new Attributes(arguments, 1);

    write(getUL(content, attributes));
}

//-----------------------------------------------------------------------------
// Custom HTML Rendering For Head Tag Elements
//-----------------------------------------------------------------------------

/**
 * getLink
 *
 * Create the HTML for an link tag
 *
 * @param href - The url of the link
 * @param ... - <object> || <name, value>
 */
function getLink(href) {
    var attributes = new Attributes(arguments, 1);

    if (href != null) {
        attributes.href = href;
    }

    // Dont wrap the tag, it doesn't make sense!

    return getTagStart("link", attributes);
}

/**
 * writeLink
 *
 * Write the HTML for an link tag
 *
 * @param href - The url of the link
 * @param ... - <object> || <name, value>
 */
function writeLink(href) {
    var attributes = new Attributes(arguments, 1);

    write(getLink(href, attributes));
}

/**
 * getMetaHttpEquiv
 *
 * Create the HTML for a meta http-equiv as used in the head tag
 *
 * @param httpEquiv - The value of the http-equiv attribute
 * @param content - The content ATTRIBUTE
 * @param ... - <object> || <name, value>
 */
function getMetaHttpEquiv(httpEquiv, content) {
    var attributes = new Attributes(arguments, 2);

    if (httpEquiv != null) {
        attributes.httpEquiv = httpEquiv;
    }

    if (content != null) {
        attributes.content = content;
    }

    // Dont wrap the tag, it doesn't make sense!

    return getTagStart("meta", attributes);
}

/**
 * writetMetaHttpEquiv
 *
 * Write the HTML for a meta http-equiv as used in the head tag
 *
 * @param httpEquiv - The value of the http-equiv attribute
 * @param content - The content ATTRIBUTE
 * @param ... - <object> || <name, value>
 */
function writetMetaHttpEquiv(httpEquiv, content) {
    var attributes = new Attributes(arguments, 2);

    write(getMetaHttpEquiv(httpEquiv, content, attributes));
}

/**
 * getMetaName
 *
 * Create the HTML for a meta http-equiv as used in the head tag
 *
 * @param name - The value of the name attribute
 * @param content - The content ATTRIBUTE
 * @param ... - <object> || <name, value>
 */
function getMetaName(name, content) {
    var attributes = new Attributes(arguments, 2);

    if (name != null) {
        attributes.name = name;
    }

    if (content != null) {
        attributes.content = content;
    }

    // Dont wrap the tag, it doesn't make sense!

    return getTagStart("meta", attributes);
}

/**
 * writeMetaName
 *
 * Write the HTML for a meta http-equiv as used in the head tag
 *
 * @param name - The value of the name attribute
 * @param content - The content ATTRIBUTE
 * @param ... - <object> || <name, value>
 */
function writeMetaName(name, content) {
    var attributes = new Attributes(arguments, 2);

    write(getMetaName(name, content, attributes));
}

/**
 * getStyleSheet
 *
 * Create the HTML for a style sheet as used in the head tag
 *
 * @param href - The url of the style sheet
 * @param ... - <object> || <name, value>
 */
function getStyleSheet(href) {
    var attributes = new Attributes(arguments, 1);

    if (href != null) {
        attributes.href = href;
    }

    attributes.rel = "stylesheet";
    attributes.type = "text/css";

    // Dont wrap the tag, it doesn't make sense!

    return getTagStart("link", attributes);
}

/**
 * writeStyleSheet
 *
 * Write the HTML for a style sheet as used in the head tag
 *
 * @param href - The url of the style sheet
 * @param ... - <object> || <name, value>
 */
function writeStyleSheet(href) {
    var attributes = new Attributes(arguments, 1);

    write(getStyleSheet(href, attributes));
}

//#############################################################################
// List - A Convenience Class To Define/Render AN HTML List (Down With Tables!)
//#############################################################################

/**
 * List 'Constructor'
 *
 * @param contentOpt - Optional content
 * @param orderedOpt - Is ordered list, defaults to false
 * @param listClazzOpt - Optional style for entire list
 * @param itemClazzOpt - Optional style for list items
 * @param lastItemClazzOpt - Optional style for last list item
 * @param ... - <object> || <name, value>
*/
function List(contentOpt, isOrderedOpt, listClazzOpt, itemClazzOpt, firstItemClazzOpt, lastItemClazzOpt) {

    // class variables --------------------------------------------------------

    this.attributes = new Attributes(arguments, 6);

    this.items = new Array();
    this.itemAttributes = new Array();

    // construct class --------------------------------------------------------

    // Default to false

    if (isOrderedOpt != null) {
        this.isOrdered = isOrderedOpt;
    } else {
        this.isOrdered = false;
    }

    // Add list's class right to attributes!

    if (listClazzOpt != null) {
        this.attributes.setAttribute("clazz", listClazzOpt);
    }

    if (itemClazzOpt != null) {
        this.itemClazz = itemClazzOpt;
    }

    if (firstItemClazzOpt != null) {
        this.firstItemClazz = firstItemClazzOpt;
    }

    if (lastItemClazzOpt != null) {
        this.lastItemClazz = lastItemClazzOpt;
    }

    // Then add the optional content

    if (contentOpt != null) {
        this.add(contentOpt);
    }
}

/**
 * List.add
 *
 * Add items to the list, which may be:
 *
 * - Any Object
 * - Nested arrays containing a mix of either of the above
 *
 * @param content - The content
 * @param ... - <object> || <name, value>
 */
List.prototype.add = function(content) {
    var attributes = new Attributes(arguments, 1);

    if (content != null) {
        if (isArray(content)) {
            for (var ii = 0; ii < content.length; ii++) {
                this.add(content[ii]);
            }
        } else {
            this.items[this.items.length] = content.toString();
            this.itemAttributes[this.itemAttributes.length] = attributes;
        }
    }
}

/**
 * List.toString
 *
 * Gets the HTML used to render an List object
 */
List.prototype.toString = function() {
    var lastIndex = this.items.length - 1;

    // Loop through the items, embedding them in li tags

    var buffer = new StringBuffer("\n", 1, true);

    for (var ii = 0; ii < this.items.length; ii++) {
        var item = "\n".append(this.items[ii].toString().prependLines(), "\n");
        var clazz = this.itemClazz;

        // The first/last items might use custom classes

        if (this.firstItemClazz && ii == 0) {
            clazz = this.firstItemClazz;
        } else if (this.lastItemClazz && ii == lastIndex) {
            clazz = this.lastItemClazz;
        }

        buffer.append(getLI(item, this.itemAttributes[ii], "clazz", clazz));
    }

    buffer.newline();

    // Then embed this data into the right type of list!

    return this.isOrdered ?
        getOL(buffer.toString(), this.attributes) :
        getUL(buffer.toString(), this.attributes);
}