/*
*  Licensed to the Apache Software Foundation (ASF) under one
*  or more contributor license agreements.  See the NOTICE file
*  distributed with this work for additional information
*  regarding copyright ownership.  The ASF licenses this file
*  to you 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.nhttp.util;

import org.apache.synapse.commons.jmx.MBeanRegistrar;

import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;

/**
 * <p>LatencyView provides statistical information related to the latency (overhead) incurred by
 * the Synapse NHTTP transport, when mediating messages back and forth. Statistics are available
 * under two main categories, namely short term data and long term data. Short term data is
 * statistical information related to the last 15 minutes of execution and these metrics are
 * updated every 5 seconds. Long term data is related to the last 24 hours of execution and
 * they are updated every 5 minutes. Two timer tasks and a single threaded scheduled executor
 * is used to perform these periodic calculations.</p>
 *
 * <p>Latency calculation for a single invocation is carried out by taking timestamps on
 * following events:</p>
 *
 * <ul>
 *  <li>t1 - Receiving a new request (ServerHandler#requestReceived)</li>
 *  <li>t2 - Obtaining a connection to forward the request (ClientHandler#processConnection)</li>
 *  <li>t3 - Reading the complete response from the backend server (ClientHandler#inputReady)</li>
 *  <li>t4 - Writing the complete response to the client (ServerHandler#outputReady)</li>
 * <ul>
 *
 * <p>Having taken these timestamps, the latency for the invocation is calculated as follows:<br/>
 *    Latency = (t4 - t1) - (t3 - t2)
 * </p>
 *
 */
public class LatencyView implements LatencyViewMBean {

    private static final int SMALL_DATA_COLLECTION_PERIOD = 5;
    private static final int LARGE_DATA_COLLECTION_PERIOD = 5 * 60;

    /** Keeps track of th last reported latency value */
    private LatencyParameter lastLatency = new LatencyParameter(true);

    /** -Following are used to calculate BackEnd(Be) latency - */
    /** Keeps track of th last reported BE latency value */
    private LatencyParameter lastLatencyBe = new LatencyParameter(true);

    private LatencyParameter serverDecodeLatency;

    private LatencyParameter serverEncodeLatency;

    private LatencyParameter clientEncodeLatency;

    private LatencyParameter clientDecodeLatency;

    private LatencyParameter serverWorkerWaitTime;

    private LatencyParameter clientWorkerWaitTime;

    private LatencyParameter requestMediationLatency;

    private LatencyParameter responseMediationLatency;

    private List<LatencyParameter> latencies = new ArrayList<LatencyParameter>(10);

    /** Scheduled executor on which data collectors are executed */
    private ScheduledExecutorService scheduler;

    private Date resetTime = Calendar.getInstance().getTime();

    private String latencyMode;
    private String name;


    public LatencyView(final String latencyMode, boolean isHttps) {
        this(latencyMode, isHttps, "", false);
    }

    public LatencyView(final String latencyMode, boolean isHttps, final String namePostfix, final boolean showAdvancedParameters) {
        this.latencyMode = latencyMode;
        name = "nio-http" + (isHttps ? "s" : "") + namePostfix;

        scheduler =  Executors.newSingleThreadScheduledExecutor(new ThreadFactory() {
            public Thread newThread(Runnable r) {
                return new Thread(r, latencyMode + "-" + name + "-latency-view");
            }
        });

        scheduler.scheduleAtFixedRate(new ShortTermDataCollector(), SMALL_DATA_COLLECTION_PERIOD,
                SMALL_DATA_COLLECTION_PERIOD, TimeUnit.SECONDS);
        scheduler.scheduleAtFixedRate(new LongTermDataCollector(), LARGE_DATA_COLLECTION_PERIOD,
                LARGE_DATA_COLLECTION_PERIOD, TimeUnit.SECONDS);
        boolean registered = false;
        try {
            registered = MBeanRegistrar.getInstance().registerMBean(this, this.latencyMode, name);
        } finally {
            if (!registered) {
                scheduler.shutdownNow();
            }
        }
        registerAllLatencies(showAdvancedParameters);
    }

    public void destroy() {
        MBeanRegistrar.getInstance().unRegisterMBean(latencyMode, name);
        if (!scheduler.isShutdown()) {
            scheduler.shutdownNow();
        }
    }

    /**
     * Report the timestamp values captured during mediating messages back and forth
     *
     * @param reqArrival The request arrival time
     * @param reqDeparture The request departure time (backend connection establishment)
     * @param resArrival The resoponse arrival time
     * @param resDeparture The response departure time
     */
    private void notifyTimes(long reqArrival, long reqDeparture,
                             long resArrival, long resDeparture) {
        long latencyBe = (resArrival - reqDeparture);
        long latency = (resDeparture - reqArrival) - latencyBe;
        lastLatency.update(latency);
        lastLatencyBe.update(latencyBe);
    }

    public void notifyTimes(LatencyCollector collector) {
        lastLatency.update(collector.getLatency());
        lastLatencyBe.update(collector.getBackendLatency());
        serverDecodeLatency.update(collector.getServerDecodeLatency());
        clientEncodeLatency.update(collector.getClientEncodeLatency());
        clientDecodeLatency.update(collector.getClientDecodeLatency());
        serverEncodeLatency.update(collector.getServerEncodeLatency());
        serverWorkerWaitTime.update(collector.getServerWorkerQueuedTime());
        clientWorkerWaitTime.update(collector.getClientWorkerQueuedTime());
        requestMediationLatency.update(collector.getServerWorkerLatency());
        responseMediationLatency.update(collector.getClientWorkerLatency());
    }

    private void registerAllLatencies(boolean recordAdditionalLatencies) {
        latencies.add(lastLatency);
        latencies.add(lastLatencyBe);
        serverDecodeLatency = new LatencyParameter(recordAdditionalLatencies);
        serverEncodeLatency = new LatencyParameter(recordAdditionalLatencies);
        clientEncodeLatency = new LatencyParameter(recordAdditionalLatencies);
        clientDecodeLatency = new LatencyParameter(recordAdditionalLatencies);
        serverWorkerWaitTime = new LatencyParameter(recordAdditionalLatencies);
        clientWorkerWaitTime = new LatencyParameter(recordAdditionalLatencies);
        requestMediationLatency = new LatencyParameter(recordAdditionalLatencies);
        responseMediationLatency = new LatencyParameter(recordAdditionalLatencies);
        latencies.add(serverDecodeLatency);
        latencies.add(clientEncodeLatency);
        latencies.add(clientDecodeLatency);
        latencies.add(serverEncodeLatency);
        latencies.add(serverWorkerWaitTime);
        latencies.add(clientWorkerWaitTime);
        latencies.add(requestMediationLatency);
        latencies.add(responseMediationLatency);
    }

    public double getAvg_Latency() {
        return lastLatency.getAllTimeAverage();
    }

    public double getAvg_Client_To_Esb_RequestReadTime() {
        return serverDecodeLatency.getAllTimeAverage();
    }

    public double getAvg_Esb_To_BackEnd_RequestWriteTime() {
        return clientEncodeLatency.getAllTimeAverage();
    }

    public double getAvg_BackEnd_To_Esb_ResponseReadTime() {
        return clientDecodeLatency.getAllTimeAverage();
    }

    public double getAvg_Esb_To_Client_ResponseWriteTime() {
        return serverEncodeLatency.getAllTimeAverage();
    }

    public double get1m_Avg_Client_To_Esb_RequestReadTime() {
        return serverDecodeLatency.getAverageLatency1m();
    }

    public double get1m_Avg_Esb_To_BackEnd_RequestWriteTime() {
        return clientEncodeLatency.getAverageLatency1m();
    }

    public double get1m_Avg_BackEnd_To_Esb_ResponseReadTime() {
        return clientDecodeLatency.getAverageLatency1m();
    }

    public double get1m_Avg_Esb_To_Client_ResponseWriteTime() {
        return serverEncodeLatency.getAverageLatency1m();
    }

    public double get5m_Avg_Client_To_Esb_RequestReadTime() {
        return serverDecodeLatency.getAverageLatency5m();
    }

    public double get5m_Avg_Esb_To_BackEnd_RequestWriteTime() {
        return clientEncodeLatency.getAverageLatency5m();
    }

    public double get5m_Avg_BackEnd_To_Esb_ResponseReadTime() {
        return clientDecodeLatency.getAverageLatency5m();
    }

    public double get5m_Avg_Esb_To_Client_ResponseWriteTime() {
        return serverEncodeLatency.getAverageLatency5m();
    }

    public double get15m_Avg_Client_To_Esb_RequestReadTime() {
        return serverDecodeLatency.getAverageLatency15m();
    }

    public double get15m_Avg_Esb_To_BackEnd_RequestWriteTime() {
        return clientEncodeLatency.getAverageLatency15m();
    }

    public double get15m_Avg_BackEnd_To_Esb_ResponseReadTime() {
        return clientDecodeLatency.getAverageLatency15m();
    }

    public double get15m_Avg_Esb_To_Client_ResponseWriteTime() {
        return serverEncodeLatency.getAverageLatency15m();
    }

    public double getAvg_ClientWorker_QueuedTime() {
        return clientWorkerWaitTime.getAllTimeAverage();
    }

    public double get1m_Avg_ClientWorker_QueuedTime() {
        return clientWorkerWaitTime.getAverageLatency1m();
    }

    public double get5m_Avg_ClientWorker_QueuedTime() {
        return clientWorkerWaitTime.getAverageLatency5m();
    }

    public double get15m_Avg_ClientWorker_QueuedTime() {
        return clientWorkerWaitTime.getAverageLatency15m();
    }

    public double getAvg_ServerWorker_QueuedTime() {
        return serverWorkerWaitTime.getAllTimeAverage();
    }

    public double get1m_Avg_ServerWorker_QueuedTime() {
        return serverWorkerWaitTime.getAverageLatency1m();
    }

    public double get5m_Avg_ServerWorker_QueuedTime() {
        return serverWorkerWaitTime.getAverageLatency5m();
    }

    public double get15m_Avg_ServerWorker_QueuedTime() {
        return serverWorkerWaitTime.getAverageLatency15m();
    }

    public double get1m_Avg_Latency() {
        return lastLatency.getAverageLatency1m();
    }

    public double get5m_Avg_Latency() {
        return lastLatency.getAverageLatency5m();
    }

    public double get15m_Avg_Latency() {
        return lastLatency.getAverageLatency15m();
    }

    public double get1h_Avg_Latency() {
        return lastLatency.getAverageLatency1h();
    }

    public double get8h_Avg_Latency() {
        return lastLatency.getAverageLatency8h();
    }

    public double get24h_Avg_Latency() {
        return lastLatency.getAverageLatency24h();
    }

    public double getAvg_Latency_BackEnd() {
        return lastLatencyBe.getAllTimeAverage();
    }

    public double get1m_Avg_Latency_BackEnd() {
        return lastLatencyBe.getAverageLatency1m();
    }

    public double get5m_Avg_Latency_BackEnd() {
        return lastLatencyBe.getAverageLatency5m();
    }

    public double get15m_Avg_Latency_BackEnd() {
        return lastLatencyBe.getAverageLatency15m();
    }

    public double get1h_Avg_Latency_BackEnd() {
        return lastLatencyBe.getAverageLatency1h();
    }

    public double get8h_Avg_Latency_BackEnd() {
        return lastLatencyBe.getAverageLatency8h();
    }

    public double get24h_Avg_Latency_BackEnd() {
        return lastLatencyBe.getAverageLatency24h();
    }

    public double get1h_Avg_Client_To_Esb_RequestReadTime() {
        return serverDecodeLatency.getAverageLatency1h();
    }

    public double get1h_Avg_Esb_To_BackEnd_RequestWriteTime() {
        return clientEncodeLatency.getAverageLatency1h();
    }

    public double get1h_Avg_BackEnd_To_Esb_ResponseReadTime() {
        return clientDecodeLatency.getAverageLatency1h();
    }

    public double get1h_Avg_Esb_To_Client_ResponseWriteTime() {
        return serverEncodeLatency.getAverageLatency1h();
    }

    public double get1h_Avg_ServerWorker_QueuedTime() {
        return serverWorkerWaitTime.getAverageLatency1h();
    }

    public double get1h_Avg_ClientWorker_QueuedTime() {
        return clientWorkerWaitTime.getAverageLatency1h();
    }

    public double get8h_Avg_Client_To_Esb_RequestReadTime() {
        return serverDecodeLatency.getAverageLatency8h();
    }

    public double get8h_Avg_Esb_To_BackEnd_RequestWriteTime() {
        return clientEncodeLatency.getAverageLatency8h();
    }

    public double get8h_Avg_BackEnd_To_Esb_ResponseReadTime() {
        return clientDecodeLatency.getAverageLatency8h();
    }

    public double get8h_Avg_Esb_To_Client_ResponseWriteTime() {
        return serverEncodeLatency.getAverageLatency8h();
    }

    public double get8h_Avg_ServerWorker_QueuedTime() {
        return serverWorkerWaitTime.getAverageLatency8h();
    }

    public double get8h_Avg_ClientWorker_QueuedTime() {
        return clientWorkerWaitTime.getAverageLatency8h();
    }

    public double get24h_Avg_Client_To_Esb_RequestReadTime() {
        return serverDecodeLatency.getAverageLatency24h();
    }

    public double get24h_Avg_Esb_To_BackEnd_RequestWriteTime() {
        return clientEncodeLatency.getAverageLatency24h();
    }

    public double get24h_Avg_BackEnd_To_Esb_ResponseReadTime() {
        return clientDecodeLatency.getAverageLatency24h();
    }

    public double get24h_Avg_Esb_To_Client_ResponseWriteTime() {
        return serverEncodeLatency.getAverageLatency24h();
    }

    public double get24h_Avg_ServerWorker_QueuedTime() {
        return serverWorkerWaitTime.getAverageLatency24h();
    }

    public double get24h_Avg_ClientWorker_QueuedTime() {
        return clientWorkerWaitTime.getAverageLatency24h();
    }

    public double getAvg_request_Mediation_Latency() {
        return requestMediationLatency.getAllTimeAverage();
    }

    public double getAvg_response_Mediation_Latency() {
        return responseMediationLatency.getAllTimeAverage();
    }

    public double get1m_Avg_request_Mediation_Latency() {
        return requestMediationLatency.getAverageLatency1m();
    }

    public double get1m_Avg_response_Mediation_Latency() {
        return responseMediationLatency.getAverageLatency1m();
    }

    public double get5m_Avg_request_Mediation_Latency() {
        return requestMediationLatency.getAverageLatency5m();
    }

    public double get5m_Avg_response_Mediation_Latency() {
        return responseMediationLatency.getAverageLatency5m();
    }

    public double get15m_Avg_request_Mediation_Latency() {
        return requestMediationLatency.getAverageLatency15m();
    }

    public double get15m_Avg_response_Mediation_Latency() {
        return responseMediationLatency.getAverageLatency15m();
    }

    public double get1h_Avg_request_Mediation_Latency() {
        return requestMediationLatency.getAverageLatency1h();
    }

    public double get1h_Avg_response_Mediation_Latency() {
        return responseMediationLatency.getAverageLatency1h();
    }

    public double get8h_Avg_request_Mediation_Latency() {
        return requestMediationLatency.getAverageLatency8h();
    }

    public double get8h_Avg_response_Mediation_Latency() {
        return responseMediationLatency.getAverageLatency8h();
    }

    public double get24h_Avg_request_Mediation_Latency() {
        return requestMediationLatency.getAverageLatency24h();
    }

    public double get24h_Avg_response_Mediation_Latency() {
        return responseMediationLatency.getAverageLatency24h();
    }

    public void reset() {
        for (LatencyParameter latency : latencies) {
            latency.reset();
        }
        resetTime = Calendar.getInstance().getTime();
    }

    public Date getLastResetTime() {
        return resetTime;
    }

    private class ShortTermDataCollector implements Runnable {
        public void run() {
            for (LatencyParameter latency : latencies) {
                latency.updateCache();
            }
        }
    }

    private class LongTermDataCollector implements Runnable {
        public void run() {
            for (LatencyParameter latency : latencies) {
                latency.updateLongTermCache();
            }
        }
    }
}
