package com.r35157.evelyn.emc.impl.ref;

import com.fanitas.evelyn.core.Evelyn;
import com.fanitas.evelyn.core.EvelynStatusIndexPoint;
import com.r35157.evelyn.emc.EvelynMissionControl;
import com.r35157.libs.javafx.JavaFxRuntime;
import com.r35157.nenjim.component.NenjimApplication;
import org.jetbrains.annotations.NotNull;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.application.Platform;
import javafx.scene.Scene;
import javafx.scene.chart.LineChart;
import javafx.scene.chart.NumberAxis;
import javafx.scene.chart.XYChart;
import javafx.scene.control.Tab;
import javafx.scene.control.TabPane;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;
import javafx.util.StringConverter;
import javafx.util.Duration;

import java.math.BigDecimal;
import java.time.Instant;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.Objects;
import java.util.function.Function;

public final class EvelynMissionControlImpl implements
        EvelynMissionControl,
        NenjimApplication {

    public EvelynMissionControlImpl(
            @NotNull Evelyn evelynServiceProd,
            @NotNull Evelyn evelynServiceTest
    ) {
        this.evelynProd = Objects.requireNonNull(evelynServiceProd, "evelynServiceProd");
        this.evelynTest = Objects.requireNonNull(evelynServiceTest, "evelynServiceTest");
        refreshTimeline.setCycleCount(Timeline.INDEFINITE);
    }

    @Override
    public void start() {
        JavaFxRuntime.runLater(this::showWindow);
    }

    private void showWindow() {
        if (window != null && window.isShowing()) {
            window.toFront();
            return;
        }

        if (window == null) {
            window = new Stage();
            window.setTitle("Evelyn Mission Control");
            window.setOnHidden(event -> refreshTimeline.stop());
        }
        window.setScene(new Scene(createRootTabs(), 600, 400));

        refreshCharts();
        window.show();
        refreshTimeline.playFromStart();
    }

    private TabPane createMissionControlTabs(String color, StatusIndexChart statusIndexChart) {
        TabPane tabs = new TabPane(
                createOverviewTab(color, statusIndexChart),
                createMissionControlTab("Portfolio", color),
                createMissionControlTab("Perps", color),
                createMissionControlTab("Spot", color),
                createMissionControlTab("Liquidity Pools", color),
                createMissionControlTab("Alarms", color),
                createMissionControlTab("Status", color),
                createMissionControlTab("Logs", color)
        );

        tabs.setTabClosingPolicy(TabPane.TabClosingPolicy.UNAVAILABLE);
        return tabs;
    }

    private Tab createOverviewTab(String color, StatusIndexChart statusIndexChart) {
        StackPane content = new StackPane(statusIndexChart.chart());
        content.setStyle("-fx-background-color: " + color + ";");

        Tab tab = new Tab("Overview", content);
        tab.setStyle("-fx-background-color: " + color + ";");
        return tab;
    }

    static StatusIndexChart createStatusIndexChart() {
        NumberAxis xAxis = new NumberAxis(0, 1, 1);
        xAxis.setLabel("Time");
        xAxis.setTickLabelFormatter(TIMESTAMP_FORMATTER);
        xAxis.setForceZeroInRange(false);

        double yBound = EMPTY_Y_BOUND;
        NumberAxis yAxis = new NumberAxis(-yBound, yBound, yBound / 5.0);
        yAxis.setLabel("Index value");
        yAxis.setForceZeroInRange(true);

        LineChart<Number, Number> chart = new LineChart<>(xAxis, yAxis);
        chart.setTitle("Evelyn Status Index History");
        chart.setCreateSymbols(false);
        chart.setAnimated(false);
        XYChart.Series<Number, Number> priceIndexSeries = createSeries(
                "Evelyn Price Index",
                List.of(),
                EvelynStatusIndexPoint::evelynPriceIndex
        );
        chart.getData().add(priceIndexSeries);
        //chart.getData().add(createSeries("EVE_SYRUP Pool Depth Index", history, EvelynStatusIndexPoint::eveSyrupPoolDepthIndex));
        //chart.getData().add(createSeries("EVE_SYRUP Pool Balance Index", history, EvelynStatusIndexPoint::eveSyrupPoolBalanceIndex));
        //chart.getData().add(createSeries("AAZDKK_USDT Pool Balance Index", history, EvelynStatusIndexPoint::aazdkkUsdtPoolBalanceIndex));
        //chart.getData().add(createSeries("AAZDKK_USDT Pool Price Index", history, EvelynStatusIndexPoint::aazdkkUsdtPoolPriceIndex));
        //chart.getData().add(createSeries("AAZDKK_USDT Pool Depth Index", history, EvelynStatusIndexPoint::aazdkkUsdtPoolDepthIndex));
        return new StatusIndexChart(chart, xAxis, yAxis, priceIndexSeries);
    }

    private static XYChart.Series<Number, Number> createSeries(
            String name,
            List<EvelynStatusIndexPoint> history,
            Function<EvelynStatusIndexPoint, BigDecimal> valueExtractor
    ) {
        XYChart.Series<Number, Number> series = new XYChart.Series<>();
        series.setName(name);
        for (EvelynStatusIndexPoint point : history) {
            series.getData().add(new XYChart.Data<>(
                    point.timestamp().toEpochMilli(),
                    valueExtractor.apply(point)
            ));
        }
        return series;
    }

    static double calculateYBound(List<EvelynStatusIndexPoint> history) {
        BigDecimal maxAbsoluteValue = BigDecimal.ZERO;
        for (EvelynStatusIndexPoint point : history) {
            maxAbsoluteValue = max(maxAbsoluteValue, point.evelynPriceIndex());
            //maxAbsoluteValue = max(maxAbsoluteValue, point.eveSyrupPoolDepthIndex());
            //maxAbsoluteValue = max(maxAbsoluteValue, point.eveSyrupPoolBalanceIndex());
            //maxAbsoluteValue = max(maxAbsoluteValue, point.aazdkkUsdtPoolBalanceIndex());
            //maxAbsoluteValue = max(maxAbsoluteValue, point.aazdkkUsdtPoolPriceIndex());
            //maxAbsoluteValue = max(maxAbsoluteValue, point.aazdkkUsdtPoolDepthIndex());
        }

        double bound = maxAbsoluteValue.doubleValue();
        return bound == 0.0 ? EMPTY_Y_BOUND : bound;
    }

    private static BigDecimal max(BigDecimal currentMaximum, BigDecimal candidate) {
        return currentMaximum.max(candidate.abs());
    }

    static void refreshChart(
            StatusIndexChart statusIndexChart,
            List<EvelynStatusIndexPoint> history
    ) {
        if (!Platform.isFxApplicationThread()) {
            throw new IllegalStateException("Status-index charts must be updated on the JavaFX Application Thread");
        }

        int commonPointCount = 0;
        int maximumCommonPointCount = Math.min(
                history.size(),
                statusIndexChart.renderedHistory.size()
        );
        while (commonPointCount < maximumCommonPointCount
                && history.get(commonPointCount).equals(
                        statusIndexChart.renderedHistory.get(commonPointCount)
                )) {
            commonPointCount++;
        }
        if (commonPointCount < statusIndexChart.renderedHistory.size()) {
            statusIndexChart.priceIndexSeries().getData().clear();
            statusIndexChart.renderedHistory = List.of();
            commonPointCount = 0;
        }
        for (int index = commonPointCount; index < history.size(); index++) {
            EvelynStatusIndexPoint point = history.get(index);
            statusIndexChart.priceIndexSeries().getData().add(new XYChart.Data<>(
                    point.timestamp().toEpochMilli(),
                    point.evelynPriceIndex()
            ));
        }
        statusIndexChart.renderedHistory = List.copyOf(history);
        updateAxisBounds(statusIndexChart, history);
    }

    private static void updateAxisBounds(
            StatusIndexChart statusIndexChart,
            List<EvelynStatusIndexPoint> history
    ) {
        long minimumTimestamp;
        long maximumTimestamp;
        if (history.isEmpty()) {
            minimumTimestamp = 0;
            maximumTimestamp = 1;
        } else {
            minimumTimestamp = history.getFirst().timestamp().toEpochMilli();
            maximumTimestamp = history.getLast().timestamp().toEpochMilli();
            if (minimumTimestamp == maximumTimestamp) {
                minimumTimestamp -= SINGLE_POINT_X_PADDING_MILLIS;
                maximumTimestamp += SINGLE_POINT_X_PADDING_MILLIS;
            }
        }
        double xTickUnit = Math.max(1, (maximumTimestamp - minimumTimestamp) / 4.0);
        statusIndexChart.xAxis().setLowerBound(minimumTimestamp);
        statusIndexChart.xAxis().setUpperBound(maximumTimestamp);
        statusIndexChart.xAxis().setTickUnit(xTickUnit);

        double yBound = calculateYBound(history);
        statusIndexChart.yAxis().setLowerBound(-yBound);
        statusIndexChart.yAxis().setUpperBound(yBound);
        statusIndexChart.yAxis().setTickUnit(yBound / 5.0);
    }

    private Tab createMissionControlTab(String title, String color) {
        StackPane content = new StackPane();
        content.setStyle("-fx-background-color: " + color + ";");

        Tab tab = new Tab(title, content);
        tab.setStyle("-fx-background-color: " + color + ";");

        return tab;
    }

    private TabPane createRootTabs() {
        productionChart = createStatusIndexChart();
        testChart = createStatusIndexChart();

        Tab productionTab = new Tab(
                "Production",
                createMissionControlTabs(PRODUCTION_COLOR, productionChart)
        );
        productionTab.setStyle("-fx-background-color: " + PRODUCTION_COLOR + ";");

        Tab testTab = new Tab(
                "Test",
                createMissionControlTabs(TEST_COLOR, testChart)
        );
        testTab.setStyle("-fx-background-color: " + TEST_COLOR + ";");

        TabPane rootTabs = new TabPane(productionTab, testTab);
        rootTabs.setTabClosingPolicy(TabPane.TabClosingPolicy.UNAVAILABLE);

        return rootTabs;
    }

    private void refreshCharts() {
        refreshChart(productionChart, evelynProd.getStatusIndexHistory());
        refreshChart(testChart, evelynTest.getStatusIndexHistory());
    }

    static final class StatusIndexChart {
        private StatusIndexChart(
                LineChart<Number, Number> chart,
                NumberAxis xAxis,
                NumberAxis yAxis,
                XYChart.Series<Number, Number> priceIndexSeries
        ) {
            this.chart = chart;
            this.xAxis = xAxis;
            this.yAxis = yAxis;
            this.priceIndexSeries = priceIndexSeries;
        }

        LineChart<Number, Number> chart() {
            return chart;
        }

        NumberAxis xAxis() {
            return xAxis;
        }

        NumberAxis yAxis() {
            return yAxis;
        }

        XYChart.Series<Number, Number> priceIndexSeries() {
            return priceIndexSeries;
        }

        private final LineChart<Number, Number> chart;
        private final NumberAxis xAxis;
        private final NumberAxis yAxis;
        private final XYChart.Series<Number, Number> priceIndexSeries;
        private List<EvelynStatusIndexPoint> renderedHistory = List.of();
    }

    private static final String PRODUCTION_COLOR = "#ffd6d6";
    private static final String TEST_COLOR = "#d8f3dc";
    private static final double EMPTY_Y_BOUND = 1.0;
    private static final double CHART_REFRESH_SECONDS = 1.0;
    private static final long SINGLE_POINT_X_PADDING_MILLIS = 30_000;
    private static final DateTimeFormatter TIMESTAMP_TICK_FORMATTER =
            DateTimeFormatter.ofPattern("HH:mm dd/MM/yy")
                    .withZone(ZoneId.of("Europe/Copenhagen"));
    private static final StringConverter<Number> TIMESTAMP_FORMATTER = new StringConverter<>() {
        @Override
        public String toString(Number timestamp) {
            return TIMESTAMP_TICK_FORMATTER.format(Instant.ofEpochMilli(timestamp.longValue()));
        }

        @Override
        public Number fromString(String value) {
            throw new UnsupportedOperationException("Timestamp axis labels are display-only");
        }
    };

    private final Evelyn evelynProd;
    private final Evelyn evelynTest;
    private final Timeline refreshTimeline = new Timeline(new KeyFrame(
            Duration.seconds(CHART_REFRESH_SECONDS),
            event -> refreshCharts()
    ));
    private Stage window;
    private StatusIndexChart productionChart;
    private StatusIndexChart testChart;
}
