/*******************************************************************************
 * Copyright 2013-2015 alladin-IT GmbH
 * Copyright 2013-2015 Rundfunk und Telekom Regulierungs-GmbH (RTR-GmbH)
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *   http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 ******************************************************************************/
package at.rtr.rmbt.android.test;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Paint.Cap;
import android.graphics.Paint.Join;
import android.graphics.Paint.Style;
import android.graphics.Path;
import android.graphics.PointF;
import android.os.Bundle;
import at.rtr.rmbt.android.graphview.GraphService;
import at.rtr.rmbt.android.graphview.GraphView;

public class SmoothGraph implements GraphService {

	public final static int FLAG_NONE = 0;
	public final static int FLAG_ALIGN_RIGHT = 1;
	public final static int FLAG_ALIGN_LEFT = 2;
	
	/**
	 * some functions don't calculate the current point but previous ones (like the centered moving avarage).
	 * this flag let the smooth graph take the position of the current added node to draw the current calcutaled avarage (instead of a previous time) 
	 */
	public final static int FLAG_USE_CURRENT_NODE_TIME = 4;
	
	public static final String OPTION_STARTTIME = "startTime";
	public static final String OPTION_FIRSTPOINT = "firstPoint";
	public static final String OPTION_VALUELIST = "valueList";

	
	/**
	 * available smoothing function
	 * @author lb
	 *
	 */
	public enum SmoothingFunction {
		/**
		 * equal number of data on each side<br>
		 * function for centered moving avarage with n amount of data for element with the index x:<br>
		 * f(x, n) = 1/n * (element(x-n/2) + element(x-n/2+1) + ... + element (x-n/2+n))<br>
		 */
		CENTERED_MOVING_AVARAGE,
		
		/**
		 * previous n number of data<br>
		 * function for simple moving avarage with n amount of data for element with the index x:<br>
		 * f(x, n) = 1/n * (element(x-n) + element(x-n+1) + ... + element(x))<br>
		 */
		SIMPLE_MOVING_AVARAGE;
		
		public static double smooth(final SmoothingFunction smoothingFunction, final int element, final List<ValueEntry> valueList, final int dataAmount) {
			if (valueList == null || valueList.size() < 1) {
				return 0d;
			}

			int startingIndex = 0;
			int realDataAmount = SmoothingFunction.getDataAmountNeeded(smoothingFunction, dataAmount);
			
			switch (smoothingFunction) {
			case CENTERED_MOVING_AVARAGE:
				startingIndex = element - dataAmount/2;
				break;
			case SIMPLE_MOVING_AVARAGE:
				startingIndex = element - dataAmount;
				break;
			default:
				startingIndex = 0;
			}
			
			double sum = 0d;
			
			if (startingIndex < 0) {
				final int underflow = Math.abs(startingIndex);
				sum += underflow * valueList.get(0).value;
				realDataAmount -= underflow;
				startingIndex = 0;
			}
						
			if (startingIndex >= valueList.size()) {
				return 0d;
			}
			
			if ((startingIndex + realDataAmount) > valueList.size()) {
				final int overlfow = Math.abs(valueList.size() - (startingIndex + realDataAmount));
				sum += overlfow * valueList.get(valueList.size()-1).value;
				realDataAmount -= overlfow;
			}
				
			for (int i = startingIndex; i < (startingIndex + realDataAmount); i++) {
				sum += valueList.get(i).getValue();
//				System.out.print("[i:" + i + ", sum: " + sum + "]");
			}
			
			return (sum / (double)SmoothingFunction.getDataAmountNeeded(smoothingFunction, dataAmount));
		}
		
		public static int getDataAmountNeeded(SmoothingFunction smoothingFunction, int dataAmount) {
			switch (smoothingFunction) {
			case CENTERED_MOVING_AVARAGE:
				return dataAmount;
			case SIMPLE_MOVING_AVARAGE:
				return dataAmount;
			default:
				return 0;
			}
		}
	}
	
	private class ValueEntry {
		protected final double value;
		protected final double time;
		protected int flag;
		
		public ValueEntry(final double value, final double time, final int flag) {
			this.value = value;
			this.time = time;
			this.flag = flag;
		}

		public double getValue() {
			return value;
		}

		public double getTime() {
			return time;
		}
		
		public int getFlag() {
			return flag;
		}
	}
	
    private final float height;
    private final float width;
    private final List<ValueEntry> valueList = new ArrayList<ValueEntry>();
    private final Path pathStroke;
    private final Path pathFill;
    private final Paint paintStroke;
    private final Paint paintFill;
    private PointF firstPoint;
    
    private SmoothingFunction smoothingFunction = SmoothingFunction.CENTERED_MOVING_AVARAGE;
    private int dataAmount = 0;
    
    private boolean matchHorizontally = false;
    
    private long startTime = -1;
    private long maxTimeNs = 0;
    
    /**
     * 
     * @param graphView
     * @param color
     * @param dataAmount number of data to use for smoothing function
     * @return
     */
    public static SmoothGraph addGraph(final GraphView graphView, final int color, final int dataAmount, final SmoothingFunction smoothingFunction) {
    	return SmoothGraph.addGraph(graphView, color, dataAmount, smoothingFunction, true);
    }
    
    /**
     * 
     * @param graphView
     * @param color
     * @param dataAmount number of data to use for smoothing function
     * @param matchHorizontally
     * @return
     */
    public static SmoothGraph addGraph(final GraphView graphView, final int color, final int dataAmount, final SmoothingFunction smoothingFunction, final boolean matchHorizontally)
    {
        final SmoothGraph graph = new SmoothGraph(color, graphView.getGraphWidth(), graphView.getGraphHeight(),
                graphView.getGraphStrokeWidth());
        graph.setMatchHorizontally(matchHorizontally);
        graph.setSmoothingFunction(smoothingFunction);
        graph.setDataAmount(dataAmount);
        graphView.addGraph(graph);
        return graph;
    }
    
    /**
     * 
     * @param graphView
     * @param dataAmount
     * @param smoothingFunction
     * @param matchHorizontally
     * @param graphData
     * @return
     */
    @SuppressWarnings("unchecked")
	public static SmoothGraph addGraph(final GraphView graphView,final int dataAmount, final SmoothingFunction smoothingFunction, 
    		final boolean matchHorizontally, final GraphData graphData) {
    	final SmoothGraph graph = SmoothGraph.addGraph(graphView, dataAmount, smoothingFunction, matchHorizontally, graphData.getPathStroke(), graphData.getPathFill(), 
    			graphData.getPaintStroke(), graphData.getPaintFill());
    	
    	graph.valueList.addAll((Collection<? extends ValueEntry>) graphData.getOptions().getSerializable(OPTION_VALUELIST));
    	if (graphData.getOptions().getParcelable(OPTION_FIRSTPOINT) != null) {
    		graph.firstPoint = new PointF();
    		graph.firstPoint.set((PointF) graphData.getOptions().getParcelable(OPTION_FIRSTPOINT));
    	}
    	graph.startTime = graphData.getOptions().getLong(OPTION_STARTTIME);
    	
    	return graph;
    }
    
    /**
     * 
     * @param graphView
     * @param color
     * @param dataAmount
     * @param smoothingFunction
     * @param matchHorizontally
     * @param pathStroke
     * @param pathFill
     * @param paintStroke
     * @param paintFill
     * @return
     */
    public static SmoothGraph addGraph(final GraphView graphView,final int dataAmount, final SmoothingFunction smoothingFunction, 
    		final boolean matchHorizontally, final Path pathStroke, final Path pathFill, final Paint paintStroke, final Paint paintFill)
    {
        final SmoothGraph graph = new SmoothGraph(graphView.getGraphWidth(), graphView.getGraphHeight(), pathStroke, pathFill, paintStroke, paintFill);
        graph.setMatchHorizontally(matchHorizontally);
        graph.setSmoothingFunction(smoothingFunction);
        graph.setDataAmount(dataAmount);
        graphView.addGraph(graph);
        return graph;
    }
    
    
    public SmoothingFunction getSmoothingFunction() {
		return smoothingFunction;
	}

	public void setSmoothingFunction(SmoothingFunction smoothingFunction) {
		this.smoothingFunction = smoothingFunction;
	}
	
	public int getDataAmount() {
		return dataAmount;
	}

	public void setDataAmount(int dataAmount) {
		this.dataAmount = dataAmount;
	}

	private SmoothGraph(final int color, final float width, final float height, final float strokeWidth)
    {
        this.height = height;
        this.width = width;
        
        paintStroke = new Paint();
        paintStroke.setColor(color);
        paintStroke.setAlpha(204); // 80%
        paintStroke.setStyle(Style.STROKE);
        paintStroke.setStrokeWidth(strokeWidth);
        paintStroke.setStrokeCap(Cap.ROUND);
        paintStroke.setStrokeJoin(Join.ROUND);
        paintStroke.setAntiAlias(true);
        
        paintFill = new Paint();
        paintFill.setColor(color);
        paintFill.setAlpha(51); // 20%
        paintFill.setStyle(Style.FILL);
        paintFill.setAntiAlias(true);

        pathStroke = new Path();
        pathFill = new Path();
    }
	
	private SmoothGraph(final float width, final float height, final Path pathStroke, final Path pathFill, final Paint paintStroke, final Paint paintFill) {
		this.height = height;
		this.width = width;
		this.paintFill = new Paint(paintFill);
		this.paintStroke = new Paint(paintStroke);
		this.pathFill = new Path(pathFill);
		this.pathStroke = new Path(pathStroke);
	}
    
    /*
     * (non-Javadoc)
     * @see at.alladin.rmbt.android.test.Graph#addValue(double)
     */
    public void addValue(double value) {
    	addValue(value, FLAG_NONE);
    }
    
    public void addValue(double value, int flag) {
        final long relTime;
        if (startTime == -1) {
            startTime = System.nanoTime();
            relTime = 0;
        }
        else {
            relTime = System.nanoTime() - startTime;
        }
        
        if (relTime >= maxTimeNs) return;

        final double time = (double)relTime / (double)maxTimeNs;
        
    	addValue(value, time, flag);
    }

	/*
	 * (non-Javadoc)
	 * @see at.alladin.rmbt.android.graphview.GraphService#addValue(double, double)
	 */
	public void addValue(double value, double time) {
		addValue(value, time, FLAG_NONE);
	}

    /*
     * (non-Javadoc)
     * @see at.alladin.rmbt.android.graphview.GraphService#addValue(double, double, int)
     */
    public void addValue(double value, double time, int flag)
    {
        if (value < 0d) {
        	value = 0d;
        }
        else if (value > 1d) {
        	value = 1d;
        }
        
        if (time < 0d) {
        	time = 0d;
        }
        else if (time > 1d) {
        	time = 1d;
        }
        
        valueList.add(new ValueEntry(value, time, flag));
        
        if (valueList.size() >= SmoothingFunction.getDataAmountNeeded(smoothingFunction, dataAmount)) {
        	
        	final int index;
        	final int timeIndex;
        	
        	switch (smoothingFunction) {
        	case CENTERED_MOVING_AVARAGE:
        		index = valueList.size() - dataAmount / 2 - 1;
        		break;
        	case SIMPLE_MOVING_AVARAGE:
        		index = valueList.size() - 1;
        		break;
        	default:
        		index = dataAmount;
        		break;
        	}
        	
        	if ((flag & FLAG_USE_CURRENT_NODE_TIME) == FLAG_USE_CURRENT_NODE_TIME) {
        		timeIndex = valueList.size() - 1;
        	}
        	else {
        		timeIndex = index;
        	}
        	
            if (firstPoint == null) {
            	for (int i = 0; i < index; i++) {
                	value = SmoothingFunction.smooth(smoothingFunction, i, valueList, dataAmount);
                	time = valueList.get(i).getTime();
                    final float x = getXCoord(time, valueList.get(i).getFlag());            
                    final float y = (float) (height * (1 - value));
                	
                	if (firstPoint == null) {
                		pathStroke.moveTo(x, y);
                		firstPoint = new PointF(x, y);
                	}
                	else {
                		pathStroke.lineTo(x, y);
                        pathFill.rewind();
                        pathFill.addPath(pathStroke);
                        pathFill.lineTo(x, height);
                       	pathFill.lineTo(firstPoint.x, height);
                	}
            	}
            }
        	
        	value = SmoothingFunction.smooth(smoothingFunction, index, valueList, dataAmount);
        	time = valueList.get(timeIndex).getTime();
            final float x = getXCoord(time, flag);            
            final float y = (float) (height * (1 - value));
            pathStroke.lineTo(x, y);
            pathFill.rewind();
            pathFill.addPath(pathStroke);
            pathFill.lineTo(x, height);
           	pathFill.lineTo(firstPoint.x, height);
        }
    }
    
    protected float getXCoord(final double time, final int flag) {
        if ((flag & FLAG_ALIGN_LEFT) == FLAG_ALIGN_LEFT) {
        	return 0f;
        }
       	else if ((flag & FLAG_ALIGN_RIGHT) == FLAG_ALIGN_RIGHT) {
       		return width;
       	}
       	else {
       		return (float) (width * time);
       	}
    }
    
    public void draw(final Canvas canvas)
    {
    	if (valueList.size() == 1 && isMatchHorizontally()) {
    		pathStroke.lineTo(width, firstPoint.y);
    		pathFill.rewind();
    		pathFill.addPath(pathStroke);
    		pathFill.lineTo(width, height);
    		pathFill.lineTo(0, height);
    	}
    	
        canvas.drawPath(pathStroke, paintStroke);
        canvas.drawPath(pathFill, paintFill);
    }
    
    public void reset()
    {
    	firstPoint = null;
    	valueList.clear();
        pathStroke.rewind();
        pathFill.rewind();
        startTime = -1;
    }
    
    public boolean hasBeenStarted()
    {
        return true;
    }
    
    public void clearGraphDontResetTime()
    {
    	firstPoint = null;
    	valueList.clear();
        pathStroke.rewind();
        pathFill.rewind();        
    }

	public boolean isMatchHorizontally() {
		return matchHorizontally;
	}

	public void setMatchHorizontally(boolean matchHorizontally) {
		this.matchHorizontally = matchHorizontally;
	}
	
    /**
     * 
     * @param alpha
     */
    public void setPaintAlpha(int alpha) {
    	paintStroke.setAlpha(alpha);
    }
    
    /**
     * 
     * @return
     */
    public int getPaintAlpha() {
    	return paintStroke.getAlpha();
    }
    
    
    /**
     * 
     * @param alpha
     */
    public void setFillAlpha(int alpha) {
    	paintFill.setAlpha(alpha);
    }
    
    /**
     * 
     * @return
     */
    public int getFillAlpha() {
    	return paintFill.getAlpha();
    }

	@Override
	public void setMaxTime(long maxTimeNs) {
		this.maxTimeNs = maxTimeNs;
	}

	@Override
	public Path getPathStroke() {
		return pathStroke;
	}

	@Override
	public Path getPathFill() {
		return pathFill;
	}

	@Override
	public Paint getPaintStroke() {
		return paintStroke;
	}

	@Override
	public Paint getPaintFill() {
		return paintFill;
	}
	
	/*
	 * (non-Javadoc)
	 * @see at.alladin.rmbt.android.graphview.GraphService#getGraphData()
	 */
	@Override
	public GraphData getGraphData() {
		final Bundle options = new Bundle();
		options.putLong(OPTION_STARTTIME, startTime);
		options.putSerializable(OPTION_VALUELIST, (Serializable) valueList);
		options.putParcelable(OPTION_FIRSTPOINT, firstPoint);

		return new GraphData(pathStroke, pathFill, paintStroke, paintFill, options);
	}
}