"Parses a raw percent-encoded path parameter"
shared Parameter parseParameter(String part) {
    Integer? sep = part.firstOccurrence('=');
    if(exists sep) {
        return Parameter(decodePercentEncoded(part.initial(sep)),
            decodePercentEncoded(part.terminal(part.size - sep - 1)));
    }else{
        return Parameter(decodePercentEncoded(part));
    }
}

Authority defaultAuthority = Authority();
Query defaultQuery = Query();
Path defaultPath = Path();

"Parses a URI"
throws(`class InvalidUriException`,
    "If the URI is invalid")
shared Uri parse(String uri) {
    variable String? scheme = null;

    variable String? authorityUser = null;
    variable String? authorityPassword = null;
    variable String? authorityHost = null;
    variable Integer? authorityPort = null;
    variable Boolean authorityIPLiteral = false;

    variable Path path = defaultPath;
    variable Query query = defaultQuery;
    variable String? fragment = null;

    String parseScheme(String uri) {
        Integer? sep = uri.firstInclusion(":");
        if(exists sep) {
            if(sep > 0) {
                scheme = uri.measure(0, sep);
                return uri[sep+1...];
            }
        }
        // no scheme, it must be relative
        return uri;
    }

    void parseUserInfo(String userInfo) {
        Integer? sep = userInfo.firstOccurrence(':');
        if(exists sep) {
            authorityUser = decodePercentEncoded(userInfo.measure(0, sep));
            authorityPassword = decodePercentEncoded(userInfo[sep+1...]);
        }else{
            authorityUser = decodePercentEncoded(userInfo);
            authorityPassword = null;
        }
    }

    void parseHostAndPort(String hostAndPort) {
        String? portString;
        if(hostAndPort.startsWith("[")) {
            authorityIPLiteral = true;
            Integer? end = hostAndPort.firstOccurrence(']');
            if(exists end) {
                // eat the delimiters
                authorityHost = hostAndPort.measure(1, end-1);
                String rest = hostAndPort[end+1...];
                if(rest.startsWith(":")) {
                    portString = rest[1...];
                }else{
                    portString = null;
                }
            }else{
                throw InvalidUriException("Invalid IP literal: " + hostAndPort);
            }
        }else{
            authorityIPLiteral = false;
            Integer? sep = hostAndPort.lastOccurrence(':');
            if(exists sep) {
                authorityHost = decodePercentEncoded(hostAndPort.measure(0, sep));
                portString = hostAndPort[sep+1...];
            }else{
                authorityHost = decodePercentEncoded(hostAndPort);
                portString = null;
            }
        }
        if(exists portString) {
            authorityPort
                    = if (is Integer port = Integer.parse(portString))
                    then port else null;
            if(exists Integer port = authorityPort) {
                if(port < 0) {
                    throw InvalidUriException("Invalid port number: "+portString);
                }
            }else{
                throw InvalidUriException("Invalid port number: "+portString);
            }
        }else{
            authorityPort = null;
        }
    }

    String parseAuthority(String uri) {
        if(!uri.startsWith("//")) {
            return uri;
        }
        // eat the two slashes
        String part = uri[2...];
        Integer? sep = part.firstOccurrence('/')
            else part.firstOccurrence('?')
            else part.firstOccurrence('#');
        String authority;
        String remains;
        if(exists sep) {
            authority = part.measure(0, sep);
            remains = part[sep...];
        }else{
            // no path part
            authority = part;
            remains = "";
        }
        Integer? userInfoSep = authority.firstOccurrence('@');
        String hostAndPort;
        if(exists userInfoSep) {
            parseUserInfo(authority.measure(0, userInfoSep));
            hostAndPort = authority[userInfoSep+1...];
        }else{
            hostAndPort = authority;
        }
        parseHostAndPort(hostAndPort);
        return remains;
    }

    {Parameter*} parsePathSegmentParameters(String part)
        =>  [ for(param in part.split { ';'.equals; groupSeparators = false; })
                parseParameter(param) ];

    "Parse a raw (percent-encoded) segment, with optional
     parameters to be parsed"
    PathSegment parseRawPathSegment(String part) {
        Integer? sep = part.firstOccurrence(';');
        String name;
        if(exists sep) {
            name = part[0..sep-1];
        }else{
            name = part;
        }
        PathSegment path;
        if(exists sep) {
            path = PathSegment(decodePercentEncoded(name),
                               *parsePathSegmentParameters(part[sep+1...]));
        }else{
            path = PathSegment(decodePercentEncoded(name));
        }
        return path;
    }

    String parsePath(String uri) {
        Integer? sep = uri.firstOccurrence('?') else uri.firstOccurrence('#');
        String pathPart;
        String remains;
        if(exists sep) {
            pathPart = uri.measure(0, sep);
            remains = uri[sep...];
        }else{
            // no query/fragment part
            pathPart = uri;
            remains = "";
        }
        if(!pathPart.empty) { // else, use default `path` already initialized
            value parts = pathPart.split { '/'.equals; groupSeparators = false; }.sequence();
            value absolute = parts.first.empty;
            path = Path {
                absolute = absolute;
                segments = if (absolute)
                           then parts.rest.collect(parseRawPathSegment)
                           else parts.collect(parseRawPathSegment);
            };
        }

        return remains;
    }

    Query parseQueryPart(String queryPart)
        =>  Query {
                parameters = queryPart.split { '&'.equals; groupSeparators = false; }
                                      .collect(parseParameter);
            };

    String parseQuery(String uri) {
        Character? c = uri[0];
        if(exists c) {
            if(c == '?') { // else, use default `query` already initialized
                // we have a query part
                Integer end = uri.firstOccurrence('#') else uri.size;
                query = parseQueryPart(uri.measure(1, end-1));
                return uri[end...];
            }
        }
        // no query/fragment part
        return uri;
    }

    String parseFragment(String uri) {
        Character? c = uri[0];
        if(exists c) {
            if(c == '#') {
                // we have a fragment part
                fragment = decodePercentEncoded(uri[1...]);
                return "";
            }
        }
        // no query/fragment part
        return uri;
    }

    void parseURI(String uri) {
        variable String remains = parseScheme(uri);
        remains = parseAuthority(remains);
        remains = parsePath(remains);
        remains = parseQuery(remains);
        remains = parseFragment(remains);
    }

    parseURI(uri);

    Authority authority =
        if (authorityUser exists
                || authorityPassword exists
                || authorityHost exists
                || authorityPort exists)
        then Authority {
            user = authorityUser;
            password = authorityPassword;
            host = authorityHost;
            port = authorityPort;
            ipLiteral = authorityIPLiteral; }
        else defaultAuthority;

   return Uri(scheme, authority, path, query, fragment);
}