package logbook.internal.gui;

import java.io.IOException;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.TreeMap;
import java.util.function.Function;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.chart.BubbleChart;
import javafx.scene.chart.CategoryAxis;
import javafx.scene.chart.PieChart;
import javafx.scene.chart.StackedBarChart;
import javafx.scene.chart.XYChart;
import javafx.scene.layout.VBox;
import javafx.scene.text.Text;
import logbook.bean.Ship;
import logbook.bean.ShipCollection;
import logbook.bean.Stype;
import logbook.internal.ExpTable;
import logbook.internal.LoggerHolder;
import logbook.internal.Ships;

/**
 * 統計
 *
 */
public class StatisticsPane extends VBox {

    @FXML
    private VBox content;

    /** 総計 */
    @FXML
    private Text total;

    /** 比率 */
    @FXML
    private PieChart ratio;

    /** 平均レベル */
    @FXML
    private StackedBarChart<Double, String> average;

    /** 平均レベル カテゴリ軸 */
    @FXML
    private CategoryAxis averageCategory;

    /** レベル分布 */
    @FXML
    private StackedBarChart<Long, String> spectrum;

    /** レベル分布 カテゴリ軸 */
    @FXML
    private CategoryAxis spectrumCategory;

    /** レベル分布 */
    @FXML
    private BubbleChart<Double, Double> bubble;

    public StatisticsPane() {
        try {
            FXMLLoader loader = InternalFXMLLoader.load("logbook/gui/statistics.fxml");
            loader.setRoot(this);
            loader.setController(this);
            loader.load();
        } catch (IOException e) {
            LoggerHolder.get().error("FXMLのロードに失敗しました", e);
        }
    }

    @FXML
    void initialize() {
    }

    /**
     * 画像ファイルに保存
     * @param event ActionEvent
     */
    @FXML
    void storeImageAction(ActionEvent event) {
        Tools.Conrtols.storeSnapshot(this.content, "統計情報", this.content.getScene().getWindow());
    }

    /**
     * 更新
     */
    public void update() {
        // 対象艦抽出
        List<Ship> ships = ShipCollection.get()
                .getShipMap()
                .values()
                .stream()
                // ロックしている艦
                .filter(Ship::getLocked)
                .collect(Collectors.toList());
        // 総計
        this.setTotal(ships);
        // 経験値比率
        this.setRatio(ships);
        // 平均レベル
        this.setAverage(ships);
        // レベル分布
        this.setSpectrum(ships);
        this.setBubble(ships);
    }

    /**
     * 総計
     * @param ships 対象艦
     */
    private void setTotal(List<Ship> ships) {
        this.total.setText(this.suffixDecimal(ships.stream()
                .mapToLong(this::getExp)
                .sum()));
    }

    /**
     * 経験値比率
     * @param ships 対象艦
     */
    private void setRatio(List<Ship> ships) {
        Map<TypeGroup, Long> collect = ships.stream()
                .collect(Collectors.groupingBy(TypeGroup::toTypeGroup, TreeMap::new,
                        Collectors.summingLong(this::getExp)));

        ObservableList<PieChart.Data> value = FXCollections.observableArrayList();
        for (Entry<TypeGroup, Long> data : collect.entrySet()) {
            if (data.getKey() != null)
                value.add(new PieChart.Data(data.getKey().name(), data.getValue()));
        }
        this.ratio.setData(value);
    }

    /**
     * 平均レベル
     * @param ships 対象艦
     */
    private void setAverage(List<Ship> ships) {
        this.average.getData().clear();

        this.averageCategory.setCategories(
                Arrays.stream(TypeGroup.values())
                        .map(TypeGroup::name)
                        .collect(Collectors.toCollection(FXCollections::observableArrayList)));
        ships.stream()
                .collect(Collectors.groupingBy(TypeGroup::toTypeGroup,
                        Collectors.averagingLong(Ship::getLv)))
                .entrySet().stream()
                .filter(e -> e.getKey() != null)
                .sorted(Comparator.comparing(Entry<TypeGroup, Double>::getKey))
                .map(e -> new XYChart.Data<>(e.getValue(), e.getKey().name()))
                .forEach(data -> {
                    XYChart.Series<Double, String> series = new XYChart.Series<>();
                    series.setName(data.getYValue());
                    series.getData().add(data);
                    this.average.getData().add(series);
                });
    }

    /**
     * レベル分布
     * @param ships 対象艦
     */
    private void setSpectrum(List<Ship> ships) {
        this.spectrum.getData().clear();

        Map<TypeGroup, Map<Integer, Long>> value = ships.stream()
                .collect(Collectors.groupingBy(TypeGroup::toTypeGroup, Collectors.mapping(this::tickLevel,
                        Collectors.groupingBy(Function.identity(), Collectors.counting()))));
        this.spectrumCategory.setCategories(
                IntStream.rangeClosed(1, ExpTable.maxLv())
                        .map(i -> i / 10 * 10)
                        .distinct()
                        .mapToObj(Integer::valueOf)
                        .map(i -> i + "-" + (i + 9))
                        .collect(Collectors.toCollection(FXCollections::observableArrayList)));
        for (TypeGroup group : TypeGroup.values()) {
            Map<Integer, Long> tick = value.get(group);
            if (tick != null) {
                ObservableList<XYChart.Data<Long, String>> data = tick.entrySet()
                        .stream()
                        .map(e -> new XYChart.Data<>(e.getValue(), e.getKey() + "-" + (e.getKey() + 9)))
                        .collect(Collectors.toCollection(FXCollections::observableArrayList));

                XYChart.Series<Long, String> series = new XYChart.Series<>();
                series.setName(group.name());
                series.getData().addAll(data);
                this.spectrum.getData().add(series);
            }
        }
    }

    /**
     * レベル分布
     * @param ships 対象艦
     */
    private void setBubble(List<Ship> ships) {
        this.bubble.getData().clear();

        Map<TypeGroup, List<Integer>> value = ships.stream()
                .collect(Collectors.groupingBy(TypeGroup::toTypeGroup,
                        Collectors.mapping(Ship::getLv, Collectors.toList())));

        for (TypeGroup group : TypeGroup.values()) {
            List<Integer> levels = value.get(group);
            if (levels != null) {
                double average = levels.stream()
                        .mapToInt(Integer::intValue)
                        .average()
                        .orElse(0);
                int size = levels.size();
                levels.sort(Integer::compareTo);
                double median = (size % 2 == 0)
                        ? (double) (levels.get(size / 2) + levels.get(Math.max(size / 2 - 1, 0))) / 2
                        : (double) levels.get(size / 2);

                ObservableList<XYChart.Data<Double, Double>> datas = FXCollections.observableArrayList();
                datas.add(new XYChart.Data<>(average, median, ((double) size) / 5));
                XYChart.Series<Double, Double> series = new XYChart.Series<>(group.name(), datas);
                this.bubble.getData().add(series);
            }
        }
    }

    /**
     * 接尾辞付きの数値に変換する
     * @param v 数値
     * @return 接尾辞付きの数値 (100Mなど)
     */
    private String suffixDecimal(long v) {
        BigDecimal d;
        String prefix;
        int scale;
        if (1000_000_000L <= v) {
            d = BigDecimal.valueOf(1000_000_000L);
            prefix = "G";
            scale = 3;
        } else if (1000_000L <= v) {
            d = BigDecimal.valueOf(1000_000L);
            prefix = "M";
            scale = 2;
        } else {
            d = BigDecimal.valueOf(1000L);
            prefix = "K";
            scale = 1;
        }
        return BigDecimal.valueOf(v)
                .divide(d, scale, RoundingMode.HALF_EVEN)
                .toPlainString() + prefix;
    }

    private long getExp(Ship ship) {
        return ship.getExp().get(0);
    }

    private int tickLevel(Ship ship) {
        return ship.getLv() / 10 * 10;
    }

    /**
     * 艦種のグループわけ
     */
    private enum TypeGroup {

        駆逐艦("駆逐艦"),
        海防艦("海防艦"),
        正規空母("正規空母", "装甲空母"),
        軽空母("軽空母"),
        戦艦("戦艦", "航空戦艦"),
        巡洋艦("軽巡洋艦", "重雷装巡洋艦", "練習巡洋艦", "重巡洋艦", "航空巡洋艦"),
        潜水艦("潜水艦", "潜水空母"),
        特殊艦("水上機母艦", "揚陸艦", "工作艦", "潜水母艦", "補給艦");

        private String[] group;

        private TypeGroup(String... shipTypes) {
            this.group = shipTypes;
        }

        public static TypeGroup toTypeGroup(Ship ship) {
            Stype stype = Ships.stype(ship).orElse(null);
            if (stype != null) {
                String name = stype.getName();
                for (TypeGroup group : values()) {
                    for (String v : group.group) {
                        if (v.equals(name))
                            return group;
                    }
                }
            }
            return null;
        }
    }
}