package de.ivu.elect.presentation.auffaelligkeit;

import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;

import org.apache.commons.lang3.StringUtils;
import org.apache.commons.math3.util.Precision;
import org.primefaces.model.charts.ChartData;
import org.primefaces.model.charts.axes.cartesian.CartesianScales;
import org.primefaces.model.charts.axes.cartesian.linear.CartesianLinearAxes;
import org.primefaces.model.charts.axes.cartesian.linear.CartesianLinearTicks;
import org.primefaces.model.charts.bar.BarChartDataSet;
import org.primefaces.model.charts.bar.BarChartModel;
import org.primefaces.model.charts.bar.BarChartOptions;
import org.primefaces.model.charts.optionconfig.title.Title;

import de.ivu.elect.business.auffaelligkeit.entity.ArtBetrachteterStimmen;
import de.ivu.elect.business.auffaelligkeit.entity.AuffaelligkeitBemerkungUngueltige;
import de.ivu.elect.business.ergebnis.entity.Stimmenart;
import de.ivu.elect.business.gebietsbaum.entity.Gebiet;
import de.ivu.elect.business.gruppe.entity.PolitischeGruppe;
import de.ivu.elect.business.hochrechnung.entity.AnzahlStimmenProWaehlerNachStimmenartBestimmer;
import de.ivu.elect.business.hochrechnung.entity.GebietDaten;
import de.ivu.elect.business.hochrechnung.entity.ParteiStimmen;
import de.ivu.elect.business.hochrechnung.entity.StimmenhochrechnerGebietAusgabedaten;
import de.ivu.elect.business.stimmeingabe.entity.StimmeingabeAuffaelligkeit;


import static de.ivu.elect.business.hochrechnung.control.StimmenHochrechnerHilfsfunktionen.notNull;
import static org.apache.commons.lang3.StringUtils.EMPTY;
import static org.apache.commons.lang3.StringUtils.isBlank;

public class AuffaelligkeitBarChartBuilder {

  private final StimmeingabeAuffaelligkeit auffaelligkeitVomSelectedGebiet;

  private Long anzahlVonAnzuzeigendenGruppenBeiGebietsvergleich;

  private ArtBetrachteterStimmen artBetrachteterStimmen;

  private Function<String, String> getMessageFunction;

  private final AnzahlStimmenProWaehlerNachStimmenartBestimmer anzahlStimmenProWaehlerNachStimmenartBestimmer;
  private final Map<PolitischeGruppe, String> politischeGruppeKurznameAufGebiet = new HashMap<>();

  public AuffaelligkeitBarChartBuilder(StimmeingabeAuffaelligkeit auffaelligkeitVomSelectedGebiet, AnzahlStimmenProWaehlerNachStimmenartBestimmer anzahlStimmenProWaehlerNachStimmenartBestimmer, Long anzahlVonAnzuzeigendenGruppenBeiGebietsvergleich, ArtBetrachteterStimmen artBetrachteterStimmen, Function<String, String> getMessageFunction) {
    assert auffaelligkeitVomSelectedGebiet != null : "Auffaelligkeit sollte nicht null sein";
    this.auffaelligkeitVomSelectedGebiet = auffaelligkeitVomSelectedGebiet;
    this.anzahlVonAnzuzeigendenGruppenBeiGebietsvergleich = anzahlVonAnzuzeigendenGruppenBeiGebietsvergleich;
    this.artBetrachteterStimmen = artBetrachteterStimmen;
    this.getMessageFunction = getMessageFunction;
    this.anzahlStimmenProWaehlerNachStimmenartBestimmer = anzahlStimmenProWaehlerNachStimmenartBestimmer;
  }

  public BarChartModel getBarChartModelAktuellVsHochgerechnet(Stimmenart stimmenart) {

    BarChartModel barModel = new BarChartModel();
    ChartData data = new ChartData();
    barModel.setOptions(getOptionsAktuellVsHochgerechnet());

    if (auffaelligkeitVomSelectedGebiet == null || auffaelligkeitVomSelectedGebiet.getStimmenhochrechnerAusgabedaten() == null) {
      return barModel;
    }

    double alpha = 1;

    StimmenhochrechnerGebietAusgabedaten ausgabedaten = auffaelligkeitVomSelectedGebiet.getStimmenhochrechnerAusgabedaten();

    BarChartDataSet datasetAktuell = createDataSet(getMessageFunction.apply(artBetrachteterStimmen.getMessageKeyFuerStimmenAktuell()),
        artBetrachteterStimmen.getGebietDatenAktuell(auffaelligkeitVomSelectedGebiet.getStimmenhochrechnerAusgabedaten()),
        stimmenart,
        alpha * .9);
    data.addChartDataSet(datasetAktuell);

    GebietDaten<Number> gebietDatenHochgerechnet = artBetrachteterStimmen
        .getGebietDatenHochgerechnet(auffaelligkeitVomSelectedGebiet.getStimmenhochrechnerAusgabedaten(), this.anzahlStimmenProWaehlerNachStimmenartBestimmer);
    if (gebietDatenHochgerechnet != null) {
      BarChartDataSet datasetHochgerechnet = createDataSet(getMessageFunction.apply(artBetrachteterStimmen.getMessageKeyFuerStimmenHochgerechnet()),
          gebietDatenHochgerechnet,
          stimmenart,
          alpha * .3);
      data.addChartDataSet(datasetHochgerechnet);
    }

    data.setLabels(getLabels(stimmenart));

    barModel.setData(data);

    return barModel;
  }

  public List<BarChartModel> getBarChartModelSelectedGebietVsAehnlicheGebiet(Stimmenart stimmenart, Long anzahlVonGebietVergleichen) {
    if (auffaelligkeitVomSelectedGebiet == null || auffaelligkeitVomSelectedGebiet.getStimmenhochrechnerAusgabedaten() == null) {
      return new ArrayList<>();
    }

    return auffaelligkeitVomSelectedGebiet.getAusgabeDatenAehnlichsterGebiete(anzahlVonGebietVergleichen).stream()
        .map(ausgabedatenVomAehnlichenGebiet -> getBarChartModelSelectedGebietVsAehnlichesGebiet(stimmenart, ausgabedatenVomAehnlichenGebiet))
        .collect(Collectors.toList());
  }

  private BarChartModel getBarChartModelSelectedGebietVsAehnlichesGebiet(Stimmenart stimmenart, StimmenhochrechnerGebietAusgabedaten ausgabedatenVomAehnlichenGebiet) {
    BarChartModel barModel = new BarChartModel();
    ChartData data = new ChartData();

    if (ausgabedatenVomAehnlichenGebiet == null) {
      return barModel;
    }

    double alpha = 1;

    BarChartDataSet datasetAktuellSelectedGebiet = createDataSet(getMessageFunction.apply(artBetrachteterStimmen.getMessageKeyFuerStimmenAktuell()) + " (" + getSelectedGebietNameFormatted() + ")",
        artBetrachteterStimmen.getGebietDatenAktuell(auffaelligkeitVomSelectedGebiet.getStimmenhochrechnerAusgabedaten()),
        stimmenart,
        alpha * .9);

    BarChartDataSet datasetAktuellAehnlichesGebiet = createDataSet(getMessageFunction.apply(artBetrachteterStimmen.getMessageKeyFuerStimmenAktuell()) + " (" + formatGebietName(ausgabedatenVomAehnlichenGebiet.getGebiet()) + ")",
        artBetrachteterStimmen.getGebietDatenAktuell(ausgabedatenVomAehnlichenGebiet),
        stimmenart,
        alpha * .25);

    BarChartDataSet datasetVorperiodeSelectedGebiet = createDataSet(getMessageFunction.apply(artBetrachteterStimmen.getMessageKeyFuerStimmenVorperiode()) + " (" + getSelectedGebietNameFormatted() + ")",
        artBetrachteterStimmen.getGebietDatenVorperiode(auffaelligkeitVomSelectedGebiet.getStimmenhochrechnerAusgabedaten()),
        stimmenart,
        alpha * .7);

    BarChartDataSet datasetVorperiodeAehnlichesGebiet = createDataSet(getMessageFunction.apply(artBetrachteterStimmen.getMessageKeyFuerStimmenVorperiode()) + " (" + formatGebietName(ausgabedatenVomAehnlichenGebiet.getGebiet()) + ")",
        artBetrachteterStimmen.getGebietDatenVorperiode(ausgabedatenVomAehnlichenGebiet),
        stimmenart,
        alpha * .10);

    data.addChartDataSet(datasetAktuellSelectedGebiet);
    data.addChartDataSet(datasetAktuellAehnlichesGebiet);
    data.addChartDataSet(datasetVorperiodeSelectedGebiet);
    data.addChartDataSet(datasetVorperiodeAehnlichesGebiet);

    data.setLabels(getLabels(stimmenart));

    barModel.setData(data);

    barModel.setOptions(getOptionsSelectedGebietVsAehnlicheGebiet(ausgabedatenVomAehnlichenGebiet));

    return barModel;
  }

  private List<String> getLabels(Stimmenart stimmenart) {
    List<String> result = new ArrayList<>();
    if (isWahlbeteiligungAnzuzeigen()) {
      result.add(getWahlbeteiligungLabel());
    }
    if (isUngueltigeAnzuzeigen()) {
      result.add(getUngueltigeLabel());
    }
    getAnzuzeigendePolitischeGruppenSortiert(stimmenart)
        .forEach(politischeGruppe -> result.add(getPolitischeGruppeLabel(politischeGruppe)));
    return result;
  }


  private <N extends Number> BarChartDataSet createDataSet(String dataSetLabel,
                                        GebietDaten<N> gebietDaten,
                                        Stimmenart stimmenart, double alpha) {

    BarChartDataSet dataset = new BarChartDataSet();

    dataset.setLabel(dataSetLabel);
    dataset.setBorderWidth(1);

    List<Number> values = new ArrayList<>();
    List<String> bgColor = new ArrayList<>();
    List<String> borderColor = new ArrayList<>();

    if (isWahlbeteiligungAnzuzeigen()) {
      values.add(getNumberFormattedInProzent(artBetrachteterStimmen.getWahlbeteiligungOderWaehlerAnzahl(gebietDaten)));
      bgColor.add(getGraueFarbe(alpha));
      borderColor.add(getGraueFarbe(1));
    }

    if (isUngueltigeAnzuzeigen()) {
      Number ungueltigeAnteil = gebietDaten.getUngueltige().get(stimmenart);
      values.add(getNumberFormattedInProzent(ungueltigeAnteil));
      bgColor.add(getGraueFarbe(alpha));
      borderColor.add(getGraueFarbe(1));
    }

    List<PolitischeGruppe> anzuzeigendeGruppen = getAnzuzeigendePolitischeGruppenSortiert(stimmenart);
    for (PolitischeGruppe gruppe : anzuzeigendeGruppen) {
      ParteiStimmen<N> parteiStimmen = gebietDaten.getParteiStimmen().get(gruppe);
      Number stimmenanteil = parteiStimmen != null? parteiStimmen.getStimmen(stimmenart) : null;
      values.add(getNumberFormattedInProzent(stimmenanteil));

      bgColor.add(convertToRGBA(gruppe.getFarbe(), alpha));
      borderColor.add(convertToRGBA(gruppe.getFarbe(), 1));
    }

    dataset.setData(values);
    dataset.setBackgroundColor(bgColor);
    dataset.setBorderColor(borderColor);

    return dataset;
  }

  private String getGraueFarbe(double alpha) {
    return convertToRGBA("d6cdb4", alpha);
  }

  private BarChartOptions getOptionsAktuellVsHochgerechnet() {
    BarChartOptions options = new BarChartOptions();
    CartesianScales cScales = new CartesianScales();
    CartesianLinearAxes linearAxes = new CartesianLinearAxes();
    CartesianLinearTicks ticks = new CartesianLinearTicks();
    ticks.setBeginAtZero(true);
    linearAxes.setTicks(ticks);
    cScales.addYAxesData(linearAxes);
    options.setScales(cScales);

    Title title = new Title();
    title.setDisplay(true);
    title.setText(getMessageFunction.apply("stimmeingabe.auffaelligkeit.visualisiert.vergleich.mit.den.erwarteten.stimmenanteilen.label"));
    options.setTitle(title);

    return options;
  }

  private BarChartOptions getOptionsSelectedGebietVsAehnlicheGebiet(StimmenhochrechnerGebietAusgabedaten ausgabedatenVomAehnlichenGebiet) {
    BarChartOptions options = getOptionsAktuellVsHochgerechnet();
    options.getTitle().setText("Vs. " +
        formatGebietName(ausgabedatenVomAehnlichenGebiet.getGebiet()));
    return options;
  }

  // ===========================================================================================
  private boolean isWahlbeteiligungAnzuzeigen() {
    return true; // da die Wahlbeteiligung eine wichtige Information bei der Auffaelligkeitssuche
    /*return auffaelligkeitVomSelectedGebiet.getBemerkungen().stream()
        .anyMatch(auffaelligkeitBemerkung -> auffaelligkeitBemerkung instanceof AuffaelligkeitBemerkungWahlbeteiligung);*/
  }

  private boolean isUngueltigeAnzuzeigen() {
    return auffaelligkeitVomSelectedGebiet.getBemerkungen().stream()
        .anyMatch(auffaelligkeitBemerkung -> auffaelligkeitBemerkung instanceof AuffaelligkeitBemerkungUngueltige);
  }

  private List<PolitischeGruppe> getAnzuzeigendePolitischeGruppenSortiert(Stimmenart stimmenart) {
    Set<PolitischeGruppe> politischeGruppen = new HashSet<>();
    auffaelligkeitVomSelectedGebiet.getBemerkungen().forEach(auffaelligkeitBemerkung -> politischeGruppen.addAll(auffaelligkeitBemerkung.getInvolvedPartys()));

    return ergaenzeAnzuzeigendePolitischeGruppen(stimmenart, politischeGruppen);
  }

  private List<PolitischeGruppe> ergaenzeAnzuzeigendePolitischeGruppen(Stimmenart stimmenart, Set<PolitischeGruppe> politischeGruppen) {
    List<PolitischeGruppe> allePolitischeGruppenSortiertNachAbweichung = getPolitischeGruppenSortiertNachAbweichungAbsteigend(stimmenart);

    List<PolitischeGruppe> result = new ArrayList<>();
    int i = 0;
    for (; i < allePolitischeGruppenSortiertNachAbweichung.size() && i < anzahlVonAnzuzeigendenGruppenBeiGebietsvergleich; i++) {
      result.add(allePolitischeGruppenSortiertNachAbweichung.get(i));
      politischeGruppen.remove(allePolitischeGruppenSortiertNachAbweichung.get(i));
    }

    for (; i < allePolitischeGruppenSortiertNachAbweichung.size(); i++) {
      if (politischeGruppen.remove(allePolitischeGruppenSortiertNachAbweichung.get(i))) {
        result.add(allePolitischeGruppenSortiertNachAbweichung.get(i));
      }
    }

    return result;
  }

  private List<PolitischeGruppe> getPolitischeGruppenSortiertNachAbweichungAbsteigend(Stimmenart stimmenart) {
    return auffaelligkeitVomSelectedGebiet.getStimmenhochrechnerAusgabedaten()
        .getAbweichungAktuellVonHochgerechnet().getParteiStimmen()
        .values()
        .stream()
        .filter(Objects::nonNull)
        .sorted(Comparator.comparing(parteiStimmen -> -Math.abs(notNull(parteiStimmen.getStimmen(stimmenart)))))
        .limit(anzahlVonAnzuzeigendenGruppenBeiGebietsvergleich)
        .map(ParteiStimmen::getPartei)
        .collect(Collectors.toList());
  }

  private String getWahlbeteiligungLabel() {
    return getMessageFunction.apply(artBetrachteterStimmen.getMessageKeyFuerWahlbeteiligungOderWaehlerAktuell());
  }

  private String getUngueltigeLabel() {
    return getMessageFunction.apply("stimmeingabe.auffaelligkeit.visualisiert.ungueltige.label");
  }

  private String getPolitischeGruppeLabel(PolitischeGruppe politischeGruppe) {
    String result = politischeGruppe.getKurzname();
    return result != null? result : "";
  }

  private String getSelectedGebietNameFormatted() {
    return formatGebietName(auffaelligkeitVomSelectedGebiet.getGebiet());
  }

  private String formatGebietName(Gebiet gebiet) {
    if (gebiet == null || isBlank(gebiet.getName())) {
      return EMPTY;
    }
    return gebiet.getName();
  }


  private static String convertToRGBA(String color, double alpha) {
    if (StringUtils.isBlank(color)) {
      return "";
    }
    String hexString = color;
    if (!hexString.startsWith("#")) {
      hexString = "#" + hexString;
    }

    int hex = Integer.decode(hexString);
    int r = (hex & 0xFF0000) >> 16;
    int g = (hex & 0xFF00) >> 8;
    int b = (hex & 0xFF);

    return "rgba(" + r + ", " + g + ", " + b + ", " + alpha + ")";
  }

  public Number getNumberFormattedInProzent(Number x) {
    if (x != null) {
      if (x instanceof Long) {
        return x;
      }
      return Precision.round((double)x * 100.0, 2);
    }
    return null;
  }
}