/**
 * Copyright (c) 2004-2005, Regents of the University of California
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * Redistributions of source code must retain the above copyright notice,
 * this list of conditions and the following disclaimer.
 *
 * Redistributions in binary form must reproduce the above copyright
 * notice, this list of conditions and the following disclaimer in the
 * documentation and/or other materials provided with the distribution.
 *
 * Neither the name of the University of California, Los Angeles nor the
 * names of its contributors may be used to endorse or promote products
 * derived from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

package avrora.gui;

import cck.stat.Sequence;
import javax.swing.*;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import java.awt.*;
import java.awt.event.AdjustmentEvent;
import java.awt.event.AdjustmentListener;

/**
 * The class assists visual monitors with graphing time-series data
 * values.  It visually displays them using a line graph
 */
public class GraphNumbers extends JPanel implements ChangeListener, AdjustmentListener {

    private static final long serialVersionUID = 1L;
    private Sequence publicNumbers; //access by monitors to add stuff
    private Sequence privateNumbers; //only accessed by paint
    private final JPanel parentPanel;

    private static final int valueMargin = 5;

    /**
    * This is the bar that determines what part of
    * the graph is displayed
    */
    public JScrollBar horzBar;

    TimeScale timeScale;

    /**
     * The visual widget that sets the step size
     */
    public SpinnerNumberModel stepsizeVisual;

    /**
     * The max value of the y-axis
     */
    public int maxvalue;

    /**
     * The visual wdiget that sets the max value for the y-axis
     */
    public SpinnerNumberModel maxvalueVisual;

    //options not done yet
    private Color lineColor; //color of line that is drawn
    private Color backColor; //color of background
    private Color cursorColor;
    private int minvalue;
    protected static final int VALUE_SCALE_WIDTH = 15;
    protected static final int MIN__VALUE__TICK = 3;

    //Other features to add:
    //ability to user this class "on top of" another GraphNumbers class => multiple lines on one graph
    //ability to see/get the value of the line based upon a mouse over/mouse click event
    //double check to see if scroll bar is sizing correctly

    /**
     * Called by a visual action that wants this class to help with displaying time series data
     */
    public GraphNumbers(JPanel parent) {

        parentPanel = parent;

        publicNumbers = new Sequence();
        privateNumbers = new Sequence();

        //Set option defaults
        lineColor = Color.GREEN; //default line color is green
        backColor = Color.BLACK; //default background color is black
        cursorColor = Color.CYAN;
        minvalue = 0; //min and max values for the y-axis
        maxvalue = 0;
        timeScale = new TimeScale();
    }

    /**
     * Returns a panel which can be displayed that contains the graph numbers
     * panel and a horz scrollbar at the bottom that makes changes viewing area easy
     * @return Basically, what you want to display to the screen
     */
    public JPanel chalkboardAndBar() {
        JPanel temppanel = new JPanel();
        temppanel.setLayout(new BorderLayout());
        horzBar = new JScrollBar(JScrollBar.HORIZONTAL);
        horzBar.addAdjustmentListener(this);

        //init the scroll bar
        updateHorzBar();

        JPanel innertemppanel = new JPanel();
        innertemppanel.setLayout(new OverlayLayout(innertemppanel));
        innertemppanel.add(this);
        temppanel.add(innertemppanel, BorderLayout.NORTH);

        //we need to adjust the this panel's preferred size
        //by subtracting the horz scrollbar's size from it
        Dimension newDimen = parentPanel.getSize();
        newDimen.height = newDimen.height - horzBar.getPreferredSize().height;
        this.setPreferredSize(newDimen);

        temppanel.add(this, BorderLayout.NORTH);
        temppanel.add(horzBar, BorderLayout.SOUTH);

        return temppanel;
    }

    /**
     * This function updates the scroll bar as new
     * numbers are added to the vector or if we decided to
     * jump to a certian value
     * Synchronized because GUI thread and paintthread will access the horz bar
     */
    public synchronized void updateHorzBar() {

        int width = this.getSize().width;
        long maxtime = privateNumbers.size();
        int newExtent = timeScale.getExtent(width, maxtime);
        int size = timeScale.getScrollBarSize(maxtime);
        //to handle the case where we really don't need the bar
        if (size < newExtent) {
            //then we just have a scroll bar that does nothing
            horzBar.setValues(0, 0, 0, 0);
            return;
        }

        int newValue = horzBar.getValue();
        //we check to see if the bar is current at it's maximum...if so
        //then keep it that way despite adding new values
        if (newValue + horzBar.getModel().getExtent() == horzBar.getMaximum()) {
            newValue = size - newExtent;
        }

        horzBar.setValues(newValue, newExtent, 0, size);
    }

    /**
     * This is called to get the visual widget that the user can set step 
     * size with.
     * @return A panel containing a spinner that controls stepsize value
     */
    public JPanel getZoomLevelOption() {
        if (stepsizeVisual == null) {
            makeZoomLevelOption();
        }
        JPanel returnthis = new JPanel();
        returnthis.setLayout(new BorderLayout());
        JLabel stepSizeLabel = new JLabel("Zoom Level: ");
        returnthis.add(stepSizeLabel, BorderLayout.WEST);
        JSpinner spinit = new JSpinner(stepsizeVisual);
        spinit.setPreferredSize(new Dimension(80, 20));
        returnthis.add(spinit, BorderLayout.EAST);
        returnthis.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
        return returnthis;
    }

    private void makeZoomLevelOption() {
        stepsizeVisual = new SpinnerNumberModel();
        stepsizeVisual.setValue(new Integer(timeScale.getZoom()+1));
        stepsizeVisual.setMinimum(new Integer(1));
        stepsizeVisual.setMaximum(new Integer(timeScale.getMaxZoom()+1));
        stepsizeVisual.addChangeListener(this);
    }

    /**
    * This is called to get the visual widget that the user can set y-axis
    * max value with.
    * @return A panel containing a spinner that controls maxvalue value
   */
    public JPanel visualSetMaxValue() {
        if (maxvalueVisual == null) {
            createmaxvalueVisual();
        }
        JPanel returnthis = new JPanel();
        returnthis.setLayout(new BorderLayout());
        JLabel maxvalueLabel = new JLabel("Y-Axis max value");
        returnthis.add(maxvalueLabel, BorderLayout.WEST);
        JSpinner spinit = new JSpinner(maxvalueVisual);
        spinit.setPreferredSize(new Dimension(80, 20));
        returnthis.add(spinit, BorderLayout.EAST);
        returnthis.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
        return returnthis;
    }

    private void createmaxvalueVisual() {
        maxvalueVisual = new SpinnerNumberModel();
        maxvalueVisual.setValue(new Integer(maxvalue));
        stepsizeVisual.setMinimum(new Integer(1));
        maxvalueVisual.addChangeListener(this);
    }

    /**
     * This function returns a panel that has all
     * the visual options aligned in a column
     * @return a panel that can be directly displayed to the screen
     */
    public JPanel getOptionsPanel() {
        JPanel allOptions = new JPanel();
        allOptions.setLayout(new GridLayout(10, 1));
        //allOptions.setLayout(new BorderLayout());
        allOptions.add(getZoomLevelOption());
        allOptions.add(visualSetMaxValue());
        allOptions.add(new JPanel()); //filler so there is blank space
        return allOptions;
    }

    /**
     * This function is called by fire methods inside a monitor.  It
     * physically adds data values that will be displayed upon
     * next update/repaint
     * @param number the value for the time series data in question
     */
    public void recordNumber(int number) {
        synchronized (this) {
            publicNumbers.add(number);
        }
    }

    /**
     * This function is called by paint and it does what is necessary
     * to update the privateNumbers vector
     * returns true if it actually got some numbers, otherwise returns false
     * It might also be called by paint thread
     */
    public boolean internalUpdate() {
        Sequence newNumbers = publicNumbers;
        synchronized ( this ) {
            if ( newNumbers.size() == 0 ) {
                return false;
            }
            publicNumbers = new Sequence();
        }

        // add all the new numbers
        privateNumbers.addAll(newNumbers);
        int max = privateNumbers.max();
        if ( max > maxvalue ) maxvalue = max;

        //and update the horz scroll bar to reflect the new values
        updateHorzBar();
        return true;
    }

    /**
     * This actually paints the graph...note that it repaints the whole graph
     * everytime its called (to improve performance, we could make use of an update function)
     * The code here is actually faily ugly
     * but eh..
     * @param g The graphic that represents the panel to be painted
     */
    public void paint(Graphics g) {

        Dimension panelDimen = this.getSize();

        //Set up background color, will erase all previous lines
        g.setColor(backColor);
        g.fillRect(0, 0, panelDimen.width, panelDimen.height);

        // draw the time scale (horizontal axis)
        timeScale.setPosition(horzBar.getValue());
        timeScale.drawScale(panelDimen, g);
        long startTime = timeScale.getStartTime();

        // draw the value scale (vertical axis)
        drawValueScale(panelDimen, g);

        int height = panelDimen.height - timeScale.height;
        int maxY = height - valueMargin;
        double scalingfactor = (maxY - valueMargin) / (double)(maxvalue - minvalue);
        int eofpx = 0; //holds coorinates of last line we drew
        int eofpy = 0;
        boolean firstone = true;  //the first line is a special case

        //we have to look up the starting value for the x-axis
        Sequence.Iterator mi = privateNumbers.iterator((int)startTime);
        g.setColor(lineColor);
        for (long i = startTime; mi.hasNext() ; i++) {
            double currentValue = mi.next();
            if (firstone) {
                //don't draw the vertical line, just the horzintal
                eofpy = maxY - (int)(currentValue * scalingfactor);
                eofpx = timeScale.getX(i);
                g.drawLine(0, eofpy, eofpx, eofpy);
                firstone = false;
            } else {
                //two lines (one horizontal and one vertical), using the previous point as a starting location
                int npy = maxY - (int)(currentValue * scalingfactor);
                //vertical
                int npx = timeScale.getX(i);
                g.drawLine(eofpx, eofpy, eofpx, npy);
                //horz
                g.drawLine(eofpx, npy, npx, npy);
                eofpy = npy;
                eofpx = npx;
            }

            // are there any measurements left?
            if (!mi.hasNext() ) {
                g.setColor(Color.DARK_GRAY);
                g.fillRect(eofpx, 0, panelDimen.width, height);
                g.setColor(cursorColor);
                g.drawLine(eofpx, 0, eofpx, height);
            }

            // did we go off the end to the right?
            if ( eofpx > panelDimen.width ) break;
        }
    }

    private void drawValueScale(Dimension dim, Graphics g) {
        int height = dim.height - timeScale.height;
        g.setColor(Color.GRAY);
        g.fillRect(0, 0, VALUE_SCALE_WIDTH, height);
        int startY = height - valueMargin;
        double scale = (startY - valueMargin) / (double)(maxvalue - minvalue);
        int vtick = 1;
        for ( ; vtick < 1000000000; vtick *= 10 ) {
            if ( scale * vtick > MIN__VALUE__TICK ) break;
        }
        int majorTick = vtick * 10;
        int value = (minvalue / majorTick) * majorTick;
        if ( minvalue < 0 ) value -= majorTick;

        g.setColor(Color.DARK_GRAY);
        g.setFont(g.getFont().deriveFont(9.0f));

        for ( ; ; value += vtick) {
            int y = (int)(startY - (value - minvalue) * scale);
            if ( y <= 0 ) break;
            int modulus = value % majorTick;
            if ( modulus == 0 ) {
                // draw line across whole screen
                g.drawLine(0, y, dim.width, y);
                g.setColor(Color.RED);
                g.drawString(Integer.toString(value), VALUE_SCALE_WIDTH+3, y+3);
                g.setColor(Color.DARK_GRAY);
            } else {
                if ( modulus == 5 )
                    g.drawLine(5, y, VALUE_SCALE_WIDTH-1, y);
                else
                    g.drawLine(8, y, VALUE_SCALE_WIDTH-1, y);
            }
        }
        // draw the border line
        g.setColor(Color.WHITE);
        g.drawLine(VALUE_SCALE_WIDTH, 0, VALUE_SCALE_WIDTH, height);
    }

    /**
     * this function processes the monitor options and resets the internal variables appropriately
     ** @param e Info about the event that happened
     */
    public void stateChanged(ChangeEvent e) {
        if (e.getSource() == stepsizeVisual) {
            adjustZoom();
            repaint();
        } else if (e.getSource() == maxvalueVisual) {
            maxvalue = ((Integer) maxvalueVisual.getValue()).intValue();
            repaint();
        }
    }

    private void adjustZoom() {
        int zoomlevel = ((Integer) stepsizeVisual.getValue()).intValue() - 1;
        timeScale.setZoom(zoomlevel);
        this.horzBar.setValue(timeScale.getPosition());
        this.updateHorzBar();
    }

    /**
     * This function handles a user change to the scroll bar
     * @param e Info about the event that happened
     */
    public void adjustmentValueChanged(AdjustmentEvent e) {
        repaint();
    }

}
