package graphicsDomain;

import java.awt.*;

/** Performs conversions between world and pixel coordinates */
public class GraphMediator
{
    private GraphicsFacade graphFacade;
    private GraphDataInGraphics[] graphData;
    private GraphOptionsInGraphics graphOptions;
    private double minXData,maxXData,minYData,maxYData;
    private int borderSize,xSize,ySize,currX=0,currY=0;
    private int closestGraphIdx; // index of closest wave to mouse click
    private String message=""; // possible text message to draw
    private Point[] scaledPoints;
    private boolean isGraphData,isDataError;

    private GraphDecorator decorator=null;
    private DrawAxisTicksX axisXDecorator;
    private DrawAxisTicksY axisYDecorator;
    private DrawCrossHair crossHairDecorator;
    private DrawCentredMessage warningDecorator;
    private DrawLabels labelsDecorator;
    private DrawZoomBox zoomDecorator;

    public GraphMediator(GraphicsFacade g)
    {
	graphFacade = g;
	graphData = null;
	graphOptions = null;
	isGraphData = false;
	isDataError = false;
        //Instantiate decorators
        warningDecorator = new DrawCentredMessage(this);
        zoomDecorator = new DrawZoomBox(this);
        crossHairDecorator = new DrawCrossHair(this);
        labelsDecorator = new DrawLabels(this);
        axisXDecorator = new DrawAxisTicksX(this);
        axisYDecorator = new DrawAxisTicksY(this);
    }

    public void updateData(GraphDataInGraphics[] gD)
    {
	graphData=gD;
	update();
	createDecorator();
    }

    public void updateOptions(GraphOptionsInGraphics gO)
    {
	graphOptions = gO;
	createDecorator();
    }

    public void update()
    {
	if (graphData!=null) //is null for empty constructor
	{
            minXData=graphData[0].getMinX();
	    maxXData=graphData[0].getMaxX();
	    minYData=graphData[0].getMinY();
	    maxYData=graphData[0].getMaxY();
            for (int i = 1; i < graphData.length; i++)
            {
                if (minXData>graphData[i].getMinX())
                    minXData = graphData[i].getMinX();
                if (maxXData<graphData[i].getMaxX())
                    maxXData = graphData[i].getMaxX();
                if (minYData > graphData[i].getMinY())
                    minYData = graphData[i].getMinY();
                if (maxYData < graphData[i].getMaxY())
                    maxYData = graphData[i].getMaxY();
            }
            isGraphData = true;
            for (int i = 0; i < graphData.length; i++)
                if (graphData[i].getIsGraphData() == false)
                    isGraphData = false;
            isDataError = false;
            for (int i = 0; i < graphData.length; i++)
                if (graphData[i].getIsDataError() == true)
                    isDataError = true;
            if (maxXData <= minXData)
                minXData = maxXData - 1E-20; //Guard against divide by zero
	}
    }

    public void createDecorator()
    {
        /*Build the decorator chain. The elements in the chain define
          what is going to be displayed.

           If no data or bad data then the decorator chain is only composed
           of a DrawCentredMessage object containing an error message.*/
        if (graphData == null || !isGraphData)
        {
            if (graphData != null && isDataError)
                warningDecorator.setMessage(
                    "Unexpected data. See console message.");
            else
                warningDecorator.setMessage("No data");

            //Build decorator chain with a centred message
            decorator = warningDecorator;
        }
        else //Build the decorator chain based on the current display mode
	{
	    if (graphOptions.getDisplayMode()==graphOptions.getCROSSHAIR())
                //decorator chain with X and Y axis, crosshair
                decorator = concatenateDecorators(
                        labelsDecorator,
                        crossHairDecorator,
                        axisYDecorator,
                        axisXDecorator);

	    if (graphOptions.getDisplayMode()==graphOptions.getZOOM())
                //decorator chain with X and Y axis, zoom box
                decorator = concatenateDecorators(
                        labelsDecorator,
                        zoomDecorator,
                        axisYDecorator,
                        axisXDecorator);

	    if (graphOptions.getDisplayMode()==graphOptions.getGRAB())
                //decorator chain with X and Y axis
                decorator = concatenateDecorators(
                        labelsDecorator,
                        axisYDecorator,
                        axisXDecorator);
	    //Add waveforms
	    for (int nWave = 0; nWave<graphData.length; nWave++)
	    {
                DrawWaveform wave = new DrawWaveform(this,nWave);
	        decorator = concatenateDecorators(wave,decorator);
	    }
        }
    }

    /** Forms decorator chain. Note variable argument list. */
    private GraphDecorator concatenateDecorators(GraphDecorator ... decorators)
    {
        for (int i = decorators.length - 2; i >= 0; i--)
            decorators[i].setDecorator(decorators[i + 1]);
        return decorators[0];
    }

    public GraphDecorator getDecorator()
    {
	return decorator;
    }

    //For Cross-Hair
    //Find the closest wave to the point (currX, currY)
    public void recordClosestWave()
    {
        double xVal = getXValue(currX);
        double yVal = getYValue(currY);
        double minDist = graphData[0].getYDistance(xVal,yVal);
        closestGraphIdx = 0;
        for(int i = 1; i < graphData.length; i++)
        {
            double currDist = graphData[i].getYDistance(xVal,yVal);
            if(currDist < minDist)
            {
                minDist = currDist;
                closestGraphIdx = i;
            }
        }
     }

    // Mappings from world to pixel coordinates.
    /** World to pixel in X*/
    public int getXPixel(double xVal)
    {
        return (int) ( (xVal-minXData)/(maxXData-minXData)
                      * (xSize-2*borderSize) + borderSize );
    }

    /** World to pixel in Y*/
    public int getYPixel(double yVal)
    {
        return (int) ( (yVal-minYData)/(maxYData-minYData)
		       * (2*borderSize-ySize) + ySize-borderSize );
    }

    /** Pixel to world in X*/
    public double getXValue(double xPixel)
    {
        return ( (xPixel-borderSize)/(xSize-2*borderSize)
                 * (maxXData-minXData) + minXData );
    }

    /** Pixel to world in Y*/
    public double getYValue(double yPixel)
    {
        return ( (yPixel-ySize + borderSize)/(2*borderSize - ySize)
             * (maxYData-minYData) + minYData );
    }

    /** Returns an array of (possibly filtered) pixel Point objects */
    public Point[] getPixelArray(double[] xVal, double[] yVal)
    {
      int actDim = -1;
      int currXVal, currYVal, prevXVal = 0;
      for (int i = 0; i < yVal.length; i++) { //Find how many points can be filtered
        currXVal = (int) ( (xVal[i] - minXData) / (maxXData - minXData)
                          * (xSize - 2 * borderSize) + borderSize);
        if (actDim == -1 || currXVal != prevXVal) {
	    actDim++; // increment counter if x pixel value is different
            prevXVal = currXVal;
        }
      }

      if (actDim > yVal.length / 2) { //Not enough points to bother filtering
        scaledPoints = new Point[yVal.length];
        for (int i = 0; i < yVal.length; i++) {
          scaledPoints[i] = new Point();
          scaledPoints[i].x = (int) ( (xVal[i] - minXData) / (maxXData - minXData)
                                     * (xSize - 2 * borderSize) + borderSize);
          scaledPoints[i].y = (int) ( (yVal[i] - minYData) / (maxYData - minYData)
                                     * (2 * borderSize - ySize) + ySize -
                                     borderSize);
        }
        return scaledPoints;

      }
      else { //It makes sense to filter. Adjacent filtered points store max/min Y vals
        scaledPoints = new Point[2 * (actDim + 1)];
        actDim = -1;
        for (int i = 0; i < yVal.length; i++) {
          currXVal = (int) ( (xVal[i] - minXData) / (maxXData - minXData)
                            * (xSize - 2 * borderSize) + borderSize);
          currYVal = (int) ( (yVal[i] - minYData) / (maxYData - minYData)
                            * (2 * borderSize - ySize) + ySize - borderSize);
          if (actDim == -1 || currXVal != prevXVal) {
            actDim++;
            prevXVal = currXVal;
            scaledPoints[2 * actDim] = new Point(currXVal, currYVal);
            scaledPoints[2 * actDim + 1] = new Point(currXVal, currYVal);
          }
          else { // store max/min y vals
            if (currYVal < scaledPoints[2 * actDim].y)
              scaledPoints[2 * actDim].y = currYVal;
            if (currYVal > scaledPoints[2 * actDim + 1].y)
              scaledPoints[2 * actDim + 1].y = currYVal;

          }
        }
        return scaledPoints;
      }
    }


    public void setXSize(int x) {xSize=x;}
    public void setYSize(int y) {ySize=y;}
    public void setBorderSize(int b){borderSize=b;}

    public double getMinXData() { return minXData;}
    public double getMaxXData() { return maxXData;}
    public double getMinYData() { return minYData;}
    public double getMaxYData() { return maxYData;}
    public void setMinXData(double minXData) { this.minXData = minXData;}
    public void setMaxXData(double maxXData) { this.maxXData = maxXData;}
    public void setMinYData(double minYData) { this.minYData = minYData;}
    public void setMaxYData(double maxYData) { this.maxYData = maxYData;}

    public int getXSize() {return xSize;}
    public int getYSize() {return ySize;}
    public int getBorderSize(){return borderSize;}

    public void setIsGraphData(boolean isGraphData){this.isGraphData = isGraphData;}
    public boolean getIsGraphData() {return isGraphData;}
    public boolean getIsDataError() {return isDataError;}

    public GraphicsFacade getGraphicsFacade() { return graphFacade;}
    public GraphOptionsInGraphics getGraphOptions() {return graphOptions;}
    public GraphDataInGraphics[] getGraphDataArr() {return graphData;}
    //A single graph dataset will select the first one in the array
    public GraphDataInGraphics getGraphData() {return getGraphDataArr()[0];}
    public DrawCrossHair getCrossHairDecorator() { return crossHairDecorator;}
    public DrawZoomBox getZoomDecorator() { return zoomDecorator;}

    /** Message string for writing on graph */
    public String getMessage() { return message;}
    public void setMessage(String _message) { message = _message;}

    /** Current XY positions of cursor */
    public void setCurrX(int xPixel) { currX = xPixel;}
    public int getCurrX() { return currX;}
    public void setCurrY(int yPixel) { currY = yPixel;}
    public int getCurrY() { return currY;}
    /** closest waveform to cursor */
    public int getClosestWave() { return this.closestGraphIdx;}

}
