package jforex.strategies;

import java.text.DecimalFormat;
import java.text.SimpleDateFormat;
import java.util.Iterator;
import java.util.Queue;
import java.util.TimeZone;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ConcurrentLinkedQueue;

import com.dukascopy.api.*;
import com.dukascopy.api.IEngine.OrderCommand;
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 SIMPLESTOC_Strat2 implements IStrategy {

    public static final int OPEN = 0;
    public static final int CLOSE = 1;
    public static final int HIGH = 2;
    public static final int LOW = 3;
    public static final int MED = 4;
    public static final int HIST = 2;
    private IConsole console;
    private IHistory history;
    private IEngine engine;
    private IIndicators indicators;
    private SimpleDateFormat sdf;    
    @Configurable("Price range (pips)")
    public int priceRangePips = 2; 
    // range bars
    @Configurable("Instrument")
    public Instrument instrument = Instrument.EURUSD;
    @Configurable("Offer Side")
    public OfferSide offerSide = OfferSide.BID;
    @Configurable("Amount")
    public double amount = 0.02;
    @Configurable("Slippage")
    public double slippage = 0;
    @Configurable("Take profit pips")
    public int takeProfitPips = 0;
    @Configurable("Stop loss in pips")
    public int stopLossPips = 0;
    // MACD
    @Configurable("MACD Fast period")
    public int fastMACDPeriod = 12;
    @Configurable("MACD Slow period")
    public int slowMACDPeriod = 26;
    @Configurable("MACD Signal period")
    public int signalMACDPeriod = 9;
    // STOCH
    @Configurable("STOCH Fast K period")
    public int fastKPeriod = 5;
    @Configurable("STOCH Slow K period")
    public int slowKPeriod = 3;
    @Configurable("STOCH K MA Type")
    public MaType slowKMaType = MaType.SMA;
    @Configurable("STOCH Slow D period")
    public int slowDPeriod = 5;
    @Configurable("STOCH D MA Type")
    public MaType slowDMaType = MaType.SMA;
    public static DecimalFormat df = new DecimalFormat("0.00000");
    private PriceRange priceRange;
    private BlockingQueue<IRangeBar> rangeBars;
    private int maxShift = 1;
    private int period;
    private IIndicator macd;
    private IIndicator stoch;
    

    private IOrder order;
    private int counter;
    
    //Support: order request queue - it is filled in range bar feed and emptied in next onTick
	private Queue<OrderRequest> orderRequests = new ConcurrentLinkedQueue<OrderRequest>();

    @Override
    public void onStart(IContext context) throws JFException {

        this.history = context.getHistory();
        this.console = context.getConsole();
        this.indicators = context.getIndicators();

        this.engine = context.getEngine();

        priceRange = PriceRange.valueOf(priceRangePips);

        sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        sdf.setTimeZone(TimeZone.getTimeZone("GMT"));


        macd = indicators.getIndicator("MACD");

        //set optional inputs
        macd.setOptInputParameter(0, fastMACDPeriod);
        macd.setOptInputParameter(1, slowMACDPeriod);
        macd.setOptInputParameter(2, signalMACDPeriod);

        stoch = indicators.getIndicator("STOCH");

        //set optional inputs
        stoch.setOptInputParameter(0, fastKPeriod);
        stoch.setOptInputParameter(1, slowKPeriod);
        stoch.setOptInputParameter(2, slowKMaType.ordinal());
        stoch.setOptInputParameter(3, slowDPeriod);
        stoch.setOptInputParameter(4, slowDMaType.ordinal());

        period = Math.max(macd.getLookback(), stoch.getLookback()) + 1;
        rangeBars = new ArrayBlockingQueue<IRangeBar>(maxShift + period);

        context.subscribeToRangeBarFeed(instrument, offerSide, priceRange,
                new IRangeBarFeedListener() {

                    public void onBar(Instrument instrument, OfferSide offerSide, PriceRange priceRange, IRangeBar bar) {
                    	
                    	//Support: reduced nesting
                        try {     
                        	execOnBar(bar);                    	 
                        } catch (JFException ex) {
                            ex.printStackTrace(console.getErr());
                        }
                    }
        }
        );


    }
    
    private void execOnBar(IRangeBar bar) throws JFException{   	
        	
            double[][] closePrices = getClosePrices(bar);

            if (!isActive(order)) {
                order = null;
            }

            // MACD SIGNAL
            OrderCommand macdSign = null;
            OrderCommand stochSign = null;

            double[] macd0 = getMACD(closePrices[CLOSE], 0);
            double[] macd1 = getMACD(closePrices[CLOSE], 1);

            if (macd0[HIST] > 0 /*&& macd1[HIST] <= 0*/) {
                macdSign = OrderCommand.BUY;
            }
            if (macd0[HIST] < 0 /*&& macd1[HIST] >= 0*/) {
                macdSign = OrderCommand.SELL;
            }

            // STOCH SIGNAL  
            int K = 0;
            int D = 1;
            double[] stoch0 = getSTOCH(closePrices, 0);
            double[] stoch1 = getSTOCH(closePrices, 1);

            if (stoch0[K] > stoch0[D] && stoch1[K] <= stoch1[D]) {
                stochSign = OrderCommand.BUY;

            } else if (stoch0[K] < stoch0[D] && stoch1[K] >= stoch1[D]) {
                stochSign = OrderCommand.SELL;
            }

            // PLACE ORDER      
            //Support: we enqueue order instead of placing it
            orderRequests.offer(new OrderRequest(macdSign, stochSign));

    
    }
    
    //Support: object holding needed information for order submission - note that you can add here any additional information
    // that you may find relevant at the moment of the order creation
    private class OrderRequest{
    	
       public final OrderCommand macdSign;
       public final OrderCommand stochSign;
       
       public OrderRequest(OrderCommand macdSign, OrderCommand stochSign){
    	   this.macdSign = macdSign;
    	   this.stochSign = stochSign;
       }
    	
    }
    

    private double[][] getClosePrices(IRangeBar bar) {

        if (rangeBars.remainingCapacity() == 0) {
            //our queue is full
            //lets take out the oldest range bar
            print("Our Queue is full, Removing item: " + rangeBars.poll());
        }

        //store the new range bar
        rangeBars.offer(bar);

        if (rangeBars.remainingCapacity() > 0) {
            print("we do not have enough items for calculating indicator values");
            return null;
        }

        //get close prices                        
        double[][] closePrices = new double[5][rangeBars.size()];

        Iterator<IRangeBar> barsIterator = rangeBars.iterator();
        int i = 0;
        while (barsIterator.hasNext()) {
            IRangeBar rangeBar = barsIterator.next();

            closePrices[OPEN][i] = rangeBar.getOpen();
            closePrices[CLOSE][i] = rangeBar.getClose();
            closePrices[HIGH][i] = rangeBar.getHigh();
            closePrices[LOW][i] = rangeBar.getLow();
            closePrices[MED][i] = (rangeBar.getHigh() - rangeBar.getLow()) / 2;
            
            i++;
        }

        return closePrices;
    }
    
    private void placeOrder(OrderCommand stochSign, OrderCommand macdSign) throws JFException {
    	
        if (stochSign == OrderCommand.BUY) {
            if (order != null) {
                if (!order.isLong()) {
                    order.close();
                    order = submitOrder(OrderCommand.BUY);
                }
            } else {
                order = submitOrder(OrderCommand.BUY);
            }

        } else if (stochSign == OrderCommand.SELL) {
            if (order != null) {
                if (order.isLong()) {
                    order.close();
                    order = submitOrder(OrderCommand.SELL);
                }
            } else {
                order = submitOrder(OrderCommand.SELL);
            }
        }
    }
    
    private IOrder submitOrder(OrderCommand orderCmd) throws JFException {

        double stopLossPrice = 0.0, takeProfitPrice = 0.0;

        // Calculating order price, stop loss and take profit prices
        if (orderCmd == OrderCommand.BUY) {
            if (stopLossPips > 0) {
                stopLossPrice = history.getLastTick(instrument).getBid() - getPipPrice(stopLossPips);
            }
            if (takeProfitPips > 0) {
                takeProfitPrice = history.getLastTick(instrument).getBid() + getPipPrice(takeProfitPips);
            }
        } else {
            if (stopLossPips > 0) {
                stopLossPrice = history.getLastTick(instrument).getBid() + getPipPrice(stopLossPips);
            }
            if (takeProfitPips > 0) {
                takeProfitPrice = history.getLastTick(instrument).getBid() - getPipPrice(takeProfitPips);
            }
        }

        return engine.submitOrder(getLabel(instrument), instrument, orderCmd, amount, 0, slippage, stopLossPrice, takeProfitPrice);
    }

    private void closeOrder(IOrder order) throws JFException {
        if (order == null) {
            return;
        }
        if (order.getState() != IOrder.State.CLOSED && order.getState() != IOrder.State.CREATED && order.getState() != IOrder.State.CANCELED) {
            order.close();
            order = null;
        }
    }

    private boolean isActive(IOrder order) throws JFException {
        if (order != null && order.getState() != IOrder.State.CLOSED && order.getState() != IOrder.State.CREATED && order.getState() != IOrder.State.CANCELED) {
            return true;
        }
        return false;
    }

    private double getPipPrice(int pips) {
        return pips * instrument.getPipValue();
    }

    private String getLabel(Instrument instrument) {
        String label = instrument.name();
        label = label + (counter++);
        label = label.toUpperCase();
        return label;
    }

    private double[] getMACD(double[] priceArr, int shift) {

        //set inputs
        macd.setInputParameter(0, priceArr);

        //set outputs
        double[][] resultArr = new double[3][priceArr.length];
        macd.setOutputParameter(0, resultArr[0]);
        macd.setOutputParameter(1, resultArr[1]);
        macd.setOutputParameter(2, resultArr[2]);

        macd.calculate(0, priceArr.length - 1);

        int index = resultArr[0].length - shift - 1 - macd.getLookback();
        double[] result = {resultArr[0][index], resultArr[1][index], resultArr[2][index]};
        return result;
    }

    private double[] getSTOCH(double[][] priceArr, int shift) {

        //set inputs
        stoch.setInputParameter(0, priceArr);

        //set outputs
        double[][] resultArr = new double[2][priceArr[0].length];
        stoch.setOutputParameter(0, resultArr[0]);
        stoch.setOutputParameter(1, resultArr[1]);

        //calculate
        stoch.calculate(0, priceArr[0].length - 1);

        int index = resultArr[0].length - shift - 1 - stoch.getLookback();
        double[] result = {resultArr[0][index], resultArr[1][index]};
        return result;
    }

    @Override
    public void onTick(Instrument instrument, ITick tick) throws JFException {
		//execute all order commands made in range bar feed
		while(!orderRequests.isEmpty()){
			OrderRequest orderRequest = orderRequests.poll();
			placeOrder(orderRequest.stochSign, orderRequest.macdSign);
		}
    }

    @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 {
    }

    private void print(Object... o) {
        for (Object ob : o) {
            //console.getOut().print(ob + "  ");
            if (ob instanceof double[]) {
                print((double[]) ob);
            } else if (ob instanceof double[]) {
                print((double[][]) ob);
            } else if (ob instanceof Long) {
                print(dateToStr((Long) ob));
            } else if (ob instanceof Double) {
                print((Double) ob);
            } else {
                print2(ob);
            }
            print(" ");
        }

        console.getOut().println();
    }

    private void print(Object o) {
        console.getOut().println(o);
    }

    private void print2(Object o) {
        console.getOut().print(o);
    }

    private void printTime(Long time) {
        console.getOut().println(dateToStr(time));
    }

    private void print(double[] arr) {
        print(arrayToString(arr));
    }

    private void print(double[][] arr) {
        print(arrayToString(arr));
    }

    private void print(double ddd) {
        print((new DecimalFormat("#.#######")).format(ddd));
    }

    private void printIndicatorInfos(IIndicator ind) {
        for (int i = 0; i < ind.getIndicatorInfo().getNumberOfInputs(); i++) {
            print(ind.getIndicatorInfo().getName() + " Input " + ind.getInputParameterInfo(i).getName() + " " + ind.getInputParameterInfo(i).getType());
        }
        for (int i = 0; i < ind.getIndicatorInfo().getNumberOfOptionalInputs(); i++) {
            print(ind.getIndicatorInfo().getName() + " Opt Input " + ind.getOptInputParameterInfo(i).getName() + " " + ind.getOptInputParameterInfo(i).getType());
        }
        for (int i = 0; i < ind.getIndicatorInfo().getNumberOfOutputs(); i++) {
            print(ind.getIndicatorInfo().getName() + " Output " + ind.getOutputParameterInfo(i).getName() + " " + ind.getOutputParameterInfo(i).getType());
        }
        console.getOut().println();
    }

    public static String arrayToString(double[] arr) {
        String str = "";
        for (int r = 0; r < arr.length; r++) {
            str += "[" + r + "] " + (new DecimalFormat("#.#######")).format(arr[r]) + "; ";
        }
        return str;
    }

    public static String arrayToString(double[][] arr) {
        String str = "";
        if (arr == null) {
            return "null";
        }
        for (int r = 0; r < arr.length; r++) {
            for (int c = 0; c < arr[r].length; c++) {
                str += "[" + r + "][" + c + "] " + (new DecimalFormat("#.#######")).format(arr[r][c]);
            }
            str += "; ";
        }
        return str;
    }

    public String toDecimalToStr(double d) {
        return (new DecimalFormat("#.#######")).format(d);
    }

    public String dateToStr(Long time) {
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss") {

            {
                setTimeZone(TimeZone.getTimeZone("GMT"));
            }
        };
        return sdf.format(time);
    }
}
