package controllers;

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.net.URL;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.Collection;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.ResourceBundle;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;

import com.google.common.collect.Lists;

import data.GuiMysql;
import javafx.beans.binding.Bindings;
import javafx.beans.property.StringProperty;
import javafx.beans.value.ChangeListener;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.Node;
import javafx.scene.control.Button;
import javafx.scene.control.ComboBox;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TablePosition;
import javafx.scene.control.TableView;
import javafx.scene.control.TextField;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.control.cell.TextFieldTableCell;
import javafx.util.Callback;
import javafx.util.StringConverter;
import javafx.util.converter.FloatStringConverter;
import objects.BetDTO;
import objects.SoccerMatch;

@SuppressWarnings("rawtypes")
public class MatchTabController implements Initializable {

    @FXML
    private TableView<SoccerMatch> matchTable;
    @FXML
    TableColumn<SoccerMatch, String> gameDateColumn = new TableColumn<>("Gamedate");
    @FXML
    TableColumn<SoccerMatch, String> homeTeamColumn = new TableColumn<>("Home Team");
    @FXML
    TableColumn<SoccerMatch, String> awayTeamColumn = new TableColumn<>("Away Team");
    @FXML
    TableColumn<SoccerMatch, Float> odds1Column = new TableColumn<>("odds 1");
    @FXML
    TableColumn<SoccerMatch, Float> oddsXColumn = new TableColumn<>("odds X");
    @FXML
    TableColumn<SoccerMatch, Float> odds2Column = new TableColumn<>("odds 2");
    @FXML
    TableColumn<SoccerMatch, Float> goalsHomeColumn = new TableColumn<>("Goals home");
    @FXML
    TableColumn<SoccerMatch, Float> goalsAwayColumn = new TableColumn<>("Goals away");
    @FXML
    TableColumn<SoccerMatch, Float> goalDiff = new TableColumn<>("Difference");
    @FXML
    TableColumn<SoccerMatch, Float> avgScoreHomeColumn = new TableColumn<>("Avg score home");
    @FXML
    TableColumn<SoccerMatch, Float> avgConcededHomeColumn = new TableColumn<>("Avg conceded home");
    @FXML
    TableColumn<SoccerMatch, Float> avgScoreAwayColumn = new TableColumn<>("Avg score away");
    @FXML
    TableColumn<SoccerMatch, Float> avgConcededAwayColumn = new TableColumn<>("Avg conceded away");
    @FXML
    TableColumn<SoccerMatch, String> countryColumn = new TableColumn<>("Country");
    @FXML
    TableColumn<SoccerMatch, String> leagueColumn = new TableColumn<>("League");

    @FXML
    TableColumn<SoccerMatch, String> scoringScore = new TableColumn<>("scoringScore");

    @FXML
    TableColumn<SoccerMatch, Float> ownOddsPerdiction = new TableColumn<>("ownOdds");
    @FXML
    TableColumn<SoccerMatch, Float> givenOdds = new TableColumn<>("givenOdds");
    @FXML
    TableColumn<SoccerMatch, String> percentageToBet = new TableColumn<>("toBetPercent");
    @FXML
    TableColumn<SoccerMatch, String> betAmount = new TableColumn<>("betAmount");

    @FXML
    TableColumn<Map, Boolean> addToBetSlip = new TableColumn<>("addToBetslip");

    @FXML
    TextField betAmountTextField;

    @FXML
    private TableView<BetDTO> activeBettingTable;
    private final ObservableList<BetDTO> betObservableList = FXCollections.observableArrayList();
    private final ObservableList<SoccerMatch> soccerMatchesObservableList = FXCollections.observableArrayList();

    @FXML
    TableColumn<BetDTO, StringProperty> betSeries = new TableColumn<>("Bet series");
    @FXML
    TableColumn<BetDTO, String> gameDate = new TableColumn<>("Game date");
    @FXML
    TableColumn<BetDTO, String> match = new TableColumn<>("Match");
    @FXML
    TableColumn<BetDTO, String> betType = new TableColumn<>("Bet type");
    @FXML
    TableColumn<BetDTO, Float> bet = new TableColumn<>("Bet amount");
    @FXML
    TableColumn<BetDTO, Float> odds = new TableColumn<>("Odds");
    @FXML
    TableColumn<BetDTO, String> result = new TableColumn<>("Result");
    @FXML
    TableColumn<BetDTO, String> winAmount = new TableColumn<>("Win amount");
    @FXML
    TableColumn<BetDTO, Void> submitButton = new TableColumn<>("Submit row");

    private ObservableList<SoccerMatch> coutryFilteredMatches;
    private List<SoccerMatch> upcomingMatches;
    private int countryId;
    private static final String FLOAT_FORMAT = "%.02f";

    private ObservableList<String> betSeriesOptions = FXCollections.observableArrayList();
    private List<BetDTO> dbBetSeries;
    private int lastBetSeries;

    @Override
    public void initialize(URL arg0, ResourceBundle arg1) {
        gameDateColumn.setCellValueFactory(new PropertyValueFactory<>("gameDate"));
        homeTeamColumn.setCellValueFactory(new PropertyValueFactory<>("homeTeamName"));
        awayTeamColumn.setCellValueFactory(new PropertyValueFactory<>("awayTeamName"));
        odds1Column.setCellValueFactory(new PropertyValueFactory<>("odds1"));
        oddsXColumn.setCellValueFactory(new PropertyValueFactory<>("oddsX"));
        odds2Column.setCellValueFactory(new PropertyValueFactory<>("odds2"));
        goalsHomeColumn.setCellValueFactory(new PropertyValueFactory<>("goalsHome"));
        goalsAwayColumn.setCellValueFactory(new PropertyValueFactory<>("goalsAway"));
        goalDiff.setCellValueFactory(new PropertyValueFactory<>("goalsDiff"));
        avgScoreHomeColumn.setCellValueFactory(new PropertyValueFactory<>("avgScoreHome"));
        avgConcededHomeColumn.setCellValueFactory(new PropertyValueFactory<>("avgConcededHome"));
        avgScoreAwayColumn.setCellValueFactory(new PropertyValueFactory<>("avgScoreAway"));
        avgConcededAwayColumn.setCellValueFactory(new PropertyValueFactory<>("avgConcededAway"));
        countryColumn.setCellValueFactory(new PropertyValueFactory<>("countryName"));
        leagueColumn.setCellValueFactory(new PropertyValueFactory<>("leagueName"));

        scoringScore.setCellValueFactory(new PropertyValueFactory<>("scoringScore"));

        ownOddsPerdiction.setCellValueFactory(new PropertyValueFactory<>("ownOdds"));
        givenOdds.setCellValueFactory(new PropertyValueFactory<>("givenOdds"));
        percentageToBet.setCellValueFactory(new PropertyValueFactory<>("percentageToBet"));
        betAmount.setCellValueFactory(new PropertyValueFactory<>("betAmount"));

        ownOddsPerdiction.setCellFactory(TextFieldTableCell.forTableColumn(new OwnFloatStringConverter()));
        ownOddsPerdiction.setEditable(true);
        ownOddsPerdiction.setOnEditCommit(e -> {
            SoccerMatch soccerMatch = e.getTableView().getItems().get(e.getTablePosition().getRow());
            soccerMatch.setOwnOdds(e.getNewValue());
            if (soccerMatch.getGivenOdds() != null && soccerMatch.getGivenOdds() > 0f) {
                calculateBetPercent(soccerMatch);
                setBetAmount(soccerMatch);
                matchTable.getItems().set(e.getTablePosition().getRow(), soccerMatch);
            }
        });

        givenOdds.setCellFactory(TextFieldTableCell.forTableColumn(new OwnFloatStringConverter()));
        givenOdds.setEditable(true);
        givenOdds.setOnEditCommit(e -> {
            SoccerMatch soccerMatch = e.getTableView().getItems().get(e.getTablePosition().getRow());
            soccerMatch.setGivenOdds(e.getNewValue());

            if (soccerMatch.getOwnOdds() != null && soccerMatch.getOwnOdds() > 0f) {
                calculateBetPercent(soccerMatch);
                setBetAmount(soccerMatch);
                matchTable.getItems().set(e.getTablePosition().getRow(), soccerMatch);
            }

        });

        betSeries.setCellValueFactory(i -> {
            return Bindings.createObjectBinding(() -> i.getValue().optionProperty());
        });

        matchTable.getColumns().addAll(gameDateColumn, homeTeamColumn, awayTeamColumn, odds1Column, oddsXColumn,
                odds2Column, goalsHomeColumn,
                goalsAwayColumn, goalDiff, avgScoreHomeColumn, avgConcededHomeColumn, avgScoreAwayColumn,
                avgConcededAwayColumn, countryColumn,
                leagueColumn, scoringScore, ownOddsPerdiction, givenOdds, percentageToBet, betAmount);
        matchTable.setItems(soccerMatchesObservableList);

        matchTable.setEditable(true);

        gameDate.setCellValueFactory(new PropertyValueFactory<>("gameDate"));
        match.setCellValueFactory(new PropertyValueFactory<>("match"));
        betType.setCellValueFactory(new PropertyValueFactory<>("betType"));
        bet.setCellValueFactory(new PropertyValueFactory<>("bet"));
        odds.setCellValueFactory(new PropertyValueFactory<>("odds"));
        result.setCellValueFactory(new PropertyValueFactory<>("result"));
        winAmount.setCellValueFactory(new PropertyValueFactory<>("winAmount"));

        makeColumnsEditable();

        activeBettingTable.getColumns().addAll(betSeries, gameDate, match, betType, bet, odds, result, winAmount);
        activeBettingTable.setItems(betObservableList);

        activeBettingTable.setEditable(true);

        addSubmitButtonToTable();

        addActiveBetsFromDatabase();

        betAmountTextField.setText(GuiMysql.getInstance().getBetBaseAmount());

        betAmountTextField.focusedProperty().addListener(new ChangeListener<Boolean>() {
            @Override
            public void changed(javafx.beans.value.ObservableValue<? extends Boolean> observable, Boolean oldValue,
                    Boolean newValue) {
                if (!newValue) { // Textfield focus lost
                    GuiMysql.getInstance().updateBetBaseAmount(betAmountTextField.getText());
                }
            };
        });
    }

    private void setBetAmount(SoccerMatch soccerMatch) {
        if (betAmountTextField.getText().length() > 0) {
            float betAmountValue = Float.parseFloat(betAmountTextField.getText());
            soccerMatch.setBetAmount(betAmountValue * (soccerMatch.getPercentageToBet() / 100f));
        }
    }

    /*
     * Kelly Criterion formula (BP - Q) / B B = Decimal odds -1 P = Probability of
     * success Q = Probability of failure (1 - P)
     */
    private void calculateBetPercent(SoccerMatch soccerMatch) {
        float oddsMinusOne = soccerMatch.getGivenOdds() - 1;
        float probabilityOfSuccess = (1 / soccerMatch.getOwnOdds());
        float probabilityOfFailure = 1 - probabilityOfSuccess;
        float value = ((oddsMinusOne * probabilityOfSuccess) - probabilityOfFailure) / oddsMinusOne;

        soccerMatch.setPercentageToBet(value * 100f);
    }

    private void addActiveBetsFromDatabase() {
        dbBetSeries = GuiMysql.getInstance().getBetSeries(false);

        betSeriesOptions.addAll(dbBetSeries.stream().map(BetDTO::getBetSeries).sorted().map(String::valueOf).distinct()
                .collect(Collectors.toCollection(LinkedList::new)));

        lastBetSeries = GuiMysql.getInstance().getBetSeriesEndNumber();
        betSeriesOptions.add(String.valueOf(lastBetSeries + 1));

        betObservableList.addAll(dbBetSeries);
    }

    private void makeColumnsEditable() {

        betSeries.setCellFactory(col -> {
            TableCell<BetDTO, StringProperty> c = new TableCell<>();
            final ComboBox<String> comboBox = new ComboBox<>(betSeriesOptions);
            c.itemProperty().addListener((observable, oldValue, newValue) -> {
                if (oldValue != null) {
                    comboBox.valueProperty().unbindBidirectional(oldValue);
                }
                if (newValue != null) {
                    comboBox.valueProperty().bindBidirectional(newValue);
                }
            });
            c.graphicProperty().bind(Bindings.when(c.emptyProperty()).then((Node) null).otherwise(comboBox));
            return c;
        });

        match.setCellFactory(TextFieldTableCell.forTableColumn());
        match.setOnEditCommit(
                e -> e.getTableView().getItems().get(e.getTablePosition().getRow()).setMatch(e.getNewValue()));
        betType.setCellFactory(TextFieldTableCell.forTableColumn());
        betType.setOnEditCommit(
                e -> e.getTableView().getItems().get(e.getTablePosition().getRow()).setBetType(e.getNewValue()));
        bet.setCellFactory(TextFieldTableCell.forTableColumn(new FloatStringConverter()));
        bet.setOnEditCommit(
                e -> e.getTableView().getItems().get(e.getTablePosition().getRow()).setBet(e.getNewValue()));
        odds.setCellFactory(TextFieldTableCell.forTableColumn(new FloatStringConverter()));
        odds.setOnEditCommit(
                e -> e.getTableView().getItems().get(e.getTablePosition().getRow()).setOdds(e.getNewValue()));
    }

    private void addSubmitButtonToTable() {
        Callback<TableColumn<BetDTO, Void>, TableCell<BetDTO, Void>> cellFactory = new Callback<TableColumn<BetDTO, Void>, TableCell<BetDTO, Void>>() {
            @Override
            public TableCell<BetDTO, Void> call(final TableColumn<BetDTO, Void> param) {
                return new TableCell<BetDTO, Void>() {

                    private final Button btn = new Button("Submit");
                    {
                        btn.setOnAction((ActionEvent event) -> {
                            BetDTO data = getTableView().getItems().get(getIndex());
                            GuiMysql.getInstance().addBetSeries(data);
                        });
                    }

                    @Override
                    public void updateItem(Void item, boolean empty) {
                        super.updateItem(item, empty);
                        if (empty) {
                            setGraphic(null);
                        } else {
                            setGraphic(btn);
                        }
                    }
                };
            }
        };

        submitButton.setCellFactory(cellFactory);

        activeBettingTable.getColumns().add(submitButton);

    }

    public void setMatches(List<SoccerMatch> upcomingMatches) {
        ObservableList<Map<String, Object>> matches = FXCollections.<Map<String, Object>>observableArrayList();
        for (final SoccerMatch soccerMatch : upcomingMatches) {

            final List<Float> avgHomeTeamGoals = calculateAvgHomeScore(soccerMatch.getHomeTeam().getTeamId());
            final List<Float> avgAwayTeamGoals = calculateAvgAwayScore(soccerMatch.getAwayTeam().getTeamId());

            List<Float> avgHomeScoreThisSeason = GuiMysql.getInstance().getAvgHomeScoreThisSeason(
                    soccerMatch.getHomeTeam().getTeamId(),
                    soccerMatch.getHomeTeam().getCountryId(), soccerMatch.getHomeTeam().getTeamLeagueId());
            List<Float> avgAwayScoreThisSeason = GuiMysql.getInstance().getAvgAwayScoreThisSeason(
                    soccerMatch.getAwayTeam().getTeamId(),
                    soccerMatch.getAwayTeam().getCountryId(), soccerMatch.getAwayTeam().getTeamLeagueId());

            soccerMatch.setGoalsHome(
//                    GuiMysql.getInstance().round(
                    BigDecimal.valueOf(avgHomeScoreThisSeason.get(0) * avgAwayScoreThisSeason.get(1)));// ,
//                    GuiMysql.getInstance().getIncrement()
//                            , RoundingMode.HALF_UP));
            soccerMatch.setGoalsAway(
//                    GuiMysql.getInstance().round(
                    BigDecimal.valueOf(avgAwayScoreThisSeason.get(0) * avgHomeScoreThisSeason.get(1))); // ,
//                            GuiMysql.getInstance().getIncrement(), RoundingMode.HALF_UP));

            soccerMatch.setGoalsDiff(GuiMysql.getInstance()
                    .round(BigDecimal.valueOf((avgHomeScoreThisSeason.get(0) * avgAwayScoreThisSeason.get(1))
                            + (avgAwayScoreThisSeason.get(0) * avgHomeScoreThisSeason.get(1))),
                            GuiMysql.getInstance().getIncrement(),
                            RoundingMode.HALF_UP));

            soccerMatch.setAvgScoreHome(avgHomeTeamGoals.get(0));
            soccerMatch.setAvgConcededHome(avgHomeTeamGoals.get(1));
            soccerMatch.setAvgScoreAway(avgAwayTeamGoals.get(0));
            soccerMatch.setAvgConcededAway(avgAwayTeamGoals.get(1));
            soccerMatch.setCountryName(soccerMatch.getHomeTeam().getCountryName());
            soccerMatch.setLeagueName(soccerMatch.getHomeTeam().getTeamLeague());

            final DecimalFormat df = new DecimalFormat("#.##");
            df.setRoundingMode(RoundingMode.CEILING);

            soccerMatch.setScoringScore((avgHomeScoreThisSeason.get(0) * avgAwayScoreThisSeason.get(1))
                    + (avgAwayScoreThisSeason.get(0) * avgHomeScoreThisSeason.get(1)));

            soccerMatch.setAddToBetSlip(false);
            soccerMatch.setHomeTeamName(soccerMatch.getHomeTeam().getTeamName());
            soccerMatch.setAwayTeamName(soccerMatch.getAwayTeam().getTeamName());
            soccerMatchesObservableList.add(soccerMatch);
        }
    }

    public void filterLeagueMatches(String filterByName, int countryId) {
        List<SoccerMatch> filteredMatches = upcomingMatches.stream()
                .filter(p -> p.getHomeTeam().getCountryId() == countryId)
                .filter(p -> p.getHomeTeam().getTeamLeague().equals(filterByName)).toList();
        this.countryId = countryId;
        matchTable.getItems().clear();
        setMatches(new ArrayList<>(filteredMatches));
    }

    public void filterCountryMatches(String filterByName) {
        List<SoccerMatch> filteredMatches = upcomingMatches.stream()
                .filter(p -> p.getHomeTeam().getCountryName().equals(filterByName)).toList();

        matchTable.getItems().clear();
        setMatches(new ArrayList<>(filteredMatches));
        coutryFilteredMatches = matchTable.getItems();
    }

    public void resetFilter() {
        matchTable.getItems().clear();
    }

    private List<Float> calculateAvgHomeScore(int teamId) {
        return GuiMysql.getInstance().getAvgHomeScore(teamId);
    }

    private List<Float> calculateAvgAwayScore(int teamId) {
        return GuiMysql.getInstance().getAvgAwayScore(teamId);
    }

    public Collection<SoccerMatch> getTableMatches() {
        return matchTable.getItems();
    }

    public List<String> getLeaguesFromSelection() {
        final ArrayList<String> leagues = Lists.newArrayList();
        coutryFilteredMatches.forEach(f -> {
            if (!leagues.contains(f.getLeagueName())) {
                leagues.add(f.getLeagueName());
            }
        });
        return leagues;
    }

    public static <T> Predicate<T> distinctByKey(Function<? super T, ?> keyExtractor) {

        final Map<Object, Boolean> seen = new ConcurrentHashMap<>();
        return t -> seen.putIfAbsent(keyExtractor.apply(t), Boolean.TRUE) == null;
    }

    public TableView getTable() {
        return matchTable;
    }

    public void setUpcomingMatches(List<SoccerMatch> upcomingMatches) {
        this.upcomingMatches = upcomingMatches;
        matchTable.getItems().clear();
        setMatches(new ArrayList<>(upcomingMatches));
    }

    @FXML
    public void addRowAction(ActionEvent event) {
        ObservableList<TablePosition> selectedCells = matchTable.getSelectionModel().getSelectedCells();
        if (selectedCells.size() > 0) {
            SoccerMatch selectedRow = matchTable.getItems().get(selectedCells.get(0).getRow());
            BetDTO betDTO = new BetDTO();
            betDTO.setCountryId(countryId);
            betDTO.setLeague(selectedRow.getLeagueName());
            betDTO.setHomeTeam(selectedRow.getHomeTeam().getTeamName());
            betDTO.setAwayTeam(selectedRow.getAwayTeam().getTeamName());
            betDTO.setGameDate(MainController.DATE_FORMAT.format(selectedRow.getGameDate()));
            betDTO.setBetSeries(lastBetSeries + 1);
            betDTO.setMatch(selectedRow.getHomeTeam().getTeamName(), selectedRow.getAwayTeam().getTeamName());
            betDTO.setBetType("O/U 1.5");
            betDTO.setWinAmount(-1);

            betDTO.setGameId(selectedRow.getMatchId());

            betObservableList.add(betDTO);
        } else {
            System.out.println("No row selected");
        }
    }

    public class OwnFloatStringConverter extends StringConverter {

        @Override
        public String toString(Object object) {

            return object == null || Float.isNaN((float) object) ? "" : Float.toString((float) object);
        }

        @Override
        public Object fromString(String string) {

            return Float.valueOf(string.replace(",", "."));
        }

    }

}
