/*
 *  Copyright 1999-2004 The Apache Software Foundation
 *
 *  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.
 */

/*
 * The contents of this file are subject to the terms
 * of the Common Development and Distribution License
 * (the License).  You may not use this file except in
 * compliance with the License.
 *
 * You can obtain a copy of the license at
 * https://glassfish.dev.java.net/public/CDDLv1.0.html or
 * glassfish/bootstrap/legal/CDDLv1.0.txt.
 * See the License for the specific language governing
 * permissions and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL
 * Header Notice in each file and include the License file
 * at glassfish/bootstrap/legal/CDDLv1.0.txt.
 * If applicable, add the following below the CDDL Header,
 * with the fields enclosed by brackets [] replaced by
 * you own identifying information:
 * "Portions Copyrighted [year] [name of copyright owner]"
 *
 * Copyright (c) Ericsson AB, 2004-2007. All rights reserved.
 * Copyright 1997-2007 Sun Microsystems, Inc. All rights reserved.
 */
package org.jvnet.glassfish.comms.clb.proxy;

import com.sun.enterprise.web.connector.grizzly.ssl.SSLByteBufferInputStream;

import com.sun.grizzly.http.Constants;
import com.sun.grizzly.http.SocketChannelOutputBuffer;
import com.sun.grizzly.tcp.Request;
import com.sun.grizzly.tcp.Response;
import com.sun.grizzly.util.ByteBufferInputStream;
import com.sun.grizzly.util.buf.Ascii;
import com.sun.grizzly.util.buf.ByteChunk;
import com.sun.grizzly.util.buf.MessageBytes;
import com.sun.grizzly.util.http.MimeHeaders;

import org.jvnet.glassfish.comms.clb.proxy.api.Endpoint;
import org.jvnet.glassfish.comms.clb.proxy.config.LoadBalancerProxyConstants;
import org.jvnet.glassfish.comms.clb.proxy.config.ProxyConfig;
import org.jvnet.glassfish.comms.clb.proxy.http.util.HttpInputBuffer;
import org.jvnet.glassfish.comms.clb.proxy.http.util.HttpRequest;

import java.io.InputStream;

import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.SocketChannel;

import java.util.logging.Level;
import java.util.logging.Logger;

import javax.net.ssl.SSLEngine;


/**
 * This class encapsulates the pluggable framework invocation work.
 * Implementation has come code that has been used from Apache.
 * The inputs to the task are the channel and the byte buffer that has
 * been read. The task invokes all the HttpLayers that have been configured
 * in the dispatcher.xml, any functionality that has to be plugged in
 * has to be done through dispatcher.xml
 *
 * @author
 */
public class ProxyTask {
    /**
     * Byte buffer that has been read from the client channel
     */
    protected ByteBuffer buffer;

    /**
     * Client socket channel
     */
    protected SocketChannel clientChannel;

    /**
     * Http request interface, extended from grizzly tcp request interface.
     */
    protected HttpRequest request;

    /**
     * Http response interface, this is a grizzly tcp response interface.
     */
    protected Response response;

    /**
     * Extended from the grizzly inter
     */
    protected HttpInputBuffer inputBuffer;
    protected SocketChannelOutputBuffer outputBuffer;
    protected SelectionKey selectionKey;
    protected Logger _logger;
    protected InputStream inputStream;

    /**
     * Holds value of property bytesWritten.
     */
    protected long bytesWritten = 0;
    protected long payloadlength = 0;
    protected boolean recycle = true;
    protected boolean initialized = false;
    protected boolean keepAlive = false;
    protected SSLEngine sslEngine;

    /** Creates a new instance of ProxyTask */
    public ProxyTask() {
        _logger = ProxyConfig.getInstance().getLogger();
        initialize();
    }

    public void initialize() {
        request = new HttpRequest();
        inputBuffer = new HttpInputBuffer(request);
        inputStream = new ByteBufferInputStream();
        inputBuffer.setInputStream(inputStream);
        request.setInputBuffer(inputBuffer);
        response = new Response();
        outputBuffer = new SocketChannelOutputBuffer(response,
                LoadBalancerProxyConstants.DEFAULT_HTTP_HEADER_BUFFER_SIZE,
                false);
        response.setOutputBuffer(outputBuffer);
        request.setResponse(response);
        initialized = true;
    }

    public void setByteBuffer(ByteBuffer buffer) {
        this.buffer = buffer;
    }

    public void setChannel(SocketChannel channel) {
        this.clientChannel = channel;
    }

    public void setSelectionKey(SelectionKey key) {
        this.selectionKey = key;

        if (key != null) {
            this.clientChannel = (SocketChannel) key.channel();
            this.outputBuffer.setChannel(this.clientChannel);
        }
    }

    public void recycle() {
        inputBuffer.nextRequest();
        outputBuffer.nextRequest();
        request.recycle();
        response.recycle();
        payloadlength = 0;
        bytesWritten = 0;
        clientChannel = null;
        buffer = null;
        selectionKey = null;
    }

    public void doTask() {
        doTask(this.buffer, this.clientChannel);
    }

    public void doTask(ByteBuffer buff, SocketChannel channel) {
        _logger.log(Level.FINE, "ProxyTask : doTask Start");
        process(buff, channel);
        _logger.log(Level.FINE, "ProxyTask : doTask Finished process");
        postProcess(buff, channel);
        _logger.log(Level.FINE, "ProxyTask : doTask Finished postProcess");
    }

    public void process(ByteBuffer buff, SocketChannel channel) {
        this.buffer = buff;
        this.clientChannel = channel;
        buff.flip();

        if (this.isSecure()) {
            ((SSLByteBufferInputStream) inputStream).setByteBuffer(buff);
            ((SSLByteBufferInputStream) inputStream).setSelectionKey(this.selectionKey);
            ((SSLByteBufferInputStream) inputStream).setReadTimeout(LoadBalancerProxyConstants.DEFAULT_READ_TIMEOUT);
        } else {
            ((ByteBufferInputStream) inputStream).setByteBuffer(buff);
            ((ByteBufferInputStream) inputStream).setSelectionKey(this.selectionKey);
            ((ByteBufferInputStream) inputStream).setReadTimeout(LoadBalancerProxyConstants.DEFAULT_READ_TIMEOUT);
        }

        /** Try catch exception and check for failures
         */
        HttpProxy.getInstance().getLayerHandler().doInvoke(request, response);

        /* The above API can return under 2 conditions.
         * 1. With status 200, which means all layers went through
         * and we have to query the Endpoint to remote this request
         * 2. With status NON 200, means that this is an error response
         * and needs to be returned to the HTTP client.
         */
    }

    public boolean getKeepAlive() {
        return keepAlive;
    }

    public void setKeepAlive(boolean keepalive) {
        keepAlive = keepalive;
    }

    public Endpoint getEndpoint() {
        return request.getConvergedLoadBalancerEndpoint();
    }

    public void postProcess(ByteBuffer buff, SocketChannel channel) {
        this.buffer.clear();
        this.buffer.put(inputBuffer.getBytes(), this.buffer.position(),
            inputBuffer.lastValidPos());

        long contentlength = (request.getContentLengthLong() < 0) ? 0
                                                                  : request.getContentLengthLong();
        _logger.log(Level.FINE, "Content length = " + contentlength);
        payloadlength = contentlength + inputBuffer.getHeaderLength();

        MessageBytes protocolMB = request.protocol();

        if (protocolMB.equals(Constants.HTTP_11)) {
            keepAlive = true;
        } else if (protocolMB.equals(Constants.HTTP_10)) {
            keepAlive = false;
        } else if (protocolMB.equals("")) {
            // HTTP/0.9
            keepAlive = false;
        } else {
            _logger.log(Level.FINE, "Setting Error response 500, in postProcess");
            response.setStatus(505);
        }

        MimeHeaders headers = request.getMimeHeaders();

        // Check connection header
        MessageBytes connectionValueMB = headers.getValue(LoadBalancerProxyConstants.HTTP_CONNECTION_HEADER);

        if (connectionValueMB != null) {
            ByteChunk connectionValueBC = connectionValueMB.getByteChunk();

            if (findBytes(connectionValueBC, Constants.CLOSE_BYTES) != -1) {
                keepAlive = false;
            } else if (findBytes(connectionValueBC, Constants.KEEPALIVE_BYTES) != -1) {
                keepAlive = true;
            }
        }
    }

    public void setSecure(boolean secure) {
        this.request.setSecure(secure);
    }

    public boolean isSecure() {
        return this.request.isSecure();
    }

    /**
     * Specialized utility method: find a sequence of lower case bytes inside
     * a ByteChunk.
     */
    protected int findBytes(ByteChunk bc, byte[] b) {
        byte first = b[0];
        byte[] buff = bc.getBuffer();
        int start = bc.getStart();
        int end = bc.getEnd();

        // Look for first char
        int srcEnd = b.length;

        for (int i = start; i <= (end - srcEnd); i++) {
            if (Ascii.toLower(buff[i]) != first) {
                continue;
            }

            // found first char, now look for a match
            int myPos = i + 1;

            for (int srcPos = 1; srcPos < srcEnd;) {
                if (Ascii.toLower(buff[myPos++]) != b[srcPos++]) {
                    break;
                }

                if (srcPos == srcEnd) {
                    return i - start; // found it
                }
            }
        }

        return -1;
    }

    public ByteBuffer getBuffer() {
        return this.buffer;
    }

    public long getPayloadLength() {
        return payloadlength;
    }

    /**
     * (non-Javadoc)
     *
     * @see java.lang.Object#equals(java.lang.Object)
     */
    public boolean equals(Object obj) {
        if (!(obj instanceof ProxyTask)) {
            return false;
        }

        ProxyTask task = (ProxyTask) obj;

        if (task.getEndpoint().getPort() != this.getEndpoint().getPort()) {
            return false;
        }

        if ((task.getEndpoint().getIp() != null) &&
                !task.getEndpoint().getIp().equals(this.getEndpoint().getIp())) {
            return false;
        }

        if (task.getEndpoint().isSecure() != this.getEndpoint().isSecure()) {
            return false;
        }

        return true;
    }

    /**
     * (non-Javadoc)
     *
     * @see java.lang.Object#hashCode()
     */
    public int hashCode() {
        return this.getEndpoint().getIp().hashCode() ^
        this.getEndpoint().getPort();
    }

    public Request getRequest() {
        return this.request;
    }

    public Response getResponse() {
        return this.response;
    }

    /**
     * Getter for property bytesWritten.
     * @return Value of property bytesWritten.
     */
    public long getBytesWritten() {
        return this.bytesWritten;
    }

    /**
     * Setter for property bytesWritten.
     * @param bytesWritten New value of property bytesWritten.
     */
    public void setBytesWritten(long bytesWritten) {
        this.bytesWritten = bytesWritten;
    }

    /**
     * Util method to check if there are more
     * bytes in this request
     */
    public boolean hasRemaining() {
        if (bytesWritten < payloadlength) {
            return true;
        } else {
            return false;
        }
    }

    public void setSSLEngine(SSLEngine engine) {
        this.sslEngine = engine;
    }

    public SSLEngine getSSLEngine() {
        return this.sslEngine;
    }
}
