define(
    [ "./declare" ],
    function(declare) {
        function parseURI(uri) {
            var schema, host, port, path, query, hash, i;
            if (typeof (uri) == "string") {
                if ((i = uri.indexOf(":")) >= 0 &&
                    uri.substr(0, i).match(/^\w+$/)) {
                    schema = uri.substr(0, i);
                    uri = uri.substr(i + 1);
                }

                if (uri.indexOf("//") === 0) {
                    uri = uri.substr(2);
                    if ((i = uri.indexOf("/")) >= 0) {
                        host = uri.substr(0, i);
                        uri = uri.substr(i);
                    } else {
                        host = uri;
                        uri = "";
                    }
                }

                if ((i = uri.indexOf("?")) >= 0) {
                    path = uri.substr(0, i);
                    uri = uri.substr(i + 1);

                } else {
                    path = uri;
                    uri = "";

                    if ((i = path.indexOf("#")) >= 0) {
                        hash = path.substr(i + 1);
                        path = path.substr(0, i);
                    }
                }

                if ((i = uri.indexOf("#")) >= 0) {
                    query = uri.substr(0, i);
                    hash = uri.substr(i + 1);
                } else {
                    query = uri;
                }
            }

            if (host && (i = host.lastIndexOf(":")) >= 0) {
                port = host.substr(i + 1);
                host = host.substr(0, i);
            }

            return {
                schema : schema,
                host : host,
                port : port,
                path : path,
                query : query,
                hash : hash
            };
        }

        function makeURI(options) {
            var uri = [];

            if (options.schema)
                uri.push(options.schema, ":");
            if (options.host)
                uri.push("//", options.host);
            if (options.host && options.port)
                uri.push(":", options.port);

            if (options.path) {
                if (options.host && options.path[0] != "/")
                    uri.push("/");
                uri.push(options.path);
            } else if (options.host) {
                uri.push("/");
            }

            if (options.query)
                uri.push("?", options.query);
            if (options.hash)
                uri.push("#", options.hash);

            return uri.join("");
        }

        function reducePath(parts) {
            var balance = 0, result = [], isRoot;

            for (var i = 0; i < parts.length; i++) {
                var part = parts[i];
                switch (part) {
                case "..":
                    if (balance > 0) {
                        result.pop();
                    } else {
                        if (isRoot)
                            throw new Error("Unbalanced path: " + parts);

                        result.push(part);
                    }
                    balance--;
                    break;
                case ".":
                    break;
                case "":
                    if (i === 0) {
                        isRoot = true;
                        result.push(part);
                    }
                    break;
                default:
                    result.push(part);
                    balance++;
                    break;
                }
            }

            return result.join("/");
        }

        var meta = {
            schema : null,
            host : null,
            port : null,
            path : null,
            query : null,
            hash : null
        };

        var URI = declare(null, {
            constructor : function(opts) {
                if (typeof (opts) == "string")
                    opts = parseURI(opts);
                for ( var p in meta)
                    if (p in opts)
                        this[p] = opts[p];
            },

            clone : function() {
                return new URI(this);
            },

            combine : function(rel) {
                var me = this;

                if (typeof (rel) === "string")
                    rel = new URI(rel);
                else
                    rel = rel.clone();

                // //some.host:123/path?q=a#123
                if (rel.host)
                    return rel;

                // /abs/path?q=a#123
                if (rel.path && rel.path[0] == "/") {
                    if (me.host) {
                        rel.schema = me.schema;
                        rel.host = me.host;
                        rel.port = me.port;
                    }
                    return rel;
                }

                var base = me.clone();

                // rel/path?a=b#cd
                if (rel.path) {
                    var segments = base.getSegments();
                    segments.pop();
                    segments.push.apply(segments, rel.getSegments());

                    base.path = reducePath(segments);
                }

                // ?q=a#123
                if (rel.query)
                    base.query = rel.query;
                if (rel.hash)
                    base.hase = rel.hash;

                return base;
            },

            optimize : function() {
                this.path = reducePath(this.getSegments());
            },

            getSegments : function() {
                if (typeof (this.path) === "string")
                    return this.path.split("/");
                else
                    return [];
            },

            toString : function() {
                var uri = [], me = this;

                if (me.schema)
                    uri.push(me.schema, ":");
                if (me.host)
                    uri.push("//", me.host);
                if (me.host && me.port)
                    uri.push(":", me.port);

                if (me.path) {
                    if (me.host && me.path[0] != "/")
                        uri.push("/");
                    uri.push(me.path);
                } else if (me.host) {
                    uri.push("/");
                }

                if (me.query)
                    uri.push("?", me.query);
                if (me.hash)
                    uri.push("#", me.hash);

                return uri.join("");
            }

        });

        URI.combine = function(base, rel) {
            if (typeof (base) === "string")
                base = new URI(base);
            return base.combine(rel).toString();
        };

        return URI;
    });