package org.leo.web.rest;

import static io.netty.handler.codec.http.HttpResponseStatus.OK;
import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1;

import java.io.IOException;
import java.io.RandomAccessFile;
import java.lang.reflect.Method;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import org.leo.web.core.WebServer;
import org.leo.web.exception.HandleRequestException;
import org.leo.web.exception.ResourceNotFoundException;
import org.leo.web.rest.convert.Converter;
import org.leo.web.rest.convert.ConverterFactory;
import org.leo.web.rest.mapping.ControllerBean;
import org.leo.web.rest.mapping.ControllerMapping;
import org.leo.web.rest.mapping.ControllerMappingParameter;
import org.leo.web.rest.mapping.ControllerMappingRegistry;

import com.alibaba.fastjson.JSON;

import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.DefaultFileRegion;
import io.netty.handler.codec.http.DefaultFullHttpResponse;
import io.netty.handler.codec.http.DefaultHttpResponse;
import io.netty.handler.codec.http.FullHttpResponse;
import io.netty.handler.codec.http.HttpChunkedInput;
import io.netty.handler.codec.http.HttpHeaderNames;
import io.netty.handler.codec.http.HttpHeaderValues;
import io.netty.handler.codec.http.HttpResponse;
import io.netty.handler.codec.http.HttpResponseStatus;
import io.netty.handler.codec.http.HttpUtil;
import io.netty.handler.codec.http.LastHttpContent;
import io.netty.handler.codec.http.cookie.Cookie;
import io.netty.handler.codec.http.cookie.DefaultCookie;
import io.netty.handler.codec.http.cookie.ServerCookieEncoder;
import io.netty.handler.ssl.SslHandler;
import io.netty.handler.stream.ChunkedFile;
import io.netty.util.CharsetUtil;

/**
 * 请求处理器类
 * 
 * @author Leo
 * @date 2018/3/27
 */
final class RequestHandler {

    /**
     * 处理请求
     * 
     * @param requestInfo
     * @return
     */
    public ChannelFuture handleRequest(RequestInfo requestInfo) {
        // 查找匹配的Mapping
        ControllerMapping mapping = this.lookupMappings(requestInfo);
        if (mapping == null) {
            HttpContextHolder.setRequest(requestInfo.getRequest());
            HttpContextHolder.setResponse(requestInfo.getResponse());
            
            // 全局异常处理
            if(WebServer.getExceptionHandler() != null) {
                WebServer.getExceptionHandler().doHandle(new ResourceNotFoundException());
                return null;
            }
            throw new ResourceNotFoundException();
        }

        // 准备方法参数
        Object[] paramValues = new Object[mapping.getParameters().size()];
        Class<?>[] paramTypes = new Class[mapping.getParameters().size()];
        for (int i = 0; i < paramValues.length; i++) {
            ControllerMappingParameter cmp = mapping.getParameters().get(i);
            Converter<?> converter = null;
            switch (cmp.getType()) {
            case HTTP_REQUEST:
                paramValues[i] = requestInfo.getRequest();
                break;
            case HTTP_RESPONSE:
                paramValues[i] = requestInfo.getResponse();
                break;                
            case REQUEST_BODY:
                paramValues[i] = requestInfo.getBody();
                break;
            case REQUEST_PARAM:
                paramValues[i] = requestInfo.getParameters().get(cmp.getName());
                converter = ConverterFactory.create(cmp.getDataType());
                if (converter != null) {
                    paramValues[i] = converter.convert(paramValues[i]);
                }
                break;
            case REQUEST_HEADER:
                paramValues[i] = requestInfo.getParameters().get(cmp.getName());
                converter = ConverterFactory.create(cmp.getDataType());
                if (converter != null) {
                    paramValues[i] = converter.convert(requestInfo.getHeaders().get(cmp.getName()));
                }              
                break;
            case PATH_VARIABLE:
                paramValues[i] = this.getPathVariable(requestInfo.getRequest().uri(), mapping.getUrl(), cmp.getName());
                converter = ConverterFactory.create(cmp.getDataType());
                if (converter != null) {
                    paramValues[i] = converter.convert(paramValues[i]);
                }
                break;
            case URL_ENCODED_FORM:
                paramValues[i] = requestInfo.getFormData();
                break;
            case UPLOAD_FILE:
                paramValues[i] = requestInfo.getFiles().size() > 0 ? requestInfo.getFiles().get(0) : null;
                break;
            case UPLOAD_FILES:
                paramValues[i] = requestInfo.getFiles().size() > 0 ? requestInfo.getFiles() : null;
                break;
            }
            if (cmp.getRequired() && paramValues[i] == null) {
                throw new HandleRequestException("参数 " + cmp.getName() + " 为null");
            }
            paramTypes[i] = cmp.getDataType();
        }

        // 执行method
        try {
            HttpContextHolder.setRequest(requestInfo.getRequest());
            HttpContextHolder.setResponse(requestInfo.getResponse());
            Object result = this.execute(mapping, paramTypes, paramValues);
            
            if(!(result instanceof ResponseEntity)) {
                result = ResponseEntity.ok().build();
            }
            return writeResponse((ResponseEntity<?>)result, mapping.getJsonResponse());
        } catch (Exception e) {
            // 全局异常处理
            if(WebServer.getExceptionHandler() != null) {
                WebServer.getExceptionHandler().doHandle(e);
                return null;
            }
            throw new HandleRequestException(e);
        } finally {
            HttpContextHolder.removeRequest();
            HttpContextHolder.removeResponse();
        }
    }
    
    /**
     * 得到Controller类的实例
     * @param className
     * @return
     * @throws Exception 
     */
    private Object execute(ControllerMapping mapping, Class<?>[] paramTypes, Object[] paramValues) throws Exception {
        ControllerBean bean = ControllerMappingRegistry.getBean(mapping.getClassName());
        Object instance = null;
        if(bean.getSingleton()) {
            instance = ControllerMappingRegistry.getSingleton(mapping.getClassName());
        } else {
            Class<?> clazz = Class.forName(mapping.getClassName());
            instance = clazz.newInstance();
        }
        Method method = instance.getClass().getMethod(mapping.getClassMethod(), paramTypes);
        return method.invoke(instance, paramValues);
    }
    
    /**
     * 输出响应结果
     * @param responseEntity
     * @param jsonResponse
     * @return
     * @throws IOException 
     */
    private ChannelFuture writeResponse(ResponseEntity<?> responseEntity, boolean jsonResponse) throws IOException {
        if(responseEntity.getBody() instanceof RandomAccessFile) {
            return writeFileResponse(responseEntity);
        }
        FullHttpResponse response = null;
        HttpResponseStatus status = HttpResponseStatus.parseLine(String.valueOf(responseEntity.getStatus().value()));
        if(responseEntity.getBody() != null) {
            String jsonStr = JSON.toJSONString(responseEntity.getBody());
            response = new DefaultFullHttpResponse(HTTP_1_1, status, Unpooled.copiedBuffer(jsonStr, CharsetUtil.UTF_8));
        } else {
            response = new DefaultFullHttpResponse(HTTP_1_1, status);
        }
        
        String contentType = jsonResponse ? "application/json; charset=UTF-8" : "text/plain; charset=UTF-8";
        response.headers().set("Content-Type", contentType);
        
        // 写入Cookie
        Map<String, String> cookies = HttpContextHolder.getResponse().getCookies();
        Set<Entry<String, String>> cookiesEntrySet = cookies.entrySet();
        for(Entry<String, String> entry : cookiesEntrySet) {
            Cookie cookie = new DefaultCookie(entry.getKey(), entry.getValue());
            cookie.setPath("/");
            response.headers().set(HttpHeaderNames.SET_COOKIE, ServerCookieEncoder.STRICT.encode(cookie));
        }
        
        Map<String, String> responseHeaders = HttpContextHolder.getResponse().getHeaders();
        Set<Entry<String, String>> headersEntrySet = responseHeaders.entrySet();
        for(Entry<String, String> entry : headersEntrySet) {
            response.headers().add(entry.getKey(), entry.getValue());
        }
        response.headers().setInt("Content-Length", response.content().readableBytes());
        return HttpContextHolder.getResponse().getChannelHandlerContext().writeAndFlush(response);
    }
    
    /**
     * 输出文件响应
     * 
     * @param responseEntity
     * @return
     * @throws IOException
     */
    private ChannelFuture writeFileResponse(ResponseEntity<?> responseEntity) throws IOException {
        RandomAccessFile raf = (RandomAccessFile) responseEntity.getBody();
        long fileLength = raf.length();
        
        HttpResponse response = new DefaultHttpResponse(HTTP_1_1, OK);
        HttpUtil.setContentLength(response, fileLength);
        if(responseEntity.getMimetype() != null && !responseEntity.getMimetype().trim().equals("")) {
            response.headers().set(HttpHeaderNames.CONTENT_TYPE, responseEntity.getMimetype());
        }
        if(responseEntity.getFileName() != null && !responseEntity.getFileName().trim().equals("")) {
            String fileName = new String(responseEntity.getFileName().getBytes("gb2312"), "ISO8859-1");
            response.headers().set(HttpHeaderNames.CONTENT_DISPOSITION, "attachment; filename=" + fileName); 
        }
        if (HttpUtil.isKeepAlive(HttpContextHolder.getRequest())) {
            response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE);
        }
        
        ChannelHandlerContext ctx = HttpContextHolder.getResponse().getChannelHandlerContext();
        ctx.write(response);
        ChannelFuture sendFileFuture;
        ChannelFuture lastContentFuture = null;
        if (ctx.pipeline().get(SslHandler.class) == null) {
            sendFileFuture =
                    ctx.write(new DefaultFileRegion(raf.getChannel(), 0, fileLength), ctx.newProgressivePromise());
            // Write the end marker.
            lastContentFuture = ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT);
        } else {
            sendFileFuture = ctx.writeAndFlush(new HttpChunkedInput(new ChunkedFile(raf, 0, fileLength, 8192)),
                    ctx.newProgressivePromise());
            // HttpChunkedInput will write the end marker (LastHttpContent) for us.
            lastContentFuture = sendFileFuture;
        }
        return lastContentFuture;
    }

    /**
     * 查找映射
     * 
     * @param requestInfo
     * @return
     */
    private ControllerMapping lookupMappings(RequestInfo requestInfo) {
        String lookupPath = requestInfo.getRequest().uri().endsWith("/")
                ? requestInfo.getRequest().uri().substring(0, requestInfo.getRequest().uri().length() - 1)
                : requestInfo.getRequest().uri();
        int paramStartIndex = lookupPath.indexOf("?");
        if(paramStartIndex > 0) {
            lookupPath = lookupPath.substring(0, paramStartIndex);
        }

        Map<String, ControllerMapping> mappings = this.getMappings(requestInfo.getRequest().method().name());
        if (mappings == null || mappings.size() == 0) {
            return null;
        }
        Set<Entry<String, ControllerMapping>> entrySet = mappings.entrySet();
        for (Entry<String, ControllerMapping> entry : entrySet) {
            // 完全匹配
            if (entry.getKey().equals(lookupPath)) {
                return entry.getValue();
            }
        }
        for (Entry<String, ControllerMapping> entry : entrySet) {
            // 包含PathVariable
            String matcher = this.getMatcher(entry.getKey());
            if (lookupPath.startsWith(matcher)) {
                boolean matched = true;
                String[] lookupPathSplit = lookupPath.split("/");
                String[] mappingUrlSplit = entry.getKey().split("/");
                if (lookupPathSplit.length != mappingUrlSplit.length) {
                    continue;
                }
                for (int i = 0; i < lookupPathSplit.length; i++) {
                    if (!lookupPathSplit[i].equals(mappingUrlSplit[i])) {
                        if (!mappingUrlSplit[i].startsWith("{")) {
                            matched = false;
                            break;
                        }
                    }
                }
                if(matched) {
                    return entry.getValue();
                }
            }
        }
        return null;
    }

    /**
     * 得到控制器映射哈希表
     * 
     * @param httpMethod
     * @return
     */
    private Map<String, ControllerMapping> getMappings(String httpMethod) {
        if (httpMethod == null) {
            return null;
        }
        switch (httpMethod.toUpperCase()) {
        case "GET":
            return ControllerMappingRegistry.getGetMappings();
        case "POST":
            return ControllerMappingRegistry.getPostMappings();
        case "PUT":
            return ControllerMappingRegistry.getPutMappings();
        case "DELETE":
            return ControllerMappingRegistry.getDeleteMappings();
        case "PATCH":
            return ControllerMappingRegistry.getPatchMappings();
        default:
            return null;
        }
    }

    /**
     * 得到匹配url
     * 
     * @param url
     * @return
     */
    private String getMatcher(String url) {
        StringBuilder matcher = new StringBuilder(128);
        for (char c : url.toCharArray()) {
            if (c == '{') {
                break;
            }
            matcher.append(c);
        }
        return matcher.toString();
    }

    /**
     * 得到路径变量
     * 
     * @param url
     * @param mappingUrl
     * @param name
     * @return
     */
    private String getPathVariable(String url, String mappingUrl, String name) {
        String[] urlSplit = url.split("/");
        String[] mappingUrlSplit = mappingUrl.split("/");
        for (int i = 0; i < mappingUrlSplit.length; i++) {
            if (mappingUrlSplit[i].equals("{" + name + "}")) {
                if(urlSplit[i].contains("?")) {
                    return urlSplit[i].split("[?]")[0];
                }
                if(urlSplit[i].contains("&")) {
                    return urlSplit[i].split("&")[0];
                }
                return urlSplit[i];
            }
        }
        return null;
    }

}