/**
 *  Copyright (c) 2009, WSO2 Inc. (http://www.wso2.org) 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.apache.synapse.transport.passthru;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.http.HttpRequest;
import org.apache.http.nio.NHttpConnection;
import org.apache.synapse.commons.logger.ContextAwareLogger;
import org.apache.synapse.transport.passthru.config.PassThroughCorrelationConfigDataHolder;
import org.apache.synapse.transport.passthru.config.SourceConfiguration;
import org.apache.synapse.transport.passthru.util.ControlledByteBuffer;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * This class represents the information about a TCP Connection at a given point in time.
 * In a Single TCP Connection there can be multiple HTTP Requests.
 */
public class SourceContext {
    public static final String CONNECTION_INFORMATION = "CONNECTION_INFORMATION";

    /** logger for correlation.log */
    private static final Log correlationLog = LogFactory.getLog(PassThroughConstants.CORRELATION_LOGGER);

    private SourceConfiguration sourceConfiguration;

    private boolean isSourceRequestMarkedToBeDiscarded = false;

    private ProtocolState state = ProtocolState.REQUEST_READY;

    private SourceRequest request;

    private SourceResponse response;

    /** Mark the connection to be shut down after the current request-response is completed. */
    private boolean shutDown = false;

    private Pipe reader;

    private Pipe writer;

    private Lock lock = new ReentrantLock();

    /** Time that state got updated*/
    private long lastStateUpdatedTime;

    public SourceContext(SourceConfiguration sourceConfiguration) {
        this.sourceConfiguration = sourceConfiguration;
    }

    public SourceConfiguration getSourceConfiguration() {
        return sourceConfiguration;
    }

    public ProtocolState getState() {
        return state;
    }

    public boolean isSourceRequestMarkedToBeDiscarded() {
        return isSourceRequestMarkedToBeDiscarded;
    }

    public void setIsSourceRequestMarkedToBeDiscarded(boolean isPipeDiscarded) {
        this.isSourceRequestMarkedToBeDiscarded = isPipeDiscarded;
    }


    public void setState(ProtocolState state) {
        this.state = state;
    }

    public SourceRequest getRequest() {
        return request;
    }

    public void setRequest(SourceRequest request) {
        this.request = request;
    }

    public SourceResponse getResponse() {
        return response;
    }

    public void setResponse(SourceResponse response) {
        this.response = response;
    }

    /**
     * Reset the resources associated with this context
     */
    public void reset() {
		reset(false);
	}

    /**
     * Reset the resources associated with this context
     *
     * @param isError whether an error is causing this shutdown of the connection.
     *                It is very important to set this flag correctly.
     *                When an error causing the shutdown of the connections we should not
     *                release associated writer buffer to the pool as it might lead into
     *                situations like same buffer is getting released to both source and target
     *                buffer factories
     */
    public void reset(boolean isError) {
		this.request = null;
		this.response = null;
		this.state = ProtocolState.REQUEST_READY;

		if (writer != null) {
			if (!isError) {      // If there is an error we do not release the buffer to the factory
                ControlledByteBuffer buffer = writer.getBuffer();
				sourceConfiguration.getBufferFactory().release(buffer);
			}
		}

		this.reader = null;
		this.writer = null;
	}

    public Lock getLock() {
        return lock;
    }

    public boolean isShutDown() {
        return shutDown;
    }

    public void setShutDown(boolean shutDown) {
        this.shutDown = shutDown;
    }

    public Pipe getReader() {
        return reader;
    }

    public void setReader(Pipe reader) {
        this.reader = reader;
    }

    public Pipe getWriter() {
        return writer;
    }

    public void setWriter(Pipe writer) {
        this.writer = writer;
    }

    public static void create(NHttpConnection conn, ProtocolState state,
                              SourceConfiguration configuration) {
        SourceContext sourceContext = new SourceContext(configuration);
        conn.getContext().setAttribute(CONNECTION_INFORMATION, sourceContext);
        sourceContext.setState(state);

        if (PassThroughCorrelationConfigDataHolder.isEnable()) {
            sourceContext.updateLastStateUpdatedTime();
        }
    }

    public static void updateState(NHttpConnection conn, ProtocolState state) {
        SourceContext sourceContext = (SourceContext)
                conn.getContext().getAttribute(CONNECTION_INFORMATION);
        if (sourceContext != null) {
            if(sourceContext.getState() == state) {
                return;
            }
            if (PassThroughCorrelationConfigDataHolder.isEnable()) {
                long lastStateUpdateTime = sourceContext.getLastStateUpdatedTime();
                // If the Correlation Logs are toggled (off -> on) during runtime, we need to reset the last state updated time.
                if (PassThroughCorrelationConfigDataHolder.isToggled()) {
                    lastStateUpdateTime = sourceContext.updateLastStateUpdatedTime();
                    PassThroughCorrelationConfigDataHolder.resetToggled();
                }
                String url = "", method = "";
                if (sourceContext.getRequest() != null) {
                    url = sourceContext.getRequest().getUri();
                    method = sourceContext.getRequest().getMethod();
                } else {
                    HttpRequest httpRequest = conn.getHttpRequest();
                    if (httpRequest != null) {
                        url = httpRequest.getRequestLine().getUri();
                        method = httpRequest.getRequestLine().getMethod();
                    }
                }
                if ((method.length() != 0) && (url.length() != 0)) {
                    ContextAwareLogger.getLogger(conn.getContext(), correlationLog, false)
                            .info((sourceContext.updateLastStateUpdatedTime() - lastStateUpdateTime)
                                    + "|HTTP State Transition|"
                                    + conn.getContext().getAttribute("http.connection") + "|"
                                    + method + "|" + url + "|"
                                    + state.name());
                }
            }
            sourceContext.setState(state);
        } else {
            throw new IllegalStateException("Connection information should be present");
        }
    }

    public static boolean assertState(NHttpConnection conn, ProtocolState state) {
        SourceContext info = (SourceContext)
                conn.getContext().getAttribute(CONNECTION_INFORMATION);

        return info != null && info.getState() == state;

    }

    public static ProtocolState getState(NHttpConnection conn) {
        SourceContext info = (SourceContext)
                conn.getContext().getAttribute(CONNECTION_INFORMATION);

        return info != null ? info.getState() : null;
    }

    public static void setRequest(NHttpConnection conn, SourceRequest request) {
        SourceContext info = (SourceContext)
                conn.getContext().getAttribute(CONNECTION_INFORMATION);

        if (info != null) {
            info.setRequest(request);
        } else {
            throw new IllegalStateException("Connection information should be present");
        }
    }

    public static void setResponse(NHttpConnection conn, SourceResponse response) {
        SourceContext info = (SourceContext)
                conn.getContext().getAttribute(CONNECTION_INFORMATION);

        if (info != null) {
            info.setResponse(response);
        } else {
            throw new IllegalStateException("Connection information should be present");
        }
    }

    public static SourceRequest getRequest(NHttpConnection conn) {
        SourceContext info = (SourceContext)
                conn.getContext().getAttribute(CONNECTION_INFORMATION);

        return info != null ? info.getRequest() : null;
    }

    public static SourceResponse getResponse(NHttpConnection conn) {
        SourceContext info = (SourceContext)
                conn.getContext().getAttribute(CONNECTION_INFORMATION);

        return info != null ? info.getResponse() : null;
    }

    public static SourceContext get(NHttpConnection conn) {
        return (SourceContext) conn.getContext().getAttribute(CONNECTION_INFORMATION);
    }

    public static Lock getLock(NHttpConnection conn) {
        SourceContext info = (SourceContext)
                conn.getContext().getAttribute(CONNECTION_INFORMATION);

        return info != null ? info.getLock() : null;
    }

    public long getLastStateUpdatedTime() {
        return lastStateUpdatedTime;
    }

    public long updateLastStateUpdatedTime() {
        this.lastStateUpdatedTime = System.currentTimeMillis();
        return this.lastStateUpdatedTime;
    }
}
