/*
 * 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.jmeter.visualizers;

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.Image;
import java.awt.event.ItemEvent;
import java.awt.event.ItemListener;
import java.text.NumberFormat;
import java.util.Deque;
import java.util.concurrent.ConcurrentLinkedDeque;

import javax.swing.BorderFactory;
import javax.swing.Box;
import javax.swing.JCheckBox;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextField;
import javax.swing.ScrollPaneConstants;
import javax.swing.SwingConstants;
import javax.swing.Timer;
import javax.swing.border.Border;
import javax.swing.border.EmptyBorder;

import org.apache.jmeter.gui.TestElementMetadata;
import org.apache.jmeter.gui.util.JMeterColor;
import org.apache.jmeter.samplers.Clearable;
import org.apache.jmeter.samplers.SampleResult;
import org.apache.jmeter.util.JMeterUtils;
import org.apache.jmeter.visualizers.gui.AbstractVisualizer;

/**
 * This class implements a statistical analyser that calculates both the average
 * and the standard deviation of the sampling process and outputs them as
 * autoscaling plots.
 *
 */
@TestElementMetadata(labelResource = "graph_results_title")
public class GraphVisualizer extends AbstractVisualizer implements ImageVisualizer, ItemListener, Clearable {

    private static final long serialVersionUID = 240L;

    private static final String ZERO = "0";  //$NON-NLS-1$

    private final NumberFormat nf = NumberFormat.getInstance(); // OK, because used in synchronised method

    private final CachingStatCalculator model;

    private JTextField maxYField = null;

    private JTextField minYField = null;

    private JTextField noSamplesField = null;

    private final String minute = JMeterUtils.getResString("minute"); // $NON-NLS-1$

    private final int REFRESH_PERIOD = JMeterUtils.getPropDefault("jmeter.gui.refresh_period", 500); // $NON-NLS-1$

    private final Graph graph;

    private JCheckBox data;

    private JCheckBox average;

    private JCheckBox deviation;

    private JCheckBox throughput;

    private JCheckBox median;

    private JTextField dataField;

    private JTextField averageField;

    private JTextField deviationField;

    private JTextField throughputField;

    private JTextField medianField;

    private final Deque<SampleResult> newSamples = new ConcurrentLinkedDeque<>();

    /**
     * Constructor for the GraphVisualizer object.
     */
    public GraphVisualizer() {
        model = new CachingStatCalculator("Graph");
        graph = new Graph(model);
        init();
    }

    /**
     * Gets the Image attribute of the GraphVisualizer object.
     *
     * @return the Image value
     */
    @Override
    public Image getImage() {
        Image result = graph.createImage(graph.getWidth(), graph.getHeight());

        graph.paintComponent(result.getGraphics());

        return result;
    }

    /**
     * @param s Sample
     * @deprecated use {@link GraphVisualizer#add(SampleResult)} instead
     */
    @Deprecated
    public void updateGui(Sample s) {
        JMeterUtils.runSafe(false, () -> updateGuiInAWTThread(s));
    }

    // called inside AWT Thread
    private void collectSamplesFromQueue() {
        // We have received one more sample
        Sample s = null;
        synchronized (graph) {
            while (!newSamples.isEmpty()) {
                s = model.addSample(newSamples.pop());
            }
        }
        updateGuiInAWTThread(s);
    }

    // called inside AWT Thread
    private void updateGuiInAWTThread(Sample s) {
        if (s == null) {
            return;
        }
        graph.updateGui(s);
        noSamplesField.setText(Long.toString(s.getCount()));
        dataField.setText(Long.toString(s.getData()));
        averageField.setText(Long.toString(s.getAverage()));
        deviationField.setText(Long.toString(s.getDeviation()));
        throughputField.setText(nf.format(60 * s.getThroughput()) + "/" + minute); // $NON-NLS-1$
        medianField.setText(Long.toString(s.getMedian()));
        updateYAxis();
    }

    @Override
    public void add(final SampleResult res) {
        newSamples.add(res);
    }

    @Override
    public String getLabelResource() {
        return "graph_results_title"; // $NON-NLS-1$
    }

    @Override
    public void itemStateChanged(ItemEvent e) {
        if (e.getItem() == data) {
            this.graph.enableData(e.getStateChange() == ItemEvent.SELECTED);
        } else if (e.getItem() == average) {
            this.graph.enableAverage(e.getStateChange() == ItemEvent.SELECTED);
        } else if (e.getItem() == deviation) {
            this.graph.enableDeviation(e.getStateChange() == ItemEvent.SELECTED);
        } else if (e.getItem() == throughput) {
            this.graph.enableThroughput(e.getStateChange() == ItemEvent.SELECTED);
        } else if (e.getItem() == median) {
            this.graph.enableMedian(e.getStateChange() == ItemEvent.SELECTED);
        }
        this.graph.repaint();
    }

    @Override
    public void clearData() {
        synchronized (graph) {
            graph.clearData();
            model.clear();
            newSamples.clear();
        }
        dataField.setText(ZERO);
        averageField.setText(ZERO);
        deviationField.setText(ZERO);
        throughputField.setText("0/" + minute); //$NON-NLS-1$
        medianField.setText(ZERO);
        noSamplesField.setText(ZERO);
        updateYAxis();
        repaint();
    }

    @Override
    public String toString() {
        return "Show the samples analysis as dot plots";
    }

    /**
     * Update the max and min value of the Y axis.
     */
    private void updateYAxis() {
        maxYField.setText(Long.toString(graph.getGraphMax()));
        minYField.setText(ZERO);
    }

    /**
     * Initialize the GUI.
     */
    private void init() { // WARNING: called from ctor so must not be overridden (i.e. must be private or final)
        this.setLayout(new BorderLayout());

        // MAIN PANEL
        Border margin = new EmptyBorder(10, 10, 5, 10);

        this.setBorder(margin);

        // Set up the graph with header, footer, Y axis and graph display
        JPanel graphPanel = new JPanel(new BorderLayout());
        graphPanel.add(createYAxis(), BorderLayout.WEST);
        graphPanel.add(createChoosePanel(), BorderLayout.NORTH);
        graphPanel.add(createGraphPanel(), BorderLayout.CENTER);
        graphPanel.add(createGraphInfoPanel(), BorderLayout.SOUTH);

        // Add the main panel and the graph
        this.add(makeTitlePanel(), BorderLayout.NORTH);
        this.add(graphPanel, BorderLayout.CENTER);

        new Timer(REFRESH_PERIOD, e -> collectSamplesFromQueue()).start();
    }

    // Methods used in creating the GUI

    /**
     * Creates the panel containing the graph's Y axis labels.
     *
     * @return the Y axis panel
     */
    private JPanel createYAxis() {
        JPanel graphYAxisPanel = new JPanel();

        graphYAxisPanel.setLayout(new BorderLayout());

        maxYField = createYAxisField(5);
        minYField = createYAxisField(3);

        graphYAxisPanel.add(createYAxisPanel("graph_results_ms", maxYField), BorderLayout.NORTH); // $NON-NLS-1$
        graphYAxisPanel.add(createYAxisPanel("graph_results_ms", minYField), BorderLayout.SOUTH); // $NON-NLS-1$

        return graphYAxisPanel;
    }

    /**
     * Creates a text field to be used for the value of a Y axis label. These
     * fields hold the minimum and maximum values for the graph. The units are
     * kept in a separate label outside of this field.
     *
     * @param length
     *            the number of characters which the field will use to calculate
     *            its preferred width. This should be set to the maximum number
     *            of digits that are expected to be necessary to hold the label
     *            value.
     *
     * @see #createYAxisPanel(String, JTextField)
     *
     * @return a text field configured to be used in the Y axis
     */
    private JTextField createYAxisField(int length) {
        JTextField field = new JTextField(length);
        field.setBorder(BorderFactory.createEmptyBorder(0, 10, 0, 0));
        field.setEditable(false);
        field.setForeground(Color.black);
        field.setBackground(getBackground());
        field.setHorizontalAlignment(SwingConstants.RIGHT);
        return field;
    }

    /**
     * Creates a panel for an entire Y axis label. This includes the dynamic
     * value as well as the unit label.
     *
     * @param labelResourceName
     *            the name of the label resource. This is used to look up the
     *            label text using {@link JMeterUtils#getResString(String)}.
     *
     * @return a panel containing both the dynamic and static parts of a Y axis
     *         label
     */
    private JPanel createYAxisPanel(String labelResourceName, JTextField field) {
        JPanel panel = new JPanel(new FlowLayout());
        JLabel label = new JLabel(JMeterUtils.getResString(labelResourceName));

        panel.add(field);
        panel.add(label);
        return panel;
    }

    /**
     * Creates a panel which allows the user to choose which graphs to display.
     * This panel consists of a check box for each type of graph (current
     * sample, average, deviation, and throughput).
     *
     * @return a panel allowing the user to choose which graphs to display
     */
    private JPanel createChoosePanel() {
        JPanel chooseGraphsPanel = new JPanel();

        chooseGraphsPanel.setLayout(new FlowLayout());
        JLabel selectGraphsLabel = new JLabel(JMeterUtils.getResString("graph_choose_graphs")); //$NON-NLS-1$
        data = createChooseCheckBox("graph_results_data", Color.black); // $NON-NLS-1$
        average = createChooseCheckBox("graph_results_average", Color.blue); // $NON-NLS-1$
        deviation = createChooseCheckBox("graph_results_deviation", Color.red); // $NON-NLS-1$
        throughput = createChooseCheckBox("graph_results_throughput", JMeterColor.DARK_GREEN); // $NON-NLS-1$
        median = createChooseCheckBox("graph_results_median", JMeterColor.PURPLE); // $NON-NLS-1$

        chooseGraphsPanel.add(selectGraphsLabel);
        chooseGraphsPanel.add(data);
        chooseGraphsPanel.add(average);
        chooseGraphsPanel.add(median);
        chooseGraphsPanel.add(deviation);
        chooseGraphsPanel.add(throughput);
        return chooseGraphsPanel;
    }

    /**
     * Creates a check box configured to be used to in the choose panel allowing
     * the user to select whether or not a particular kind of graph data will be
     * displayed.
     *
     * @param labelResourceName
     *            the name of the label resource. This is used to look up the
     *            label text using {@link JMeterUtils#getResString(String)}.
     * @param color
     *            the color used for the checkbox text. By convention this is
     *            the same color that is used to draw the graph and for the
     *            corresponding info field.
     *
     * @return a checkbox allowing the user to select whether or not a kind of
     *         graph data will be displayed
     */
    private JCheckBox createChooseCheckBox(String labelResourceName, Color color) {
        JCheckBox checkBox = new JCheckBox(JMeterUtils.getResString(labelResourceName));
        checkBox.setSelected(true);
        checkBox.addItemListener(this);
        checkBox.setForeground(color);
        return checkBox;
    }

    /**
     * Creates a scroll pane containing the actual graph of the results.
     *
     * @return a scroll pane containing the graph
     */
    private Component createGraphPanel() {
        JScrollPane graphScrollPanel = makeScrollPane(graph, ScrollPaneConstants.VERTICAL_SCROLLBAR_NEVER,
                ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED);
        graphScrollPanel.setViewportBorder(BorderFactory.createEmptyBorder(2, 2, 2, 2));
        graphScrollPanel.setPreferredSize(graphScrollPanel.getMinimumSize());

        return graphScrollPanel;
    }

    /**
     * Creates a panel which numerically displays the current graph values.
     *
     * @return a panel showing the current graph values
     */
    private Box createGraphInfoPanel() {
        Box graphInfoPanel = Box.createHorizontalBox();

        noSamplesField = createInfoField(Color.black, 6);
        dataField = createInfoField(Color.black, 5);
        averageField = createInfoField(Color.blue, 5);
        deviationField = createInfoField(Color.red, 5);
        throughputField = createInfoField(JMeterColor.DARK_GREEN, 15);
        medianField = createInfoField(JMeterColor.PURPLE, 5);

        graphInfoPanel.add(createInfoColumn(createInfoLabel("graph_results_no_samples", noSamplesField), // $NON-NLS-1$
                noSamplesField, createInfoLabel("graph_results_deviation", deviationField), deviationField)); // $NON-NLS-1$
        graphInfoPanel.add(Box.createHorizontalGlue());

        graphInfoPanel.add(createInfoColumn(createInfoLabel("graph_results_latest_sample", dataField), dataField, // $NON-NLS-1$
                createInfoLabel("graph_results_throughput", throughputField), throughputField)); // $NON-NLS-1$
        graphInfoPanel.add(Box.createHorizontalGlue());

        graphInfoPanel.add(createInfoColumn(createInfoLabel("graph_results_average", averageField), averageField, // $NON-NLS-1$
                createInfoLabel("graph_results_median", medianField), medianField)); // $NON-NLS-1$
        graphInfoPanel.add(Box.createHorizontalGlue());
        return graphInfoPanel;
    }

    /**
     * Creates one of the fields used to display the graph's current values.
     *
     * @param color
     *            the color used to draw the value. By convention this is the
     *            same color that is used to draw the graph for this value and
     *            in the choose panel.
     * @param length
     *            the number of digits which the field should be able to display
     *
     * @return a text field configured to display one of the current graph
     *         values
     */
    private JTextField createInfoField(Color color, int length) {
        JTextField field = new JTextField(length);
        field.setBorder(BorderFactory.createEmptyBorder(0, 0, 0, 0));
        field.setEditable(false);
        field.setForeground(color);
        field.setBackground(getBackground());

        // The text field should expand horizontally, but have
        // a fixed height
        field.setMaximumSize(new Dimension(field.getMaximumSize().width, field.getPreferredSize().height));
        return field;
    }

    /**
     * Creates a label for one of the fields used to display the graph's current
     * values. Neither the label created by this method or the
     * <code>field</code> passed as a parameter is added to the GUI here.
     *
     * @param labelResourceName
     *            the name of the label resource. This is used to look up the
     *            label text using {@link JMeterUtils#getResString(String)}.
     * @param field
     *            the field this label is being created for.
     */
    private JLabel createInfoLabel(String labelResourceName, JTextField field) {
        JLabel label = new JLabel(JMeterUtils.getResString(labelResourceName));
        label.setForeground(field.getForeground());
        label.setLabelFor(field);
        return label;
    }

    /**
     * Creates a panel containing two pairs of labels and fields for displaying
     * the current graph values. This method exists to help with laying out the
     * fields in columns. If one or more components are null then these
     * components will be represented by blank space.
     *
     * @param label1
     *            the label for the first field. This label will be placed in
     *            the upper left section of the panel. If this parameter is
     *            null, this section of the panel will be left blank.
     * @param field1
     *            the field corresponding to the first label. This field will be
     *            placed in the upper right section of the panel. If this
     *            parameter is null, this section of the panel will be left
     *            blank.
     * @param label2
     *            the label for the second field. This label will be placed in
     *            the lower left section of the panel. If this parameter is
     *            null, this section of the panel will be left blank.
     * @param field2
     *            the field corresponding to the second label. This field will
     *            be placed in the lower right section of the panel. If this
     *            parameter is null, this section of the panel will be left
     *            blank.
     */
    private Box createInfoColumn(JLabel label1, JTextField field1, JLabel label2, JTextField field2) {
        // This column actually consists of a row with two sub-columns
        // The first column contains the labels, and the second
        // column contains the fields.
        Box row = Box.createHorizontalBox();
        Box col = Box.createVerticalBox();
        col.add(label1 != null ? label1 : Box.createVerticalGlue());
        col.add(label2 != null ? label2 : Box.createVerticalGlue());
        row.add(col);

        row.add(Box.createHorizontalStrut(5));

        col = Box.createVerticalBox();
        col.add(field1 != null ? field1 : Box.createVerticalGlue());
        col.add(field2 != null ? field2 : Box.createVerticalGlue());
        row.add(col);

        row.add(Box.createHorizontalStrut(5));

        return row;
    }

}
