/*
 * 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.FlowLayout;
import java.text.Format;
import java.text.SimpleDateFormat;
import java.util.Comparator;
import java.util.Deque;
import java.util.concurrent.ConcurrentLinkedDeque;

import javax.swing.BorderFactory;
import javax.swing.ImageIcon;
import javax.swing.JCheckBox;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.JTextField;
import javax.swing.Timer;
import javax.swing.border.Border;
import javax.swing.border.EmptyBorder;
import javax.swing.table.TableCellRenderer;

import org.apache.jmeter.JMeter;
import org.apache.jmeter.gui.TestElementMetadata;
import org.apache.jmeter.gui.util.HeaderAsPropertyRendererWrapper;
import org.apache.jmeter.gui.util.HorizontalPanel;
import org.apache.jmeter.samplers.Clearable;
import org.apache.jmeter.samplers.SampleResult;
import org.apache.jmeter.util.Calculator;
import org.apache.jmeter.util.JMeterUtils;
import org.apache.jmeter.visualizers.gui.AbstractVisualizer;
import org.apache.jorphan.gui.ObjectTableModel;
import org.apache.jorphan.gui.ObjectTableSorter;
import org.apache.jorphan.gui.RendererUtils;
import org.apache.jorphan.gui.RightAlignRenderer;
import org.apache.jorphan.gui.layout.VerticalLayout;
import org.apache.jorphan.reflect.Functor;

/**
 * This class implements a statistical analyser that calculates both the average
 * and the standard deviation of the sampling process. The samples are displayed
 * in a JTable, and the statistics are displayed at the bottom of the table.
 *
 */
@TestElementMetadata(labelResource = "view_results_in_table")
public class TableVisualizer extends AbstractVisualizer implements Clearable {

    private static final long serialVersionUID = 241L;

    private static final String ICON_SIZE = JMeterUtils.getPropDefault(JMeter.TREE_ICON_SIZE, JMeter.DEFAULT_TREE_ICON_SIZE);

    private static final int REFRESH_PERIOD = JMeterUtils.getPropDefault("jmeter.gui.refresh_period", 500);

    // Note: the resource string won't respond to locale-changes,
    // however this does not matter as it is only used when pasting to the clipboard
    private static final ImageIcon imageSuccess = JMeterUtils.getImage(
            JMeterUtils.getPropDefault("viewResultsTree.success",  //$NON-NLS-1$
                                       "vrt/" + ICON_SIZE + "/security-high-2.png"),    //$NON-NLS-1$ $NON-NLS-2$
            JMeterUtils.getResString("table_visualizer_success")); //$NON-NLS-1$

    private static final ImageIcon imageFailure = JMeterUtils.getImage(
            JMeterUtils.getPropDefault("viewResultsTree.failure",  //$NON-NLS-1$
                                       "vrt/" + ICON_SIZE + "/security-low-2.png"),    //$NON-NLS-1$ $NON-NLS-2$
            JMeterUtils.getResString("table_visualizer_warning")); //$NON-NLS-1$

    private static final String[] COLUMNS = new String[] {
            "table_visualizer_sample_num",  // $NON-NLS-1$
            "table_visualizer_start_time",  // $NON-NLS-1$
            "table_visualizer_thread_name", // $NON-NLS-1$
            "sampler_label",                // $NON-NLS-1$
            "table_visualizer_sample_time", // $NON-NLS-1$
            "table_visualizer_status",      // $NON-NLS-1$
            "table_visualizer_bytes",       // $NON-NLS-1$
            "table_visualizer_sent_bytes",       // $NON-NLS-1$
            "table_visualizer_latency",     // $NON-NLS-1$
            "table_visualizer_connect"};    // $NON-NLS-1$

    private ObjectTableModel model = null;

    private JTable table = null;

    private JTextField dataField = null;

    private JTextField averageField = null;

    private JTextField deviationField = null;

    private JTextField noSamplesField = null;

    private JScrollPane tableScrollPanel = null;

    private JCheckBox autoscroll = null;

    private JCheckBox childSamples = null;

    private final transient Calculator calc = new Calculator();

    private Format format = new SimpleDateFormat("HH:mm:ss.SSS"); //$NON-NLS-1$

    private Deque<SampleResult> newRows = new ConcurrentLinkedDeque<>();

    // Column renderers
    private static final TableCellRenderer[] RENDERERS =
        new TableCellRenderer[]{
            new RightAlignRenderer(), // Sample number (string)
            new RightAlignRenderer(), // Start Time
            null, // Thread Name
            null, // Label
            null, // Sample Time
            null, // Status
            null, // Bytes
        };

    /**
     * Constructor for the TableVisualizer object.
     */
    public TableVisualizer() {
        super();
        model = new ObjectTableModel(COLUMNS,
                TableSample.class,         // The object used for each row
                new Functor[] {
                new Functor("getSampleNumberString"),  // $NON-NLS-1$
                new Functor("getStartTimeFormatted",   // $NON-NLS-1$
                        new Object[]{format}),
                new Functor("getThreadName"),          // $NON-NLS-1$
                new Functor("getLabel"),               // $NON-NLS-1$
                new Functor("getElapsed"),             // $NON-NLS-1$
                new SampleSuccessFunctor("isSuccess"), // $NON-NLS-1$
                new Functor("getBytes"),               // $NON-NLS-1$
                new Functor("getSentBytes"),               // $NON-NLS-1$
                new Functor("getLatency"),             // $NON-NLS-1$
                new Functor("getConnectTime") },       // $NON-NLS-1$
                new Functor[] { null, null, null, null, null, null, null, null, null, null },
                new Class[] {
                String.class, String.class, String.class, String.class, Long.class, ImageIcon.class, Long.class, Long.class, Long.class, Long.class });
        init();
    }

    public static boolean testFunctors(){
        TableVisualizer instance = new TableVisualizer();
        return instance.model.checkFunctors(null,instance.getClass());
    }


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

    protected synchronized void updateTextFields(SampleResult res) {
        noSamplesField.setText(Long.toString(calc.getCount()));
        if(res.getSampleCount() > 0) {
            dataField.setText(Long.toString(res.getTime()/res.getSampleCount()));
        } else {
            dataField.setText("0");
        }
        averageField.setText(Long.toString((long) calc.getMean()));
        deviationField.setText(Long.toString((long) calc.getStandardDeviation()));
    }

    @Override
    public void add(final SampleResult res) {
        if (childSamples.isSelected()) {
            SampleResult[] subResults = res.getSubResults();
            if (subResults.length > 0) {
                for (SampleResult sr : subResults) {
                    add(sr);
                }
                return;
            }
        }
        newRows.add(res);

    }

    @Override
    public synchronized void clearData() {
        synchronized (calc) {
            model.clearData();
            calc.clear();
            newRows.clear();
            noSamplesField.setText("0"); // $NON-NLS-1$
            dataField.setText("0"); // $NON-NLS-1$
            averageField.setText("0"); // $NON-NLS-1$
            deviationField.setText("0"); // $NON-NLS-1$
        }
        repaint();
    }

    @Override
    public String toString() {
        return "Show the samples in a table";
    }

    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
        JPanel mainPanel = new JPanel();
        Border margin = new EmptyBorder(10, 10, 5, 10);

        mainPanel.setBorder(margin);
        mainPanel.setLayout(new VerticalLayout(5, VerticalLayout.BOTH));

        // NAME
        mainPanel.add(makeTitlePanel());

        // Set up the table itself
        table = new JTable(model);
        table.setRowSorter(new ObjectTableSorter(model).setValueComparator(5,
                Comparator.nullsFirst(
                        (ImageIcon o1, ImageIcon o2) -> {
                            if (o1 == o2) {
                                return 0;
                            }
                            if (o1 == imageSuccess) {
                                return -1;
                            }
                            if (o1 == imageFailure) {
                                return 1;
                            }
                            throw new IllegalArgumentException("Only success and failure images can be compared");
                        })));
        JMeterUtils.applyHiDPI(table);
        HeaderAsPropertyRendererWrapper.setupDefaultRenderer(table);
        RendererUtils.applyRenderers(table, RENDERERS);

        tableScrollPanel = new JScrollPane(table);
        tableScrollPanel.setViewportBorder(BorderFactory.createEmptyBorder(2, 2, 2, 2));

        autoscroll = new JCheckBox(JMeterUtils.getResString("view_results_autoscroll")); //$NON-NLS-1$

        childSamples = new JCheckBox(JMeterUtils.getResString("view_results_childsamples")); //$NON-NLS-1$

        // Set up footer of table which displays numerics of the graphs
        JPanel dataPanel = new JPanel();
        JLabel dataLabel = new JLabel(JMeterUtils.getResString("graph_results_latest_sample")); // $NON-NLS-1$
        dataLabel.setForeground(Color.black);
        dataField = new JTextField(5);
        dataField.setBorder(BorderFactory.createEmptyBorder(0, 0, 0, 0));
        dataField.setEditable(false);
        dataField.setForeground(Color.black);
        dataField.setBackground(getBackground());
        dataPanel.add(dataLabel);
        dataPanel.add(dataField);

        JPanel averagePanel = new JPanel();
        JLabel averageLabel = new JLabel(JMeterUtils.getResString("graph_results_average")); // $NON-NLS-1$
        averageLabel.setForeground(Color.blue);
        averageField = new JTextField(5);
        averageField.setBorder(BorderFactory.createEmptyBorder(0, 0, 0, 0));
        averageField.setEditable(false);
        averageField.setForeground(Color.blue);
        averageField.setBackground(getBackground());
        averagePanel.add(averageLabel);
        averagePanel.add(averageField);

        JPanel deviationPanel = new JPanel();
        JLabel deviationLabel = new JLabel(JMeterUtils.getResString("graph_results_deviation")); // $NON-NLS-1$
        deviationLabel.setForeground(Color.red);
        deviationField = new JTextField(5);
        deviationField.setBorder(BorderFactory.createEmptyBorder(0, 0, 0, 0));
        deviationField.setEditable(false);
        deviationField.setForeground(Color.red);
        deviationField.setBackground(getBackground());
        deviationPanel.add(deviationLabel);
        deviationPanel.add(deviationField);

        JPanel noSamplesPanel = new JPanel();
        JLabel noSamplesLabel = new JLabel(JMeterUtils.getResString("graph_results_no_samples")); // $NON-NLS-1$

        noSamplesField = new JTextField(8);
        noSamplesField.setBorder(BorderFactory.createEmptyBorder(0, 0, 0, 0));
        noSamplesField.setEditable(false);
        noSamplesField.setForeground(Color.black);
        noSamplesField.setBackground(getBackground());
        noSamplesPanel.add(noSamplesLabel);
        noSamplesPanel.add(noSamplesField);

        JPanel tableInfoPanel = new JPanel();
        tableInfoPanel.setLayout(new FlowLayout());
        tableInfoPanel.setBorder(BorderFactory.createEmptyBorder(0, 0, 0, 0));

        tableInfoPanel.add(noSamplesPanel);
        tableInfoPanel.add(dataPanel);
        tableInfoPanel.add(averagePanel);
        tableInfoPanel.add(deviationPanel);

        JPanel tableControlsPanel = new JPanel(new BorderLayout());
        tableControlsPanel.setBorder(BorderFactory.createEmptyBorder(0, 0, 0, 0));
        JPanel jp = new HorizontalPanel();
        jp.add(autoscroll);
        jp.add(childSamples);
        tableControlsPanel.add(jp, BorderLayout.WEST);
        tableControlsPanel.add(tableInfoPanel, BorderLayout.CENTER);

        // Set up the table with footer
        JPanel tablePanel = new JPanel();

        tablePanel.setLayout(new BorderLayout());
        tablePanel.add(tableScrollPanel, BorderLayout.CENTER);
        tablePanel.add(tableControlsPanel, BorderLayout.SOUTH);

        // Add the main panel and the graph
        this.add(mainPanel, BorderLayout.NORTH);
        this.add(tablePanel, BorderLayout.CENTER);
        new Timer(REFRESH_PERIOD, e -> collectNewSamples()).start();
    }

    private void collectNewSamples() {
        synchronized (calc) {
            SampleResult res = null;
            while (!newRows.isEmpty()) {
                res = newRows.pop();
                calc.addSample(res);
                int count = calc.getCount();
                TableSample newS = new TableSample(
                        count,
                        res.getSampleCount(),
                        res.getStartTime(),
                        res.getThreadName(),
                        res.getSampleLabel(),
                        res.getTime(),
                        res.isSuccessful(),
                        res.getBytesAsLong(),
                        res.getSentBytes(),
                        res.getLatency(),
                        res.getConnectTime()
                        );
                model.addRow(newS);
            }
            if (res == null) {
                return;
            }
            updateTextFields(res);
            if (autoscroll.isSelected()) {
                table.scrollRectToVisible(table.getCellRect(table.getRowCount() - 1, 0, true));
            }
        }
    }

    public static class SampleSuccessFunctor extends Functor {
        public SampleSuccessFunctor(String methodName) {
            super(methodName);
        }

        @Override
        public Object invoke(Object pInvokee) {
            Boolean success = (Boolean) super.invoke(pInvokee);

            if (success != null) {
                if (success) {
                    return imageSuccess;
                } else {
                    return imageFailure;
                }
            } else {
                return null;
            }
        }
    }
}
