//#############################################################################
//
// A JavaScript library for string manipulation
//
// ©2007 Peter A. Kemmer
//
//#############################################################################

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

/*
if (!isDefined(REQUIRED_LIBRARY_LOADED)) {
    alert("Error: The library Required.js needs to be loaded before This.js");
}
*/

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

var CORE_LIBRARY_LOADED = true;

//#############################################################################
// General Utilities
//#############################################################################

/**
 * isDefined
 *
 * Is an object defined?
 *
 * @param object - The object to test
 */
function isDefined(object) {
    return object === undefined ? false : true;
}

/**
 * isArray
 *
 * Is an object an array? This might be fooled by objects that define length
 *
 * DON'T TEST 'INSTANCEOF ARRAY' BECAUSE 'arguments' IS NOT REALLY AN ARRAY!
 * That's right! When using the automatic 'arguments' var, instanceof borks!
 *
 * @param object - The object to test
 */
function isArray(object) {
    return typeof object == "object" && typeof object.length == "number";
}

/**
 * write
 *
 * A shortcut to document.write()?
 *
 * @param content - The content to write
 */
function write(content) {
    document.write(content);
}

/**
 * trimArguments
 *
 * Convenience routine to trim off the start of an arguments 'array'
 * since it can't be sliced for some crazy reason I don't understand
 *
 * @param newArguments - The arguments to clip
 * @param startIndex - The index of the first argument to keep
 */
function trimArguments(allArguments, startIndex) {
    var newArguments = new Array();

    for (var ii = 0; ii < allArguments.length - startIndex; ii++) {
        newArguments[ii] = allArguments[ii + startIndex];
    }

    return newArguments
}

//#############################################################################
// Modifications To String Object
//#############################################################################

/**
 * String.contains
 *
 * Test to see if this string contains another string
 */
String.prototype.contains = function(string) {
    return this.indexOf(string) >= 0;
}

/**
 * String.startsWith
 *
 * Test to see if this string starts with another string
 */
String.prototype.startsWith = function(string, ignoreCase) {
    if (ignoreCase) {
        return string.toLowerCase() == this.substring(0, string.length).toLowerCase();
    } else {
        return string == this.substring(0, string.length);
    }
}

/**
 * String.endsWith
 *
 * Test to see if this string ends with another string
 */
String.prototype.endsWith = function(string, ignoreCase) {
    if (ignoreCase) {
        return string.toLowerCase() == this.substring(this.length - string.length).toLowerCase();
    } else {
        return string == this.substring(this.length - string.length);
    }
}

/**
 * String.trim
 *
 * Remove all the whitespace from the ends of the string,
 */
String.prototype.trim = function() {
    return this.replace(/^\s\s*/, '').replace(/\s\s*$/, '');
}

/**
 * prependLines
 *
 * Prepend each actual line of content with a custom value. If the
 * value is a Number, it will prepend a string with that many tabs
 *
 * @param prependOpt - The value to prepend, defaults to one tab
 * @param prependEmptylLinesOpt - Prepend lines even when empty
 */
String.prototype.prependLines = function(prependOpt, prependEmptylLinesOpt) {
    var prepend = isDefined(prependOpt) ? prependOpt : 1;

    // If prepend is a number, create a
    // string containing that many tabs

    if (!isNaN(prepend)) {
        var buffer = new StringBuffer();

        // Build tabs string to prepend

        for (var ii = 0; ii < prepend; ii++) {
            buffer.append("\t");
        }

        prepend = buffer.toString();
    }

    // Split on newline and add prepend

    var lines = this.split("\n");
    var buffer = new StringBuffer();

    for (var ii = 0; ii < lines.length; ii++) {
        // Only prepend to actual lines!!!

        if (prependEmptylLinesOpt || lines[ii] != "") {
            buffer.append(prepend);
            buffer.append(lines[ii]);
        }

        // Replace the split newlines!

        if (ii < lines.length - 1) {
            buffer.append("\n");
        }
    }

    return buffer.toString();
}

/**
 * String._secretAppend
 *
 * The secret method that does the work for String.append*
 *
 * @param content - The content, or arrays of contnet
 * @param separator - The separator
 * @param buffer - The buffer used to build the whole string before joining
 */
String.prototype._secretAppend = function(content, separator, buffer) {
    var isRoot = false;

    if (!isDefined(buffer)) {
        isRoot = true;

        buffer = [ this ];
    }

    // Loop through the content, adding it
    // to the buffer for assembly later!!!

    for (var ii = 0; ii < content.length; ii++) {
        var item = content[ii];

        if (isArray(item)) {
            // if the argument is an array, unroll it and add items

            for (var jj = 0; jj < item.length; jj++) {
                this._secretAppend(item[jj], separator, buffer);
            }
        } else if (item != null) {
            // if it's not an array, just shove it into the buffer!

            buffer[buffer.length] = item;
        }
    }

    if (isRoot) {
        return buffer.join(separator);
    }
}

/**
 * String.append
 *
 * Efficiently append a bunch of strings
 * to this one, then return the value!!!
 */
String.prototype.append = function() {
    return this._secretAppend(arguments, "");
}

/**
 * String.appendSep
 *
 * Efficiently append a bunch of strings to this
 * one using a separator, then return the value!
 *
 * @param separator - The separator
 */
String.prototype.appendSep = function(separator) {
    return this._secretAppend(trimArguments(arguments, 1), separator);
}

/**
 * String.escapeHtml
 *
 * Escapes all the HTML tags and escape codes in the string
 */
String.prototype.escapeHtml = function() {
    var toEscape = { '&':'&', '<':'<', '>':'>', '"':'"' };
    var proxy = this;

    for (var ii in toEscape) {
        proxy = proxy.replace(new RegExp(ii, 'g'), toEscape[ii]);
    }

    return proxy
}

/**
 * String.unescapeHtml
 *
 * Unescapes all the HTML tags and escape codes in the string
 */
String.prototype.unescapeHtml = function() {
    var toUnescape = { '&':'&', '<':'<', '>':'>', '"':'"' };
    var proxy = this;

    for (var ii in toUnescape) {
        proxy = proxy.replace(new RegExp(ii, 'g'), toUnescape[ii]);
    }

    return proxy
}

/**
 * String.urlEncode
 *
 * URL encode the string:
 */
String.prototype.urlEncode = function() {
    return encodeURIComponent(this);
}

/**
 * String.isEmail
 *
 * Test to see if the string is a valid email address
 */
String.prototype.isEmail = function () {
    var regex = new RegExp("\\w+([-+.\']\\w+)*@\\w+([-.]\\w+)*\\.\\w+([-.]\\w+)*");

    var matches = regex.exec(this);

    return matches != null && this == matches[0];
}

/**
 * String.isURL
 *
 * Test to see if the string is a valid URL
 */
String.prototype.isUrl = function () {
    var regex = new RegExp("http(s)?://([\\w-]+\\.)+[\\w-]+(/[\\w-\\+ ./?%:&=#\\[\\]]*)?");

    var matches = regex.exec(this);

    return matches != null && this == matches[0];
}

//#############################################################################
// StringBuffer - A Helper Class To Speed String Manipulation
//#############################################################################

/**
 * StringBuffer 'Constructor'
 *
 * A convenience function to 'construct' a StringBuffer object that
 * drastically speeds up string concatenation.
 *
 * If a prepend value is defined the buffer's toString will perform
 * a extra step, prepending the value using the normal prepend code
 * This is useful for block indentation and other
 *
 * If a default separator is defined, it will be added before every
 * append that doesn't otherwise define it's own separator. Useful!
 *
 * The separator can be set to prefix instead of follow items, too
 *
 * @param defaultSeparatorOpt - Optional separator used to divide appends
 * @param prependOpt - Optional value to prepend onto each line
 * @param startWithSeparatorOpt - Optional start buffer with default separator
 */
function StringBuffer(defaultSeparatorOpt, prependOpt, startWithSeparatorOpt) {

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

    this.defaultSeparator = defaultSeparatorOpt ? defaultSeparatorOpt : null;
    this.prepend = isDefined(prependOpt) ? prependOpt : 0;
    this.startWithSeparator = startWithSeparatorOpt ? startWithSeparatorOpt : false;

    this.buffer = new Array();
}

/**
 * StringBuffer.setPrepend
 *
 * Set the value to prepend onto all lines in the buffer on toString()
 *
 * @param prependLines - The value to prepend
 */
StringBuffer.prototype.setPrepend = function(prepend) {
    this.prepend = prepend;
};

/**
 * StringBuffer.getPrepend
 *
 * Set the value to prepend onto all lines in the buffer on toString()
 */
StringBuffer.prototype.getPrepend = function() {
    return this.prepend;
};

/**
 * StringBuffer.setStartWithSeparator
 *
 * Set the boolean that tells the buffer to prefix the entire results
 * with the default separator if one has been provided to the buffer!
 *
 * @param prefixing - Prefix the results, or not
 */
StringBuffer.prototype.setStartWithSeparator = function(startWithSeparator) {
    this.startWithSeparator = startWithSeparator;
};

/**
 * StringBuffer.getStartWithSeparator
 *
 * Set the boolean that tells the buffer to prefix the entire results
 * with the default separator if one has been provided to the buffer!
 */
StringBuffer.prototype.getStartWithSeparator = function() {
    return this.startWithSeparator;
};

/**
 * StringBuffer.empty()
 *
 * Empty the buffer
 */
StringBuffer.prototype.empty = function() {
    this.buffer = new Array();
};

/**
 * StringBuffer.isEmpty()
 *
 * Test to see if buffer is empty
 */
StringBuffer.prototype.isEmpty = function() {
    return this.buffer.length == 0;
};

/**
 * StringBuffer.getActualData()
 *
 * Get the actual data
 */
StringBuffer.prototype.getActualData = function() {
    return this.buffer;
};

/**
 * StringBuffer.append
 *
 * Append a string to the buffer
 *
 * Use this instead of lots of string concatenation with a +=
 *
 * @param ... - The content to append
 */
StringBuffer.prototype.append = function() {
    this.appendSep(null, arguments);
};

/**
 * StringBuffer.appendSep
 *
 * Append a string to the buffer, with an optional separator!
 *
 * Use this instead of lots of string concatenation with a +=
 *
 * @param separator - The separator
 * @param ... - The content to append
 */
StringBuffer.prototype.appendSep = function(separator) {
    var currSeparator = separator != null ? separator : this.defaultSeparator;

    for (var ii = 1; ii < arguments.length; ii++) {
        var item = arguments[ii];

        if (isArray(item)) {
            // if the argument is an array, unroll it and add items

            for (var jj = 0; jj < item.length; jj++) {
                this.appendSep(separator, item[jj]);
            }
        } else if (item != null) {
            // if it's not an array, convert to a string and buffer

            item = item.toString();

            if (item != "") {
                if (currSeparator && (this.startWithSeparator || this.buffer.length > 0)) {
                    this.buffer[this.buffer.length] = currSeparator;
                }

                this.buffer[this.buffer.length] = item;
            }
        }
    }
};

/**
 * StringBuffer.newline
 *
 * Append newline to the buffer
 */
StringBuffer.prototype.newline = function() {
   this.buffer[this.buffer.length] = "\n";
};

/**
 * StringBuffer.toString
 *
 * Act like a String! Yeah! Do it!
 */
StringBuffer.prototype.toString = function() {
    var string;

    if (this.buffer.length == 1) {
        // Return a single item as a string. This becomes handy
        // if someone calls toString over and over. The initial
        // call constructs the string and stores it as a single
        // item in the buffer. The next time through it'll skip
        // the concatenation step by hitting this block! Quick!

        string = this.buffer[0];
    } else {
        string = this.buffer.join("");

        // Before we return the concatenated string, wipe clear
        // the old buffer and add the new string to it. This'll
        // make toStringing the buffer next time so much faster

        this.buffer = new Array();

        this.buffer[this.buffer.length] = string;
    }

    if (this.prepend != null && this.prepend != "") {
        return string.prependLines(this.prepend);
    } else {
        return string;
    }
};

//#############################################################################
// AJAX
//#############################################################################

if (!isDefined(window.XMLHttpRequest)) {
    window.XMLHttpRequest = function() {
        var types = [
            "Microsoft.XMLHTTP",
            "MSXML2.XMLHTTP.5.0",
            "MSXML2.XMLHTTP.4.0",
            "MSXML2.XMLHTTP.3.0",
            "MSXML2.XMLHTTP"
        ];

        for (var ii = 0; ii < types.length; ii++) {
            try {
                return new ActiveXObject(types[ii]);
            } catch (e) {
                // Do nothing!
            }
        }

        return undefined;
    }
}