//
//  ========================================================================
//  Copyright (c) 1995-2020 Mort Bay Consulting Pty Ltd and others.
//  ------------------------------------------------------------------------
//  All rights reserved. This program and the accompanying materials
//  are made available under the terms of the Eclipse Public License v1.0
//  and Apache License v2.0 which accompanies this distribution.
//
//      The Eclipse Public License is available at
//      http://www.eclipse.org/legal/epl-v10.html
//
//      The Apache License v2.0 is available at
//      http://www.opensource.org/licenses/apache2.0.php
//
//  You may elect to redistribute this code under either of these licenses.
//  ========================================================================
//

package org.eclipse.jetty.http;

import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;

import org.eclipse.jetty.util.MultiMap;
import org.eclipse.jetty.util.StringUtil;
import org.eclipse.jetty.util.TypeUtil;
import org.eclipse.jetty.util.URIUtil;
import org.eclipse.jetty.util.UrlEncoded;
import org.eclipse.jetty.util.Utf8StringBuilder;


/* ------------------------------------------------------------ */
/** Http URI.
 * Parse a HTTP URI from a string or byte array.  Given a URI
 * <code>http://user@host:port/path/info;param?query#fragment</code>
 * this class will split it into the following undecoded optional elements:<ul>
 * <li>{@link #getScheme()} - http:</li>
 * <li>{@link #getAuthority()} - //name@host:port</li>
 * <li>{@link #getHost()} - host</li>
 * <li>{@link #getPort()} - port</li>
 * <li>{@link #getPath()} - /path/info</li>
 * <li>{@link #getParam()} - param</li>
 * <li>{@link #getQuery()} - query</li>
 * <li>{@link #getFragment()} - fragment</li>
 * </ul>
 *
 */
public class HttpURI
{
    private static final byte[] __empty={};
    private final static int
    START=0,
    AUTH_OR_PATH=1,
    SCHEME_OR_PATH=2,
    AUTH=4,
    IPV6=5,
    PORT=6,
    PATH=7,
    PARAM=8,
    QUERY=9,
    ASTERISK=10;

    final Charset _charset;
    boolean _partial=false;
    byte[] _raw=__empty;
    String _rawString;
    int _scheme;
    int _authority;
    int _host;
    int _port;
    int _portValue;
    int _path;
    int _param;
    int _query;
    int _fragment;
    int _end;
    boolean _encoded=false;

    public HttpURI()
    {
        _charset = URIUtil.__CHARSET;
    }

    public HttpURI(Charset charset)
    {
        _charset = charset;
    }

    /* ------------------------------------------------------------ */
    /**
     * @param parsePartialAuth If True, parse auth without prior scheme, else treat all URIs starting with / as paths
     */
    public HttpURI(boolean parsePartialAuth)
    {
        _partial=parsePartialAuth;
        _charset = URIUtil.__CHARSET;
    }

    public HttpURI(String raw)
    {
        _rawString=raw;
        byte[] b = raw.getBytes(StandardCharsets.UTF_8);
        parse(b,0,b.length);
        _charset = URIUtil.__CHARSET;
    }

    public HttpURI(byte[] raw,int offset, int length)
    {
        parse2(raw,offset,length);
        _charset = URIUtil.__CHARSET;
    }

    public HttpURI(URI uri)
    {
        parse(uri.toASCIIString());
        _charset = URIUtil.__CHARSET;
    }

    public void parse(String raw)
    {
        byte[] b = StringUtil.getUtf8Bytes(raw);
        parse2(b,0,b.length);
        _rawString=raw;
    }

    public void parseConnect(String raw)
    {
        byte[] b = StringUtil.getBytes(raw);
        parseConnect(b,0,b.length);
        _rawString=raw;
    }

    public void parse(byte[] raw,int offset, int length)
    {
        _rawString=null;
        parse2(raw,offset,length);
    }


    public void parseConnect(byte[] raw,int offset, int length)
    {
        _rawString=null;
        _encoded=false;
        _raw=raw;
        int i=offset;
        int e=offset+length;
        int state=AUTH;
        _end=offset+length;
        _scheme=offset;
        _authority=offset;
        _host=offset;
        _port=_end;
        _portValue=-1;
        _path=_end;
        _param=_end;
        _query=_end;
        _fragment=_end;

        loop: while (i<e)
        {
            char c=(char)(0xff&_raw[i]);
            int s=i++;

            switch (state)
            {
                case AUTH:
                {
                    switch (c)
                    {
                        case ':':
                        {
                            _port = s;
                            break loop;
                        }
                        case '[':
                        {
                            state = IPV6;
                            break;
                        }
                    }
                    continue;
                }

                case IPV6:
                {
                    switch (c)
                    {
                        case '/':
                        {
                            throw new IllegalArgumentException("No closing ']' for " + new String(_raw,offset,length,_charset));
                        }
                        case ']':
                        {
                            state = AUTH;
                            break;
                        }
                    }

                    continue;
                }
            }
        }

        if (_port<_path)
            _portValue=TypeUtil.parseInt(_raw, _port+1, _path-_port-1,10);
        else
            throw new IllegalArgumentException("No port");
        _path=offset;
    }


    private void parse2(byte[] raw,int offset, int length)
    {
        _encoded=false;
        _raw=raw;
        int i=offset;
        int e=offset+length;
        int state=START;
        int m=offset;
        _end=offset+length;
        _scheme=offset;
        _authority=offset;
        _host=offset;
        _port=offset;
        _portValue=-1;
        _path=offset;
        _param=_end;
        _query=_end;
        _fragment=_end;
        while (i<e)
        {
            char c=(char)(0xff&_raw[i]);
            int s=i++;

            state: switch (state)
            {
                case START:
                {
                    m=s;
                    switch(c)
                    {
                        case '/':
                            state=AUTH_OR_PATH;
                            break;
                        case ';':
                            _param=s;
                            state=PARAM;
                            break;
                        case '?':
                            _param=s;
                            _query=s;
                            state=QUERY;
                            break;
                        case '#':
                            _param=s;
                            _query=s;
                            _fragment=s;
                            break;
                        case '*':
                            _path=s;
                            state=ASTERISK;
                            break;

                        default:
                            state=SCHEME_OR_PATH;
                    }

                    continue;
                }

                case AUTH_OR_PATH:
                {
                    if ((_partial||_scheme!=_authority) && c=='/')
                    {
                        _host=i;
                        _port=_end;
                        _path=_end;
                        state=AUTH;
                    }
                    else if (c==';' || c=='?' || c=='#')
                    {
                        i--;
                        state=PATH;
                    }
                    else
                    {
                        _host=m;
                        _port=m;
                        state=PATH;
                    }
                    continue;
                }

                case SCHEME_OR_PATH:
                {
                    // short cut for http and https
                    if (length>6 && c=='t')
                    {
                        if (_raw[offset+3]==':')
                        {
                            s=offset+3;
                            i=offset+4;
                            c=':';
                        }
                        else if (_raw[offset+4]==':')
                        {
                            s=offset+4;
                            i=offset+5;
                            c=':';
                        }
                        else if (_raw[offset+5]==':')
                        {
                            s=offset+5;
                            i=offset+6;
                            c=':';
                        }
                    }

                    switch (c)
                    {
                        case ':':
                        {
                            m = i++;
                            _authority = m;
                            _path = m;
                            c = (char)(0xff & _raw[i]);
                            if (c == '/')
                                state = AUTH_OR_PATH;
                            else
                            {
                                _host = m;
                                _port = m;
                                state = PATH;
                            }
                            break;
                        }

                        case '/':
                        {
                            state = PATH;
                            break;
                        }

                        case ';':
                        {
                            _param = s;
                            state = PARAM;
                            break;
                        }

                        case '?':
                        {
                            _param = s;
                            _query = s;
                            state = QUERY;
                            break;
                        }

                        case '#':
                        {
                            _param = s;
                            _query = s;
                            _fragment = s;
                            break;
                        }
                    }
                    continue;
                }

                case AUTH:
                {
                    switch (c)
                    {

                        case '/':
                        {
                            m = s;
                            _path = m;
                            _port = _path;
                            state = PATH;
                            break;
                        }
                        case '@':
                        {
                            _host = i;
                            break;
                        }
                        case ':':
                        {
                            _port = s;
                            state = PORT;
                            break;
                        }
                        case '[':
                        {
                            state = IPV6;
                            break;
                        }
                    }
                    continue;
                }

                case IPV6:
                {
                    switch (c)
                    {
                        case '/':
                        {
                            throw new IllegalArgumentException("No closing ']' for " + new String(_raw,offset,length,_charset));
                        }
                        case ']':
                        {
                            state = AUTH;
                            break;
                        }
                    }

                    continue;
                }

                case PORT:
                {
                    if (c=='/')
                    {
                        m=s;
                        _path=m;
                        if (_port<=_authority)
                            _port=_path;
                        state=PATH;
                    }
                    continue;
                }

                case PATH:
                {
                    switch (c)
                    {
                        case ';':
                        {
                            _param = s;
                            state = PARAM;
                            break;
                        }
                        case '?':
                        {
                            _param = s;
                            _query = s;
                            state = QUERY;
                            break;
                        }
                        case '#':
                        {
                            _param = s;
                            _query = s;
                            _fragment = s;
                            break state;
                        }
                        case '%':
                        {
                            _encoded=true;
                        }
                    }
                    continue;
                }

                case PARAM:
                {
                    switch (c)
                    {
                        case '?':
                        {
                            _query = s;
                            state = QUERY;
                            break;
                        }
                        case '#':
                        {
                            _query = s;
                            _fragment = s;
                            break state;
                        }
                    }
                    continue;
                }

                case QUERY:
                {
                    if (c=='#')
                    {
                        _fragment=s;
                        break state;
                    }
                    continue;
                }

                case ASTERISK:
                {
                    throw new IllegalArgumentException("only '*'");
                }
            }
        }

        if (_port<_path)
            _portValue=TypeUtil.parseInt(_raw, _port+1, _path-_port-1,10);
    }

    public String getScheme()
    {
        if (_scheme==_authority)
            return null;
        int l=_authority-_scheme;
        if (l==5 &&
                _raw[_scheme]=='h' &&
                _raw[_scheme+1]=='t' &&
                _raw[_scheme+2]=='t' &&
                _raw[_scheme+3]=='p' )
            return HttpScheme.HTTP.asString();
        if (l==6 &&
                _raw[_scheme]=='h' &&
                _raw[_scheme+1]=='t' &&
                _raw[_scheme+2]=='t' &&
                _raw[_scheme+3]=='p' &&
                _raw[_scheme+4]=='s' )
            return HttpScheme.HTTPS.asString();

        return new String(_raw,_scheme,_authority-_scheme-1,_charset);
    }

    public String getAuthority()
    {
        if (_authority==_path)
            return null;
        return new String(_raw,_authority,_path-_authority,_charset);
    }

    public String getHost()
    {
        if (_host==_port)
            return null;
        return new String(_raw,_host,_port-_host,_charset);
    }

    public int getPort()
    {
        return _portValue;
    }

    public String getPath()
    {
        if (_path==_param)
            return null;
        return new String(_raw,_path,_param-_path,_charset);
    }

    public String getDecodedPath()
    {
        if (_path==_param)
            return null;

        Utf8StringBuilder utf8b=null;

        for (int i=_path;i<_param;i++)
        {
            byte b = _raw[i];

            if (b=='%')
            {
                if (utf8b==null)
                {
                    utf8b=new Utf8StringBuilder();
                    utf8b.append(_raw,_path,i-_path);
                }
                
                if ((i+2)>=_param)
                    throw new IllegalArgumentException("Bad % encoding: "+this);
                if (_raw[i+1]=='u')
                {
                    if ((i+5)>=_param)
                        throw new IllegalArgumentException("Bad %u encoding: "+this);
                    try
                    {
                        String unicode = new String(Character.toChars(TypeUtil.parseInt(_raw,i+2,4,16)));
                        utf8b.getStringBuilder().append(unicode);
                        i+=5;
                    }
                    catch(Exception e)
                    {
                        throw new RuntimeException(e);
                    }
                }
                else
                {
                    b=(byte)(0xff&TypeUtil.parseInt(_raw,i+1,2,16));
                    utf8b.append(b);
                    i+=2;
                }
                continue;
            }
            else if (utf8b!=null)
            {
                utf8b.append(b);
            }
        }

        if (utf8b==null)
            return StringUtil.toUTF8String(_raw, _path, _param-_path);
        return utf8b.toString();
    }

    public String getDecodedPath(String encoding)
    {
        return getDecodedPath(Charset.forName(encoding));
    }

    public String getDecodedPath(Charset encoding)
    {
        if (_path==_param)
            return null;

        int length = _param-_path;
        byte[] bytes=null;
        int n=0;

        for (int i=_path;i<_param;i++)
        {
            byte b = _raw[i];

            if (b=='%')
            {
                if (bytes==null)
                {
                    bytes=new byte[length];
                    System.arraycopy(_raw,_path,bytes,0,n);
                }
                
                if ((i+2)>=_param)
                    throw new IllegalArgumentException("Bad % encoding: "+this);
                if (_raw[i+1]=='u')
                {
                    if ((i+5)>=_param)
                        throw new IllegalArgumentException("Bad %u encoding: "+this);

                    try
                    {
                        String unicode = new String(Character.toChars(TypeUtil.parseInt(_raw,i+2,4,16)));
                        byte[] encoded = unicode.getBytes(encoding);
                        System.arraycopy(encoded,0,bytes,n,encoded.length);
                        n+=encoded.length;
                        i+=5;
                    }
                    catch(Exception e)
                    {
                        throw new RuntimeException(e);
                    }
                }
                else
                {
                    b=(byte)(0xff&TypeUtil.parseInt(_raw,i+1,2,16));
                    bytes[n++]=b;
                    i+=2;
                }
                continue;
            }
            else if (bytes==null)
            {
                n++;
                continue;
            }

            bytes[n++]=b;
        }


        if (bytes==null)
            return new String(_raw,_path,_param-_path,encoding);

        return new String(bytes,0,n,encoding);
    }

    public String getPathAndParam()
    {
        if (_path==_query)
            return null;
        return new String(_raw,_path,_query-_path,_charset);
    }

    public String getCompletePath()
    {
        if (_path==_end)
            return null;
        return new String(_raw,_path,_end-_path,_charset);
    }

    public String getParam()
    {
        if (_param==_query)
            return null;
        return new String(_raw,_param+1,_query-_param-1,_charset);
    }

    public String getQuery()
    {
        if (_query==_fragment)
            return null;
        return new String(_raw,_query+1,_fragment-_query-1,_charset);
    }

    public String getQuery(String encoding)
    {
        if (_query==_fragment)
            return null;
        return StringUtil.toString(_raw,_query+1,_fragment-_query-1,encoding);
    }

    public boolean hasQuery()
    {
        return (_fragment>_query);
    }

    public String getFragment()
    {
        if (_fragment==_end)
            return null;
        return new String(_raw,_fragment+1,_end-_fragment-1,_charset);
    }

    public void decodeQueryTo(MultiMap<String> parameters)
    {
        if (_query==_fragment)
            return;
        if (_charset.equals(StandardCharsets.UTF_8))
            UrlEncoded.decodeUtf8To(_raw,_query+1,_fragment-_query-1,parameters);
        else
            UrlEncoded.decodeTo(new String(_raw,_query+1,_fragment-_query-1,_charset),parameters,_charset,-1);
    }

    public void decodeQueryTo(MultiMap<String> parameters, String encoding) throws UnsupportedEncodingException
    {
        if (_query==_fragment)
            return;

        if (encoding==null || StringUtil.isUTF8(encoding))
            UrlEncoded.decodeUtf8To(_raw,_query+1,_fragment-_query-1,parameters);
        else
            UrlEncoded.decodeTo(StringUtil.toString(_raw,_query+1,_fragment-_query-1,encoding),parameters,encoding,-1);
    }

    public void decodeQueryTo(MultiMap<String> parameters, Charset encoding) throws UnsupportedEncodingException
    {
        if (_query==_fragment)
            return;

        if (encoding==null || StandardCharsets.UTF_8.equals(encoding))
            UrlEncoded.decodeUtf8To(_raw,_query+1,_fragment-_query-1,parameters);
        else
            UrlEncoded.decodeTo(new String(_raw,_query+1,_fragment-_query-1,encoding),parameters,encoding,-1);
    }

    public void clear()
    {
        _scheme=_authority=_host=_port=_path=_param=_query=_fragment=_end=0;
        _raw=__empty;
        _rawString="";
        _encoded=false;
    }

    @Override
    public String toString()
    {
        if (_rawString==null)
            _rawString=new String(_raw,_scheme,_end-_scheme,_charset);
        return _rawString;
    }

    public void writeTo(Utf8StringBuilder buf)
    {
        buf.append(_raw,_scheme,_end-_scheme);
    }

}
