/*
 * ElectionResultDeterminator
 *
 * Created on 15.12.2008
 * Copyright (c) 2008 Kiesraad
 */
package de.ivu.wahl.result.determination;

import de.ivu.wahl.result.ElectionAndCandidatesAndVotes;
import de.ivu.wahl.result.Fraction;
import de.ivu.wahl.result.Util;
import de.ivu.wahl.result.drawlots.DrawingLotsCallback;
import de.ivu.wahl.result.drawlots.DrawingLotsException;
import de.ivu.wahl.result.gsda.GeneralSeatDistributor;
import de.ivu.wahl.result.gsda.GsdaParameters;
import de.ivu.wahl.result.gsda.GsdaResult;
import de.ivu.wahl.result.i18n.MessageKeys;
import de.ivu.wahl.result.i18n.Messages;
import de.ivu.wahl.result.result.AnomalyFactory;
import de.ivu.wahl.result.result.CandidateResult;
import de.ivu.wahl.result.result.Distribution;
import de.ivu.wahl.result.result.ElectionResult;
import de.ivu.wahl.result.result.ElectionResultCollector;
import de.ivu.wahl.result.result.ElectionResultCollectorImpl;
import de.ivu.wahl.wus.electioncategory.ElectionSubcategory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

public class ElectionResultDeterminator {
  public static final Logger APPLOG = LoggerFactory.getLogger("applog"); //$NON-NLS-1$
  private static final String THREE_ASTERISKS = "*** "; //$NON-NLS-1$

  public static ElectionResult determineElectionResult(DrawingLotsCallback lotingCallback,
                                                       ElectionAndCandidatesAndVotes ecv) {
    ElectionResultCollector electionResult = new ElectionResultCollectorImpl();
    determineElectionResult(lotingCallback, ecv, electionResult);
    return electionResult.getElectionResult();
  }

  public static void determineElectionResult(DrawingLotsCallback lotingCallback,
                                             ElectionAndCandidatesAndVotes ecv, ElectionResultCollector electionResultCollector) {
    try {
      ElectionResultDeterminator calculator = new ElectionResultDeterminator(lotingCallback, ecv);
      calculator.calculate(electionResultCollector);
    } catch (DrawingLotsException exc) {
      electionResultCollector.setConflict(exc.getDecisionType(), exc.getAlternatives());
    }
  }

  private final DrawingLotsCallback drawingLotsCallback;
  private final ElectionAndCandidatesAndVotes ecv;

  private VotesCounter votesCounter;
  private CandidatesCounter candidatesCounter;

  private ElectionResultDeterminator(DrawingLotsCallback drawingLotsCallback,
                                     ElectionAndCandidatesAndVotes ecv) {
    this.drawingLotsCallback = drawingLotsCallback;
    this.ecv = ecv;
  }

  private ElectionResultCollector calculate(ElectionResultCollector electionResultCollector) {
    AnomalyFactory anomalyFactory = new AnomalyFactory(electionResultCollector, ecv);

    // Count the votes
    APPLOG
            .info(THREE_ASTERISKS + Messages.getString(MessageKeys.Result_Tracelog_CalculationStarts));
    APPLOG.info(Messages.bind(MessageKeys.Result_Tracelog_ElectionType_0,
            ecv.getElectionSubcategory()));
    APPLOG.info(Messages.bind(MessageKeys.Result_Tracelog_TotalNumberOfSeats_0,
            ecv.getNumberOfSeats()));
    votesCounter = new VotesCounter(ecv);
    votesCounter.logVotes();

    // Determine electoral quota
    Fraction electoralQuota = new Fraction(votesCounter.getTotalNumberOfVotes(), ecv.getNumberOfSeats());
    electionResultCollector.setElectoralQuota(votesCounter.getTotalNumberOfVotes(), ecv.getNumberOfSeats());
    APPLOG.info(THREE_ASTERISKS
            + Messages.bind(MessageKeys.Result_Tracelog_TheElectoralQuotaIs_0,
            Util.displayQuotient(electoralQuota)));

    // Calculate the votes per P3List
    Map<P3List, Long> p3ListsAndVotes = Collections.unmodifiableMap(votesCounter.getVotesPerP3List());

    // Count candidates
    Set<P3List> p3Lists = Util.createUnmodifiableCopy(p3ListsAndVotes.keySet());
    candidatesCounter = new CandidatesCounter(ecv, p3Lists);

    // Assign seats to P3-lists
    Map<P3List, Long> p3ListsAndSeats = assignSeatsToP3Lists(electionResultCollector,
            anomalyFactory,
            p3ListsAndVotes);

    // Assign seats to P2-lists
    Map<P3List, GsdaResult<P2List>> p2ListsAndSeats = assignSeatsWithinP3Lists(electionResultCollector,
            anomalyFactory,
            p3ListsAndSeats);

    // Save some data for displaying the election result
    electionResultCollector.setPreferentialBarrierInPercent(
            100 * ecv.getPreferencialBarrierNumerator() / ecv.getPreferencialBarrierDenominator());
    electionResultCollector.setMustExceedPreferentialBarrier(
            ElectionResultForCandidatesDeterminator.isMustExceedPreferentialBarrier(ecv.getElectionSubcategory()));

    // Assign seats to candidates
    APPLOG.info(THREE_ASTERISKS
            + Messages.getString(MessageKeys.Result_Tracelog_AssigningSeatsToCandidates));
    Map<P2List, List<CandidateResult>> allCandidateResults = new HashMap<>();
    for (P3List p3List : Util.sortByNumber(p3ListsAndSeats.keySet())) {
      GsdaResult<P2List> p2ListsAndSeatsInP3List = p2ListsAndSeats.get(p3List);
      long seats = p3ListsAndSeats.get(p3List);
      Map<P2List, List<CandidateResult>> newOrder = ElectionResultForCandidatesDeterminator
              .calculate(drawingLotsCallback,
                      votesCounter,
                      ecv,
                      p3List,
                      seats,
                      p2ListsAndSeatsInP3List,
                      anomalyFactory);
      allCandidateResults.putAll(newOrder);
    }

    // Handle candidates that are elected on more than one P3-list
    new MultipleElectedCandidateHandler(ecv, votesCounter, anomalyFactory)
            .handleCandidatesElectedOnMoreThanOnP3List(allCandidateResults);

    writeCandidateResultsToElectionResult(electionResultCollector, allCandidateResults);

    return electionResultCollector;
  }

  private void writeCandidateResultsToElectionResult(ElectionResultCollector electionResultCollector,
                                                     Map<P2List, List<CandidateResult>> allCandidateResults) {
    for (P3List p3List : Util.sortByNumber(Util.asList(ecv.getP3Lists()))) {
      for (P2List p2List : p3List.getP2ListsSorted()) {
        List<CandidateResult> candidateResults = allCandidateResults.get(p2List);
        for (CandidateResult candidateResult : candidateResults) {
          electionResultCollector.candidateResult(candidateResult);
        }
      }
    }
  }

  private Map<P3List, Long> assignSeatsToP3Lists(ElectionResultCollector electionResultCollector, AnomalyFactory anomalyFactory,
                                                 Map<P3List, Long> votesPerP3List) {
    GsdaParameters parameters = getGsdaParameters(ecv.getElectionSubcategory());
    ParentOfP3List parent = null;
    Map<P3List, Long> numbersOfCandidatesPerP3List = candidatesCounter.getCandidatesPerP3List();
    GeneralSeatDistributor<P3List, ParentOfP3List> generalSeatDistributor = new GeneralSeatDistributor<>(
            drawingLotsCallback, parent, ecv.getNumberOfSeats(), votesPerP3List,
            numbersOfCandidatesPerP3List, parameters, electionResultCollector, anomalyFactory, Distribution.P3);
    GsdaResult<P3List> p3ListGsdaResult = generalSeatDistributor.calculate();

    return p3ListGsdaResult.getListsAndSeats();
  }

  private GsdaParameters getGsdaParameters(ElectionSubcategory electionSubcategory) {
    return GsdaParameters.getGsdaParameters(electionSubcategory);
  }

  private Map<P3List, GsdaResult<P2List>> assignSeatsWithinP3Lists(ElectionResultCollector electionResultCollector,
                                                                   AnomalyFactory anomalyFactory, Map<P3List, Long> p3ListsAndSeats) {
    Map<P3List, GsdaResult<P2List>> result = new HashMap<>();
    List<P3List> p3Lists = Util.asSortList(p3ListsAndSeats.keySet());
    // To make the order of drawing lots events deterministic, sort the P3-lists
    for (P3List p3List : p3Lists) {
      long p3Seats = p3ListsAndSeats.get(p3List);
      GsdaResult<P2List> p2ListsAndSeats = assignSeatsWithinP3List(p3List,
              p3Seats,
              electionResultCollector,
              anomalyFactory);
      result.put(p3List, p2ListsAndSeats);
    }
    return Collections.unmodifiableMap(result);
  }

  private GsdaResult<P2List> assignSeatsWithinP3List(P3List p3List, long p3Seats,
                                                     ElectionResultCollector electionResultCollector, AnomalyFactory anomalyFactory) {
    Set<P2List> p2Lists = p3List.getP2Lists();
    Map<P2List, Long> p2ListsAndVotes = Util.createUnmodifiableSubMap(votesCounter
            .getVotesPerP2List(), p2Lists);
    Map<P2List, Long> p2ListsAndCandidates = Util.createUnmodifiableSubMap(candidatesCounter
            .getCandidatesPerP2List(), p2Lists);
    GsdaResult<P2List> result = new ElectionResultForP2ListsDeterminator()
            .calculate(drawingLotsCallback,
                    p3List,
                    p3Seats,
                    p2ListsAndVotes,
                    p2ListsAndCandidates,
                    electionResultCollector,
                    anomalyFactory,
                    Distribution.P2);

    for (P2List p2List : p2Lists) {
      Long p2Seats = result.get(p2List);
      electionResultCollector.totalSeatsAssigned(p2List, p2Seats);
    }
    return result;
  }

}
