/*
 * 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
 *
 *     https://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.avro.ipc.stats;

import java.io.IOException;
import java.io.Writer;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Set;
import java.util.Map.Entry;

import javax.servlet.ServletException;
import javax.servlet.UnavailableException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.velocity.Template;
import org.apache.velocity.VelocityContext;
import org.apache.velocity.app.VelocityEngine;

import org.apache.avro.Protocol.Message;
import org.apache.avro.ipc.RPCContext;

/**
 * Exposes information provided by a StatsPlugin as a web page.
 *
 * This class follows the same synchronization conventions as StatsPlugin, to
 * avoid requiring StatsPlugin to serve a copy of the data.
 */
public class StatsServlet extends HttpServlet {
  private final StatsPlugin statsPlugin;
  private VelocityEngine velocityEngine;
  private static final SimpleDateFormat FORMATTER = new SimpleDateFormat("dd-MMM-yyyy HH:mm:ss");

  public StatsServlet(StatsPlugin statsPlugin) throws UnavailableException {
    this.statsPlugin = statsPlugin;
    this.velocityEngine = new VelocityEngine();

    // These two properties tell Velocity to use its own classpath-based loader
    velocityEngine.addProperty("resource.loaders", "class");
    velocityEngine.addProperty("resource.loader.class.class",
        "org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader");

    velocityEngine.setProperty("runtime.strict_mode.enable", true);
    String logChuteName = "org.apache.velocity.runtime.log.NullLogChute";
    velocityEngine.setProperty("runtime.log.logsystem.class", logChuteName);
  }

  /*
   * Helper class to store per-message data which is passed to templates.
   *
   * The template expects a list of charts, each of which is parameterized by map
   * key-value string attributes.
   */
  public class RenderableMessage { // Velocity brakes if not public
    public String name;
    public int numCalls;
    public ArrayList<HashMap<String, String>> charts;

    public RenderableMessage(String name) {
      this.name = name;
      this.charts = new ArrayList<>();
    }

    public ArrayList<HashMap<String, String>> getCharts() {
      return this.charts;
    }

    public String getname() {
      return this.name;
    }

    public int getNumCalls() {
      return this.numCalls;
    }
  }

  /*
   * Surround each string in an array with quotation marks and escape existing
   * quotes.
   *
   * This is useful when we have an array of strings that we want to turn into a
   * javascript array declaration.
   */
  protected static List<String> escapeStringArray(List<String> input) {
    for (int i = 0; i < input.size(); i++) {
      input.set(i, "\"" + input.get(i).replace("\"", "\\\"") + "\"");
    }
    return input;
  }

  @Override
  protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
    resp.setContentType("text/html");
    try {
      writeStats(resp.getWriter());
    } catch (Exception e) {
      e.printStackTrace();
    }
  }

  public void writeStats(Writer w) throws IOException {
    VelocityContext context = new VelocityContext();
    context.put("title", "Avro RPC Stats");

    ArrayList<String> rpcs = new ArrayList<>(); // in flight rpcs

    ArrayList<RenderableMessage> messages = new ArrayList<>();

    for (Entry<RPCContext, Stopwatch> rpc : this.statsPlugin.activeRpcs.entrySet()) {
      rpcs.add(renderActiveRpc(rpc.getKey(), rpc.getValue()));
    }

    // Get set of all seen messages
    Set<Message> keys = null;
    synchronized (this.statsPlugin.methodTimings) {
      keys = this.statsPlugin.methodTimings.keySet();

      for (Message m : keys) {
        messages.add(renderMethod(m));
      }
    }

    context.put("inFlightRpcs", rpcs);
    context.put("messages", messages);

    context.put("currTime", FORMATTER.format(new Date()));
    context.put("startupTime", FORMATTER.format(statsPlugin.startupTime));

    Template t;
    try {
      t = velocityEngine.getTemplate("org/apache/avro/ipc/stats/templates/statsview.vm");
    } catch (Exception e) {
      throw new IOException();
    }
    t.merge(context, w);
  }

  private String renderActiveRpc(RPCContext rpc, Stopwatch stopwatch) throws IOException {
    String out = new String();
    out += rpc.getMessage().getName() + ": " + formatMillis(StatsPlugin.nanosToMillis(stopwatch.elapsedNanos()));
    return out;
  }

  private RenderableMessage renderMethod(Message message) {
    RenderableMessage out = new RenderableMessage(message.getName());

    synchronized (this.statsPlugin.methodTimings) {
      FloatHistogram<?> hist = this.statsPlugin.methodTimings.get(message);
      out.numCalls = hist.getCount();

      HashMap<String, String> latencyBar = new HashMap<>();
      // Fill in chart attributes for velocity
      latencyBar.put("type", "bar");
      latencyBar.put("title", "All-Time Latency");
      latencyBar.put("units", "ms");
      latencyBar.put("numCalls", Integer.toString(hist.getCount()));
      latencyBar.put("avg", Float.toString(hist.getMean()));
      latencyBar.put("stdDev", Float.toString(hist.getUnbiasedStdDev()));
      latencyBar.put("labelStr", Arrays.toString(hist.getSegmenter().getBoundaryLabels().toArray()));
      latencyBar.put("boundaryStr",
          Arrays.toString(escapeStringArray(hist.getSegmenter().getBucketLabels()).toArray()));
      latencyBar.put("dataStr", Arrays.toString(hist.getHistogram()));
      out.charts.add(latencyBar);

      HashMap<String, String> latencyDot = new HashMap<>();
      latencyDot.put("title", "Latency");
      latencyDot.put("type", "dot");
      latencyDot.put("dataStr", Arrays.toString(hist.getRecentAdditions().toArray()));
      out.charts.add(latencyDot);
    }

    synchronized (this.statsPlugin.sendPayloads) {
      IntegerHistogram<?> hist = this.statsPlugin.sendPayloads.get(message);
      HashMap<String, String> latencyBar = new HashMap<>();
      // Fill in chart attributes for velocity
      latencyBar.put("type", "bar");
      latencyBar.put("title", "All-Time Send Payload");
      latencyBar.put("units", "ms");
      latencyBar.put("numCalls", Integer.toString(hist.getCount()));
      latencyBar.put("avg", Float.toString(hist.getMean()));
      latencyBar.put("stdDev", Float.toString(hist.getUnbiasedStdDev()));
      latencyBar.put("labelStr", Arrays.toString(hist.getSegmenter().getBoundaryLabels().toArray()));
      latencyBar.put("boundaryStr",
          Arrays.toString(escapeStringArray(hist.getSegmenter().getBucketLabels()).toArray()));
      latencyBar.put("dataStr", Arrays.toString(hist.getHistogram()));
      out.charts.add(latencyBar);

      HashMap<String, String> latencyDot = new HashMap<>();
      latencyDot.put("title", "Send Payload");
      latencyDot.put("type", "dot");
      latencyDot.put("dataStr", Arrays.toString(hist.getRecentAdditions().toArray()));
      out.charts.add(latencyDot);
    }

    synchronized (this.statsPlugin.receivePayloads) {
      IntegerHistogram<?> hist = this.statsPlugin.receivePayloads.get(message);
      HashMap<String, String> latencyBar = new HashMap<>();
      // Fill in chart attributes for velocity
      latencyBar.put("type", "bar");
      latencyBar.put("title", "All-Time Receive Payload");
      latencyBar.put("units", "ms");
      latencyBar.put("numCalls", Integer.toString(hist.getCount()));
      latencyBar.put("avg", Float.toString(hist.getMean()));
      latencyBar.put("stdDev", Float.toString(hist.getUnbiasedStdDev()));
      latencyBar.put("labelStr", Arrays.toString(hist.getSegmenter().getBoundaryLabels().toArray()));
      latencyBar.put("boundaryStr",
          Arrays.toString(escapeStringArray(hist.getSegmenter().getBucketLabels()).toArray()));
      latencyBar.put("dataStr", Arrays.toString(hist.getHistogram()));
      out.charts.add(latencyBar);

      HashMap<String, String> latencyDot = new HashMap<>();
      latencyDot.put("title", "Recv Payload");
      latencyDot.put("type", "dot");
      latencyDot.put("dataStr", Arrays.toString(hist.getRecentAdditions().toArray()));
      out.charts.add(latencyDot);
    }

    return out;
  }

  private CharSequence formatMillis(float millis) {
    return String.format("%.0fms", millis);
  }
}
