package com.srbenoit.ui;

import java.awt.Color;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Insets;
import javax.swing.JPanel;
import com.srbenoit.math.Histogram;

/**
 * A panel that displays a histogram of data.
 */
public class HistogramPanel extends JPanel {

    /** version number for serialization */
    private static final long serialVersionUID = -1112693135693520783L;

    /** the histogram data */
    private final transient Histogram data;

    /** the labels for the planes of the histogram */
    private final transient String[] labels;

    /** the line color */
    private final transient Color lines;

    /** the point color */
    private final transient Color points;

    /** the line accent color */
    private final transient Color lines2;

    /** the point accent color */
    private final transient Color points2;

    /** the grid color */
    private final transient Color grid;

    /** the background color */
    private final transient Color back;

    /** a boldface font */
    protected final transient Font font;

    /** the phase of the color switch */
    private transient boolean phase;

    /**
     * Constructs a new <code>HistogramPanel</code>.
     *
     * @param  hist             the histogram to display
     * @param  histogramLabels  the labels for the histogram planes
     * @param  backgroundColor  the background color
     * @param  linesColor       the lines color
     * @param  pointsColor      the points color
     * @param  gridColor        the grid color
     * @param  backColor        the background color
     */
    public HistogramPanel(final Histogram hist, final String[] histogramLabels,
        final Color backgroundColor, final Color linesColor, final Color pointsColor,
        final Color gridColor, final Color backColor) {

        super();

        Insets insets;
        int width;

        if (histogramLabels.length != hist.getNumPlanes()) {
            throw new IllegalArgumentException("Label length mismatch");
        }

        this.data = hist;
        this.labels = histogramLabels.clone();
        this.lines = linesColor;
        this.lines2 = new Color((int) (linesColor.getRed() * 0.8),
                (int) (linesColor.getGreen() * 0.8), (int) (linesColor.getBlue() * 0.8));
        this.points = pointsColor;
        this.points2 = new Color((int) (pointsColor.getRed() * 0.8),
                (int) (pointsColor.getGreen() * 0.8), (int) (pointsColor.getBlue() * 0.8));
        this.grid = gridColor;
        this.back = backColor;

        this.font = getFont().deriveFont(Font.BOLD, 11.0f);
        setFont(this.font);

        setBackground(backgroundColor);

        // Compute the preferred width
        insets = getInsets();
        width = insets.left + insets.right + 12 + hist.getNumTimes();
        setPreferredSize(new Dimension(width, 50 * hist.getNumPlanes()));
    }

    /**
     * Gets the histogram that this panel is displaying.
     *
     * @return  the histogram.
     */
    public Histogram getData() {

        return this.data;
    }

    /**
     * Registers a tick from the owning panel, which shifts the histogram to the left one notch, and
     * initializes the newly opened slot to a particular value.
     */
    public void tick() {

        this.data.shiftHigher();
        this.phase ^= true; // NOT operation
        invalidate();
        repaint();
    }

    /**
     * Draws an activity histogram.
     *
     * @param  grx  the <code>Graphics</code> to which to draw
     */
    @Override public void paintComponent(final Graphics grx) {

        super.paintComponent(grx);

        if (isEnabled()) {

            synchronized (this.data) {
                paintContents(grx);
            }
        }
    }

    /**
     * Draws an activity histogram.
     *
     * @param  grx  the <code>Graphics</code> to which to draw
     */
    private void paintContents(final Graphics grx) {

        Insets insets;
        int height;
        int heightPerPlane;
        int top;
        FontMetrics met;
        int xPos;
        int max;
        int scale;
        int xPix;
        int yPix;
        int barHeight;
        boolean toggle;

        super.paintComponent(grx);

        insets = getInsets();
        height = getHeight() - insets.top - insets.bottom;
        heightPerPlane = height / this.data.getNumPlanes();

        met = grx.getFontMetrics();
        barHeight = heightPerPlane - met.getHeight();

        // Don't draw lines unless our panel is tall enough
        top = insets.top;
        xPos = insets.left + 3;

        for (int plane = 0; plane < this.data.getNumPlanes(); plane++) {
            scale = 1; 
            if (barHeight > 1) {
                max = maxValue(plane);

                // Construct a scale to use when drawing data
                while (max > barHeight) {
                    scale++;
                    max = max * (scale - 1) / scale;
                }

                // Draw the label with optional scale indicator.
                drawLabel(grx, scale, top + met.getAscent(), plane);

                // Draw a background
                grx.setColor(this.back);
                grx.fillRect(xPos, top + heightPerPlane - barHeight, this.data.getNumTimes() + 1,
                    barHeight + 1);

                // Draw some grid lines, and labels
                grx.setColor(this.grid);

                for (int i = 0; i < barHeight; i += 10) {
                    yPix = top + heightPerPlane - i;
                    grx.drawLine(xPos, yPix, xPos + this.data.getNumTimes(), yPix);
                }

                // Draw the lines
                xPix = xPos + this.data.getNumTimes();

                for (int i = 0; i < this.data.getNumTimes(); i++) {
                    toggle = ((i & 0x01) == 0x01) ^ this.phase;
                    yPix = top + heightPerPlane - (this.data.getValue(plane, i) / scale);
                    grx.setColor(toggle ? this.lines : this.lines2);
                    grx.drawLine(xPix, top + heightPerPlane, xPix, yPix);
                    grx.setColor(toggle ? this.points : this.points2);
                    grx.drawLine(xPix, yPix, xPix, yPix);
                    xPix--;
                }
            }

            top += heightPerPlane;
        }
    }

    /**
     * Finds the maximum data value for a plane.
     *
     * @param   plane
     * @return  the maximum value
     */
    private int maxValue(final int plane) {

        int max;
        int value;

        max = this.data.getValue(plane, 0);

        for (int i = 1; i < this.data.getNumTimes(); i++) {
            value = this.data.getValue(plane, i);

            if (value > max) {
                max = value;
            }
        }

        return max;
    }

    /**
     * Draw a label at the top of the histogram.
     *
     * @param  grx    the <code>Graphics</code> to which to draw
     * @param  scale  the scale
     * @param  yPos   the Y position at which to draw
     * @param  plane  the plane whose label to draw
     */
    private void drawLabel(final Graphics grx, final int scale, final int yPos, final int plane) {

        StringBuilder str;

        str = new StringBuilder(100);

        str.append(this.labels[plane]);
        str.append(" (");
        str.append(10 * scale);
        str.append(" per gridline)");
        str.append(":");

        grx.setColor(this.points2);
        grx.drawString(str.toString(), 4, yPos);
    }
}
