package eu.gity.jmeter.ddos.stairsddosthreadgrp.charting;

import eu.gity.jmeter.ddos.common.DDoSUtils;
import eu.gity.jmeter.ddos.stairsddosthreadgrp.charting.plotters.AbstractRowPlotter;
import eu.gity.jmeter.ddos.stairsddosthreadgrp.charting.plotters.BarRowPlotter;
import eu.gity.jmeter.ddos.stairsddosthreadgrp.charting.plotters.CSplineRowPlotter;
import eu.gity.jmeter.ddos.stairsddosthreadgrp.charting.plotters.LineRowPlotter;
import eu.gity.jmeter.ddos.stairsddosthreadgrp.gui.CustomNumberRenderer;
import org.apache.jorphan.gui.NumberRenderer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.imageio.ImageIO;
import javax.swing.*;
import javax.swing.border.BevelBorder;
import javax.swing.filechooser.FileNameExtensionFilter;
import java.awt.*;
import java.awt.datatransfer.Clipboard;
import java.awt.datatransfer.ClipboardOwner;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.Transferable;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseEvent;
import java.awt.geom.AffineTransform;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.Serializable;
import java.util.AbstractMap;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map.Entry;

public class GraphPanelChart extends JComponent implements ClipboardOwner {

    /*
     * Special type of graph were minY is forced to 0 and maxY is forced to 100
     * to display percentage charts (eg cpu monitoring)
     */
    public static final int CHART_PERCENTAGE = 0;
    public static final int CHART_DEFAULT = -1;
    private static final long serialVersionUID = 240L;
    //private static final String AD_TEXT = "gity.eu";
    private static final String AD_TEXT = "";
    private static final String NO_SAMPLES = "Waiting for samples...";
    private static final int spacing = 5;
    private static final int previewInset = 4;
    private static final Logger log = LoggerFactory.getLogger(GraphPanelChart.class);
    private static final Rectangle zeroRect = new Rectangle();
    private static final int gridLinesCount = 10;
    // Chart's gradient background end color
    private final static Color gradientColor = new Color(229, 236, 246);
    // Chart's Axis Color. For good results, use gradient color - (40, 40, 40)
    private final static Color axisColor = new Color(189, 196, 206);
    private final static int legendAdjust = 3;
    // gap in pixels relative to mouse pointer position
    private final static int hoverGap = 20;
    // cache management
    private static BufferedImage cache = null;
    private static int cacheWitdh, cacheHeight;
    private static int cacheOwner = -1;
    private static int uidGenerator = 0;
    // save file path. We remember last folder used.
    private static String savePath = null;
    // plotters
    BarRowPlotter barRowPlotter = null;
    LineRowPlotter lineRowPlotter = null;
    CSplineRowPlotter cSplineRowPlotter = null;
    AbstractRowPlotter currentPlotter = null;
    JPopupMenu popup = new JPopupMenu();
    private boolean cacheValid = false;
    private final int gpcId;
    private final Rectangle legendRect;
    private final Rectangle xAxisRect;
    private final Rectangle yAxisRect;
    private final Rectangle chartRect;
    private AbstractMap<String, AbstractGraphRow> rows;
    private double maxYVal;
    private double minYVal;
    private long maxXVal;
    private long minXVal;
    private long currentXVal;
    private NumberRenderer yAxisLabelRenderer;
    private NumberRenderer xAxisLabelRenderer;
    private long forcedMinX = -1;
    private int chartType = CHART_DEFAULT;
    // Message display in graphs. Used for perfmon error messages
    private String errorMessage = null;
    private final ChartSettings chartSettings = new ChartSettings();
    private final CustomNumberRenderer nbFormatter = new CustomNumberRenderer("#,#00.#", ' ');
    private boolean reSetColors = false;
    // hover info
    private JWindow hoverWindow;
    // force positionning between 2 clicks
    private boolean forceHoverPosition = true;
    private JTextField hoverLabel;
    private int xHoverInfo = -1;
    private int yHoverInfo = -1;
    private final HoverMotionListener motionListener = new HoverMotionListener();
    private String xAxisLabel = "X axis label";
    private String yAxisLabel = "Y axis label";
    private int precisionLabel = -1;
    private int limitPointFactor = 1;
    private boolean displayPrecision = false;
    private boolean isPreview = false;
    // relative time
    private long testStartTime = 0;
    // row zooming
    private final HashMap<String, Double> rowsZoomFactor = new HashMap<>();

    /**
     * Creates new chart object with default parameters
     */
    public GraphPanelChart(boolean allowCsvExport, boolean haveGUI) {
        gpcId = getNextId();
        setBackground(Color.white);
        setBorder(BorderFactory.createBevelBorder(BevelBorder.LOWERED, Color.lightGray, Color.darkGray));

        yAxisLabelRenderer = new CustomNumberRenderer("#,###.#", ' ');
        xAxisLabelRenderer = new CustomNumberRenderer("#,###.#", ' ');
        legendRect = new Rectangle();
        yAxisRect = new Rectangle();
        xAxisRect = new Rectangle();
        chartRect = new Rectangle();

        // no need to register anything in non GUI mode
        // second test required for unit test mode
        if (haveGUI && !GraphicsEnvironment.isHeadless()) {
            registerPopup(allowCsvExport);
            hoverLabel = new JTextField();
            hoverLabel.setEditable(false);
            hoverLabel.setOpaque(false);
            hoverLabel.setBorder(new BevelBorder(BevelBorder.RAISED));
            hoverLabel.setFont(new java.awt.Font("Tahoma", 0, 11));

            hoverWindow = new JWindow();
            hoverWindow.setBackground(gradientColor);
            hoverWindow.add(hoverLabel, BorderLayout.CENTER);
            registerHoverInfo();
        }
        barRowPlotter = new BarRowPlotter(chartSettings, yAxisLabelRenderer);
        lineRowPlotter = new LineRowPlotter(chartSettings, yAxisLabelRenderer);
        cSplineRowPlotter = new CSplineRowPlotter(chartSettings, yAxisLabelRenderer);
    }

    public GraphPanelChart(boolean haveGUI) {
        this(true, haveGUI);
    }

    public GraphPanelChart() {
        this(false);
    }

    private static synchronized int getNextId() {
        uidGenerator++;
        return uidGenerator;
    }

    public ChartSettings getChartSettings() {
        return chartSettings;
    }

    public void setReSetColors(boolean reSetColors) {
        this.reSetColors = reSetColors;
    }

    public void setDisplayPrecision(boolean displayPrecision) {
        this.displayPrecision = displayPrecision;
    }

    public void setxAxisLabel(String xAxisLabel) {
        this.xAxisLabel = xAxisLabel;
    }

    public void setYAxisLabel(String yAxisLabel) {
        this.yAxisLabel = yAxisLabel;
    }

    public void setPrecisionLabel(int precision) {
        this.precisionLabel = precision;
    }

    private void autoZoom_orig() {
        // row zooming
        if (!chartSettings.isExpendRows()) {
            return;
        }

        for (Entry<String, AbstractGraphRow> row : rows.entrySet()) {
            double[] minMax = row.getValue().getMinMaxY(chartSettings.getMaxPointPerRow());
            if (minMax[1] > 0) {
                double zoomFactor = 1;
                rowsZoomFactor.put(row.getKey(), zoomFactor);
                while (minMax[1] * zoomFactor <= maxYVal) {
                    rowsZoomFactor.put(row.getKey(), zoomFactor);
                    zoomFactor = zoomFactor * 10;
                }
            } else {
                rowsZoomFactor.put(row.getKey(), 1.0);
            }
        }
    }

    private String getXAxisLabel() {
        String label;
        if (!displayPrecision) {
            label = xAxisLabel;
        } else {
            long granularity;
            if (chartSettings.getMaxPointPerRow() <= 0) {
                granularity = precisionLabel;
            } else {
                granularity = precisionLabel * limitPointFactor;
            }

            long min = granularity / 60000;
            long sec = (granularity % 60000) / 1000;
            long ms = (granularity % 60000) % 1000;

            label = xAxisLabel + " (granularity:";

            if (min > 0) {
                label += " " + min + " min";
            }
            if (sec > 0 || (min > 0 && ms > 0)) {
                if (min > 0) {
                    label += ",";
                }
                label += " " + sec + " sec";
            }
            if (ms > 0) {
                if (sec > 0 || min > 0) {
                    label += ",";
                }
                label += " " + ms + " ms";
            }
            label += ")";
        }
        return label;
    }

    public void setIsPreview(boolean isPreview) {
        this.isPreview = isPreview;
        if (!isPreview) {
            this.setComponentPopupMenu(popup);
        } else {
            this.setComponentPopupMenu(null);
        }

    }

    public void setTestStartTime(long time) {
        testStartTime = time;
    }

    public boolean isModelContainsRow(AbstractGraphRow row) {
        return rows.containsKey(row.getLabel());
    }

    public void setChartType(int type) {
        chartType = type;
    }

    private boolean drawMessages(Graphics2D g) {
        if (errorMessage != null) {
            g.setColor(Color.RED);
            g.drawString(errorMessage,
                    g.getClipBounds().width / 2 - g.getFontMetrics(g.getFont()).stringWidth(errorMessage) / 2,
                    g.getClipBounds().height / 2);
            return true;
        }
        if (rows.isEmpty()) {
            g.setColor(Color.BLACK);
            g.drawString(NO_SAMPLES,
                    g.getClipBounds().width / 2 - g.getFontMetrics(g.getFont()).stringWidth(NO_SAMPLES) / 2,
                    g.getClipBounds().height / 2);
            return true;
        }
        return false;
    }

    private void getMinMaxDataValues() {
        maxXVal = 0L;
        maxYVal = 0L;
        minXVal = Long.MAX_VALUE;
        minYVal = Double.MAX_VALUE;

        Iterator<Entry<String, AbstractGraphRow>> it = rows.entrySet().iterator();
        Entry<String, AbstractGraphRow> row;
        AbstractGraphRow rowValue = null;
        int barValue = 0;
        while (it.hasNext()) {
            row = it.next();
            rowValue = row.getValue();
            rowValue.setExcludeOutOfRangeValues(chartSettings.isPreventXAxisOverScaling());

            if (!rowValue.isDrawOnChart()) {
                continue;
            }
            if (rowValue.getMaxX() > maxXVal) {
                maxXVal = rowValue.getMaxX();
            }
            if (rowValue.getMinX() < minXVal) {
                minXVal = rowValue.getMinX();
            }
            double[] rowMinMaxY = rowValue.getMinMaxY(chartSettings.getMaxPointPerRow());

            if (rowMinMaxY[1] > maxYVal) {
                maxYVal = rowMinMaxY[1];
            }
            if (rowMinMaxY[0] < minYVal) {
                // we draw only positives values
                minYVal = rowMinMaxY[0] >= 0 ? rowMinMaxY[0] : 0;
            }
            if (rowValue.isDrawBar()) {
                barValue = rowValue.getGranulationValue();
            }
        }
        if (barValue > 0) {
            maxXVal += barValue;
            // find nice X steps
            double barPerSquare = (double) (maxXVal - minXVal) / (barValue * gridLinesCount);
            double step = Math.floor(barPerSquare) + 1;
            maxXVal = (long) (minXVal + step * barValue * gridLinesCount);
        }
        // maxYVal *= 1 + (double) 1 / (double) gridLinesCount;
        if (forcedMinX >= 0L) {
            minXVal = forcedMinX;
        }
        // prevent X and Y axis not initialized in case of no row displayed
        if (maxXVal == 0L || maxYVal == 0L || minXVal == Long.MAX_VALUE || minYVal == Double.MAX_VALUE) {
            minYVal = 0;
            maxYVal = 10;
            // we take last known row to get x range
            if (rowValue != null) {
                maxXVal = rowValue.getMaxX();
                minXVal = rowValue.getMinX();
            }
        } else if (chartSettings.isConfigOptimizeYAxis()) {
            computeChartSteps();
        } else {
            minYVal = 0;
        }
        if (chartSettings.getForcedMaxY() > 0) {
            maxYVal = Math.max(chartSettings.getForcedMaxY(), minYVal + 1);
        }
    }

    /**
     * compute minY and step value to have better readable charts
     */
    private void computeChartSteps() {
        // if special type
        if (chartType == GraphPanelChart.CHART_PERCENTAGE) {
            minYVal = 0;
            maxYVal = 100;
            return;
        }

        // try to find the best range...
        // first, avoid special cases where maxY equal or close to minY
        if (maxYVal - minYVal < 0.1) {
            maxYVal = minYVal + 1;
        }

        // real step
        double step = (maxYVal - minYVal) / gridLinesCount;

        int pow = -1;
        double factor = -1;
        boolean found = false;
        double testStep;

        // find a step close to the real one
        while (!found) {
            pow++;
            for (double f = 0; f < 10; f++) {
                testStep = Math.pow(10, pow) * f;
                if (testStep >= step) {
                    factor = f;
                    found = true;
                    break;
                }
            }
        }

        // first proposal
        double foundStep = Math.pow(10, pow) * factor;

        // we shift to the closest lower minval to align with the step
        minYVal = minYVal - minYVal % foundStep;

        // check if step is still good with minY trimmed. If not, use next
        // factor.
        if (minYVal + foundStep * gridLinesCount < maxYVal) {
            foundStep = Math.pow(10, pow) * (factor + (pow > 0 ? 0.5 : 1));
        }

        // last visual optimization: find the optimal minYVal
        double trim = 10;

        while ((minYVal - minYVal % trim) + foundStep * gridLinesCount >= maxYVal && minYVal > 0) {
            minYVal = minYVal - minYVal % trim;
            trim = trim * 10;
        }

        // final calculation
        maxYVal = minYVal + foundStep * gridLinesCount;
    }

    private void setDefaultDimensions(Graphics g) {
        chartRect.setBounds(spacing, spacing, g.getClipBounds().width - spacing * 2,
                g.getClipBounds().height - spacing * 2);
        legendRect.setBounds(zeroRect);
        xAxisRect.setBounds(zeroRect);
        yAxisRect.setBounds(zeroRect);
    }

    private int getYLabelsMaxWidth(FontMetrics fm) {
        int ret = 0;
        for (int i = 0; i <= gridLinesCount; i++) {
            yAxisLabelRenderer.setValue((minYVal * gridLinesCount + i * (maxYVal - minYVal)) / gridLinesCount);
            int current = fm.stringWidth(yAxisLabelRenderer.getText());
            if (current > ret) {
                ret = current;
            }
        }
        return ret;
    }

    private void calculateYAxisDimensions(Graphics g) {
        FontMetrics fm = g.getFontMetrics(g.getFont());
        int axisWidth = getYLabelsMaxWidth(fm) + spacing * 3 + fm.getHeight();
        yAxisRect.setBounds(chartRect.x, chartRect.y, axisWidth, chartRect.height);
        if (!isPreview) {
            chartRect.setBounds(chartRect.x + axisWidth, chartRect.y, chartRect.width - axisWidth, chartRect.height);
        } else {
            chartRect.setBounds(chartRect.x + previewInset, chartRect.y, chartRect.width, chartRect.height);
        }
    }

    private void calculateXAxisDimensions(Graphics g) {
        FontMetrics fm = g.getFontMetrics(g.getFont());
        // we need to handle this and make Y axis wider
        int axisHeight;
        if (!isPreview) {
            axisHeight = 2 * fm.getHeight() + spacing;// labels plus name
        } else {
            axisHeight = spacing;
        }
        xAxisLabelRenderer.setValue(maxXVal);
        int axisEndSpace = fm.stringWidth(xAxisLabelRenderer.getText()) / 2;
        xAxisRect.setBounds(chartRect.x, chartRect.y + chartRect.height - axisHeight, chartRect.width, axisHeight);
        if (!isPreview) {
            chartRect.setBounds(chartRect.x, chartRect.y, chartRect.width - axisEndSpace,
                    chartRect.height - axisHeight);
        } else {
            chartRect.setBounds(chartRect.x, chartRect.y, chartRect.width - 2 * previewInset,
                    chartRect.height - (previewInset + 1)); // +1 because layout
            // take one pixel
        }
        yAxisRect.setBounds(yAxisRect.x, yAxisRect.y, yAxisRect.width, chartRect.height);
    }

    public void invalidateCache() {
        cacheValid = false;
    }

    @Override
    public void paintComponent(Graphics g) {
        super.paintComponent(g);

        int witdh = this.getWidth();
        int height = this.getHeight();

        if (cacheHeight != height || cacheWitdh != witdh || gpcId != cacheOwner) {
            cacheValid = false;
        }
        if (!cacheValid) {
            if (cache == null || cacheHeight != height || cacheWitdh != witdh) {
                cache = new BufferedImage(witdh, height, BufferedImage.TYPE_INT_ARGB);
            }
            Graphics2D g2d = cache.createGraphics();
            g2d.setClip(0, 0, witdh, height);
            g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
            drawPanel(g2d);
            cacheValid = true;
            cacheHeight = height;
            cacheWitdh = witdh;
            cacheOwner = gpcId;
        }
        g.drawImage(cache, 0, 0, this);
    }

    private void drawPanel(Graphics2D g) {
        drawPanel(g, true);
    }

    private void drawPanel(Graphics2D g, boolean drawHoverInfo) {
        g.setColor(Color.white);

        if (chartSettings.isDrawGradient()) {
            GradientPaint gdp = new GradientPaint(0, 0, Color.white, 0, g.getClipBounds().height, gradientColor);
            g.setPaint(gdp);
        }
        g.fillRect(0, 0, g.getClipBounds().width, g.getClipBounds().height);
        paintAd(g);

        if (drawMessages(g)) {
            return;
        }
        setDefaultDimensions(g);

        getMinMaxDataValues();
        autoZoom_orig();

        paintLegend(g);
        calculateYAxisDimensions(g);
        calculateXAxisDimensions(g);
        paintYAxis(g);
        paintXAxis(g);
        paintChart(g);

        if (drawHoverInfo) {
            showHoverInfo();
        }
    }

    private String getNiceNumber(int nb) {
        nbFormatter.setValue(nb);
        return nbFormatter.getText();
    }

    private void paintLegend(Graphics g) {
        FontMetrics fm = g.getFontMetrics(g.getFont());
        int rectH = fm.getHeight();

        Iterator<Entry<String, AbstractGraphRow>> it = rows.entrySet().iterator();
        Entry<String, AbstractGraphRow> row;
        int currentX = chartRect.x;
        int currentY = chartRect.y;
        int legendHeight = it.hasNext() ? rectH + spacing : 0;

        ColorsDispatcher colors = null;

        if (reSetColors) {
            colors = ColorsDispatcherFactory.getColorsDispatcher();
        }
        while (it.hasNext()) {
            row = it.next();
            // Color color = reSetColors ? colors.getNextColor() :
            // row.getValue().getColor();
            Color color = row.getValue().getColor() != null ? row.getValue().getColor() : colors.getNextColor();

            if (!row.getValue().isShowInLegend() || !row.getValue().isDrawOnChart()) {
                continue;
            }
            String rowLabel = row.getKey();
            if (chartSettings.isExpendRows() && rowsZoomFactor.get(row.getKey()) != null) {
                double zoomFactor = rowsZoomFactor.get(row.getKey());
                if (zoomFactor != 1) {
                    if (zoomFactor > 1) {
                        int iZoomFactor = (int) zoomFactor;
                        rowLabel = rowLabel + " (x" + getNiceNumber(iZoomFactor) + ")";
                    } else {
                        rowLabel = rowLabel + " (x" + zoomFactor + ")";
                    }
                }
            }
            // wrap row if overflowed
            if (currentX + rectH + spacing / 2 + fm.stringWidth(rowLabel) > g.getClipBounds().width) {
                currentY += rectH + spacing / 2;
                legendHeight += rectH + spacing / 2;
                currentX = chartRect.x;
            }

            // draw legend color box
            g.setColor(color);
            Composite oldComposite = null;
            boolean isBarChart = row.getValue().isDrawBar()
                    && chartSettings.getChartType() == ChartSettings.CHART_TYPE_DEFAULT
                    || chartSettings.getChartType() == ChartSettings.CHART_TYPE_BAR;
            if (isBarChart) {
                oldComposite = ((Graphics2D) g).getComposite();
                ((Graphics2D) g).setComposite(chartSettings.getBarComposite());
            }
            Rectangle r = new Rectangle(currentX + legendAdjust + 1, currentY + legendAdjust + 1,
                    rectH - 2 * legendAdjust, rectH - 2 * legendAdjust);
            ((Graphics2D) g).fill(r);
            row.getValue().setLegendColorBox(r);

            // g.fillRect(currentX+legendAdjust+1, currentY+legendAdjust+1,
            // rectW-2*legendAdjust, rectH-2*legendAdjust);
            if (isBarChart) {
                ((Graphics2D) g).setComposite(oldComposite);
            }
            g.setColor(color.darker());
            g.drawRect(currentX + legendAdjust + 1, currentY + legendAdjust + 1, rectH - 2 * legendAdjust,
                    rectH - 2 * legendAdjust);

            g.setColor(Color.BLACK);

            // draw legend item label
            currentX += rectH + spacing / 2;
            g.drawString(rowLabel, currentX, (int) (currentY + rectH * 0.9));
            currentX += fm.stringWidth(rowLabel) + spacing;
        }
        legendRect.setBounds(chartRect.x, chartRect.y, chartRect.width, legendHeight);
        chartRect.setBounds(chartRect.x, chartRect.y + legendHeight + spacing, chartRect.width,
                chartRect.height - legendHeight - spacing);
    }

    private void paintYAxis(Graphics g) {
        FontMetrics fm = g.getFontMetrics(g.getFont());

        String valueLabel;
        int labelXPos;
        int gridLineY;

        // shift 2nd and more lines
        int shift = 0;
        // for strokes swapping
        Stroke oldStroke = ((Graphics2D) g).getStroke();

        // draw markers
        g.setColor(axisColor);
        for (int n = 0; n <= gridLinesCount; n++) {
            gridLineY = chartRect.y + (int) ((gridLinesCount - n) * (double) chartRect.height / gridLinesCount);
            g.drawLine(chartRect.x - 3, gridLineY, chartRect.x + 3, gridLineY);
        }
        for (int n = 0; n <= gridLinesCount; n++) {
            // draw 2nd and more axis dashed and shifted
            if (n != 0) {
                ((Graphics2D) g).setStroke(chartSettings.getDashStroke());
                shift = 7;
            }
            gridLineY = chartRect.y + (int) ((gridLinesCount - n) * (double) chartRect.height / gridLinesCount);

            // draw grid line with tick
            g.setColor(axisColor);
            g.drawLine(chartRect.x + shift, gridLineY, chartRect.x + chartRect.width, gridLineY);
            g.setColor(Color.black);
            // draw label
            if (!isPreview) {
                yAxisLabelRenderer.setValue((minYVal * gridLinesCount + n * (maxYVal - minYVal)) / gridLinesCount);
                valueLabel = yAxisLabelRenderer.getText();
                labelXPos = yAxisRect.x + yAxisRect.width - fm.stringWidth(valueLabel) - spacing - spacing / 2;
                g.drawString(valueLabel, labelXPos, gridLineY + fm.getAscent() / 2);
            }
        }

        if (!isPreview) {
            Font oldFont = g.getFont();
            g.setFont(g.getFont().deriveFont(Font.ITALIC));

            // Create a rotation transformation for the font.
            AffineTransform fontAT = new AffineTransform();
            int delta = g.getFontMetrics(g.getFont()).stringWidth(yAxisLabel);
            fontAT.rotate(-Math.PI / 2d);
            g.setFont(g.getFont().deriveFont(fontAT));
            g.drawString(yAxisLabel, yAxisRect.x + 15, yAxisRect.y + yAxisRect.height / 2 + delta / 2);
            g.setFont(oldFont);
        }
        // restore stroke
        ((Graphics2D) g).setStroke(oldStroke);
    }

    private void paintXAxis(Graphics g) {
        FontMetrics fm = g.getFontMetrics(g.getFont());

        String valueLabel;
        int labelXPos;
        int gridLineX;

        // shift 2nd and more lines
        int shift = 0;
        // for strokes swapping
        Stroke oldStroke = ((Graphics2D) g).getStroke();

        g.setColor(axisColor);
        // draw markers
        for (int n = 0; n <= gridLinesCount; n++) {
            gridLineX = chartRect.x + (int) (n * ((double) chartRect.width / gridLinesCount));
            g.drawLine(gridLineX, chartRect.y + chartRect.height - 3, gridLineX, chartRect.y + chartRect.height + 3);
        }

        for (int n = 0; n <= gridLinesCount; n++) {
            // draw 2nd and more axis dashed and shifted
            if (n != 0) {
                ((Graphics2D) g).setStroke(chartSettings.getDashStroke());
                shift = 7;
            }

            gridLineX = chartRect.x + (int) (n * ((double) chartRect.width / gridLinesCount));

            // draw grid line with tick
            g.setColor(axisColor);
            g.drawLine(gridLineX, chartRect.y + chartRect.height - shift, gridLineX, chartRect.y);
            g.setColor(Color.black);

            // draw label - keep decimal precision if range is too small
            double labelValue = minXVal + n * (double) (maxXVal - minXVal) / gridLinesCount;
            if ((maxXVal - minXVal < 2 * gridLinesCount) && (minXVal != maxXVal)) {
                xAxisLabelRenderer.setValue(labelValue);
            } else {
                xAxisLabelRenderer.setValue((long) labelValue);
            }

            valueLabel = xAxisLabelRenderer.getText();
            labelXPos = gridLineX - fm.stringWidth(valueLabel) / 2;
            g.drawString(valueLabel, labelXPos, xAxisRect.y + fm.getAscent() + spacing);
        }

        Font oldFont = g.getFont();
        g.setFont(g.getFont().deriveFont(Font.ITALIC));

        // axis label
        g.drawString(getXAxisLabel(),
                chartRect.x + chartRect.width / 2 - g.getFontMetrics(g.getFont()).stringWidth(getXAxisLabel()) / 2,
                xAxisRect.y + 2 * fm.getAscent() + spacing + 3);

        g.setFont(oldFont);

        // restore stroke
        ((Graphics2D) g).setStroke(oldStroke);

        if (chartSettings.isDrawCurrentX()) {
            gridLineX = chartRect.x + (int) ((currentXVal - minXVal) * (double) chartRect.width / (maxXVal - minXVal));
            g.setColor(Color.GRAY);
            g.drawLine(gridLineX, chartRect.y, gridLineX, chartRect.y + chartRect.height);
            g.setColor(Color.black);
        }
    }

    private void paintChart(Graphics g) {
        g.setColor(Color.yellow);
        Iterator<Entry<String, AbstractGraphRow>> it;

        ColorsDispatcher dispatcher = null;

        if (reSetColors) {
            dispatcher = ColorsDispatcherFactory.getColorsDispatcher();
        }
        // first we get the aggregate point factor if maxpoint is > 0;
        limitPointFactor = 1;
        if (chartSettings.getMaxPointPerRow() > 0) {
            it = rows.entrySet().iterator();
            while (it.hasNext()) {
                Entry<String, AbstractGraphRow> row = it.next();
                int rowFactor = (int) Math.floor(row.getValue().size() / (double) chartSettings.getMaxPointPerRow())
                        + 1;
                if (rowFactor > limitPointFactor) {
                    limitPointFactor = rowFactor;
                }
            }
        }

        // paint rows in 2 phases. Raws with draw label are drawn after to have
        // label on top
        it = rows.entrySet().iterator();
        paintRows(g, dispatcher, it, false);
        it = rows.entrySet().iterator();
        paintRows(g, dispatcher, it, true);
    }

    private void paintRows(Graphics g, ColorsDispatcher dispatcher, Iterator<Entry<String, AbstractGraphRow>> it,
                           boolean rowsWithLabel) {
        while (it.hasNext()) {
            Entry<String, AbstractGraphRow> row = it.next();
            if (row.getValue().isDrawOnChart() && row.getValue().isDrawValueLabel() == rowsWithLabel) {
                // eto Color color = reSetColors ? dispatcher.getNextColor() :
                // row.getValue().getColor();
                Color color = row.getValue().getColor() != null ? row.getValue().getColor() : dispatcher.getNextColor();
                paintRow(g, row.getValue(), row.getKey(), color);
            }
        }
    }

    private void paintRow(Graphics g, AbstractGraphRow row, String rowLabel, Color color) {

        if (row.isDrawLine() && chartSettings.getChartType() == ChartSettings.CHART_TYPE_DEFAULT
                || chartSettings.getChartType() == ChartSettings.CHART_TYPE_LINE) {
            currentPlotter = lineRowPlotter;
        } else if (row.isDrawBar() && chartSettings.getChartType() == ChartSettings.CHART_TYPE_DEFAULT
                || chartSettings.getChartType() == ChartSettings.CHART_TYPE_BAR) {
            currentPlotter = barRowPlotter;
        } else if (row.isDrawSpline() && chartSettings.getChartType() == ChartSettings.CHART_TYPE_DEFAULT
                || chartSettings.getChartType() == ChartSettings.CHART_TYPE_CSPLINE) {
            currentPlotter = cSplineRowPlotter;
        }

        if (currentPlotter != null) {
            double zoomFactor = 1;
            if (chartSettings.isExpendRows() && rowsZoomFactor.get(rowLabel) != null) {
                zoomFactor = rowsZoomFactor.get(rowLabel);
            }
            currentPlotter.setBoundsValues(chartRect, minXVal, maxXVal, minYVal, maxYVal);
            currentPlotter.paintRow((Graphics2D) g, row, color, zoomFactor, limitPointFactor);
        }
    }

    public void setRows(AbstractMap<String, AbstractGraphRow> aRows) {
        rows = aRows;
    }

    /**
     * @param yAxisLabelRenderer the yAxisLabelRenderer to set
     */
    public void setyAxisLabelRenderer(NumberRenderer yAxisLabelRenderer) {
        this.yAxisLabelRenderer = yAxisLabelRenderer;
    }

    /**
     * @param xAxisLabelRenderer the xAxisLabelRenderer to set
     */
    public void setxAxisLabelRenderer(NumberRenderer xAxisLabelRenderer) {
        this.xAxisLabelRenderer = xAxisLabelRenderer;
    }

    /**
     * @param currentX the currentX to set
     */
    public void setCurrentX(long currentX) {
        this.currentXVal = currentX;
    }

    public void setForcedMinX(long minX) {
        forcedMinX = minX;
    }

    // Paint the add the same color of the axis but with transparency
    private void paintAd(Graphics2D g) {
        Font oldFont = g.getFont();
        g.setFont(g.getFont().deriveFont(10F));
        g.setColor(axisColor);
        Composite oldComposite = g.getComposite();
        g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.7f));

        g.drawString(AD_TEXT, g.getClipBounds().width - g.getFontMetrics().stringWidth(AD_TEXT) - spacing,
                g.getFontMetrics().getHeight() - spacing + 1);
        g.setComposite(oldComposite);
        g.setFont(oldFont);
    }

    /*
     * Clear error messages
     */
    public void clearErrorMessage() {
        errorMessage = null;
    }

    /*
     * Set error message if not null and not empty
     *
     * @param msg the error message to set
     */
    public void setErrorMessage(String msg) {
        if (msg != null && msg.trim().length() > 0) {
            errorMessage = msg;
        }
    }

    // Adding a popup menu to copy image in clipboard
    @Override
    public void lostOwnership(Clipboard clipboard, Transferable contents) {
        // do nothing
    }

    private Image getImage() {
        return getBufferedImage(this.getWidth(), this.getHeight());
    }

    private BufferedImage getBufferedImage(int w, int h) {
        BufferedImage image = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
        Graphics2D g2 = image.createGraphics();
        g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
        g2.setClip(0, 0, w, h);
        drawPanel(g2, false);

        return image;
    }

    /**
     * Thanks to stephane.hoblingre
     */
    private void registerPopup(boolean allowCsvExport) {
        this.setComponentPopupMenu(popup);
        String jmeterHomeDir = DDoSUtils.getInstance().getJmeterHomeDir();
        JMenuItem itemCopy = new JMenuItem("Copy Image to Clipboard");
        itemCopy.setIcon(new ImageIcon(jmeterHomeDir + "/lib/ext/ddos/copy.png"));
        JMenuItem itemSave = new JMenuItem("Save Image as...");
        itemSave.setIcon(new ImageIcon(jmeterHomeDir + "/lib/ext/ddos/save.png"));
        JMenuItem itemExport = new JMenuItem("Export to CSV...");
        itemExport.setIcon(new ImageIcon(jmeterHomeDir + "/lib/ext/ddos/export.png"));
        itemCopy.addActionListener(new CopyAction());
        itemSave.addActionListener(new SaveAction());
        itemExport.addActionListener(new CsvExportAction());
        popup.add(itemCopy);
        popup.add(itemSave);
        if (allowCsvExport) {
            popup.addSeparator();
            popup.add(itemExport);
        }
    }

    private void registerHoverInfo() {
        addMouseListener(new java.awt.event.MouseAdapter() {

            @Override
            public void mousePressed(java.awt.event.MouseEvent evt) {
                if (!isPreview && evt.getButton() == MouseEvent.BUTTON1) {
                    forceHoverPosition = true;
                    addMouseMotionListener(motionListener);
                    chartMouseMoved(evt);
                }
            }

            @Override
            public void mouseReleased(java.awt.event.MouseEvent evt) {
                if (!isPreview) {
                    hideHoverInfo();
                    removeMouseMotionListener(motionListener);
                }
            }

            @Override
            public void mouseExited(java.awt.event.MouseEvent evt) {
                if (!isPreview) {
                    hideHoverInfo();
                }
            }
        });
    }

    private void hideHoverInfo() {
        hoverWindow.setVisible(false);
        xHoverInfo = -1;
        yHoverInfo = -1;
    }

    private synchronized void showHoverInfo() {

        if (isPreview || chartRect.width == 0 || chartRect.height == 0 || xHoverInfo == -1 || yHoverInfo == -1) {
            return;
        }

        long realX = minXVal + (maxXVal - minXVal) * (xHoverInfo - chartRect.x) / chartRect.width;
        double realY = minYVal + (maxYVal - minYVal) * (chartRect.height - yHoverInfo + chartRect.y) / chartRect.height;
        xAxisLabelRenderer.setValue(realX);
        yAxisLabelRenderer.setValue(realY);

        String hoverInfo = "(" + xAxisLabelRenderer.getText() + " ; " + yAxisLabelRenderer.getText() + ")";

        hoverLabel.setText(hoverInfo);

        int labelWidth = hoverLabel.getPreferredSize().width + 5;
        int labelHeight = hoverLabel.getPreferredSize().height;

        if (hoverWindow.getWidth() < labelWidth || hoverWindow.getHeight() < labelHeight) {
            hoverWindow.setSize(labelWidth, labelHeight);
        }

        Point mousePos = MouseInfo.getPointerInfo().getLocation();

        int hoverWindowX = mousePos.x + hoverGap;
        int hoverWindowY = mousePos.y + hoverGap;

        // we move window only if far from pointer to limit cpu
        double deltaX = Math.abs(hoverWindow.getLocation().getX() - hoverWindowX);
        double deltaY = Math.abs(hoverWindow.getLocation().getY() - hoverWindowY);
        if (forceHoverPosition || deltaX >= hoverGap || deltaY >= hoverGap) {
            // prevent out of screen
            int correctedX = Math.min(hoverWindowX,
                    Toolkit.getDefaultToolkit().getScreenSize().width - hoverWindow.getSize().width);
            hoverWindow.setLocation(correctedX, hoverWindowY);
            forceHoverPosition = false;
        }
    }

    private void chartMouseMoved(java.awt.event.MouseEvent evt) {
        int x = evt.getX();
        int y = evt.getY();

        if (x >= chartRect.x && x <= (chartRect.x + chartRect.width) && y >= chartRect.y
                && y <= (chartRect.y + chartRect.height)) {
            xHoverInfo = x;
            yHoverInfo = y;
            showHoverInfo();
            hoverWindow.setVisible(true);
        } else {
            hoverWindow.setVisible(false);
            xHoverInfo = -1;
            yHoverInfo = -1;
        }
    }

    public void setUseRelativeTime(boolean selected) {
        chartSettings.setUseRelativeTime(selected);
        if (selected) {
            setxAxisLabelRenderer(new DateTimeRenderer(DateTimeRenderer.HHMMSS, testStartTime));
        } else {
            setxAxisLabelRenderer(new DateTimeRenderer(DateTimeRenderer.HHMMSS));
        }
    }

    public void saveGraphToPNG(File file, int w, int h) throws IOException {
        log.info("Saving PNG to " + file.getAbsolutePath());
        FileOutputStream fos = new FileOutputStream(file);
        ImageIO.write(getBufferedImage(w, h), "png", fos);
        fos.flush();
        fos.close();
    }

    public void saveGraphToCSV(File file) throws IOException {
        log.info("Saving CSV to " + file.getAbsolutePath());
        GraphModelToCsvExporter exporter = new GraphModelToCsvExporter(rows, file,
                chartSettings.getConfigCsvSeparator(), xAxisLabel, xAxisLabelRenderer,
                chartSettings.getHideNonRepValLimit());
        exporter.writeCsvFile();
    }

    private class CopyAction implements ActionListener {

        @Override
        public void actionPerformed(final ActionEvent e) {
            Clipboard clipboard = getToolkit().getSystemClipboard();
            Transferable transferable = new Transferable() {

                @Override
                public Object getTransferData(DataFlavor flavor) {
                    if (isDataFlavorSupported(flavor)) {
                        return getImage();
                    }
                    return null;
                }

                @Override
                public DataFlavor[] getTransferDataFlavors() {
                    return new DataFlavor[]{DataFlavor.imageFlavor};
                }

                @Override
                public boolean isDataFlavorSupported(DataFlavor flavor) {
                    return DataFlavor.imageFlavor.equals(flavor);
                }
            };
            clipboard.setContents(transferable, GraphPanelChart.this);
        }
    }

    private class SaveAction implements ActionListener {

        @Override
        public void actionPerformed(final ActionEvent e) {
            JFileChooser chooser = savePath != null ? new JFileChooser(new File(savePath))
                    : new JFileChooser(new File("."));
            chooser.setFileFilter(new FileNameExtensionFilter("PNG Images", "png"));

            int returnVal = chooser.showSaveDialog(GraphPanelChart.this);
            if (returnVal == JFileChooser.APPROVE_OPTION) {
                File file = chooser.getSelectedFile();
                if (!file.getAbsolutePath().toUpperCase().endsWith(".PNG")) {
                    file = new File(file.getAbsolutePath() + ".png");
                }
                savePath = file.getParent();
                boolean doSave = true;
                if (file.exists()) {
                    int choice = JOptionPane.showConfirmDialog(GraphPanelChart.this,
                            "Do you want to overwrite " + file.getAbsolutePath() + "?", "Save Image as",
                            JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE);
                    doSave = (choice == JOptionPane.YES_OPTION);
                }

                if (doSave) {
                    try {
                        saveGraphToPNG(file, getWidth(), getHeight());
                    } catch (IOException ex) {
                        JOptionPane.showConfirmDialog(GraphPanelChart.this,
                                "Impossible to write the image to the file:\n" + ex.getMessage(), "Save Image as",
                                JOptionPane.DEFAULT_OPTION, JOptionPane.ERROR_MESSAGE);
                    }
                }
            }
        }
    }

    private class CsvExportAction implements ActionListener {

        @Override
        public void actionPerformed(final ActionEvent e) {
            JFileChooser chooser = savePath != null ? new JFileChooser(new File(savePath))
                    : new JFileChooser(new File("."));
            chooser.setFileFilter(new FileNameExtensionFilter("CSV Files", "csv"));

            int returnVal = chooser.showSaveDialog(GraphPanelChart.this);
            if (returnVal == JFileChooser.APPROVE_OPTION) {
                File file = chooser.getSelectedFile();
                if (!file.getAbsolutePath().toUpperCase().endsWith(".CSV")) {
                    file = new File(file.getAbsolutePath() + ".csv");
                }
                savePath = file.getParent();
                boolean doSave = true;
                if (file.exists()) {
                    int choice = JOptionPane.showConfirmDialog(GraphPanelChart.this,
                            "Do you want to overwrite " + file.getAbsolutePath() + "?", "Export to CSV File",
                            JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE);
                    doSave = (choice == JOptionPane.YES_OPTION);
                }
                if (doSave) {
                    try {
                        saveGraphToCSV(file);
                    } catch (IOException ex) {
                        JOptionPane.showConfirmDialog(GraphPanelChart.this,
                                "Impossible to write the CSV file:\n" + ex.getMessage(), "Export to CSV File",
                                JOptionPane.DEFAULT_OPTION, JOptionPane.ERROR_MESSAGE);
                    }
                }
            }
        }
    }

    private class HoverMotionListener extends java.awt.event.MouseMotionAdapter implements Serializable {
        private static final long serialVersionUID = 240L;

        @Override
        public void mouseDragged(java.awt.event.MouseEvent evt) {
            chartMouseMoved(evt);
        }
    }
}
