import com.mashape.unirest.http.HttpResponse;
import com.mashape.unirest.http.JsonNode;
import com.mashape.unirest.http.Unirest;
import com.mashape.unirest.http.exceptions.UnirestException;
import org.apache.commons.math3.stat.regression.SimpleRegression;
import org.json.JSONArray;
import org.json.JSONObject;
import storage.Funds;
import storage.Order;
import storage.OrderHistory;
import storage.TradeHistory;
import utils.DrawGraph;
import utils.MovingAverage;

import javax.swing.*;

import java.awt.*;
import java.awt.geom.Point2D;
import java.io.*;
import java.nio.charset.Charset;
import java.util.*;
import java.util.List;
import java.util.concurrent.TimeUnit;

public class Main {
    private static final String APP_NAME = "den_ac_deka_ct3_15_35";
    private static final double VALUE_ETH = 100.0;
    private static final double VALUE_EUR = 0.0;
    private static final int ROUNDTRIP_TIME_MS = 20000;
    private static int NUM_ITERATIONS_FOR_ORDER = 6;
    private static int BUY_THRESHOLD = 10;
    private static int SELL_THRESHOLD = -10;

    private static SimpleRegression srbuyvolume = new SimpleRegression(true);
    private static SimpleRegression srsellvolume = new SimpleRegression(true);
    private static SimpleRegression srprice = new SimpleRegression(true);
    private static MovingAverage ma15 = new MovingAverage(200);
    private static MovingAverage ma50 = new MovingAverage(1000);

    private static OrderHistory orders = new OrderHistory();
    private static Funds funds = new Funds();
    private static TradeHistory trades;

    private static double buyvslope = 0.0;
    private static double sellvslope = 0.0;
    private static double priceslope = 0.0;
    private static int buyindicator = 0;
    private static double currentPrice = 0.0;
    private static int currentIteration = 0;
    private static double fees = 0.0;
    private static long sinceid = 0L;
    private static String trend = "";

    private static int counterbuys = 0;
    private static int countersells = 0;
    private static int counterneutrals = 0;

    private static boolean regressionbacktest = false;
    private static boolean backtest = false;

    private static Map<String, Double > results = new HashMap<>();
    private static ArrayList<Double> graphValuesBase = new ArrayList<>();
    private static ArrayList<Double> graphValues1 = new ArrayList<>();
    private static ArrayList<Double> graphValues2 = new ArrayList<>();

    public static void main(String[] args) throws InterruptedException {
        System.out.println("Starting DekaBot...");
        System.out.println("Starting simulation... initializing storage.Funds...");

        funds.addFund("ETH", VALUE_ETH);
        funds.addFund("EUR", VALUE_EUR);

        System.out.println("Do backtesting? (n = no || y = yes || r = regression-backtest)");
        Scanner in = new Scanner(System.in);
        String input = in.next();

        switch(input){
            case "y":
                startSingleBacktesting();
                break;
            case "r":
                startRegressionBacktesting();
                break;
            case "n":
                startLiveTrading();
                break;
            default:
                System.out.println("Unrecognized input. Run again...");
                break;
        }
    }

    private static void startLiveTrading() throws InterruptedException {
        sendCurrentStatsToLogServer();

        System.out.println(funds.getFundsStringOutput());
        System.out.println("-------Start-------");

        //Initial rest request to get a timeframe
        try {
            HttpResponse<JsonNode> jsonResponse = Unirest.post("https://api.kraken.com/0/public/Trades")
                    .header("accept", "application/json")
                    .queryString("pair", "ETHEUR")
                    .asJson();

            sinceid = jsonResponse.getBody().getObject().getJSONObject("result").getLong("last");
        } catch (UnirestException ue) {
            System.out.println("Could not establish connection: " + ue.getMessage());
            return;
        }

        //Bot will run forever - kill task to end trading
        while (true) {
            Thread.sleep(ROUNDTRIP_TIME_MS);

            getCurrentPrice();
            getCurrentVolume();

            // decide which trend calculation should be used
            trend = calculateTrend_3();

            outputAnalyzationResults();

            //every NUM_ITERATIONS_FOR_ORDER start decision making process
            if (((currentIteration % NUM_ITERATIONS_FOR_ORDER) == 0) && (currentIteration != 0)) {

                Order order = decide();

                if (order.ordertype != null) doOrder(order);

                buyindicator = 0;
                System.out.println("storage.Funds: " + funds.getFundsStringOutput());

                srbuyvolume.clear();
                srsellvolume.clear();
                srprice.clear();
            }

            currentIteration++;
            System.out.println("-------" + currentIteration + "-------");
        }
    }

    private static void startSingleBacktesting() {
        backtest = true;

        doBacktest();

        //Buy ETH from remaining funds so the testing will end only with ETH
        doOrder(new Order("buy"));


        System.out.println("Final ETH count: "+funds.getValue("ETH"));
        System.out.println("Buys: "+counterbuys+" Sells: "+countersells+" Neutral: "+counterneutrals+" Fees: "+fees);
    }

    private static void startRegressionBacktesting() {
        regressionbacktest = true;
        long startTime = System.currentTimeMillis();
        int numofiters = 0;

        for(int x = 4; x < 30; x++){
            numofiters += 3 * x - (x-1);
        }

        int iter = 0;
        for(int x = 4; x < 30; x++){
            NUM_ITERATIONS_FOR_ORDER = x;

            for(int y = x-1;y < 3*x;y++){
                iter++;
                printProgress(startTime,numofiters,iter);

                BUY_THRESHOLD = y;
                SELL_THRESHOLD = -y;

                doBacktest();

                results.put(x+":"+y,funds.getValue("ETH"));
            }
        }

        System.out.println("Output of best results: ");
        HashMap<String, Double> sortedResult = (HashMap<String, Double>) sortByValue(results);

        for (Map.Entry<String, Double> entry : sortedResult.entrySet()) {
            System.out.println(entry.getKey() + " Value: "+entry.getValue());
        }
    }


    private static void doOrder(Order order) {
        Double tmpFees = 0.0;

        switch(order.ordertype){
            case "buy":
                if (Double.compare(funds.getValue("EUR"), 0) > 0) {
                    orders.addOrder(order);

                    tmpFees = funds.getValue("EUR") * 0.0026;
                    funds.addValue("ETH", (funds.getValue("EUR") - tmpFees) / currentPrice);
                    funds.takeValue("EUR", funds.getValue("EUR"));

                    if(!regressionbacktest) System.out.printf("Fees for order: %.3f - Total Fees: %.3f",tmpFees,fees);
                    counterbuys++;
                } else {
                    if(!regressionbacktest) System.out.println("Would like to buy but conditions not met (out of funds)");
                }
                break;
            case "sell":
                if (Double.compare(funds.getValue("ETH"), 0) > 0) {
                    orders.addOrder(order);

                    tmpFees = funds.getValue("ETH") * currentPrice * 0.0026;
                    funds.addValue("EUR", (funds.getValue("ETH") * currentPrice) - tmpFees);
                    funds.takeValue("ETH", funds.getValue("ETH"));

                    if(!regressionbacktest) System.out.printf("Fees for order: %.3f - Total Fees: %.3f",tmpFees,fees);
                    countersells++;
                } else {
                    if(!regressionbacktest) System.out.println("Would like to sell but conditions not met (out of funds)");
                }
                break;
            default:
                System.out.println("ERROR: storage.Order not recognized: " + order.type);
                break;

        }

        fees += tmpFees;
        sendCurrentStatsToLogServer();
    }

    private static Order decide() {
        Order newOrder = new Order();
        newOrder.price = currentPrice;
        newOrder.timeframe = (double) currentIteration;

        if(!regressionbacktest) System.out.println("------- START ORDER -------");

        if (buyindicator > BUY_THRESHOLD) {
            if(!regressionbacktest)  System.out.println("BUY NOW - PRICE: " + currentPrice);
            newOrder.ordertype = "buy";

        } else if (buyindicator >= SELL_THRESHOLD) {
            if(!regressionbacktest)  System.out.println("DO NOTHING - NEUTRAL: " + currentPrice);
            counterneutrals++;
        } else {
            if (orders.getLastOrder() == null || Double.compare(orders.getLastOrder().price, currentPrice * 0.995) < 0 || Double.compare(orders.getLastOrder().price, currentPrice * 0.985) > 0) {

                if(!regressionbacktest)  System.out.println("SELL NOW - PRICE: " + currentPrice);
                newOrder.ordertype = "sell";

            } else {
                if(!regressionbacktest)  System.out.println("Would like to sell but percentage not reached");
            }
        }

        if(!regressionbacktest)  System.out.println("------- END ORDER -------");

        return newOrder;
    }

    private static Order decide2() {
        Order newOrder = new Order();
        newOrder.price = currentPrice;
        newOrder.timeframe = (double) currentIteration;

        if(!regressionbacktest) System.out.println("------- START ORDER -------");
        if (buyindicator > BUY_THRESHOLD) {

            if(!regressionbacktest)  System.out.println("BUY NOW - PRICE: " + currentPrice);
            newOrder.ordertype = "buy";

        } else if (buyindicator > SELL_THRESHOLD) {
            if(!regressionbacktest)  System.out.println("DO NOTHING - NEUTRAL: " + currentPrice);
            counterneutrals++;
        } else {

            if(!regressionbacktest)  System.out.println("SELL NOW - PRICE: " + currentPrice);
            newOrder.ordertype = "sell";


        }
        if(!regressionbacktest)  System.out.println("------- END ORDER -------");

        return newOrder;
    }

    private static String calculateTrend() {
        if ((Double.compare(buyvslope, 0) > 0) && (Double.compare(priceslope, 0) > 0)) {
            if (Double.compare(trades.volumeOfBuyTrades, trades.volumeOfSellTrades) > 0) {
                buyindicator += 3;
                return "strong buy";
            } else {
                buyindicator += 2;
                return "normal buy";
            }
        } else if ((Double.compare(sellvslope, 0) > 0) && (Double.compare(priceslope, 0) < 0)) {
            if (Double.compare(trades.volumeOfBuyTrades, trades.volumeOfSellTrades) < 0) {
                buyindicator -= 3;
                return "strong sell";
            } else {
                buyindicator -= 2;
                return "normal sell";
            }
        } else if (Double.compare(buyvslope, sellvslope) > 0 && (Double.compare(priceslope, 0) > 0)) {
            buyindicator += 1;
            return "weak buy";
        } else if (Double.compare(buyvslope, sellvslope) < 0 && (Double.compare(priceslope, 0) < 0)) {
            buyindicator -= 1;
            return "weak sell";
        } else {
            return "neutral";
        }
    }

    private static String calculateTrend_2() {
        if (Double.isNaN(priceslope)) {
            if (Double.compare(trades.volumeOfBuyTrades, trades.volumeOfSellTrades) > 0) {
                buyindicator += 1;
                return "weak buy";
            } else {
                buyindicator -= 1;
                return "weak sell";
            }
        } else if ((Double.compare(priceslope, 0) > 0)) {
            if ((Double.compare(trades.volumeOfBuyTrades, trades.volumeOfSellTrades) > 0) && (Double.compare(buyvslope, 0) > 0) && (Double.compare(sellvslope, 0) < 0)) {
                buyindicator += 3;
                return "strong buy";
            } else if ((Double.compare(trades.volumeOfBuyTrades, trades.volumeOfSellTrades) > 0) && (Double.compare(buyvslope, 0) > 0)) {
                buyindicator += 2;
                return "normal buy";
            } else {
                buyindicator += 1;
                return "weak buy";
            }
        } else if ((Double.compare(priceslope, 0) < 0)) {
            if ((Double.compare(trades.volumeOfBuyTrades, trades.volumeOfSellTrades) < 0) && (Double.compare(buyvslope, 0) < 0) && (Double.compare(sellvslope, 0) > 0)) {
                buyindicator -= 3;
                return "strong sell";
            } else if ((Double.compare(trades.volumeOfBuyTrades, trades.volumeOfSellTrades) < 0) && (Double.compare(sellvslope, 0) > 0)) {
                buyindicator -= 2;
                return "normal sell";
            } else {
                buyindicator -= 1;
                return "weak sell";
            }
        } else {
            return "neutral";
        }
    }

    private static String calculateTrend_3() {
        if (Double.isNaN(priceslope)) {
            if (Double.compare(trades.volumeOfBuyTrades, trades.volumeOfSellTrades) > 0) {
                buyindicator += 1;
                return "weak buy";
            } else {
                buyindicator -= 1;
                return "weak sell";
            }
        } else if ((Double.compare(priceslope, 0) > 0)) {
            if ((Double.compare(trades.volumeOfBuyTrades, trades.volumeOfSellTrades) > 0)) {
                buyindicator += 3;
                return "strong buy";
            } else {
                buyindicator += 2;
                return "normal buy";
            }
        } else if ((Double.compare(priceslope, 0) < 0)) {
            if ((Double.compare(trades.volumeOfBuyTrades, trades.volumeOfSellTrades) < 0)) {
                buyindicator -= 3;
                return "strong sell";
            } else {
                buyindicator -= 2;
                return "normal sell";
            }
        } else {
            return "neutral";
        }
    }

    private static String calculateTrend_4() {
        if(currentIteration < 500) return "neutral";

        if(ma15.getAverage().compareTo(ma50.getAverage()) == 1){
            buyindicator += 3;
            return "normal buy";
        }else if(ma15.getAverage().compareTo(ma50.getAverage()) == -1){
            buyindicator -= 3;
            return "normal sell";
        }else{
            return "neutral";
        }
    }

    private static void doBacktest(){
        try {
            graphValues1.clear();
            graphValues2.clear();
            funds.clearFunds();
            orders.clearOrders();
            if(trades != null) trades.clearTrades();
            funds.addFund("ETH", VALUE_ETH);
            funds.addFund("EUR", VALUE_EUR);

            String priceline;
            String volumeline;
            currentIteration = 0;

            InputStream fis = new FileInputStream(System.getProperty("user.dir")+"\\src\\main\\resources\\price.txt");
            InputStreamReader isr = new InputStreamReader(fis, Charset.forName("UTF-8"));
            BufferedReader br = new BufferedReader(isr);

            InputStream fis2 = new FileInputStream(System.getProperty("user.dir")+"\\src\\main\\resources\\volume.txt");
            InputStreamReader isr2 = new InputStreamReader(fis2, Charset.forName("UTF-8"));
            BufferedReader br2 = new BufferedReader(isr2);

            while ((priceline = br.readLine()) != null && (volumeline = br2.readLine()) != null) {

                //sometimes two lines are written - in this case read second line as well and append
                if(volumeline.charAt(volumeline.length()-1) != '}'){
                    volumeline += br2.readLine();
                }

                JSONObject jsonPriceObject = new JSONObject(priceline);
                JSONObject jsonVolumeObject = new JSONObject(volumeline);

                jsonVolumeObject = jsonVolumeObject.getJSONObject("result");
                sinceid = jsonVolumeObject.getLong("last");
                JSONArray volumeArray = jsonVolumeObject.getJSONArray("XETHZEUR");

                currentPrice = jsonPriceObject.getDouble("EUR");

                trades = new TradeHistory(volumeArray, sinceid);

                srbuyvolume.addData((double) currentIteration, trades.volumeOfBuyTrades);
                srsellvolume.addData((double) currentIteration, trades.volumeOfSellTrades);
                srprice.addData((double) currentIteration, currentPrice);

                priceslope = srprice.getSlope();
                buyvslope = srbuyvolume.getSlope();
                sellvslope = srsellvolume.getSlope();

                /* DECIDE TREND CALCULATION */
                trend = calculateTrend_4();
                /* */

                ma15.add(currentPrice);
                ma50.add(currentPrice);

                if(!regressionbacktest) return;graphValuesBase.add(currentPrice);
                graphValues1.add(ma15.getAverage().doubleValue());
                graphValues2.add(ma50.getAverage().doubleValue());

                outputAnalyzationResults();

                if (((currentIteration % NUM_ITERATIONS_FOR_ORDER) == 0) && (currentIteration != 0)) {

                    /* DECIDE DECIDE CALCULATOR */
                    Order order = decide2();
                    /* */

                    if (order.ordertype != null) doOrder(order);

                    buyindicator = 0;
                    if(!regressionbacktest) System.out.println("storage.Funds: " + funds.getFundsStringOutput());

                    srbuyvolume.clear();
                    srsellvolume.clear();
                    srprice.clear();
                }

                if(!regressionbacktest && !backtest) System.out.println("-------" + currentIteration + "-------");
                currentIteration++;
            }

            doOrder(new Order("buy"));

            if(!regressionbacktest) System.out.println("utils.DrawGraph -- "+graphValues1.size());
            if(!regressionbacktest) drawGraph("Threshold: "+BUY_THRESHOLD+" Iterations: "+NUM_ITERATIONS_FOR_ORDER, graphValuesBase, graphValues1, graphValues2);

        } catch (IOException e) {
            System.out.println("Error:" + e.getMessage());
        }
    }

    private static void getCurrentPrice() {
        try {
            HttpResponse<JsonNode> jsonResponse;
            jsonResponse = Unirest.post("https://min-api.cryptocompare.com/data/price?fsym=ETH&tsyms=BTC,USD,EUR")
                    .header("accept", "application/json")
                    .queryString("fsyn", "ETH")
                    .field("tsyms", "BTC,USD,EUR")
                    .asJson();

            writeResponseToFile(jsonResponse,"price");

            currentPrice = jsonResponse.getBody().getObject().getDouble("EUR");
            srprice.addData((double) currentIteration, currentPrice);
        } catch (UnirestException ue) {
            System.out.println("Exception was thrown - just continue and ignore");
        }
    }

    private static void getCurrentVolume() {
        try {
            HttpResponse<JsonNode> jsonResponse = Unirest.post("https://api.kraken.com/0/public/Trades")
                    .header("accept", "application/json")
                    .queryString("pair", "ETHEUR")
                    .field("since", sinceid)
                    .asJson();

            writeResponseToFile(jsonResponse,"volume");

            JSONArray array = jsonResponse.getBody().getObject().getJSONObject("result").getJSONArray("XETHZEUR");
            sinceid = jsonResponse.getBody().getObject().getJSONObject("result").getLong("last");
            trades = new TradeHistory(array, sinceid);

            srbuyvolume.addData((double) currentIteration, trades.volumeOfBuyTrades);
            srsellvolume.addData((double) currentIteration, trades.volumeOfSellTrades);

            priceslope = srprice.getSlope();
            buyvslope = srbuyvolume.getSlope();
            sellvslope = srsellvolume.getSlope();

        } catch (UnirestException ue) {
            System.out.println("Exception thrown. Just continue..");
        }
    }

    private static void sendCurrentStatsToLogServer() {
        if(regressionbacktest || backtest) return;

        try {
            Unirest.post("https://www.riskahead.de/api/v1/deka/updatestats/")
                    .header("accept", "application/json")
                    .queryString("app_id", APP_NAME)
                    .field("funds_start", "ETH: " + VALUE_ETH + " EUR: " + VALUE_EUR)
                    .field("funds_current", funds.getFundsStringOutput())
                    .field("trades_buy", counterbuys)
                    .field("trades_sell", countersells)
                    .field("trades_fees", fees)
                    .field("trades_neutral", counterneutrals)
                    .asJson();
        } catch (UnirestException ue) {
            System.out.println("Error sending stats: " + ue.getMessage());
        }
    }

    private static void outputAnalyzationResults() {
        if(regressionbacktest || backtest) return;
        System.out.printf("Price: "+ currentPrice +" => Slope: %.3f \n",srprice.getSlope());
        System.out.printf("Buy-Trades -- Amount: " + trades.numberOfBuyTrades + " Volume: %.3f => Slope: %.3f\n",trades.volumeOfBuyTrades,srbuyvolume.getSlope());
        System.out.printf("Sell-Trades -- Amount: " + trades.numberOfSellTrades + " Volume: %.3f => Slope: %.3f\n",trades.volumeOfSellTrades,srsellvolume.getSlope());
        System.out.println("Trend: " + trend);
    }

    private static void writeResponseToFile(HttpResponse<JsonNode> jsonResponse, String filename) {
        try{
            PrintWriter writer = new PrintWriter(new FileOutputStream(
                    new File(filename+"_"+APP_NAME+".txt"),
                    true ));
            writer.append(jsonResponse.getBody().toString()+"\r\n");
            writer.close();
        } catch (IOException e) {
            // do something
        }
    }

    private static <K, V> Map<K, V> sortByValue(Map<K, V> map) {
        List<Map.Entry<K, V>> list = new LinkedList<>(map.entrySet());
        list.sort( new Comparator<Object>() {
            @SuppressWarnings("unchecked")
            public int compare(Object o2, Object o1) {
                return ((Comparable<V>) ((Map.Entry<K, V>) (o1)).getValue()).compareTo(((Map.Entry<K, V>) (o2)).getValue());
            }
        });

        Map<K, V> result = new LinkedHashMap<>();
        for (Iterator<Map.Entry<K, V>> it = list.iterator(); it.hasNext();) {
            Map.Entry<K, V> entry = it.next();
            result.put(entry.getKey(), entry.getValue());
        }

        return result;
    }

    private static void drawGraph(String title, ArrayList<Double> valuesBase, ArrayList<Double> values1, ArrayList<Double> values2){
        JFrame frame = new JFrame("Graph");

        frame.setSize(700, 400);
        frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);

        JPanel panel = new JPanel();

        List<Point.Double> graphPointsBuy = new ArrayList<>();
        List<Point.Double> graphPointsSell = new ArrayList<>();

        for(Order o : orders.orders){
            if(o.ordertype.equals("buy")) graphPointsBuy.add(new Point2D.Double(o.timeframe,o.price));
            if(o.ordertype.equals("sell")) graphPointsSell.add(new Point2D.Double(o.timeframe,o.price));
        }

        DrawGraph graph = new DrawGraph(valuesBase, values1, values2, graphPointsBuy, graphPointsSell);

        frame.add(graph);

        frame.setVisible(true);
    }

    private static void printProgress(long startTime, long total, long current) {
        long eta = current == 0 ? 0 :
                (total - current) * (System.currentTimeMillis() - startTime) / current;

        String etaHms = current == 0 ? "N/A" :
                String.format("%02d:%02d:%02d", TimeUnit.MILLISECONDS.toHours(eta),
                        TimeUnit.MILLISECONDS.toMinutes(eta) % TimeUnit.HOURS.toMinutes(1),
                        TimeUnit.MILLISECONDS.toSeconds(eta) % TimeUnit.MINUTES.toSeconds(1));

        StringBuilder string = new StringBuilder(140);
        int percent = (int) (current * 100 / total);
        string
                .append('\r')
                .append(String.join("", Collections.nCopies(percent == 0 ? 2 : 2 - (int) (Math.log10(percent)), " ")))
                .append(String.format(" %d%% [", percent))
                .append(String.join("", Collections.nCopies(percent, "=")))
                .append('>')
                .append(String.join("", Collections.nCopies(100 - percent, " ")))
                .append(']')
                .append(String.join("", Collections.nCopies((int) (Math.log10(total)) - (int) (Math.log10(current)), " ")))
                .append(String.format(" %d/%d, ETA: %s", current, total, etaHms));

        System.out.print(string);
    }

}
