package com.lnwazg.httpkit.io;

import java.io.BufferedReader;
import java.io.Closeable;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.UnsupportedEncodingException;
import java.util.HashMap;
import java.util.Map;

import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.CharEncoding;
import org.apache.commons.lang3.StringUtils;

import com.lnwazg.httpkit.exception.MalformedRequestException;
import com.lnwazg.kit.http.url.URIEncoderDecoder;
import com.lnwazg.kit.http.url.UriParamUtils;
import com.lnwazg.kit.io.StreamUtils;
import com.lnwazg.kit.log.Logs;

/**
 * 请求解析类
 * @author [email protected]
 * @version 2016年11月27日
 */
public class HttpReader implements Closeable
{
    public static final String GET = "GET";
    
    public static final String POST = "POST";
    
    public static final String HEAD = "HEAD";
    
    public static final String PUT = "PUT";
    
    public static final String DELETE = "DELETE";
    
    /**
     * 输入流
     */
    public final InputStream in;
    
    /**
     * 包装后的输入流
     */
    public final BufferedReader reader;
    
    /**
     * POST GET HEAD等
     */
    private String requestType;
    
    /**
     * /card/viewCardByUserId.do?userId=2
     */
    private String uri;
    
    /**
     * HTTP/1.1   HTTP/1.0
     */
    private String version;
    
    /**
     * 请求头表
     */
    private Map<String, String> headerMap = new HashMap<>();
    
    /**
     * cookie表
     */
    private Map<String, String> cookieMap = new HashMap<>();
    
    /**
     * 参数表
     */
    private Map<String, String> paramMap = new HashMap<>();
    
    /**
     * 增设参数表
     * @author nan.li
     * @param extraMap
     */
    public void appendExtraRequestParamMap(Map<String, String> extraMap)
    {
        //因为是增设的参数,并且还是url路径中的正则参数,那么这些参数的优先级自然是最低的,因此只能原有参数覆盖它们,而不是它们覆盖原有参数
        //覆盖参数
        extraMap.putAll(paramMap);
        //覆盖完毕后,再回设给paramMap
        paramMap = extraMap;
    }
    
    /**
     * 消息体字节码
     */
    //    private byte[] body;
    
    /**
     * 消息体字符串,跟body字节码一一对应
     */
    private String payloadBody;
    
    /**
     * 客户端是否支持gzip压缩输出<br>
     * 默认不支持<br>
     * 支持gzip的意思是说,服务端多了一种输出的方式:既可以gzip输出,也可以普通输出。<br>
     * 若不支持gzip输出,那么只可以普通输出。
     */
    private boolean supportGzipOutput;
    
    public HttpReader(InputStream in)
        throws UnsupportedEncodingException
    {
        this.in = in;
        this.reader = new BufferedReader(new InputStreamReader(in, CharEncoding.UTF_8));
        //依次从头读到尾
        //1.读消息签名
        if (readSignatureFully())
        {
            //2.读消息头
            if (readHeadersFully())
            {
                //3.读消息体
                if (readBodyFully())
                {
                    //解析其他的额外信息 
                    resolveExtraInfo();
                }
            }
        }
    }
    
    /**
     * 读取请求的消息体<br>
     * 消息体是裸字节,可以是流的形式,也可能是字符数组
     * @author nan.li
     * @return
     */
    //    public byte[] readBody()
    //    {
    //        return body;
    //    }
    //    
    //    public byte[] getBody()
    //    {
    //        return body;
    //    }
    
    public String getPayloadBody()
    {
        return payloadBody;
    }
    
    /**
     * 以字符串的形式去读取请求的消息体
     * @author nan.li
     * @return
     */
    //    public String readBodyAsString()
    //    {
    //        try
    //        {
    //            return new String(body, CharEncoding.UTF_8);
    //        }
    //        catch (UnsupportedEncodingException e)
    //        {
    //            e.printStackTrace();
    //        }
    //        return null;
    //    }
    
    //    public String getBodyAsString()
    //    {
    //        return readBodyAsString();
    //    }
    
    public String getRequestType()
    {
        return requestType;
    }
    
    public String getUri()
    {
        return uri;
    }
    
    public String getVersion()
    {
        return version;
    }
    
    public String readHeader(String key)
    {
        return headerMap.get(key);
    }
    
    public String getHeader(String key)
    {
        return headerMap.get(key);
    }
    
    public Map<String, String> readHeaders()
    {
        return headerMap;
    }
    
    public Map<String, String> getHeaders()
    {
        return headerMap;
    }
    
    public boolean isSupportGzipOutput()
    {
        return supportGzipOutput;
    }
    
    /**
     * 完整地读取签名区<br>
     * 包括:requestType uri version信息
     * @author [email protected]
     */
    private boolean readSignatureFully()
    {
        // POST /path/to/call.do?param=value HTTP/1.1
        try
        {
            String line = reader.readLine();
            Logs.i(String.format("Receive request: %s", line));
            if (StringUtils.isEmpty(line))
            {
                return false;
            }
            //这样的写法实测下来性能竟然是最高的!
            int firstBlankIndex = line.indexOf(' ');
            int secondBlankIndex = line.indexOf(' ', firstBlankIndex + 1);
            this.requestType = line.substring(0, firstBlankIndex).trim();
            this.uri = line.substring(firstBlankIndex + 1, secondBlankIndex).trim();
            this.version = line.substring(secondBlankIndex + 1).trim();
            return true;
        }
        catch (Exception e)
        {
            throw new MalformedRequestException("Unable to parse incoming HTTP request.", e);
        }
    }
    
    /**
     * 完整地读消息头
     * @author [email protected]
     */
    private boolean readHeadersFully()
    {
        //完整的请求头格式如下:
        //[I] Host: 127.0.0.1
        //[I] Connection: keep-alive
        //[I] Pragma: no-cache
        //[I] Cache-Control: no-cache
        //[I] Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
        //[I] Upgrade-Insecure-Requests: 1
        //[I] User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.101 Safari/537.36
        //[I] Accept-Encoding: gzip, deflate, sdch
        //[I] Accept-Language: zh-CN,zh;q=0.8,en;q=0.6,ja;q=0.4
        try
        {
            String header;
            //逐行读取
            while ((header = reader.readLine()) != null)
            {
                if (StringUtils.isEmpty(header))
                {
                    break;
                }
                int colonIndex = header.indexOf(':');
                if (colonIndex == -1)
                {
                    throw new IllegalStateException("Unable to handle header: " + header);
                }
                headerMap.put(header.substring(0, colonIndex).trim(), header.substring(colonIndex + 1).trim());
            }
            return true;
        }
        catch (Exception e)
        {
            throw new MalformedRequestException("Unable to parse incoming HTTP request.", e);
        }
    }
    
    /**
     * 对消息头进一步分析
     * @author nan.li
     */
    private boolean resolveExtraInfo()
    {
        //1.根据reader判断是否需要gzip输出
        String acceptEncoding = getHeader("Accept-Encoding");
        if (StringUtils.isNotEmpty(acceptEncoding) && acceptEncoding.indexOf("gzip") != -1)
        {
            supportGzipOutput = true;
        }
        //2.解析cookie
        String cookies = getHeader("Cookie");
        // aaa=yyyy; bbb=zzz
        if (StringUtils.isNotEmpty(cookies))
        {
            cookies = cookies.trim();
            String[] pairs = cookies.split(";");
            if (ArrayUtils.isNotEmpty(pairs))
            {
                for (String pair : pairs)
                {
                    pair = pair.trim();
                    //aaa=yyyy
                    if (StringUtils.isNotEmpty(pair) && pair.indexOf("=") != -1)
                    {
                        String[] keyvalue = pair.split("=");
                        if (ArrayUtils.isNotEmpty(keyvalue) && keyvalue.length == 2)
                        {
                            cookieMap.put(keyvalue[0].trim(), keyvalue[1].trim());
                        }
                    }
                }
            }
        }
        
        //3.解析参数表
        String uri = getUri();
        if (StringUtils.isNotEmpty(uri))
        {
            //一个合法的请求,至少uri必须要是非空的
            //对uri参数先进行解码
            uri = URIEncoderDecoder.decode(uri);
            //解析出uri的参数表
            Map<String, String> ret = UriParamUtils.resolveUrlParamMap(uri);
            //默认就采用url里面的参数表
            paramMap = ret;
            if (HttpReader.POST.equals(getRequestType()))
            {
                String contentType = getHeader("Content-Type");
                //如果是post请求,那么这个参数表的内容会得到扩充
                if (StringUtils.isNotEmpty(contentType))
                {
                    //关于不同类型的请求的参数解析,这里有方案:http://blog.csdn.net/ye1992/article/details/49998511
                    //1.针对application/x-www-form-urlencoded这种编码的处理
                    if (contentType.indexOf("application/x-www-form-urlencoded") != -1)
                    {
                        //userId=2&last_name=Doe&action=Submit
                        //post  payload body
                        //post方式的话,会用实际数据进行覆盖参数操作
                        Map<String, String> newKv = UriParamUtils.resolveUrlParamMap("aaa.jsp?" + payloadBody);
                        //将uri里面的参数覆盖掉body里面的参数,因为uri的参数的优先级更高
                        //事实证明,当uri和body里面同时拥有某个参数的时候,uri里面的参数的优先级更高! 2017-3-14
                        //url参数比form参数优先级更高,这一点已经通过springboot做过试验了!
                        newKv.putAll(ret);
                        //覆盖完成之后,直接返回掉
                        paramMap = newKv;
                    }
                    else
                    {
                        //2.其他形式的Content-Type参数编码,需要用对应的解析算法进行处理
                        //Content-Type参数类型总结如下:
                        //multipart/form-data  多个行  多个键值对的解析  比较复杂
                        //x-www-form-urlencoded  一行,多个键值对以&相连接
                        //raw           可以上传任意格式的文本,可以上传text、json、xml、html等,这种一般是无法明确解析的。必须指定明确的格式之后才可以正常解析
                        //Content-Type:application/octet-stream   只可以上传二进制数据,通常用来上传文件,由于没有键值,所以,一次只能上传一个文件
                        
                        //multipart/form-data与x-www-form-urlencoded区别:
                        //multipart/form-data:既可以上传文件等二进制数据,也可以上传表单键值对,只是最后会转化为一条信息;
                        //x-www-form-urlencoded:只能上传键值对,并且键值对都是间隔分开的。
                    }
                }
            }
        }
        return true;
    }
    
    public Map<String, String> getCookieMap()
    {
        return cookieMap;
    }
    
    public String getCookie(String key)
    {
        return cookieMap.get(key);
    }
    
    public Map<String, String> getParamMap()
    {
        return paramMap;
    }
    
    public String getParam(String key)
    {
        return paramMap.get(key);
    }
    
    /**
     * 完整地读消息体
     * @author [email protected]
     */
    private boolean readBodyFully()
    {
        //典型的一个post请求格式如下:
        //POST /card/viewCardByUserId.do HTTP/1.1
        //Host: 127.0.0.1
        //Connection: keep-alive
        //Content-Length: 36
        //Cache-Control: no-cache
        //Origin: chrome-extension://fhbjgbiflinjbdggehcddcbncdddomop
        //Content-Type: application/x-www-form-urlencoded
        //User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.101 Safari/537.36
        //Postman-Token: 6a247e02-ce0f-c71c-6eb8-19d702d84fec
        //Accept: */*
        //Accept-Encoding: gzip, deflate
        //Accept-Language: zh-CN,zh;q=0.8,en;q=0.6,ja;q=0.4
        
        //userId=2&last_name=Doe&action=Submit
        
        //仅POST请求才要解析消息体
        //参考资料: http://stackoverflow.com/questions/3033755/reading-post-data-from-html-form-sent-to-serversocket
        //此处必须逐字符读取,不可以逐行读取(因为到了消息体传输的时候,根本没有换行符作为结尾了!)
        if (POST.equals(requestType) || PUT.equals(requestType))
        {
            //解析出Content-Type
            String contentType = readHeader("Content-Type");
            if (StringUtils.isNotEmpty(contentType))
            {
                //之所以用indexOf进行匹配,是为了能兼容“application/x-www-form-urlencoded;charset=UTF-8”类似这样的增加了编码方式的写法
                //匹配到form提交的表单的格式
                //form提交的消息体将会被以‘%’字符进行编码
                if (contentType.indexOf("application/x-www-form-urlencoded") != -1 || contentType.indexOf("application/json") != -1)
                {
                    //如果是最经典的form提交等方式的话,那么将payload解析成字符串是很正确的选择
                    StringBuilder payload = new StringBuilder();
                    try
                    {
                        while (reader.ready())
                        {
                            //逐字符读取
                            payload.append((char)reader.read());
                        }
                        payloadBody = payload.toString();
                    }
                    catch (IOException e)
                    {
                        e.printStackTrace();
                    }
                }
                //else 针对contentType为application/json等非标准形式的参数解析,可以留待后续扩充。
                //当然也可以同样解析为字符串,然后留待后续应用层进行解析
                
                //else multipart  文件上传的格式解析。可能要解析成字节流数组。
                //以后再解析
            }
        }
        return true;
    }
    
    @Override
    public void close()
        throws IOException
    {
        StreamUtils.close(reader, in);
    }
}