package kg.apc.charting.plotters;

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics2D;
import java.awt.Rectangle;
import java.awt.Stroke;
import java.io.Serializable;
import java.util.Iterator;
import java.util.Map.Entry;
import kg.apc.charting.AbstractGraphPanelChartElement;
import kg.apc.charting.AbstractGraphRow;
import kg.apc.charting.ChartSettings;
import org.apache.jorphan.gui.NumberRenderer;

public abstract class AbstractRowPlotter implements Serializable {

    private final static int labelSpacing = 5;
    private Stroke lineStroke = null;
    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;

    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 = (AbstractGraphPanelChartElement) 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 = (AbstractGraphPanelChartElement) 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 + ((AbstractGraphPanelChartElement) 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;
    }
}
