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

import eu.gity.jmeter.ddos.stairsddosthreadgrp.charting.AbstractGraphPanelChartElement;
import eu.gity.jmeter.ddos.stairsddosthreadgrp.charting.AbstractGraphRow;
import eu.gity.jmeter.ddos.stairsddosthreadgrp.charting.ChartSettings;
import org.apache.jorphan.gui.NumberRenderer;

import java.awt.*;
import java.io.Serializable;
import java.util.Iterator;
import java.util.Map.Entry;

public abstract class AbstractRowPlotter implements Serializable {

    private static final long serialVersionUID = 240L;

    private final static int labelSpacing = 5;
    protected ChartSettings chartSettings = null;
    protected NumberRenderer labelRenderer = null;
    protected Rectangle chartRect = null;
    protected long minXVal;
    protected long maxXVal;
    protected double minYVal;
    protected double maxYVal;
    // live values to paint rows
    protected double dxForDVal;
    protected double dyForDVal;
    protected double calcPointX = 0;
    protected double calcPointY = 0;
    protected int x, y;
    protected int prevX;
    protected int prevY;
    protected boolean allowMarkers = false;
    protected boolean mustDrawFirstZeroingLine;
    private Stroke lineStroke = null;

    public AbstractRowPlotter(ChartSettings chartSettings, NumberRenderer labelRenderer) {
        this.chartSettings = chartSettings;
        this.labelRenderer = labelRenderer;
    }

    public void setBoundsValues(Rectangle chartRect, long minXVal, long maxXVal, double minYVal, double maxYVal) {
        this.chartRect = chartRect;
        this.minXVal = minXVal;
        this.maxXVal = maxXVal;
        this.minYVal = minYVal;
        this.maxYVal = maxYVal;

        dxForDVal = (maxXVal <= minXVal) ? 0 : (double) chartRect.width / (maxXVal - minXVal);
        dyForDVal = (maxYVal <= minYVal) ? 0 : (double) chartRect.height / (maxYVal - minYVal);
    }

    /*
     * Check if the point (x,y) is contained in the chart area We check only
     * minX, maxX, and maxY to avoid flickering. We take max(chartRect.y, y) as
     * redering value This is done to prevent line out of range if new point is
     * added during chart paint.
     */
    protected boolean isChartPointValid(int xx, int yy) {
        boolean ret = true;

        // check x
        if (xx < chartRect.x || xx > chartRect.x + chartRect.width) {
            ret = false;
        } else // check y bellow x axis
            if (yy > chartRect.y + chartRect.height) {
                ret = false;
            }
        return ret;
    }

    // this method is responsible to maintain x, y, prevX, prevY
    public synchronized void paintRow(Graphics2D g2d, AbstractGraphRow row, Color color, double zoomFactor,
                                      int limitPointFactor) {
        Iterator<Entry<Long, AbstractGraphPanelChartElement>> it = row.iterator();
        Entry<Long, AbstractGraphPanelChartElement> element;
        Stroke olStroke;

        prevX = -1;
        prevY = chartRect.y + chartRect.height;

        mustDrawFirstZeroingLine = chartSettings.isDrawFinalZeroingLines();
        olStroke = g2d.getStroke();

        if (row.isDrawThickLines()) {
            g2d.setStroke(chartSettings.getThickStroke());
        } else {
            g2d.setStroke(getLineStroke());
        }

        g2d.setColor(color);

        while (it.hasNext()) {
            if (!row.isDrawOnChart()) {
                continue;
            }
            if (limitPointFactor == 1) {
                element = it.next();
                AbstractGraphPanelChartElement elt = element.getValue();

                // not compatible with factor != 1, ie cannot be used if limit
                // nb of point is selected.
                if (chartSettings.getHideNonRepValLimit() > 0) {
                    while (!elt.isPointRepresentative(chartSettings.getHideNonRepValLimit()) && it.hasNext()) {
                        element = it.next();
                        elt = element.getValue();
                    }
                    if (!elt.isPointRepresentative(chartSettings.getHideNonRepValLimit())) {
                        break;
                    }
                }
                calcPointX = element.getKey().doubleValue();
                calcPointY = elt.getValue();
            } else {
                calcPointX = 0;
                calcPointY = 0;
                int nbPointProcessed = 0;
                for (int i = 0; i < limitPointFactor; i++) {
                    if (it.hasNext()) {
                        element = it.next();
                        calcPointX = calcPointX + element.getKey().doubleValue();
                        calcPointY = calcPointY + element.getValue().getValue();
                        nbPointProcessed++;
                    }
                }
                calcPointX = calcPointX / (double) nbPointProcessed;
                calcPointY = calcPointY / (double) nbPointProcessed;
            }
            calcPointY = calcPointY * zoomFactor;

            x = chartRect.x + (int) ((calcPointX - minXVal) * dxForDVal);
            int yHeight = (int) ((calcPointY - minYVal) * dyForDVal);
            y = chartRect.y + chartRect.height - yHeight;

            // now x and y are set, we can call plotter
            processPoint(g2d, row.getGranulationValue());

            // set prevX, prevY
            if (isChartPointValid(x, y)) {
                if (allowMarkers) {
                    processMarker(g2d, row);
                }
                prevX = x;
                prevY = y;
            }
            if (row.isDrawValueLabel() && isChartPointValid(x, y) && y >= chartRect.y) {
                drawLabels(g2d, row, calcPointY);
            }
        }
        processFinalLines(row, g2d);

        x = 0;
        y = 0;
        prevX = -1;
        prevY = chartRect.y + chartRect.height;

        postPaintRow(row, g2d);
        g2d.setStroke(olStroke);
    }

    protected abstract void processPoint(Graphics2D g2d, int granulation);

    protected void postPaintRow(AbstractGraphRow row, Graphics2D g2d) {
    }

    private void processMarker(Graphics2D g2d, AbstractGraphRow row) {
        int radius = row.getMarkerSize();

        // check if forced via settings
        if (chartSettings.getChartMarkers() == ChartSettings.CHART_MARKERS_YES) {
            radius = Math.max(AbstractGraphRow.MARKER_SIZE_SMALL, row.getMarkerSize());
        }
        if (chartSettings.getChartMarkers() == ChartSettings.CHART_MARKERS_NO) {
            radius = AbstractGraphRow.MARKER_SIZE_NONE;
        }

        // draw markers
        if (radius != AbstractGraphRow.MARKER_SIZE_NONE && (y >= chartRect.y)) {
            if (isChartPointValid(x, y)) {
                g2d.fillOval(x - radius, y - radius, (radius) * 2, (radius) * 2);
            }
        }
    }

    private void drawLabels(Graphics2D g2d, AbstractGraphRow row, double yValue) {
        Color oldColor = g2d.getColor();
        g2d.setColor(Color.DARK_GRAY);
        Font oldFont = g2d.getFont();
        g2d.setFont(g2d.getFont().deriveFont(Font.BOLD));

        FontMetrics fm = g2d.getFontMetrics(g2d.getFont());

        labelRenderer.setValue(yValue);
        int labelSize = g2d.getFontMetrics(g2d.getFont()).stringWidth(labelRenderer.getText());
        // if close to end
        if (x + row.getMarkerSize() + labelSpacing + labelSize > chartRect.x + chartRect.width) {
            g2d.drawString(labelRenderer.getText(), x - row.getMarkerSize() - labelSpacing - labelSize,
                    y + fm.getAscent() / 2);
        } else {
            g2d.drawString(labelRenderer.getText(), x + row.getMarkerSize() + labelSpacing, y + fm.getAscent() / 2);
        }
        g2d.setFont(oldFont);
        g2d.setColor(oldColor);
    }

    private void processFinalLines(AbstractGraphRow row, Graphics2D g2d) {
        if (chartSettings.getLineWidth() == 0) {
            return;
        }
        Stroke oldStroke = null;

        if (row.isDrawLine() && chartSettings.isDrawFinalZeroingLines()) {
            if (row.isDrawThickLines()) {
                oldStroke = g2d.getStroke();
                g2d.setStroke(chartSettings.getThickStroke());
            }
            g2d.drawLine(prevX, Math.max(prevY, chartRect.y), (int) (prevX + dxForDVal),
                    chartRect.y + chartRect.height);
            if (row.isDrawThickLines()) {
                g2d.setStroke(oldStroke);
            }
        }
    }

    private Stroke getLineStroke() {
        if (chartSettings.getLineWidth() > 1) {
            lineStroke = new BasicStroke(chartSettings.getLineWidth(), BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND);
        } else {
            lineStroke = new BasicStroke(chartSettings.getLineWidth());
        }
        return lineStroke;
    }
}
