/*
 * Copyright 2009 Dukascopy® (Suisse) SA. All rights reserved.
 * DUKASCOPY PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
 */
package jforex.indicators;

import java.awt.Color;
import java.awt.Font;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Shape;
import java.awt.Stroke;
import java.awt.geom.GeneralPath;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import com.dukascopy.api.Filter;
import com.dukascopy.api.IBar;
import com.dukascopy.api.Period;
import com.dukascopy.api.Unit;
import com.dukascopy.api.indicators.BooleanOptInputDescription;
import com.dukascopy.api.indicators.IDrawingIndicator;
import com.dukascopy.api.indicators.IIndicator;
import com.dukascopy.api.indicators.IIndicatorContext;
import com.dukascopy.api.indicators.IIndicatorDrawingSupport;
import com.dukascopy.api.indicators.IndicatorInfo;
import com.dukascopy.api.indicators.IndicatorResult;
import com.dukascopy.api.indicators.InputParameterInfo;
import com.dukascopy.api.indicators.IntegerListDescription;
import com.dukascopy.api.indicators.OptInputParameterInfo;
import com.dukascopy.api.indicators.OutputParameterInfo;
import com.dukascopy.api.indicators.OutputParameterInfo.DrawingStyle;
import com.dukascopy.api.indicators.OutputParameterInfo.Type;

public class PivotIndicator implements IIndicator, IDrawingIndicator {
    private IndicatorInfo indicatorInfo;
    private InputParameterInfo[] inputParameterInfos;
    private OutputParameterInfo[] outputParameterInfos;
    private OptInputParameterInfo[] optInputParameterInfos;
    
    private IBar[][] inputs = new IBar[2][];
    
    private double[][] outputs = new double[8][];
    private InputParameterInfo dailyInput;
    private DecimalFormat decimalFormat;
    
    private final GeneralPath generalPath = new GeneralPath(); 
    private List<Point> tmpHandlesPoints = new ArrayList<Point>();
    
    private boolean showHistoricalLevels = false;
    
    private IIndicatorContext context = null;
    
    
    public void onStart(IIndicatorContext context) {
    	this.context = context;
    	
        indicatorInfo = new IndicatorInfo("PIVOT", "Pivot", "Overlap Studies", true, false, true, 2, 2, 8);
        indicatorInfo.setSparceIndicator(true);
        indicatorInfo.setRecalculateAll(true);
        
        dailyInput = new InputParameterInfo("Input data", InputParameterInfo.Type.BAR);
        dailyInput.setPeriod(Period.DAILY_SUNDAY_IN_MONDAY);
        dailyInput.setFilter(Filter.WEEKENDS);
        inputParameterInfos = new InputParameterInfo[] {
        	new InputParameterInfo("Main Input data", InputParameterInfo.Type.BAR),
            dailyInput
        };
        
        int[] periodValues = new int[10];
        String[] periodNames = new String[10];
        periodValues[0] = 0;
        periodNames[0] = "1 Min";
        periodValues[1] = 1;
        periodNames[1] = "5 Mins";
        periodValues[2] = 2;
        periodNames[2] = "10 Mins";
        periodValues[3] = 3;
        periodNames[3] = "15 Mins";
        periodValues[4] = 4;
        periodNames[4] = "30 Mins";
        periodValues[5] = 5;
        periodNames[5] = "Hourly";
        periodValues[6] = 6;
        periodNames[6] = "4 Hours";
        periodValues[7] = 7;
        periodNames[7] = "Daily";
        periodValues[8] = 8;
        periodNames[8] = "Weekly";
        periodValues[9] = 9;
        periodNames[9] = "Monthly";

        optInputParameterInfos = new OptInputParameterInfo[] {
              new OptInputParameterInfo("Period", OptInputParameterInfo.Type.OTHER, new IntegerListDescription(7 , periodValues, periodNames)),
              new OptInputParameterInfo("Show historical levels", OptInputParameterInfo.Type.OTHER, new BooleanOptInputDescription(showHistoricalLevels))
        };

        outputParameterInfos = new OutputParameterInfo[] {
        	createOutputParameterInfo("Central Point (P)", OutputParameterInfo.Type.DOUBLE, OutputParameterInfo.DrawingStyle.LINE),
        	createOutputParameterInfo("Resistance (R1)", OutputParameterInfo.Type.DOUBLE, OutputParameterInfo.DrawingStyle.LINE),
        	createOutputParameterInfo("Support (S1)", OutputParameterInfo.Type.DOUBLE, OutputParameterInfo.DrawingStyle.LINE),
        	createOutputParameterInfo("Resistance (R2)", OutputParameterInfo.Type.DOUBLE, OutputParameterInfo.DrawingStyle.LINE),
        	createOutputParameterInfo("Support (S2)", OutputParameterInfo.Type.DOUBLE, OutputParameterInfo.DrawingStyle.LINE),
        	createOutputParameterInfo("Resistance (R3)", OutputParameterInfo.Type.DOUBLE, OutputParameterInfo.DrawingStyle.LINE),
        	createOutputParameterInfo("Support (S3)", OutputParameterInfo.Type.DOUBLE, OutputParameterInfo.DrawingStyle.LEVEL_LINE),
        	createOutputParameterInfo("Separators", OutputParameterInfo.Type.DOUBLE, OutputParameterInfo.DrawingStyle.LINE)
        };
        
        decimalFormat = new DecimalFormat("0.00000");
    }
    
    private OutputParameterInfo createOutputParameterInfo(String name, Type type, DrawingStyle drawingStyle) {
    	return new OutputParameterInfo(name, type, drawingStyle, false){{
			setDrawnByIndicator(true);
		}};
    }

    public IndicatorResult calculate(int startIndex, int endIndex) {
        if (startIndex > endIndex) {
            return new IndicatorResult(0, 0);
        }
        
        int i, j;
        
        IBar previousBar = null;
        int previousTimeIndex = -1;
        
        for (i = startIndex, j = 0; i <= endIndex; i++, j++) {
            //Inputs: 0 open, 1 close, 2 high, 3 low, 4 volume
        	int index = j;
        	
        	/*
        	 * Find the same time in inputs[i] as from inputs[0] by index
        	 */
        	int timeIndex = getTimeIndex(inputs[0][index].getTime(), inputs[1]);
        	
        	if (
        			previousBar != null &&
        			timeIndex > -1 &&
        			timeIndex != previousTimeIndex
        	) {
            	// P
            	double p = (previousBar.getClose() + previousBar.getHigh() + previousBar.getLow())/3;
            	outputs[0][index] = p;
            	// R1
            	outputs[1][index] = 2 * p - previousBar.getLow();
            	// S1
            	outputs[2][index] = 2 * p - previousBar.getHigh();
            	// R2
            	outputs[3][index] = p + previousBar.getHigh() - previousBar.getLow();
            	// S2
            	outputs[4][index] = p - previousBar.getHigh() + previousBar.getLow();
            	// R3
            	outputs[5][index] = p + 2 * previousBar.getHigh() - 2 * previousBar.getLow();
            	// S3
            	outputs[6][index] = p - 2 * previousBar.getHigh() + 2 * previousBar.getLow();
            	
            	outputs[7][index] = 0d;
            	
            } else {
                outputs[0][index] = Double.NaN;
                outputs[1][index] = Double.NaN;
                outputs[2][index] = Double.NaN;
                outputs[3][index] = Double.NaN;
                outputs[4][index] = Double.NaN;
                outputs[5][index] = Double.NaN;
                outputs[6][index] = Double.NaN;
                outputs[7][index] = Double.NaN;
            }
            
           	if (timeIndex > -1 && timeIndex != previousTimeIndex) {
           		previousBar = inputs[1][timeIndex];
       			previousTimeIndex = timeIndex;
            }
           	
        }
        
        return new IndicatorResult(startIndex, i);
    }
    
	private int getTimeIndex(long time, IBar[] target) {
    	if (target == null) {
    		return -1;
    	}

	    int first = 0;
	    int upto = target.length;
	    
	    while (first < upto) {
	        int mid = (first + upto) / 2;
	        
	        IBar data = target[mid];
	        
	        if (data.getTime() == time) {
	        	return mid;
	        }
        	else if (time < data.getTime()) {
	            upto = mid;
	        } 
	        else if (time > data.getTime()) {
	            first = mid + 1;
	        } 
	    }
	    
	    if (
	    		context != null && 
	    		Unit.Week.equals(context.getPeriod().getUnit())
	    ) {
	    	/*
	    	 * Special case for weeks, because week candles has different start times than sometimes don't match month candles start times
	    	 */
	    	for (int i = 1; i < target.length; i++) {
	    		IBar previousBar = target[i - 1];
	    		IBar trg = target[i];
	    		
	    		if (time == previousBar.getTime()) {
	    			return i - 1;
	    		}	
	    		else if (time == trg.getTime()) {
	    			return i;
	    		}
	    		else if (
	    				previousBar.getTime() < time &&
	    				time < trg.getTime() &&
	    				Math.abs(trg.getTime() - time) < context.getPeriod().getInterval()
	    		) {
    				return i;
	    		}
	    		
	    		if (previousBar.getTime() > time) {
	    			break;
	    		}
	    	}
	    }
    	
    	
    	return -1;
	}


	
    public IndicatorInfo getIndicatorInfo() {
        return indicatorInfo;
    }

    public InputParameterInfo getInputParameterInfo(int index) {
        if (index <= inputParameterInfos.length) {
            return inputParameterInfos[index];
        }
        return null;
    }

    public int getLookback() {
        return 0;
    }

    public int getLookforward() {
        return 0;
    }

    public OptInputParameterInfo getOptInputParameterInfo(int index) {
        if (index <= optInputParameterInfos.length) {
            return optInputParameterInfos[index];
        }
        return null;
    }

    public OutputParameterInfo getOutputParameterInfo(int index) {
        if (index <= outputParameterInfos.length) {
            return outputParameterInfos[index];
        }
        return null;
    }

    public void setInputParameter(int index, Object array) {
        inputs[index] = (IBar[]) array;
    }

    public void setOptInputParameter(int index, Object value) {
    	if (index == 0) {
    		int period = ((Integer) value).intValue();
    		switch (period) {
    		case 0 : dailyInput.setPeriod(Period.ONE_MIN);
    		break;
    		case 1 : dailyInput.setPeriod(Period.FIVE_MINS);
    		break;
    		case 2 : dailyInput.setPeriod(Period.TEN_MINS);
    		break;
    		case 3 : dailyInput.setPeriod(Period.FIFTEEN_MINS);
    		break;
    		case 4 : dailyInput.setPeriod(Period.THIRTY_MINS);
    		break;
    		case 5 : dailyInput.setPeriod(Period.ONE_HOUR);
    		break;
    		case 6 : dailyInput.setPeriod(Period.FOUR_HOURS);
    		break;
    		case 7 : dailyInput.setPeriod(Period.DAILY_SUNDAY_IN_MONDAY);
    		break;
    		case 8 : dailyInput.setPeriod(Period.WEEKLY);
    		break;
    		case 9 : dailyInput.setPeriod(Period.MONTHLY);
    		break;
    		default: dailyInput.setPeriod(Period.DAILY_SUNDAY_IN_MONDAY);
    		}
    	}
    	else if (index == 1) {
    		showHistoricalLevels = Boolean.valueOf(String.valueOf(value)).booleanValue();
    	}
    }

    public void setOutputParameter(int index, Object array) {
        outputs[index] = (double[]) array;
    }

	@Override
	public Point drawOutput(
			Graphics g,
			int outputIdx,
			Object values,
			Color color,
			Stroke stroke,
			IIndicatorDrawingSupport indicatorDrawingSupport,
			List<Shape> shapes,
			Map<Color, List<Point>> handles
	) {
		tmpHandlesPoints.clear();
		
		if (values != null) {
			Graphics2D g2 = (Graphics2D) g;
			generalPath.reset();
			
			double[] output = (double[]) values;
			
			g2.setColor(color);
			g2.setStroke(stroke);
			
			int spaceBetweenTwoSeparators = calculateMinSpaceBetweenTwoSeparators(output, indicatorDrawingSupport);
			int fontSize = calculateFontSize(spaceBetweenTwoSeparators, (int)indicatorDrawingSupport.getCandleWidthInPixels());
			boolean drawValues = canDrawValues(fontSize);
			
			if (outputIdx == 7) {
				if (drawValues) {
					drawSeparators(
							output,
							indicatorDrawingSupport, 
							generalPath,
							spaceBetweenTwoSeparators
					);
				}
			}
			else {
				drawPivotLevels(
						g2,
						outputIdx,
						output,
						indicatorDrawingSupport, 
						generalPath,
						fontSize,
						drawValues,
						spaceBetweenTwoSeparators
				);
			}
			
			g2.draw(generalPath);
			
			shapes.add((Shape) generalPath.clone()); // cloning path, so when checking for intersection each indicator has its own path
			handles.put(color, new ArrayList<Point>(tmpHandlesPoints));
		}
		
		return null;
	}
	
	private void drawPivotLevels(
			Graphics2D g2,
			int outputIdx,
			double[] output,
			IIndicatorDrawingSupport indicatorDrawingSupport,
			GeneralPath generalPath,
			int fontSize,
			boolean drawValues,
			int spaceBetweenTwoSeparators
	) {
		g2.setFont(new Font(g2.getFont().getName(), g2.getFont().getStyle(), fontSize));
		
		int maxX = indicatorDrawingSupport.getChartWidth() + spaceBetweenTwoSeparators; //JFOREX-2432
		int minX = -spaceBetweenTwoSeparators;
		
		Integer previousX = null;

		for (int i = output.length - 1; i >= 0; i --) {
			double d = output[i];
			
			if (Double.isNaN(d)) {
				continue;
			}
			
			int x = (int)indicatorDrawingSupport.getMiddleOfCandle(i);
			int y = (int)indicatorDrawingSupport.getYForValue(d);
			
			if (previousX == null) {
				previousX = new Integer(x + spaceBetweenTwoSeparators);
			}
			
			if (
					(minX <= previousX.intValue() && previousX.intValue() <= maxX) ||
					(minX <= x && x <= maxX)
			) {
				generalPath.moveTo(previousX.intValue(), y);
				generalPath.lineTo(x, y);
				
				if (drawValues) {
					String valueStr = decimalFormat.format(d);
					String lineCode = getLineCodeText(outputIdx);
					String result = lineCode + ": " + valueStr;
					
					int lineCodeX = x + 1;
					g2.drawString(result, lineCodeX, y - 2);
				}
				
				if (!showHistoricalLevels) {
					break;
				}
			}
			else if (
					x > maxX ||
					previousX.intValue() > maxX
			) {
				/*
				 * Means that the last actual period is out of screen, so don't draw anything in case if showHistoricalLevels == false
				 */
				if (!showHistoricalLevels) {
					break;
				}
			}
			
			previousX = Integer.valueOf(x);
		}
	}
	
	private int calculateMinSpaceBetweenTwoSeparators(
			double[] output,
			IIndicatorDrawingSupport indicatorDrawingSupport
	) {
		int space1 = calculateMinSpaceBetweenTwoSeparators(output, output.length - 1, indicatorDrawingSupport);
		int space2 = calculateMinSpaceBetweenTwoSeparators(output, output.length / 2, indicatorDrawingSupport);
		int space3 = calculateMinSpaceBetweenTwoSeparators(output, output.length / 3, indicatorDrawingSupport);
		
		return Math.max(space3, Math.max(space1, space2));
	}
	
	private int calculateMinSpaceBetweenTwoSeparators(
			double[] output,
			int startIndex,
			IIndicatorDrawingSupport indicatorDrawingSupport
	) {
		Integer x1 = null;
		Integer x2 = null;
		Double previousValue = null;
		
		for (int i = startIndex; i > -1; i --) {
			double d = output[i];
			if(
					!Double.isNaN(d) &&
					previousValue != null &&
					previousValue.doubleValue() != d
			) {
				x1 = new Integer((int)indicatorDrawingSupport.getMiddleOfCandle(i));
				
				if (x2 != null) {
					int dif = x2.intValue() - x1.intValue();
					return dif;
				}
				x2 = new Integer(x1.intValue());
			}
			previousValue = new Double(d);
		}
		
		return -1;
	}

	private boolean canDrawValues(int fontSize) {
		final int MIN_FONT_SIZE = 4;
		
		if (fontSize <= MIN_FONT_SIZE) {
			return false;
		}
		return true;
	}

	private int calculateFontSize(
			int spaceBetweenTwoSeparators,
			int candleWidthInPixels
	) {
		
		final int MAX_FONT_SIZE = 12;
		final int DIVISION_COEF = 7;
		
		spaceBetweenTwoSeparators /= DIVISION_COEF;
		spaceBetweenTwoSeparators = spaceBetweenTwoSeparators < 0 ? candleWidthInPixels : spaceBetweenTwoSeparators;
		
		return spaceBetweenTwoSeparators > MAX_FONT_SIZE ? MAX_FONT_SIZE : spaceBetweenTwoSeparators;
	}

	private void drawSeparators(
			double[] output,
			IIndicatorDrawingSupport indicatorDrawingSupport,
			GeneralPath generalPath,
			int spaceBetweenTwoSeparators
	) {
		int maxWidth = indicatorDrawingSupport.getChartWidth() + spaceBetweenTwoSeparators;
		int maxHeight = indicatorDrawingSupport.getChartHeight();
		
		Integer firstDrawnX = null;
		
		for (int i = output.length - 1; i >= 0; i --) {
			double d = output[i];
			
			if(!Double.isNaN(d)) {
				int x = (int)indicatorDrawingSupport.getMiddleOfCandle(i);
				
				if (firstDrawnX == null) {
					firstDrawnX = new Integer(x);
				}
				
				drawSeparator(
						generalPath,
						x,
						maxWidth,
						maxHeight
				);
				
				if (!showHistoricalLevels) {
					/*
					 * Don't draw separators further if user don't want them
					 */
					break;
				}
			}
		}
		
		drawSeparator(
				generalPath,
				(firstDrawnX == null ? 0 : firstDrawnX.intValue()) + spaceBetweenTwoSeparators,
				maxWidth,
				maxHeight
		);
	}

	private void drawSeparator(
			GeneralPath generalPath,
			int x,
			int maxWidth,
			int maxHeight
	) {
		if (0 <= x && x <= maxWidth) {
			generalPath.moveTo(x, 0);
			generalPath.lineTo(x, maxHeight);
			
			tmpHandlesPoints.add(new Point(x, 5));
			tmpHandlesPoints.add(new Point(x, maxHeight/2));
			tmpHandlesPoints.add(new Point(x, maxHeight - 5));
		}
	}

	private String getLineCodeText(int outputIdx) {
		String lineCode = "";
		switch (outputIdx) {
			case 0 : lineCode = "P"; break;
			case 1 : lineCode = "R1"; break;
			case 2 : lineCode = "S1"; break;
			case 3 : lineCode = "R2"; break;
			case 4 : lineCode = "S2"; break;
			case 5 : lineCode = "R3"; break;
			case 6 : lineCode = "S3"; break;
			default: throw new IllegalArgumentException("Illegal outputIdx - " + outputIdx);
		}
		return lineCode;
	}
	
}
