function URL(url) {
    if (!url) return Object.create(URL.prototype);
    // Can't use String.trim() since it defines whitespace differently than HTML
    this.url = url.replace(/^[ \t\n\r\f]+|[ \t\n\r\f]+$/g, "");

    // See http://tools.ietf.org/html/rfc3986#appendix-B
    var match = URL.pattern.exec(this.url);
    if (match) {
        if (match[2]) this.scheme = match[2];
        if (match[4]) {
            // XXX ignoring userinfo before the hostname
            if (match[4].match(URL.portPattern)) {
                var pos = S.lastIndexOf(match[4], ":");
                this.host = substring(match[4], 0, pos);
                this.port = substring(match[4], pos+1);
            }
            else {
                this.host = match[4];
            }
        }
        if (match[5]) this.path = match[5];
        if (match[6]) this.query = match[7];
        if (match[8]) this.fragment = match[9];
    }
}

URL.pattern = /^(([^:\/?#]+):)?(\/\/([^\/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?$/;
URL.portPattern = /:\d+$/
URL.authorityPattern = /^[^:\/?#]+:\/\//;
URL.hierarchyPattern = /^[^:\/?#]+:\//;

// Return a percentEncoded version of s.
// S should be a single-character string
// XXX: needs to do utf-8 encoding?
URL.percentEncode = function percentEncode(s) {
    var c = charCodeAt(s, 0);
    if (c < 256) return "%" + c.toString(16);
    else throw Error("can't percent-encode codepoints > 255 yet");
};

URL.prototype = {
    constructor: URL,

    // XXX: not sure if this is the precise definition of absolute
    isAbsolute: function() { return !!this.scheme; },
    isAuthorityBased: function() {
        return URL.authorityPattern.test(this.url);
    },
    isHierarchical: function() {
        return URL.hierarchyPattern.test(this.url);
    },

    toString: function() {
        var s = "";
        if (this.scheme !== undefined) s += this.scheme + ":";
        if (this.host !== undefined) s += "//" + this.host;
        if (this.port !== undefined) s += ":" + this.port;
        if (this.path !== undefined) s += this.path;
        if (this.query !== undefined) s += "?" + this.query;
        if (this.fragment !== undefined) s += "#" + this.fragment;
        return s;
    },

    // See: http://tools.ietf.org/html/rfc3986#section-5.2
    resolve: function(relative) {
        var base = this;            // The base url we're resolving against
        var r = new URL(relative);  // The relative reference url to resolve
        var t = new URL();          // The absolute target url we will return

        if (r.scheme !== undefined) {
            t.scheme = r.scheme;
            t.host = r.host;
            t.port = r.port;
            t.path = remove_dot_segments(r.path);
            t.query = r.query;
        }
        else {
            t.scheme = base.scheme;
            if (r.host !== undefined) {
                t.host = r.host;
                t.port = r.port;
                t.path = remove_dot_segments(r.path);
                t.query = r.query;
            }
            else {
                t.host = base.host;
                t.port = base.port;
                if (!r.path) { // undefined or empty
                    t.path = base.path;
                    if (r.query !== undefined)
                        t.query = r.query;
                    else
                        t.query = base.query;
                }
                else {
                    if (r.path.charAt(0) === "/") {
                        t.path = remove_dot_segments(r.path);
                    }
                    else {
                        t.path = merge(base.path, r.path);
                        t.path = remove_dot_segments(t.path);
                    }
                    t.query = r.query;
                }
            }
        }
        t.fragment = r.fragment;

        return t.toString();


        function merge(basepath, refpath) {
            if (base.host !== undefined && !base.path)
                return "/" + refpath;

            var lastslash = basepath.lastIndexOf("/");
            if (lastslash === -1)
                return refpath;
            else
                return basepath.substring(0, lastslash+1) + refpath;
        }

        function remove_dot_segments(path) {
            if (!path) return path;  // For "" or undefined

            var output = "";
            while(path.length > 0) {
                if (path === "." || path === "..") {
                    path = "";
                    break;
                }

                var twochars = path.substring(0,2);
                var threechars = path.substring(0,3);
                var fourchars = path.substring(0,4);
                if (threechars === "../") {
                    path = path.substring(3);
                }
                else if (twochars === "./") {
                    path = path.substring(2);
                }
                else if (threechars === "/./") {
                    path = "/" + path.substring(3);
                }
                else if (twochars === "/." && path.length === 2) {
                    path = "/";
                }
                else if (fourchars === "/../" ||
                         (threechars === "/.." && path.length === 3)) {
                    path = "/" + path.substring(4);

                    output = output.replace(/\/?[^\/]*$/, "");
                }
                else {
                    var segment = path.match(/(\/?([^\/]*))/)[0];
                    output += segment;
                    path = path.substring(segment.length);
                }
            }


            return output;
        }
    },
};
