package jforex.strategies.indicators;

import java.text.DecimalFormat;
import java.text.SimpleDateFormat;
import java.util.TimeZone;

import com.dukascopy.api.*;
import com.dukascopy.api.IEngine.OrderCommand;
import com.dukascopy.api.IIndicators.AppliedPrice;
import com.dukascopy.api.indicators.IIndicator;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.Calendar;
import java.util.GregorianCalendar;

public class CCI implements IStrategy {

    private IEngine engine;
    private IConsole console;
    private IHistory history;
    private IIndicators indicators;
    private int counter = 0;
    private IOrder order;
    @Configurable("Trade Long Orders")
    public boolean tradeLong = true;
    @Configurable("Trade Short Orders")
    public boolean tradeShort = true;
    @Configurable("Instrument")
    public Instrument instrument = Instrument.EURUSD;
    @Configurable("Period")
    public Period selectedPeriod = Period.TEN_MINS;
    @Configurable("Offer side")
    public OfferSide offerSide = OfferSide.BID;
    @Configurable("Slippage")
    public double slippage = 0;
    @Configurable("Amount")
    public double amount = 0.02;
    @Configurable("Filter")
    public Filter filter = Filter.ALL_FLATS;
    @Configurable("Take profit pips")
    public int takeProfitPips = 20;
    @Configurable("Stop loss in pips")
    public int stopLossPips = 20;
    @Configurable("Applied price")
    public AppliedPrice appliedPrice = AppliedPrice.CLOSE;
    @Configurable("CCI Time period")
    public int timePeriod = 14;
    @Configurable("Open hour")
    public int openHour = 7;
    @Configurable("Open min")
    public int openMin = 0;
    @Configurable("Close hour")
    public int closeHour = 12;
    @Configurable("Close min")
    public int closeMin = 0;

    @Override
    public void onStart(IContext context) throws JFException {
        this.console = context.getConsole();
        this.indicators = context.getIndicators();
        this.history = context.getHistory();
        this.engine = context.getEngine();

        IChart chart = context.getChart(instrument);
        if (chart != null) {
            chart.addIndicator(indicators.getIndicator("CCI"), new Object[]{timePeriod});

        }
    }

    @Override
    public void onBar(Instrument instrument, Period period, IBar askBar, IBar bidBar) throws JFException {
        if (period != this.selectedPeriod || instrument != this.instrument) {
            return;
        }

        if (!isRightTime(askBar.getTime(), openHour, openMin, closeHour, closeMin)) {
            return;
        }

        if (!isActive(order)) {
            order = null;
        }

        // SIGNAL
        boolean sellSign1 = false;
        boolean buySign1 = false;

        double[] cci = indicators.cci(instrument, selectedPeriod, offerSide, timePeriod, filter, 2, askBar.getTime(), 0);

        int LAST = 1;
        int PREV = 0;

        if (tradeLong && cci[LAST] <= -100 && cci[PREV] < cci[LAST]) {
            buySign1 = true;
        }
        if (tradeShort && cci[LAST] >= 100 && cci[PREV] > cci[LAST]) {
            sellSign1 = true;
        }

        // PLACE ORDER        
        if (buySign1) {
            if (order != null && !order.isLong()) {
                //closeOrder(order);
                order = null;
            }
            if (tradeLong) {
                order = submitAndMergeOrder(OrderCommand.BUY, order);
            }

        } else if (sellSign1) {
            if (order != null && order.isLong()) {
//                closeOrder(order);
                order = null;
            }
            if (tradeShort) {
                order = submitAndMergeOrder(OrderCommand.SELL, order);
            }
        }
    }

    public void onTick(Instrument instrument, ITick tick) throws JFException {
        if (instrument != this.instrument) {
            return;
        }
    }

    private IOrder submitAndMergeOrder(OrderCommand orderCmd, IOrder oldOrder) throws JFException {
        
        // STOP LOSS / TAKE PROFIT
        double stopLossPrice = 0.0, takeProfitPrice = 0.0;
        
        // Calculating order price, stop loss and take profit prices
        if (orderCmd.isLong()) {
            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).getAsk() + getPipPrice(stopLossPips);
            }
            if (takeProfitPips > 0) {
                takeProfitPrice = history.getLastTick(instrument).getAsk() - getPipPrice(takeProfitPips);
            }
        }
        
        // PLACE ORDER
        IOrder order = engine.submitOrder(getLabel(instrument), instrument, orderCmd, amount, 0, slippage, stopLossPrice, takeProfitPrice);
                
        if(oldOrder == null) {
            return order;
        }
        
        order.waitForUpdate(2000);
        if (order.getState() == IOrder.State.OPENED) {
            order.waitForUpdate(2000);
        }

        if (order.getState() != IOrder.State.FILLED) {
            closeOrder(order);
            return oldOrder;
        }

        IOrder[] orders = {oldOrder, order};

        // MERGE ORDERS
        IOrder mergedOrder = mergeWithSlAndTp(orders);
        if(mergedOrder == null) {
            mergedOrder = oldOrder;
        }

        return mergedOrder;
    }

    private IOrder mergeWithSlAndTp(IOrder... orders) throws JFException {
        ITick tick = history.getLastTick(instrument);
        double bidPrice = tick.getBid();
        double askPrice = tick.getAsk();
        
        double slAmountWeightedTotal = 0; //SL amount - aggregation of market price distance to SL's and weighted by order amount
        double slAmountWeighted;
        
        double tpAmountWeightedTotal = 0;
        double tpAmountWeighted;
        
        int slCount = 0;
        int tpCount = 0;

        //remove sl attached orders if any
        for (IOrder o : orders) {
            double price = o.isLong() ? bidPrice : askPrice;
            
            if (Double.compare(o.getStopLossPrice(), 0) != 0) {
                slAmountWeighted = Math.abs(price - o.getStopLossPrice()) * o.getAmount();
                print(slAmountWeighted);
                
                slAmountWeightedTotal += slAmountWeighted;
                o.setStopLossPrice(0);

                o.waitForUpdate(2000);
                slCount++;
            }
            
            if (Double.compare(o.getTakeProfitPrice(), 0) != 0) {
                tpAmountWeighted = Math.abs(price - o.getTakeProfitPrice()) * o.getAmount();
                tpAmountWeightedTotal += tpAmountWeighted;
                o.setTakeProfitPrice(0);

                o.waitForUpdate(2000);
                tpCount++;
            }
        }

        
        IOrder mergedOrder = engine.mergeOrders(getLabel(instrument), orders);
        mergedOrder.waitForUpdate(10000);

        if (mergedOrder.getState() != IOrder.State.FILLED) {            
            return null;
        }

        double slPriceDelta = slAmountWeightedTotal / mergedOrder.getAmount();
        double slPrice = mergedOrder.isLong()
                ? bidPrice - slPriceDelta
                : askPrice + slPriceDelta;
        
        mergedOrder.setStopLossPrice(slPrice);
        mergedOrder.waitForUpdate(2000);
        
        double tpPriceDelta = tpAmountWeightedTotal / mergedOrder.getAmount();
        double tpPrice = mergedOrder.isLong()
                ? bidPrice + tpPriceDelta
                : askPrice - tpPriceDelta;
        
        mergedOrder.setTakeProfitPrice(tpPrice);
        mergedOrder.waitForUpdate(2000);
        
        return mergedOrder;
    }

    private void closeOrder(IOrder order) throws JFException {
        if (order != null && isActive(order)) {
            order.close();
        }
    }

    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(double pips) {
        return pips * this.instrument.getPipValue();
    }

    private String getLabel(Instrument instrument) {
        Calendar cal = new GregorianCalendar();
        String label = instrument.name();
        label = label + "_" + (counter++);
        label = label.toUpperCase() + String.valueOf(cal.getTimeInMillis()).substring(7);
        return label;
    }

    public boolean isRightTime(long time, int fromHour, int fromMin, int toHour, int toMin) {
        Calendar cal = new GregorianCalendar();
        cal.setTimeZone(TimeZone.getTimeZone("GMT"));
        cal.setTimeInMillis(time);
        cal.set(Calendar.HOUR_OF_DAY, fromHour);
        cal.set(Calendar.MINUTE, fromMin);

        Calendar cal2 = new GregorianCalendar();
        cal2.setTimeZone(TimeZone.getTimeZone("GMT"));
        cal2.setTimeInMillis(time);
        cal2.set(Calendar.HOUR_OF_DAY, toHour);
        cal2.set(Calendar.MINUTE, toMin);

        if (cal.getTimeInMillis() <= time
                && time <= cal2.getTimeInMillis()) {
            return true;
        }
        return false;
    }

    private double getRoundedPrice(double price) {
        BigDecimal bd = new BigDecimal(price);
        bd = bd.setScale(instrument.getPipScale() + 1, RoundingMode.HALF_UP);
        return bd.doubleValue();
    }

    private double getRoundedPips(double pips) {
        BigDecimal bd = new BigDecimal(pips);
        bd = bd.setScale(1, RoundingMode.HALF_UP);
        return bd.doubleValue();
    }

    public void onMessage(IMessage message) throws JFException {
    }

    public void onAccount(IAccount account) throws JFException {
    }

    public void onStop() throws JFException {
    }

    /**************** debug print functions ***********************/
    private void print(Object... o) {
        for (Object ob : o) {
            //console.getOut().print(ob + "  ");
            if (ob instanceof Double) {
                print2(toStr((Double) ob));
            } else if (ob instanceof double[]) {
                print((double[]) ob);
            } else if (ob instanceof double[]) {
                print((double[][]) ob);
            } else if (ob instanceof Long) {
                print2(toStr((Long) ob));
            } else if (ob instanceof IBar) {
                print2(toStr((IBar) ob));
            } else {
                print2(ob);
            }
            print2(" ");
        }
        console.getOut().println();
    }

    private void print(Object o) {
        console.getOut().println(o);
    }

    private void print2(Object o) {
        console.getOut().print(o);
    }

    private void print(double d) {
        print(toStr(d));
    }

    private void print(double[] arr) {
        print(toStr(arr));
    }

    private void print(double[][] arr) {
        print(toStr(arr));
    }

    private void print(IBar bar) {
        print(toStr(bar));
    }

    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 toStr(double[] arr) {
        String str = "";
        for (int r = 0; r < arr.length; r++) {
            str += "[" + r + "] " + (new DecimalFormat("#.#######")).format(arr[r]) + "; ";
        }
        return str;
    }

    public static String toStr(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 toStr(double d) {
        return (new DecimalFormat("#.#######")).format(d);
    }

    public String toStr(Long time) {
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss") {

            {
                setTimeZone(TimeZone.getTimeZone("GMT"));
            }
        };
        return sdf.format(time);
    }

    private String toStr(IBar bar) {
        return toStr(bar.getTime()) + "  O:" + bar.getOpen() + " C:" + bar.getClose() + " H:" + bar.getHigh() + " L:" + bar.getLow();
    }

    private void printTime(Long time) {
        console.getOut().println(toStr(time));
    }
}
