/**
 * 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.text.Terminal;
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;

/**
 * This is a generic helper class for visual monitors.  It's purpose is to help
 * graph events that occur in the simulator (constant this with <code> 
 * GraphNumbers </code>, which graph data values as time goes by.
 * For each event that occurs, a colored dot will appear.  The x-axis maps
 * time gone by.
 * <p>
 * Events must be inserted into this class in temporal order
 * <p>
 * This class is not yet done.  It needs to be more generic - right now it's
 * hacked to specifically allow LED output to be graphed.
 *
 * @author UCLA Compilers Group
 */
public class GraphEvents extends JPanel implements ChangeListener, AdjustmentListener {

    private static final long serialVersionUID = 1L;
    private MyVector[] publicNumbers; //access by monitors to add stuff
    private MyVector[] privateNumbers; //only accessed by paint
    private static final int VECSIZE = 5;
    private JPanel parentPanel;

    private Object vSync; //just a private sync variable

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

    //All these fields can be set by options

    /**
     * number of pixels per x-axis value
     */
    public double stepsize;

    /**
     * The visual component for setting <code> stepsize </code>
     */
    public SpinnerNumberModel stepsizeVisual;

    private Color backColor; //color of background
    private Color tickColor; //color of tick marks/graph lines


    //The min and max values of the data in question: for VERTICAL sizing
    //the step size is the HORZ step size (there is no notion of vertical step size)
    /* Called by a monitor who wants to use this class
     * @param pminvalue For vertical sizing
     * @param pmaxvalue For vertical sizing
     * @param pstepsize For horz sizing - number of pixels per unit of x-axis
     */
    public GraphEvents(int pminvalue, int pmaxvalue, double pstepsize) {

        vSync = new Object();

        publicNumbers = new MyVector[VECSIZE];
        privateNumbers = new MyVector[VECSIZE];
        for (int i = 0; i < VECSIZE; i++) {
            publicNumbers[i] = new MyVector();
            privateNumbers[i] = new MyVector();
        }

        backColor = Color.BLACK; //default background color is black
        tickColor = Color.LIGHT_GRAY; //default tick mark color is gray
        stepsize = pstepsize; //x-axis step size
    }

    /**
     * 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 What you should directly 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
    * if the paramter is true, it will set the scroll bar to be the new
    * max value...otherwise it just keeps value to whatver it used to be
    * Synchronized because GUI thread and paintthread will access the horz bar
    */
    public synchronized void updateHorzBar() {
        int newExtent = (int)((double)this.getSize().width / stepsize);

        int maxvalue;
        maxvalue = 0;

        //Here we have to find the "biggest" set of data and set maxvalue
        //to the largest datapoint
        for (int i = 0; i < VECSIZE; i++) {
            if (privateNumbers[i].size() != 0) {
                if (maxvalue < privateNumbers[i].getLast()) {
                    maxvalue = privateNumbers[i].getLast();
                }
            }
        }


        //to handle the case where we really don't need the bar
        if (maxvalue < 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 (horzBar.getValue() + horzBar.getModel().getExtent() == horzBar.getMaximum()) {
            newValue = maxvalue - newExtent;
        }
        horzBar.setValues(newValue, newExtent, 0, maxvalue);

    }

    /**
     * used by paint so it knows what value to start painting with
     */
    public int getHorzBarValue() {
        //Note: does this need to be synched?
        //Right now, no.
        return horzBar.getValue();
    }

    //Every option you can set has:
    //a) a getmethod which returns it's value
    //b) a setmethod which sets it's value
    //c) a visualSet method which returns a component that can be used to adjust/view its value
    //   (listeners are already set up and handeled by this class)

    /**
     * @return stepsize value
     */
    public double getStepSize() {
        return stepsize;
    }

    /**
     * @param pstepsize The value that step size should be set to
     */
    public void setStepSize(double pstepsize) {
        stepsize = pstepsize;
    }

    /**
     * This creates a visual object that the user
     * can interact with to set step size
     *
     * @return A panel that contains a spinner for setting step size
     */
    public JPanel visualSetStepSize() {
        if (stepsizeVisual == null) {
            createstepsizeVisual();
        }
        JPanel returnthis = new JPanel();
        returnthis.setLayout(new BorderLayout());
        JLabel stepSizeLabel = new JLabel("X-Axis Step Size: ");
        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 createstepsizeVisual() {
        stepsizeVisual = new SpinnerNumberModel();
        stepsizeVisual.setValue(new Double(stepsize));
        stepsizeVisual.setMinimum(new Double(0));
        stepsizeVisual.addChangeListener(this);
    }

    /**
     * This function creates the actual options panel
     * that can be displayed.  This allows the user to
     * set monitor options
     *
     * @return A panel (can be directly displayed) of all monitor options
     */
    public JPanel getOptionsPanel() {
        JPanel allOptions = new JPanel();
        allOptions.setLayout(new GridLayout(10, 1));
        //allOptions.setLayout(new BorderLayout());
        allOptions.add(visualSetStepSize());
        allOptions.add(new JPanel()); //filler so there is blank space
        return allOptions;
    }


    /**
     * Used in order to size thing correctly.  Should be called
     * right after the constructor is called
     */
    public void setParentPanel(JPanel pparentPanel) {
        parentPanel = pparentPanel;
    }

    /**
     * A monitor can add data using this function
     * On next repaint, it will be added to the graph
     *
     * @param vecnum an index that specified "which" event occured
     * @param anAddress the time that the event occured
     */
    public void addToVector(int vecnum, int anAddress) {
        //This is currently a hack - it should be changed to make it
        //more generic
        //0 -> transmit
        //1 -> receive
        //2 -> red
        //3 -> yellow
        //4 -> green

        synchronized (vSync) {
            publicNumbers[vecnum].add(anAddress);
        }
    }

    /**
    * This function is called by paint and it does what is necessary
    * to update the privateNumbers vector
    * @return true if it actually got some numbers, otherwise returns false
    */
    public boolean internalUpdate() {
        boolean somethingchanged;
        somethingchanged = false;
        synchronized (vSync) {
            for (int i = 0; i < VECSIZE; i++) {
                if (publicNumbers[i].size() == 0) {

                } else {
                    somethingchanged = true;
                    //so we need to take anything in private and move it to public

                    //do the move
                    try {
                        privateNumbers[i].addAll(publicNumbers[i]);
                    } catch (OutOfMemoryError e) {
                        //Note that it's possible to get an out of memory exception
                        //elsewhere, but "most probably" it will be here
                        Terminal.println("RAN OUT OF HEAP SPACE FOR MONITOR");
                        Terminal.println("SIZE OF MONITORS VECTOR AT THE TIME: " + Integer.toString(privateNumbers[i].size()));
                    }

                    publicNumbers[i].removeAllElements();
                }
            }
        }
        //and update the horz scroll bar to reflect the new values
        updateHorzBar();
        return somethingchanged;
    }

    /**
     * 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..
     */
    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);

        //So basically, we have to find all the events that occur in between starting
        //value and ending
        int startingvalue = getHorzBarValue();

        //Let's output the horz tick lines here
        for (int i = 0; i < panelDimen.width; i = i + 100) {
            //vertical line and label
            g.setColor(tickColor);
            g.drawLine(i, 0, i, panelDimen.height);
            g.drawString(Integer.toString((int) ((double) i * stepsize + startingvalue)) + "ms", i, panelDimen.height - 10);

        }

        //Output line labels
        g.setColor(Color.BLUE);
        g.drawString("Transmit", 0, panelDimen.height / 4 - 10);

        g.setColor(Color.MAGENTA);
        g.drawString("Receive", 0, panelDimen.height / 2 - 10);

        g.setColor(tickColor);
        g.drawString("LEDs", 0, 3 * panelDimen.height / 4 - 10);


        for (int j = 0; j < VECSIZE; j++) {


            //ending value is size of panel or end of list, whichever is less
            int endingvalue;
            if (privateNumbers[j].size() == 0) {
                endingvalue = 0;
            } else if ((int)(double)panelDimen.width / stepsize < privateNumbers[j].get(privateNumbers[j].size() - 1)) {
                endingvalue = startingvalue + (int)((double)panelDimen.width / stepsize);
            } else {
                endingvalue = privateNumbers[j].get(privateNumbers[j].size() - 1);
            }

            int currentYPoint;
            if (j == 0)  //transmit
            {
                currentYPoint = panelDimen.height / 4;
                g.setColor(Color.BLUE);
            } else if (j == 1) //receive
            {
                currentYPoint = panelDimen.height / 2;
                g.setColor(Color.MAGENTA);
            } else if (j == 2) //RED
            {
                currentYPoint = 3 * panelDimen.height / 4 - 20;
                g.setColor(Color.RED);
            } else if (j == 3) //YELLOW
            {
                currentYPoint = 3 * panelDimen.height / 4;
                g.setColor(Color.YELLOW);
            } else //GREEN
            {
                currentYPoint = 3 * panelDimen.height / 4 + 20;
                g.setColor(Color.GREEN);
            }

            //We have to do a check to see if, for this data series
            //we are already past what we need to draw
            if (privateNumbers[j].size() == 0 || privateNumbers[j].get(privateNumbers[j].size() - 1) <= startingvalue) {
                //we don't draw anything
            } else {


                //Let's fastfoward to starting value
                int i = 0;
                while (privateNumbers[j].get(i) < startingvalue) {
                    i++;
                }

                //Here we do this check to see if we start our area
                //with a light enabled
                boolean startColorOn = false;
                if (i % 2 == 1) {
                    //if odd, then we start with a color being on from the beginning
                    startColorOn = true;
                }

                //Start here, instead of drawing output list of values;
                //Make sure to check to see if they are indeed in sorted order

                //so now we draw!
                while (privateNumbers[j].get(i) <= endingvalue || startColorOn) {
                    //If it's a color, we draw a bar between two adjacent points
                    //anything else we just draw dots
                    if (j == 2 || j == 3 || j == 4) {
                        int currentXPoint1;
                        if (startColorOn) {
                            currentXPoint1 = 0;
                            startColorOn = false;
                        } else {
                            currentXPoint1 = (int)((double)(privateNumbers[j].get(i) - startingvalue) * stepsize);
                            i++;
                        }
                        int currentXPoint2;
                        if (i < privateNumbers[j].size()) {
                            currentXPoint2 = (int)((double)(privateNumbers[j].get(i) - startingvalue) * stepsize);
                            i++;
                        } else {
                            currentXPoint2 = panelDimen.width;
                        }
                        g.fillOval(currentXPoint1 - 2, currentYPoint - 2, 4, 4);
                        g.fillOval(currentXPoint2 - 2, currentYPoint - 2, 4, 4);
                        g.drawLine(currentXPoint1, currentYPoint, currentXPoint2, currentYPoint);
                    } else {
                        int currentXPoint = (int)((double)(privateNumbers[j].get(i) - startingvalue) * stepsize);
                        i++;
                        g.fillOval(currentXPoint - 2, currentYPoint - 2, 4, 4);
                    }
                    if (i >= privateNumbers[j].size())
                        break;
                }
            }
        }


    }

    /**
     * this function processes the monitor options and re-sets the internal variables appropiatly
     *
     *@param e Information about what event occured
     */
    public void stateChanged(ChangeEvent e) {
        if (e.getSource() == stepsizeVisual) {
            stepsize = ((Double) stepsizeVisual.getValue()).doubleValue();
            repaint();
        }
    }

    /**
     * If the scroll bar was adjusted, we should repaint.  This method
     * takes care of that
     */
    public void adjustmentValueChanged(AdjustmentEvent e) {
        repaint();
    }

    /**
     * We don't want to store millions of Integer, but we still want
     * an array that grows...so we define a MyVector class just for that
     */
    public class MyVector {
        int[] vec;
        int current;

        public MyVector() {
            vec = new int[100];
            current = 0;
        }

        public void add(int a) {
            if (current == vec.length) {
                //create a new array of double the size
                //Isn't there some proof that says that's good?  Go CS112!
                int[] vec2 = new int[vec.length * 2];
                System.arraycopy(vec, 0, vec2, 0, vec.length);
                vec = vec2;
            }
            vec[current] = a;
            current++;
        }

        public int get(int i) {
            if (i < current && i >= 0)
                return vec[i];
            else
                return 0; //this is sorta stupid, but we'll be careful
        }

        public void addAll(MyVector a) {
            for (int i = 0; i < a.size(); i++) {
                add(a.get(i));
            }
        }

        public int size() {
            return current;
        }

        public int getLast() {
            if (current > 0)
                return this.get(current - 1);
            else
                return 0;

        }

        public void removeAllElements() {
            vec = new int[100];
            current = 0;
        }
    }
}
