import ceylon.buffer.charset {
    charsetsByAlias
}

"Contains methods for percent-encoding. 
     See http://tools.ietf.org/html/rfc3986#appendix-A for specifications."
by("Stéphane Épardaud")
shared object percentEncoder {

    """gen-delims = ":" / "/" / "?" / "#" / "[" / "]" / "@"
    """
    Boolean isGenDelim(Character c); 
    isGenDelim = set {
        ':', '/', '?', '#', '[', ']', '@'
    }.contains;

    """sub-delims = "!" / "$" / "&" / "'" / "(" / ")" / "*" / "+" / "," / ";" / "="
    """
    Boolean isSubDelim(Character c);
    isSubDelim = set {
        '!', '$', '&', '\'', '(', ')', '*', '+', ',', ';', '='
    }.contains;

    // """reserved = gen-delims | sub-delims"""
    // function isReserved(Character c)
    //         => isGenDelim(c) || isSubDelim(c);

    """lowalpha = 'a'..'z'"""
    function isLowAlpha(Character c)
            => 'a' <= c <= 'z';

    """upalpha = 'A'..'Z'"""
    function isUpAlpha(Character c)
            => 'A' <= c <= 'Z';

    """alpha = lowalpha | upalpha"""
    function isAlpha(Character c)
            => isLowAlpha(c) || isUpAlpha(c);

    """digit = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9"
    """ 
    function isDigit(Character c)
            => '0' <= c <= '9';

    """alphanum = alpha | digit"""
    function isAlphaNum(Character c)
            => isAlpha(c) || isDigit(c);

    """unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~"
    """
    function isUnreserved(Character c)
            => isAlphaNum(c)
                // || c in ['-', '.', '_', '~'];
                || c == '-'
                || c == '.'
                || c == '_'
                || c == '~';

    """authority = unreserved | escaped | sub-delims

       - Note: we don`t allow escaped here since we will escape it ourselves, so we don`t want to allow them in the
         unescaped sequences.
       - Note: we don't allow ':' in userinfo as that's the user/pass delimiter
       - Note: this class doesn't exist but represents the value used for user/pass/reg-name (NOT for IPLiteral)"""
    function isAuthority(Character c)
            => isUnreserved(c) || isSubDelim(c);

    """pchar = unreserved | escaped | sub-delims | ":" | "@"
       
       Note: we don`t allow escaped here since we will escape it ourselves, so we don`t want to allow them in the
       unescaped sequences
    """
    function isPChar(Character c)
            => isUnreserved(c) || isSubDelim(c) || c == ':' || c == '@';

    """path_segment = pchar <without> ";"
    """
    function isPathSegment(Character c)
            => isPChar(c)
            // deviate from the RFC in order to disallow the path param separator
            && c != ';';

    """path_param_name = pchar <without> ";" | "="
    """
    function isPathParamName(Character c)
            => isPChar(c)
            // deviate from the RFC in order to disallow the path param separators
            // && !c in [';', '='];
            && c != ';' && c != '=';

    """path_param_value = pchar <without> ";"
    """
    function isPathParamValue(Character c)
            => isPChar(c)
            // deviate from the RFC in order to disallow the path param separator
            && !c == ';';

    """query = pchar / "/" / "?"
    """
    function isQuery(Character c)
            => (isPChar(c) || c == '/' || c == '?')
            // deviate from the RFC to disallow separators such as "=", "@" and the famous "+" which is treated as a space
            // when decoding
            && c != '=' && c != '&' && c != '+';

    """fragment = pchar / "/" / "?"
    """
    function isFragment(Character c)
            => isPChar(c) || c == '/' || c == '?';

    "Percent-encodes a string for use in an authority/user URI part"
    shared String encodeUser(String str)
            => encodePart(str, "UTF-8", isAuthority);

    "Percent-encodes a string for use in an authority/password URI part"
    shared String encodePassword(String str)
            => encodePart(str, "UTF-8", isAuthority);

    "Percent-encodes a string for use in an authority/regName URI part (host name or IPV4Literal)"
    shared String encodeRegName(String str)
            => encodePart(str, "UTF-8", isAuthority);
    
    "Percent-encodes a string for use in an path/segment name URI part"
    shared String encodePathSegmentName(String str) {
        return encodePart(str, "UTF-8", isPathSegment);
    }

    "Percent-encodes a string for use in an path/segment parameter name URI part"
    shared String encodePathSegmentParamName(String str)
            => encodePart(str, "UTF-8", isPathParamName);

    "Percent-encodes a string for use in an path/segment parameter value URI part"
    shared String encodePathSegmentParamValue(String str)
            => encodePart(str, "UTF-8", isPathParamValue);
    
    "Percent-encodes a string for use in an query parameter name or value URI part"
    shared String encodeQueryPart(String str)
            => encodePart(str, "UTF-8", isQuery);

    "Percent-encodes a string for use in a fragment URI part"
    shared String encodeFragment(String str)
            => encodePart(str, "UTF-8", isFragment);
    
    String encodePart(String str, String encoding, Boolean allowed(Character c)) {
        value encoded = StringBuilder();
        for (c in str) {
            if (allowed(c)) {
                encoded.append(c.string);
            }
            else {
                value charset = charsetsByAlias[encoding];
                if (!exists charset) {
                    throw AssertionError("Encoding not supported: '``encoding``'");
                }
                value bytes = charset.encode(c.string);
                for (byte in bytes) {
                    encoded.appendCharacter('%');
                    encoded.append(Integer.format(byte.unsigned, 16)
                        .uppercased.pad(2, '0'));
                }
            }
        }
        return encoded.string;
    }
}