package graphicsDomain;
import sharedInterfaces.*;
import sharedDataInterfaces.*;
import sharedObserverInterfaces.*;

import java.awt.*;
import java.awt.event.*;
import java.awt.geom.*;
import java.awt.font.*;
import javax.swing.*;
import java.awt.image.*;
import java.util.*;

public class GraphicsFacade extends AbstractGraphicsFacade
                    implements GraphScaleManager, GraphScaleListener
{
    private static ArrayList<GraphicsFacade> graphList=new ArrayList();
    private GraphMediator med;
    private GraphMouseHandler mouseHandler = new GraphMouseHandler();
    private GuiFacadeInterface guiFacade; //Gui stores selected graph

    //The popup menu used to select the window and to globally set scales
    private JPopupMenu popupMenu;

    //ArrayList to store the points listeners.
    private static ArrayList<GraphPointsListener> pointsListeners =
                                                     new ArrayList();
    //ArrayList to store the scale listeners.
    private static ArrayList<GraphScaleListener> scaleListeners =
                                                     new ArrayList();

    //Current mouse state
    private static final int RELEASED = 1, JUST_PRESSED = 2, JUST_RELEASED = 3,
        DRAGGING = 4;
    private int mouseState = RELEASED;

    // The following instances are used to draw into the image buffer
    private BufferedImage wavesImage; //Offline waveform image
    //Current Translation for waves image to be copied in repaint
    private AffineTransform currWaveTrans = new AffineTransform();
    //Base Transform, computed every time the offline image is created
    private AffineTransform baseWaveTrans;
    //Previous window X and Y size.
    private int prevXSize = -1, prevYSize = -1;
    private boolean forceImageUpdate=false;

    public GraphicsFacade()
    {
        med = new GraphMediator(this);
        addMouseListener(mouseHandler);
        addMouseMotionListener(mouseHandler);
        graphList.add(this);
        addGraphPointsListener(this);
        addGraphScaleListener(this);
        createPopupMenu();
    }

    public AbstractGraphicsFacade getGraphicsFacade()
    {
	GraphicsFacade newGraph = new GraphicsFacade();
	return newGraph;
    }

    public void initialise(GuiFacadeInterface guiFacade)
    {
        this.guiFacade = guiFacade;
    }

    public void setGraphData(GraphDataInterface[] data)
    {
        GraphDataInGraphics[] graphData = new GraphDataInGraphics[data.length];
        for (int i = 0; i < data.length; i++)
            graphData[i] = new GraphDataInGraphics(data[i]);
        med.updateData(graphData);
        forceImageUpdate = true;
    }

    public void resetGraphData()
    {
        med.setIsGraphData(false);
        med.createDecorator();
        repaint();
    }


    /** Sets graph options locally and broadcasts them to other windows.*/
    public void setGraphOptions(GraphOptionsInterface graphOpt)
    {
	GraphOptionsInGraphics options = new GraphOptionsInGraphics(graphOpt);
	med.updateOptions(options);
	//Set identical graph options for other graphs
	for (int i=0; i<graphList.size(); i++)
		if (graphList.get(i)!=this)
                    graphList.get(i).setThisGraphOptions(options);
    }

    /** Set broadcast graph options locally */
    public void setThisGraphOptions(GraphOptionsInGraphics options)
    {
	med.updateOptions(options);
    }

    /** Applies graph options locally and broadcasts them to other windows.*/
    public void applyGraphOptions()
    {
        med.createDecorator();
        repaint();
	for (int i=0; i<graphList.size(); i++)
		if (graphList.get(i)!=this)
                    graphList.get(i).applyThisGraphOptions();
    }

    /** Apply graph options locally */
    public void applyThisGraphOptions()
    {
        med.createDecorator();
        repaint();
    }

    public void paintComponent(Graphics g)
    {
        // Cast the graphics object to Graph2D
        Graphics2D g2 = (Graphics2D) g;

        // Get plot size
        Dimension size = getSize();
        int xSize = size.width;
        int ySize = size.height;

        // find border size
        int borderSize;
        if (med.getGraphOptions().getDefaultBorder())
        {
            borderSize = calcBorderSize(g2);
            med.getGraphOptions().setTickPix(4);
        }
        else
        {
            borderSize = 0;
            med.getGraphOptions().setTickPix(0);
        }

        // Set graph and border size in med
        med.setXSize(xSize);
        med.setYSize(ySize);
        med.setBorderSize(borderSize);

        // Set background Color
        g2.setColor(med.getGraphOptions().selectColor(
            med.getGraphOptions().getGraphColor()));
        g2.fill(new Rectangle2D.Double(0, 0, xSize, ySize));

        //call draw method of the decorator chain
	GraphDecorator decorator = med.getDecorator();
	GraphDecorator waveDecorator = med.getWaveDecorator();

	if (waveDecorator==null)
	{
		decorator.draw(g2);
	}
	else
        {
            //Check if window size, or waveform limits, changed, or mouse
            //just pressed or released.
            //In these cases, waveforms need to be plotted again in the
            //image buffer.
	    if (windowChanged() || mouseActive() || forceImageUpdate)
            {
                updateWavesImage(waveDecorator);
                prevXSize = xSize;
                prevYSize = ySize;
		forceImageUpdate=false;
            }
            //Copy image buffer onto this panel
            if (wavesImage != null)
                g2.drawImage(wavesImage, currWaveTrans, this);
	    //Now draw cursors, labels and axes
	    decorator.draw(g2);
	}
        //If this window is selected, draw a red rectangle around it
        if (guiFacade.getSelectedGraph() == this)
        {
            g2.setColor(Color.RED);
            g2.setStroke(med.getGraphOptions().getLineStroke());
            g2.drawRect(0, 0, xSize - 1, ySize - 1);
        }

        switch (mouseState)
        {
            case JUST_PRESSED:
                mouseState = DRAGGING;
                break;
            case JUST_RELEASED:
                mouseState = RELEASED;
        }
    }

    /** Tests whether a window has changed size. */
    private boolean windowChanged()
    {
        return (prevXSize != med.getXSize()
            || prevYSize != med.getYSize());
    }

    /**Tests whether a mouse interaction has started or finished*/
    private boolean mouseActive()
    {
        return  (mouseState == JUST_RELEASED ||
	         mouseState == JUST_PRESSED);
    }

    /** Calculates size of blank border around the graph*/
    // An arbitrary string, "Test", is used for font sizes.
    public int calcBorderSize(Graphics2D g2)
    {
        double border1, border2, border3, spaceFac = 1.5;

        // height of top adornment: graph title
        Font titleFont = med.getGraphOptions().getTitleFont();
        g2.setFont(titleFont); // title font
        FontRenderContext context = g2.getFontRenderContext();
        Rectangle2D bounds = titleFont.getStringBounds("Test", context);
        double stringHeight = bounds.getHeight();
        border1 = spaceFac * stringHeight;

        // height of bottom adornments: x-axis label plus x-tick-labels
        Font axisFont = med.getGraphOptions().getAxisFont();
        g2.setFont(med.getGraphOptions().getAxisFont()); // axis font
        context = g2.getFontRenderContext();
        bounds = axisFont.getStringBounds("Test", context);
        stringHeight = bounds.getHeight();
        border2 = spaceFac * stringHeight;

        Font tickFont = med.getGraphOptions().getTickFont();
        g2.setFont(tickFont); // tick font
        context = g2.getFontRenderContext();
        bounds = tickFont.getStringBounds("Test", context);
        stringHeight = bounds.getHeight();
        border3 = spaceFac * stringHeight;
        return (int) Math.max(border1, border2 + border3);
    }

    /** Draw waveforms in the image buffer.*/
    private void updateWavesImage(GraphDecorator waveDecorator)
    {
        // Make sure that this image is large enough to accomodate
        // waveforms when dragging.
        // (Note that very large buffers waste heap space)
        int fullWidth = med.getXSize() * 3;
        int fullHeight = med.getYSize() * 3;

        //Create the offline image
        //only if the image previously created is smaller
        wavesImage = (BufferedImage) createImage(fullWidth, fullHeight);
        Graphics2D wavesGraphics = wavesImage.createGraphics();

        //Start drawing waveforms in buffered image
        wavesGraphics.setColor(med.getGraphOptions().
                       selectColor(med.getGraphOptions().getGraphColor()));
        wavesGraphics.fill(new Rectangle2D.Double(0, 0, fullWidth,
                                fullHeight));

        //Adjust proper translation when drawing so that entire
        //waveform set fits in the buffered image
        int deltaX = med.getXSize();
        int deltaY = med.getYSize();

        currWaveTrans.setToTranslation(deltaX, deltaY);
        wavesGraphics.transform(currWaveTrans);
        //Draw waves in buffered image
        waveDecorator.draw(wavesGraphics);
        //Adjust now translation to the opposite way, so that
        //waveforms will be correctly displayed on the screen
        currWaveTrans.setToTranslation( -deltaX, -deltaY);
        //current translation will be updated when dragging mouse in GRAB mode
        baseWaveTrans = new AffineTransform(currWaveTrans);
                    //Record this tranformation
    }

    /** Creates popup dialog. Right click on any graph window. */
    private void createPopupMenu()
    {
        popupMenu = new JPopupMenu();
        JMenuItem selectI = new JMenuItem("Select");
        selectI.addActionListener(new ActionListener()
        {
            public void actionPerformed(ActionEvent e)
            {
                selectWindow();
            }
        });
        popupMenu.add(selectI);
        JMenuItem allSameXScaleI = new JMenuItem("All same X scale");
        allSameXScaleI.addActionListener(new ActionListener()
        {
            public void actionPerformed(ActionEvent e)
            {
                allSameXScale();
            }
        });
        popupMenu.add(allSameXScaleI);
        JMenuItem allSameScaleI = new JMenuItem("All same scale");
        allSameScaleI.addActionListener(new ActionListener()
        {
            public void actionPerformed(ActionEvent e)
            {
                allSameScale();
            }
        });
        popupMenu.add(allSameScaleI);
        popupMenu.pack();
        popupMenu.setInvoker(this);
    }

    /** Select this graph window, possibly de-selecting another one */
    private void selectWindow()
    {
        guiFacade.setSelectedGraph(GraphicsFacade.this);
        //Force repaint of the parent panel, so that the GraphicsFacade
        //which looses focus can redraw itself
        GraphicsFacade.this.getParent().repaint();
    }

    /** Set the same X scale for all windows */
    private void allSameXScale()
    {
        GraphUpdateEvent graphUpdateEvent = new GraphUpdateEvent
             (med.getMinXData(), med.getMaxXData());
        notifyGraphXScaleUpdated(graphUpdateEvent);
    }

    /** set the same scale for all graph windows */
    private void allSameScale()
    {
        GraphUpdateEvent graphUpdateEvent = new GraphUpdateEvent(
            med.getMinXData(), med.getMaxXData(), med.getMinYData(),
            med.getMaxYData());
        notifyGraphScaleUpdated(graphUpdateEvent);
    }

    /** Notify crosshair moved event. Called from DrawCrossHair */
    public void graphPointUpdated(double x, double y, boolean inRange)
    {
        if(mouseState == DRAGGING)
            notifyGraphPointsUpdated(x,y,inRange);
    }

    //Implementation of the GraphPointsManager interface
    public void addGraphPointsListener(GraphPointsListener l)
    {
        pointsListeners.add(l);
    }

    public void removeGraphPointsListener(GraphPointsListener l)
    {
        pointsListeners.remove(l);
    }

    public void notifyGraphPointsUpdated(double x, double y, boolean inRange)
    {
       for(int i = 0; i < pointsListeners.size(); i++)
       {
           GraphPointsListener currListener = (GraphPointsListener)
                                   pointsListeners.get(i);
           currListener.pointsUpdated(x,y,inRange);
       }
    }

    //Implementation of GraphPointsListener interface
    //Move cross-hair in this window to be at the same X as active cross-hair
    public void pointsUpdated(double x, double y, boolean inRange)
    {
        if(mouseState == DRAGGING) return; //this is the active window
        med.setCurrX(med.getXPixel(x));
        repaint();
    }

    //Implementation of GraphScaleManager interface
    public void addGraphScaleListener(GraphScaleListener l)
    {
        scaleListeners.add(l);
    }

    public void removeGraphScaleListener(GraphScaleListener l)
    {
        scaleListeners.remove(l);
    }

    public void  notifyGraphScaleUpdated(GraphUpdateEvent e)
    {
       for(int i = 0; i < scaleListeners.size(); i++)
       {
           scaleListeners.get(i).graphScaleUpdated(e);
       }
    }
    public void  notifyGraphXScaleUpdated(GraphUpdateEvent e)
    {
       for(int i = 0; i < scaleListeners.size(); i++)
       {
           scaleListeners.get(i).graphXScaleUpdated(e);
       }
    }

    //Implementation of GraphScaleListener interface
    public void graphScaleUpdated(GraphUpdateEvent e)
    {
        if(wavesImage == null) return; //If no waveform displayed
        med.setMinXData(e.getMinX());
        med.setMaxXData(e.getMaxX());
        med.setMinYData(e.getMinY());
        med.setMaxYData(e.getMaxY());
	forceImageUpdate=true;
        repaint();
    }

    public void graphXScaleUpdated(GraphUpdateEvent e)
    {
        if(wavesImage == null) return; //If no waveform displayed
        med.setMinXData(e.getMinX());
        med.setMaxXData(e.getMaxX());
	forceImageUpdate=true;
        repaint();
    }

    /** Inner class to handle mouse events */
    private class GraphMouseHandler
        implements MouseListener, MouseMotionListener
    {
        //Recorded mouse position when mouse button pressed
        private int startX = 0, startY = 0;
        //Recorded previous mouse position
        private int prevX = 0, prevY = 0;

        public void mousePressed(MouseEvent e)
        {
            if (e.getButton() != MouseEvent.BUTTON1)
                return; //Handle only Button 1

           //Record mouse position when clocked
            startX = prevX = e.getX();
            startY = prevY = e.getY();
            mouseState = JUST_PRESSED;

            switch (med.getGraphOptions().getDisplayMode())
            {
                //Notify decorators interested in current mouse
                //position: zoomDecorator and crossHairDecorator
               case GraphOptionsInterface.GRAB_MODE:
                {
                    GraphicsFacade.this.repaint();
                    break;
                }
                case GraphOptionsInterface.CROSSHAIR_MODE:
                {
                    med.setCurrX(startX);
                    med.setCurrY(startY);
                    med.recordClosestWave();
                    GraphicsFacade.this.repaint();
                    break;
                }
                case GraphOptionsInterface.ZOOM_MODE:
                {
                    med.getZoomDecorator().setStartX(startX);
                    med.getZoomDecorator().setStartY(startY);
                    med.getZoomDecorator().setZooming(true);
                    break;
                }
            }
        }

        public void mouseMoved(MouseEvent e)
        {   //define cursor shapes for the different modes
            if (med.getGraphOptions().getDisplayMode() ==
                          GraphOptionsInterface.CROSSHAIR_MODE)
                setCursor(Cursor.getDefaultCursor());
            if (med.getGraphOptions().getDisplayMode() ==
                          GraphOptionsInterface.ZOOM_MODE)
                setCursor(Cursor.getPredefinedCursor(Cursor.CROSSHAIR_CURSOR));
            if (med.getGraphOptions().getDisplayMode() ==
                          GraphOptionsInterface.GRAB_MODE)
                setCursor(Cursor.getPredefinedCursor(Cursor.HAND_CURSOR));
        }

        public void mouseClicked(MouseEvent e)
        {
            if (e.getClickCount()>1) //If double click
            {
                //Reset graph to draw complete waveform
                med.getZoomDecorator().setZooming(false);
                med.updateData(med.getGraphDataArr());
                GraphicsFacade.this.repaint();
            }
            else if (e.getButton() == MouseEvent.BUTTON3)
            {
                popupMenu.show(GraphicsFacade.this, e.getX(), e.getY());
            }

        }

        public void mouseEntered(MouseEvent e)
        {
        }

        public void mouseExited(MouseEvent e)
        {}

        public void mouseDragged(MouseEvent e)
        {
            int currX = e.getX();
            int currY = e.getY();
            mouseState = DRAGGING;

            //If the current display mode is GRAB, we need to change med,
            //so that the waveform is dragged
            //For the other modes (zoom, crosshair) no change in the waveform
            //is required during mouse drag
            switch (med.getGraphOptions().getDisplayMode())
            {
                case GraphOptionsInterface.GRAB_MODE:
                    if (prevX == currX && prevY == currY)
                        return; //Mouse not dragged

                    //Change waveform limits in med to perform dragging
                    double deltaXData = med.getXValue(prevX) -
                                        med.getXValue(currX);
                    double deltaYData = med.getYValue(prevY) -
                                        med.getYValue(currY);;
                    med.setMaxXData(med.getMaxXData() + deltaXData);
                    med.setMinXData(med.getMinXData() + deltaXData);
                    med.setMaxYData(med.getMaxYData() + deltaYData);
                    med.setMinYData(med.getMinYData() + deltaYData);

                    //The waveform image needs to be translated as well
                    currWaveTrans.setToTranslation(
                           currWaveTrans.getTranslateX() + currX - prevX,
                           currWaveTrans.getTranslateY() + currY - prevY);
                    break;

                case GraphOptionsInterface.CROSSHAIR_MODE:
                    med.setCurrX(currX);
                    break;
                case GraphOptionsInterface.ZOOM_MODE:
                    med.getZoomDecorator().setEndX(currX);
                    med.getZoomDecorator().setEndY(currY);
                    break;
             }
            prevX = currX;
            prevY = currY;

            //Force repaint
            GraphicsFacade.this.repaint();
        }

        public void mouseReleased(MouseEvent e)
        {
            if (e.getButton() != MouseEvent.BUTTON1)
                 return; //Handle only Button 1

            //Mouse released actions are only required when in ZOOM mode
            //In this case, med parameters are changed so that the waveform
            //region fits the zoom box
            mouseState = JUST_RELEASED;
            if (med.getGraphOptions().getDisplayMode() ==
                GraphOptionsInterface.ZOOM_MODE)
            {
                int currX = e.getX();
                int currY = e.getY();
                if (startX == currX && startY == currY) return; //Not dragged

                //Set minimum width and height for the zoom box
                if (startX == currX) currX = startX + 1;
                if (startY == currY) currY = startY + 1;

                //Get the  values corresponding to the limits of the zoom box
                //Need to handle every direction in which mouse has been moved
                double minXData, minYData, maxXData, maxYData;
                if (currX > startX)
                {
                    minXData = med.getXValue(startX);
                    maxXData = med.getXValue(currX);
                }
                else
                {
                    minXData = med.getXValue(currX);
                    maxXData = med.getXValue(startX);
                }
                if (currY < startY)
                {
                    minYData = med.getYValue(startY);
                    maxYData = med.getYValue(currY);
                }
                else
                {
                    minYData = med.getYValue(currY);
                    maxYData = med.getYValue(startY);
                }

                //Let the values corresponding to the limits of the zoom box
                //be the limits of the displayed waveform
                med.setMaxXData(maxXData);
                med.setMaxYData(maxYData);
                med.setMinXData(minXData);
                med.setMinYData(minYData);
                med.getZoomDecorator().setZooming(false);
                //Force repaint
                GraphicsFacade.this.repaint();
            }
        }
    }
}

