package jforex.strategies.rangebars;

import java.text.DecimalFormat;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.TimeZone;

import com.dukascopy.api.*;
import com.dukascopy.api.IIndicators.MaType;
import com.dukascopy.api.bar.IRangeBar;
import com.dukascopy.api.indicators.IIndicator;
import com.dukascopy.api.listener.IRangeBarFeedListener;

/**
 * The strategy shows how to emulate range bar history.
 * Also it shows how to use range bar close prices with MA indicator.
 *
 */

@RequiresFullAccess
public class RangeBarsHistory2Fix implements IStrategy {

	private IConsole console;
	private IHistory history;
	private IIndicators indicators;
	private SimpleDateFormat sdf;

	@Configurable("Instrument")
	public Instrument instrument = Instrument.EURUSD;
	@Configurable("Offer Side")
	public OfferSide offerSide = OfferSide.BID;
	@Configurable("Price range (pips)")
	public int priceRangePips = 2;
	@Configurable("MA TimePeriod")
	public int maTimePeriod = 5;
	@Configurable("MA Shift")
	public int maShift = 0;
	@Configurable("Log values")
	public boolean logValues = true;
	@Configurable("(config) History Chunk Size")
	public Period historyChunkSize = Period.ONE_MIN;

	public static DecimalFormat df = new DecimalFormat("0.00000");
	private PriceRange priceRange;
	
	@Override
	public void onStart(IContext context) throws JFException {

		this.history = context.getHistory();
		this.console = context.getConsole();
		this.indicators = context.getIndicators();
		log("start");
		priceRange = PriceRange.valueOf(priceRangePips);

		sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
		sdf.setTimeZone(TimeZone.getTimeZone("GMT"));
		
		context.subscribeToRangeBarFeed(instrument, offerSide, priceRange, 
				new IRangeBarFeedListener() {
					public void onBar(Instrument instrument, OfferSide offerSide, PriceRange priceRange, IRangeBar bar) {
						log("Received Range Bar: " + instrument + ", "  + offerSide + ", " + priceRange + ", " + bar);
						
						//previous range bars start at this range bar start time -1 milli
						long histRangeBarStartTime = bar.getTime() - 1;
						
						//this much history price values we will need for indicator calculation
						int histBarCount = maShift + maTimePeriod;
						List<IRangeBar> rangeBars = null;
						try {
							rangeBars = getHistoryRangeBars(histBarCount, histRangeBarStartTime);
						} catch (JFException e) {
							console.getErr().println(e);
						}
						//add last formed range bar 
						rangeBars.add(bar);
						
						//log range bars (note that the last one will be the platform range bar with a bit different toString method
						for(IRangeBar rangeBar : rangeBars){
							log(rangeBar);
						}
						
						//get close prices
						double[] closePrices = new double[rangeBars.size()];
						for(int i =0; i< rangeBars.size(); i++){
							closePrices[i] = rangeBars.get(i).getClose();
						}
						
						double ma = getMa(maTimePeriod, maShift,closePrices);
						print("MA value with period " + maTimePeriod+ " and shift " + maShift +": "+ df.format(ma));
						
					}
				});
		


		
		//context.stop();
	}

	/**
	 * Returns historical range bars starting from histEndTime.
	 * 
	 * @param count range bar count
	 * @param histEndTime end time of the latest range bar
	 * @return
	 * @throws JFException
	 */
	private List<IRangeBar> getHistoryRangeBars(int count, long histEndTime) throws JFException {

		long chunkInterval = historyChunkSize.getInterval();
		List<IRangeBar> rangeBars = new ArrayList<IRangeBar>();
		MockRangeBar rangeBar = null;
		ITick tick;
		double range = instrument.getPipValue() * priceRange.getPipCount();
		double maxMove;

		while (rangeBars.size() < count) {
			//read ticks in chunks
			List<ITick> ticks = history.getTicks(instrument, histEndTime - chunkInterval, histEndTime);
			for (int i = ticks.size() - 1; i >= 0; i--) {
				tick = ticks.get(i);
				
				double price = offerSide == OfferSide.BID ? tick.getBid() : tick.getAsk();
				double volume = offerSide == OfferSide.BID ? tick.getBidVolume() : tick.getAskVolume();				
				
				maxMove = rangeBar == null 
						? 0 
						: Math.max(Math.max(Math.abs(rangeBar.open - price), Math.abs(rangeBar.high - price)),Math.abs(rangeBar.low - price));
				//last price broke the range - create new range bar 
				if (rangeBar == null || maxMove > range) {
					if (rangeBar != null) {
						//store previous one
						rangeBars.add(rangeBar);
					}
					rangeBar = new MockRangeBar(price, volume, tick.getTime());
					continue;
				}

				if (rangeBar.high < price) {
					rangeBar.high = price;
				}
				if (rangeBar.low > price) {
					rangeBar.low = price;
				}
				rangeBar.close = price;
				rangeBar.vol += volume;
				rangeBar.startTime = tick.getTime();
				rangeBar.tickCount++;

				if (rangeBars.size() >= count) {
					break;
				}
			}
			if (rangeBars.size() >= count) {
				break;
			}
			histEndTime -= chunkInterval;
		}

		//reverse such that the latest range bar is the last one
		Collections.reverse(rangeBars);
		return rangeBars;

	}
	
	/**
	 * Calculates MA over an array, see more:
	 * http://www.dukascopy.com/wiki/index.php?title=Strategy_API:_Indicators#Calculate_indicator_on_array
	 * 
	 * @param timePeriod
	 * @param shift
	 * @param priceArr
	 * @return
	 */
	private double getMa(int timePeriod, int shift, double [] priceArr ) {
		IIndicator maIndicator = indicators.getIndicator("MA");
		 
		//set optional inputs
		maIndicator.setOptInputParameter(0, timePeriod);
		maIndicator.setOptInputParameter(1, MaType.SMA.ordinal());
		 
		//set inputs
		maIndicator.setInputParameter(0, priceArr);
		 
		//set outputs
		double [] resultArr = new double [priceArr.length];
		maIndicator.setOutputParameter(0, resultArr);
		 
		//calculate
		maIndicator.calculate(0, priceArr.length - 1);
		print("ma result array: " + arrayToString(resultArr));
		
		int index = resultArr.length - shift - 1 - maIndicator.getLookback();
		double result = resultArr[index];
		return result;
	}

	
	private void print(Object o){
		console.getOut().println(o);
	}
	
	private void log(Object o){
		if(logValues){
			print(o);
		}
	}
	
	public static String arrayToString(double[] arr) {
		String str = "";
		for (int r = 0; r < arr.length; r++) {
			str += "[" + r + "] " + df.format(arr[r]) + "; ";
		}
		return str;
	}

	@Override
	public void onTick(Instrument instrument, ITick tick) throws JFException {}

	@Override
	public void onBar(Instrument instrument, Period period, IBar askBar, IBar bidBar) throws JFException {}

	@Override
	public void onMessage(IMessage message) throws JFException {}

	@Override
	public void onAccount(IAccount account) throws JFException {}

	@Override
	public void onStop() throws JFException {}

	/**
	 * Custom implementation of a IRangeBar, which allows us to create range bars ourselves.
	 *
	 */
	public class MockRangeBar implements IRangeBar {

		public double open;
		public double close;
		public double low;
		public double high;
		public double vol;
		public long startTime;
		public long endTime;
		public int tickCount;

		@SuppressWarnings("serial")
		private SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS") {
			{
				setTimeZone(TimeZone.getTimeZone("GMT"));
			}
		};
		private DecimalFormat df = new DecimalFormat("0.00000");

		public MockRangeBar() {

		}

		public MockRangeBar(double price, double volume, long time) {
				open = close = low = high = price;
				vol = volume;
				startTime = time;
				endTime = time;
				tickCount = 1;
		}

		public MockRangeBar(double open, double close, double low, double high, double vol, long startTime, long endTime, int tickCount) {
			this.open = open;
			this.close = close;
			this.low = low;
			this.high = high;
			this.vol = vol;
			this.startTime = startTime;
			this.endTime = endTime;
			this.tickCount = tickCount;
		}

		@Override
		public double getOpen() {
			return open;
		}

		@Override
		public double getClose() {
			return close;
		}

		@Override
		public double getLow() {
			return low;
		}

		@Override
		public double getHigh() {
			return high;
		}

		@Override
		public double getVolume() {
			return vol;
		}

		@Override
		public long getTime() {
			return startTime;
		}

		@Override
		public String toString() {

			StringBuilder str = new StringBuilder();
			str.append("StartTime: ").append(sdf.format(startTime)).append(" EndTime: ").append(sdf.format(endTime)).append(" O: ")
					.append(df.format(open)).append(" C: ").append(df.format(close)).append(" H: ").append(df.format(high)).append(" L: ")
					.append(df.format(low)).append(" V: ").append(df.format(vol)).append(" TickCount: ").append(tickCount);
			return str.toString();
		}

		@Override    
		public long getEndTime() {
			return endTime;
		}

		@Override
		public long getFormedElementsCount() {
			return tickCount;
		}

	}

}
