package jforex;

import com.dukascopy.api.*;
import com.dukascopy.api.DataType;
import com.dukascopy.api.IAccount;
import com.dukascopy.api.IBar;
import com.dukascopy.api.IChart;
import com.dukascopy.api.IConsole;
import com.dukascopy.api.IContext;
import com.dukascopy.api.IHistory;
import com.dukascopy.api.IIndicators.AppliedPrice;
import com.dukascopy.api.IIndicators;
import com.dukascopy.api.IMessage;
import com.dukascopy.api.IStrategy;
import com.dukascopy.api.ITick;
import com.dukascopy.api.ITimedData;
import com.dukascopy.api.Instrument;
import com.dukascopy.api.JFException;
import com.dukascopy.api.OfferSide;
import com.dukascopy.api.Period;
import com.dukascopy.api.RequiresFullAccess;
import com.dukascopy.api.feed.IFeedDescriptor;
import com.dukascopy.api.feed.IFeedListener;
import com.dukascopy.api.indicators.IIndicator;
import com.dukascopy.api.indicators.IIndicatorAppearanceInfo;
import java.io.PrintStream;
import java.math.BigDecimal;
import java.text.DecimalFormat;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Map;
import java.util.TimeZone;

@RequiresFullAccess
public class IndChartVsApi implements IStrategy, IFeedListener {
    private IIndicators indicators;
    private IHistory history;
    private IConsole console;
    private IChart chart;
    private IContext context;

    public int dataCount = 10;
    public int extraSize = 1000;
    IFeedDescriptor feedDescriptor;

    @Override
    public void onStart(IContext context) throws JFException {
        this.context = context;
        indicators = context.getIndicators();
        history = context.getHistory();
        console = context.getConsole();
        
        chart = context.getLastActiveChart();
        waitForChartFeed();

        feedDescriptor = chart.getFeedDescriptor();
        if(feedDescriptor.getDataType() == DataType.TICKS) {
            console.getWarn().println("Tick charts need to get calculate with from-to method");
            context.stop();
            return;
        }
        print(feedDescriptor);


        IIndicator indicator = indicators.getIndicator("EMA");
        printIndicatorInfos(indicator);
        
        Object [] objects = new Object[] {40};
        chart.add(indicator, objects);

        //context.subscribeToFeed(feedDescriptor, this);
        compare(chart);
        context.stop();
    }

    private void waitForChartFeed() {
        try {
            Thread.sleep(3000); //let the chart feed load
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public void onFeedData(IFeedDescriptor feedDescriptor, ITimedData feedData) {
        try {
            print("compare");
            compare(chart);
            
        } catch(Exception e) {
            console.getOut().println(e);
        }
    }
    
    public void compare(IChart chart) throws JFException {                    
        for (IIndicatorAppearanceInfo info : chart.getIndicatorApperanceInfos()) {
            if(indicators.getIndicator(info.getName()) == null) {
                continue;
            }                
            
            CompareOutputs compareOutputs;
            IndicatorDescriptor id = new IndicatorDescriptor(info, feedDescriptor);
            compareOutputs = new CompareOutputs(id);
            compareOutputs.checkIndicator(info.getName(), info.getOptParams(), dataCount);                
        }
    }

    class IndicatorDescriptor {
        AppliedPrice[] appliedPrices;
        IIndicator indicator;
        ITimedData feedData;
        OfferSide[] offerSides;
        int inputCount;
        IFeedDescriptor feedDescriptor;
        String indicatorName;
        int outputCount;

        IndicatorDescriptor(IIndicatorAppearanceInfo info, IFeedDescriptor feedDescriptor) throws JFException {
            appliedPrices = new AppliedPrice[info.getDrawingStyles().length];
            Arrays.fill(appliedPrices, AppliedPrice.CLOSE);
            indicator = indicators.getIndicator(info.getName());
            this.feedDescriptor = feedDescriptor;
            feedData = history.getFeedData(feedDescriptor, 1);

            inputCount = indicator.getIndicatorInfo().getNumberOfInputs();
            offerSides = new OfferSide[inputCount];
            Arrays.fill(offerSides, chart.getSelectedOfferSide());

            indicatorName = indicator.getIndicatorInfo().getName();
            outputCount = indicator.getIndicatorInfo().getNumberOfOutputs();
        }
    }

    class Output {
        double[] output;
        double[] extraOutput;
        double[] chartOutput;

        Output(double[] output, double[] extraOutput, double[] chartOutput) {
            this.output = output;
            this.extraOutput = extraOutput;
            this.chartOutput = chartOutput;
        }
    }

    class CalculateOutputs {
        IndicatorDescriptor id;
        Output[] outputParams;

        CalculateOutputs(IndicatorDescriptor id, Object[] inputParams, int dataCount) throws JFException {
            this.id = id;

            int outputParamCount = id.indicator.getIndicatorInfo().getNumberOfOutputs();
            this.outputParams = new Output[outputParamCount];

            Object[] outputs = indicators.calculateIndicator(feedDescriptor, id.offerSides, id.indicatorName, id.appliedPrices, inputParams,
                    dataCount, id.feedData.getTime(), 0);

            Object[] extraOutputsFull = indicators.calculateIndicator(feedDescriptor, id.offerSides, id.indicatorName, id.appliedPrices, inputParams,
                    extraSize + dataCount, id.feedData.getTime(), 0);

            for (int i = 0; i < outputParamCount; i++) {

                double[] apiOutput = (double[]) outputs[i];
                double[] extraOutputFull = (double[]) extraOutputsFull[i];

                if(extraOutputFull.length < dataCount) {
                    throw new JFException("skipping, not enought indicator outputs to compare");
                }
                double[] extraOutputs = Arrays.copyOfRange(extraOutputFull, extraOutputFull.length - dataCount, extraOutputFull.length);

                double[] chartOutputs = getChartOutput(i);
                if(chartOutputs == null) {
                    throw new JFException("skipping, not enought chart ouputs");
                }

                outputParams[i] = new Output(apiOutput, extraOutputs, chartOutputs);
            }
        }

        double[] getChartOutput(int i) throws JFException {
            for (Map.Entry<IIndicator, Object[]> entry : chart.getLastCalculatedIndicatorOutputs().entrySet()) {
                IIndicator chartIndicator = entry.getKey();
                if (!chartIndicator.getIndicatorInfo().getName().equals(id.indicatorName)) {
                    continue;
                }

                Object[] onChartResult = entry.getValue();
                if(onChartResult == null) {
                    break;
                }
                double[] onChartArr = (double[]) onChartResult[i];
                double[] onChartLastValArr = Arrays.copyOfRange(onChartArr, onChartArr.length - (dataCount + 1), onChartArr.length - 1);
                return onChartLastValArr;
            }
            throw new JFException("skipping, chart outputs not found");
        }
    }

    class CompareOutputs {
        IndicatorDescriptor id;
        int precision;
        double maxDelta;
        CalculateOutputs calculateOutputs;
        Output output;

        CompareOutputs(IndicatorDescriptor id) throws JFException {
            this.id = id;
            this.precision = feedDescriptor.getInstrument().getPipScale();
            this.maxDelta = Math.pow(0.1, precision) * 5;
        }

        void checkIndicator(String indicatorName, Object[] inputParams, int dataCount) {
            try{
                print("max difference between values: ", maxDelta);

                CalculateOutputs calculateOutputs = new CalculateOutputs(id, inputParams, dataCount);

                for(int i = 0; i < calculateOutputs.outputParams.length; i++) {
                    output = calculateOutputs.outputParams[i];

                    boolean apiChartMatch = checkEqual(output.output, output.chartOutput);
                    boolean extraApiChartMatch = checkEqual(output.extraOutput, output.chartOutput);

                    String outputParameterName = id.indicator.getOutputParameterInfo(i).getName();
                    printResult(indicatorName, outputParameterName, output, apiChartMatch, extraApiChartMatch);
                }

            } catch (Exception e){
                console.getErr().println(indicatorName + " "+ e);
                e.printStackTrace();
            }
        }

        boolean checkEqual(double[] arr1, double[] arr2) {
            if(arr1 == null && arr2 == null){
                return true;
            }
            if(arr1 == null || arr2 == null){
                return false;
            }
            if(arr1.length != arr2.length){
                return false;
            }

            int shift1 = 0;
            int shift2 = 0;

            if(!valueEqual(arr1[arr1.length - 1], arr2[arr2.length - 1], precision, maxDelta)) {
                if(valueEqual(arr1[arr1.length - 1], arr2[arr2.length - 2], precision, maxDelta)) {
                    shift1 = 1;
                } else if(valueEqual(arr1[arr1.length - 2], arr2[arr2.length - 1], precision, maxDelta)) {
                    shift2 = 1;
                }
            }

            int idx1 = shift1;
            int idx2 = shift2;
            for(; idx1 < arr1.length && idx2 < arr2.length; idx1++, idx2++) {
                if (Double.isNaN(arr1[idx1]) && Double.isNaN(arr2[idx2])) {
                    continue;
                }
                if (Double.isNaN(arr1[idx1]) || Double.isNaN(arr2[idx2])) {
                    return false;
                }
                if (!valueEqual(arr1[idx1], arr2[idx2], precision, maxDelta)) {
                    console.getOut().println("[" + idx1 + "]" + toStr(arr1[idx1]) + " [" + idx2 + "]" + toStr(arr2[idx2]));
                    return false;
                }
            }
            return true;
        }

        private boolean valueEqual(double val1, double val2, int precision, double maxDelta) {
            double delta = round(val1, precision) - round(val2, precision);
            return Math.abs(delta) < (maxDelta);
        }

        private double round(double amount, int decimalPlaces) {
            if(Double.isNaN(amount)) {
                console.getErr().println("value is nan");
                printOutputs(output, console.getOut());

            }
            return (new BigDecimal(amount)).setScale(decimalPlaces, BigDecimal.ROUND_HALF_UP).doubleValue();
        }

    }

    void printResult(String indicatorName, String outputParamName, Output output, boolean apiChartMatch, boolean extraApiChartMatch) {
        PrintStream ps = getPrinter(apiChartMatch, extraApiChartMatch);

        ps.println(feedDescriptor+ " "+ feedDescriptor.getPeriod().getJFTimeZone());

        if(apiChartMatch) {
            ps.println("Chart vs API outputs - OK");
        } else {
            ps.println("Chart vs API outputs - ERROR");
            context.stop();
        }
        if(extraApiChartMatch) {
            ps.println("Chart vs API with long outputs - OK");
        } else {
            ps.println("Chart vs API with long outputs - WARNING");
        }

        ps.format("%s %s last %s API values:", indicatorName, outputParamName, dataCount);
        ps.println();
        printOutputs(output, ps);
    }

    private void printOutputs(Output output, PrintStream ps) {
        ps.println(arrToString(output.output)+ " API");
        ps.println(arrToString(output.extraOutput)+ " API + extra inputs");
        ps.println(arrToString(output.chartOutput)+ " chart");
    }


    PrintStream getPrinter(boolean apiChartMatch, boolean apiWithBuffChartMatch) {
        PrintStream ps;
        if(apiChartMatch && apiWithBuffChartMatch) {
            ps = console.getOut();
        } else if(apiWithBuffChartMatch) {
            ps = console.getWarn();
        } else {
            ps = console.getErr();
        }
        return ps;
    }

    private String arrToString(double[] arr) {
        return this.arrToString(arr, 0);
    }

    private String arrToString(double[] arr, int from) {
        StringBuffer sb = new StringBuffer();
        for (int r = from; r < arr.length; r++) {
            sb.append(String.format("[%s] %." + (feedDescriptor.getInstrument().getPipScale() + 1) + "f; ", r, arr[r]));
        }
        return sb.toString();
    }

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



    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 (Long.class.isInstance(ob)) {
                print2(toStr((Long) ob));
            } else if (ob instanceof IBar) {
                print2(toStr((IBar) ob));
            } else {
                print2(ob);
            }
            print2(" ");
        }
        console.getOut().println();
    }

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

    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 print(Long time) {
        console.getOut().println(toStr(time));
    }

    private void print(Throwable th) {
        StackTraceElement[] elem = th.getStackTrace();

        // print stack trace in reverse order because console in jforex client prints in reverse
        for(int i = elem.length - 1; i >= 0; i--) {
            console.getErr().println(elem[i]);
        }
    }

}