/**
 * Copyright 2007-2015, Kaazing Corporation. All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.kaazing.k3po.driver.internal.netty.bootstrap.http;

import static java.lang.String.format;
import static org.jboss.netty.channel.Channels.fireChannelBound;
import static org.jboss.netty.channel.Channels.fireChannelClosed;
import static org.jboss.netty.channel.Channels.fireChannelConnected;
import static org.jboss.netty.channel.Channels.fireChannelDisconnected;
import static org.jboss.netty.channel.Channels.fireChannelOpen;
import static org.jboss.netty.channel.Channels.fireChannelUnbound;
import static org.jboss.netty.channel.Channels.fireExceptionCaught;
import static org.jboss.netty.channel.Channels.fireMessageReceived;
import static org.jboss.netty.channel.Channels.future;
import static org.jboss.netty.channel.Channels.write;
import static org.jboss.netty.handler.codec.http.HttpHeaders.getContentLength;
import static org.jboss.netty.handler.codec.http.HttpHeaders.getHost;
import static org.jboss.netty.handler.codec.http.HttpHeaders.isContentLengthSet;
import static org.jboss.netty.handler.codec.http.HttpHeaders.isTransferEncodingChunked;
import static org.jboss.netty.handler.codec.http.HttpResponseStatus.BAD_REQUEST;
import static org.jboss.netty.handler.codec.http.HttpResponseStatus.NOT_FOUND;
import static org.jboss.netty.handler.codec.http.HttpResponseStatus.SWITCHING_PROTOCOLS;
import static org.kaazing.k3po.driver.internal.channel.Channels.remoteAddress;
import static org.kaazing.k3po.driver.internal.netty.channel.Channels.fireInputShutdown;

import java.net.URI;
import java.util.Map.Entry;
import java.util.NavigableMap;

import org.jboss.netty.buffer.ChannelBuffer;
import org.jboss.netty.channel.Channel;
import org.jboss.netty.channel.ChannelConfig;
import org.jboss.netty.channel.ChannelFactory;
import org.jboss.netty.channel.ChannelFuture;
import org.jboss.netty.channel.ChannelHandlerContext;
import org.jboss.netty.channel.ChannelPipeline;
import org.jboss.netty.channel.ChannelPipelineFactory;
import org.jboss.netty.channel.ChannelStateEvent;
import org.jboss.netty.channel.ExceptionEvent;
import org.jboss.netty.channel.MessageEvent;
import org.jboss.netty.handler.codec.http.DefaultHttpResponse;
import org.jboss.netty.handler.codec.http.HttpChunk;
import org.jboss.netty.handler.codec.http.HttpChunkTrailer;
import org.jboss.netty.handler.codec.http.HttpHeaders;
import org.jboss.netty.handler.codec.http.HttpRequest;
import org.jboss.netty.handler.codec.http.HttpResponse;
import org.jboss.netty.handler.codec.http.HttpResponseStatus;
import org.jboss.netty.handler.codec.http.HttpVersion;
import org.jboss.netty.handler.codec.http.QueryStringDecoder;
import org.kaazing.k3po.driver.internal.behavior.handler.codec.http.QueryStringEncoder;
import org.kaazing.k3po.driver.internal.netty.bootstrap.http.HttpChildChannel.HttpReadState;
import org.kaazing.k3po.driver.internal.netty.channel.ChannelAddress;
import org.kaazing.k3po.driver.internal.netty.channel.Channels;

public class HttpChildChannelSource extends HttpChannelHandler {

    private final NavigableMap<ChannelAddress, HttpServerChannel> httpBindings;

    private volatile HttpChildChannel httpChildChannel;

    public HttpChildChannelSource(NavigableMap<ChannelAddress, HttpServerChannel> httpBindings) {
        this.httpBindings = httpBindings;
    }

    @Override
    public void exceptionCaught(ChannelHandlerContext ctx, ExceptionEvent e) throws Exception {
        if (httpChildChannel != null) {
            HttpChildChannel httpChildChannel = this.httpChildChannel;
            this.httpChildChannel = null;

            if (httpChildChannel.setReadClosed() || httpChildChannel.setWriteClosed()) {
                fireExceptionCaught(httpChildChannel, e.getCause());
                fireChannelClosed(httpChildChannel);
            }
        }

        Channel channel = ctx.getChannel();
        channel.close();
    }

    @Override
    public void channelClosed(ChannelHandlerContext ctx, ChannelStateEvent e) throws Exception {
        HttpChildChannel httpChildChannel = this.httpChildChannel;
        if (httpChildChannel != null) {

            this.httpChildChannel = null;

            switch (httpChildChannel.readState()) {
            case UPGRADED:
                if (httpChildChannel.setReadClosed()) {
                    fireChannelDisconnected(httpChildChannel);
                    fireChannelUnbound(httpChildChannel);
                    fireChannelClosed(httpChildChannel);
                }
                break;
            case CONTENT_COMPLETE:
                break;
            default:
                Channels.fireChannelAborted(httpChildChannel);
                break;
            }

            switch (httpChildChannel.writeState()) {
            case UPGRADED:
            case CONTENT_CLOSE:
                if (httpChildChannel.setWriteClosed()) {
                    fireChannelDisconnected(httpChildChannel);
                    fireChannelUnbound(httpChildChannel);
                    fireChannelClosed(httpChildChannel);
                }
                break;
            case CONTENT_COMPLETE:
                break;
            default:
                Channels.fireChannelAborted(httpChildChannel);
                break;
            }
        }
    }

    @Override
    protected void httpMessageReceived(ChannelHandlerContext ctx, MessageEvent e, HttpRequest httpRequest) throws Exception {

        HttpVersion version = httpRequest.getProtocolVersion();
        URI httpLocation = getEffectiveURI(httpRequest);
        if (httpLocation == null) {
            // see RFC-7230 section 5.4 Host
            HttpResponse httpResponse = new DefaultHttpResponse(version, BAD_REQUEST);
            ChannelFuture future = future(ctx.getChannel());
            write(ctx, future, httpResponse);
            return;
        }

        // channel's local address is resolved address so get the bind address from
        // server channel's attachment
        ChannelAddress transportCandidate = (ChannelAddress) ctx.getChannel().getParent().getAttachment();
        ChannelAddress candidate = new ChannelAddress(httpLocation, transportCandidate);

        Entry<ChannelAddress, HttpServerChannel> httpBinding = httpBindings.floorEntry(candidate);

        if (httpBinding == null) {
            HttpResponse httpResponse = new DefaultHttpResponse(version, NOT_FOUND);
            ChannelFuture future = future(ctx.getChannel());
            write(ctx, future, httpResponse);
            return;
        }

        HttpServerChannel parent = httpBinding.getValue();
        ChannelFactory factory = parent.getFactory();
        ChannelConfig config = parent.getConfig();
        ChannelPipelineFactory pipelineFactory = config.getPipelineFactory();
        ChannelPipeline pipeline = pipelineFactory.getPipeline();
        ChannelAddress httpLocalAddress = parent.getLocalAddress();

        Channel transport = ctx.getChannel();
        ChannelAddress remoteAddress = remoteAddress(transport);
        ChannelAddress httpRemoteAddress = new ChannelAddress(httpLocation, remoteAddress, true);

        HttpChildChannelSink sink = new HttpChildChannelSink(transport);
        HttpChildChannel httpChildChannel = new HttpChildChannel(parent, factory, pipeline, sink);
        HttpChannelConfig httpChildConfig = httpChildChannel.getConfig();
        httpChildConfig.setMethod(httpRequest.getMethod());
        httpChildConfig.setVersion(version);
        httpChildConfig.getReadHeaders().set(httpRequest.headers());
        httpChildConfig.setReadQuery(new QueryStringDecoder(httpRequest.getUri()));
        httpChildConfig.setWriteQuery(new QueryStringEncoder(httpRequest.getUri()));
        httpChildConfig.setStatus(HttpResponseStatus.OK);

        this.httpChildChannel = httpChildChannel;

        ChannelBuffer content = httpRequest.getContent();

        // update read state before firing channel events
        if (isTransferEncodingChunked(httpRequest)) {
            httpChildChannel.readState(HttpReadState.CONTENT_CHUNKED);
        }
        else if (isContentLengthSet(httpRequest)) {
            long contentLength = getContentLength(httpRequest);
            contentLength -= content.readableBytes();
            if (contentLength > 0) {
                httpChildChannel.readState(HttpReadState.CONTENT_CHUNKED);
            }
            else {
                httpChildChannel.readState(HttpReadState.CONTENT_COMPLETE);
            }
        }
        else {
            // see RFC-7230 section 3.3
            // content indicated by presence of Content-Length or Transfer-Encoding
            httpChildChannel.readState(HttpReadState.CONTENT_COMPLETE);
        }

        fireChannelOpen(httpChildChannel);

        httpChildChannel.setLocalAddress(httpLocalAddress);
        httpChildChannel.setBound();
        fireChannelBound(httpChildChannel, httpLocalAddress);

        httpChildChannel.setRemoteAddress(httpRemoteAddress);
        httpChildChannel.setConnected();
        fireChannelConnected(httpChildChannel, httpRemoteAddress);

        if (content.readable()) {
            fireMessageReceived(httpChildChannel, content);
        }

        // note: status may be set in reaction to one of the above events, such as CONNECTED
        //       so defer status code check until this point
        if (httpChildConfig.getStatus().getCode() == SWITCHING_PROTOCOLS.getCode()) {
            httpChildChannel.readState(HttpReadState.UPGRADED);
        }

        switch (httpChildChannel.readState()) {
        case CONTENT_COMPLETE:
            fireInputShutdown(httpChildChannel);
            this.httpChildChannel = null;
            if (httpChildChannel.setReadClosed()) {
                fireChannelDisconnected(httpChildChannel);
                fireChannelUnbound(httpChildChannel);
                fireChannelClosed(httpChildChannel);
            }
            break;
        default:
            break;
        }
    }

    @Override
    protected void httpMessageReceived(ChannelHandlerContext ctx, MessageEvent e, HttpChunk chunk) throws Exception {
        ChannelBuffer content = chunk.getContent();
        if (content.readable()) {
            fireMessageReceived(httpChildChannel, content);
        }

        boolean last = chunk.isLast();
        if (last) {
            HttpHeaders trailingHeaders = ((HttpChunkTrailer) chunk).trailingHeaders();
            httpChildChannel.getConfig().getReadTrailers().add(trailingHeaders);
            HttpChildChannel httpChildChannel = this.httpChildChannel;
            httpChildChannel.readState(HttpReadState.CONTENT_COMPLETE);
            this.httpChildChannel = null;
            fireInputShutdown(httpChildChannel);
            if (httpChildChannel.setReadClosed()) {
                fireChannelDisconnected(httpChildChannel);
                fireChannelUnbound(httpChildChannel);
                fireChannelClosed(httpChildChannel);
            }
        }
    }

    @Override
    protected void httpMessageReceived(ChannelHandlerContext ctx, MessageEvent e, ChannelBuffer message) throws Exception {
        if (message.readable()) {
            // after 101 switching protocols
            fireMessageReceived(httpChildChannel, message);
        }
    }

    private static URI getEffectiveURI(HttpRequest httpRequest) {
        URI requestURI = URI.create(httpRequest.getUri());
        if (requestURI.isAbsolute()) {
            return requestURI;
        }

        String host = getHost(httpRequest);
        return (host != null) ? URI.create(format("http://%s%s", host, requestURI)) : null;
    }

}
