use strict;
use warnings;
use 5.024;
use feature qw /postderef signatures/;

package Vote::Count::Method::MinMax;

use namespace::autoclean;
use Moose;
extends 'Vote::Count';

our $VERSION='2.02';

=head1 NAME

Vote::Count::Method::MinMax

=head1 VERSION 2.02

=cut

# ABSTRACT: Methods in the MinMax Family.

=pod

=head1 SYNOPSIS

 my $MinMaxElection =
 Vote::Count::Method::MinMax->new( 'BallotSet' => $ballotset );

 # $method is one of: winning margin opposition
 my $Winner = $MinMaxElection->MinMax( $method )->{'winner'};
 say $MinMaxElection->logv();

=head1 The MinMax Methods

MinMax (also known as Minimax and Simpson-Kramer) uses a Pairwise comparison Matrix. Instead of looking at wins and losses as with Condorcet Methods, it scores each pairing, the choice with the lowest worst pairing score wins.

=head2 The Three MinMax Scoring Rules

=head3 Winning Votes ('winning')

When the choice being scored loses, the votes for the winner in the pairing are scored. When the choice wins or ties the pairing is scored as 0.

This scoring method meets the Condorcet Winner, but not the Smith, Condorcet Loser or Later Harm Criteria.

=head3 Margin ('margin')

When the choice being scored loses, the votes for the winner minus the votes for that choice in the pairing are scored. When the choice wins or ties the pairing is scored as 0.

This scoring method meets the Condorcet Winner, but not the Smith, Condorcet Loser or Later Harm Criteria.

=head3 Opposition ('opposition')

The votes for the other choice in the pairing are scored regardless of whether the choice won or lost.

This scoring method is claimed to meet the Later Harm Criteria, but fails Condorcet Winner and Smith.

=head2 Tie Breaker

As a Tie Breaker it is recommended to use the next worst pairing score. Because it follows the method and should resolve well, this Tie Breaker is implemented by Vote::Count within the MinMax method itself. If it returns a tie your implementation can apply another method like Modified Grand-Junction.

=cut

no warnings 'experimental';

use Vote::Count::TextTableTiny qw/generate_table/;
use Carp;
use Try::Tiny;
# use Data::Dumper;

=pod

=head2 ScoreMinMax

Generate hashref scoring according the requested $method which is one of three scoring rules: 'winning', 'margin', 'opposition'.

  my $scores = $MinMaxElection->ScoreMinMax( $method );

=cut

sub ScoreMinMax ( $self, $method ) {
  my $scores = {};
  # Always grab the matrix by calling PairMatrix,
  # build is lazy.
  my $Matrix = $self->PairMatrix()->{'Matrix'};
  my @choices = sort ( keys $self->Active()->%* );
  for my $Choice (@choices) {
    my @ChoiceLoss = ();
  LOOPMMMO: for my $Opponent (@choices) {
      next LOOPMMMO if $Opponent eq $Choice;
      my $M = $Matrix->{$Choice}{$Opponent};
      my $S = undef;
      if ( $method eq 'opposition' ) {
        $S = $M->{$Opponent};
      }
      elsif ( $M->{'winner'} eq $Opponent ) {
        $S = $M->{$Opponent} if $method eq 'winning';
        $S = $M->{$Opponent} - $M->{$Choice} if $method eq 'margin';
      }
      else {
        $S = 0;
      }
      $scores->{$Choice}{$Opponent} = $S;
      # there was a bug where sometimes @ChoiceLoss was sorted
      # alphanumerically. resolution force the sort to be numeric.
      push @ChoiceLoss, ( $S );
    }  # LOOPMMMO:
    $scores->{$Choice}{score}
      = [ reverse sort { $a <=> $b } @ChoiceLoss ];
  }
  return $scores;
}

sub _pairmatrixtable1 ( $I, $scores ) {
  my @rows = ( [qw/Choice Choice Votes Opponent Votes Score/] );
  my @choices = sort ( keys $I->Active()->%* );
  my $Matrix = $I->PairMatrix()->{'Matrix'};
  for my $Choice (@choices) {
    push @rows, [$Choice];
    for my $Opponent (@choices) {
      my $Cstr = $Choice;
      my $Ostr = $Opponent;
      next if $Opponent eq $Choice;
      my $CVote = $Matrix->{$Choice}{$Opponent}{$Choice};
      my $OVote = $Matrix->{$Choice}{$Opponent}{$Opponent};
      if ( $Matrix->{$Choice}{$Opponent}{'winner'} eq $Choice ) {
        $Cstr = "**$Cstr**";
      }
      if ( $Matrix->{$Choice}{$Opponent}{'winner'} eq $Opponent ) {
        $Ostr = "**$Ostr**";
      }
      my $Score = $scores->{$Choice}{$Opponent};
      push @rows, [ ' ', $Cstr, $CVote, $Ostr, $OVote, $Score ];
    }
  }
  return generate_table( rows => \@rows );
}

sub _pairmatrixtable2 ( $I, $scores ) {
  my @rows = ( [qw/Choice Scores/] );
  my @choices = sort ( keys $I->Active()->%* );
  for my $Choice (@choices) {
    my $scores = join ', ', ( $scores->{$Choice}{'score'}->@* );
    push @rows, [ $Choice, $scores ];
  }
  return generate_table( rows => \@rows );
}

=pod

=head2 MinMaxPairingVotesTable

Generate a formatted table of the Pairing Matrix from a set of scores generated by ScoreMinMax.

  say $MinMaxElection->MinMaxPairingVotesTable( $scores );

=cut

sub MinMaxPairingVotesTable ( $I, $scores ) {
  my $table1 = $I->_pairmatrixtable1($scores);
  my $table2 = $I->_pairmatrixtable2($scores);
  return "\n$table1\n\n$table2\n";
}

=pod

=head2 MinMax

Run and log the election with MinMax according to scoring $method: 'winning', 'margin', 'opposition'.

  my $result = $MinMaxElection->MinMax( $method );

The returned value is a HashRef:

 { 'tie'    => true or false value,
   'winner' => will be false if tie is true --
               otherwise the winning choice.
   # tied is only present when tie is true.
   'tied'   => [ array ref of tied choices ],
 }

=cut

sub MinMax ( $self, $method ) {
  my $score = $self->ScoreMinMax($method);
  my @active = $self->GetActiveList();
  $self->logt( "MinMax $method Choices: ", join( ', ', @active ) );
  $self->logv( $self->MinMaxPairingVotesTable($score) );
  my $winner = '';
  my @tied  = ();
  my $round = 0;
  # round inited to 0. The 7th round is 6. round increments at
  # end of the loop. this sets correct number of rounds.
  my $roundlimit = scalar(@active) -1;
  LOOPMINMAX: while ( $round < $roundlimit ) {
    # start with $bestscore larger than any possible score
    my $bestscore = $self->VotesCast() + 1;
    my @hasbest  = ();
    for my $a (@active) {
      my $this = $score->{$a}{'score'}[$round];
      if ( $this == $bestscore ) { push @hasbest, $a }
      elsif ( $this < $bestscore ) {
        $bestscore = $this;
        @hasbest  = ($a);
      }
    }
    if ( scalar(@hasbest) == 1 ) {
      $winner = shift @hasbest;
      $self->logt("Winner is $winner.");
      return { 'tie' => 0, 'winner' => $winner };
    }
    # only choices that are tied continue to tie breaking.
    @active = @hasbest;
    # if this is the last round @tied must be set.
    @tied = @hasbest;
    if( $bestscore == 0 ) {
      $self->logt(
        "Tie between @tied. Best Score is 0. No more Tiebreakers available." );
      last LOOPMINMAX;
    }
    $round++;
    $self->logt(
      "Tie between @tied. Best Score is $bestscore. Going to Tiebreaker Round $round."
    );
  }
  $self->logt( "Tied: " . join( ', ', @tied ) );
  return { 'tie' => 1, 'tied' => \@tied, 'winner' => 0 };
}

=pod

=head2 Floor Rules

It is recommended to use a low Floor.

This method specifies that the scores from less worst pairings be used as the tie breaker, removing inconsequential choices can affect the resolveability of the tie breaker. Unlike IRV where the presence of inconsequential choices can be seen as a randomizing factor, and their bulk removal as improving the consistency of the method, this method does not benefit from that.

=cut

1;

#FOOTER

=pod

BUG TRACKER

L<https://github.com/brainbuz/Vote-Count/issues>

AUTHOR

John Karr (BRAINBUZ) brainbuz@cpan.org

CONTRIBUTORS

Copyright 2019-2021 by John Karr (BRAINBUZ) brainbuz@cpan.org.

LICENSE

This module is released under the GNU Public License Version 3. See license file for details. For more information on this license visit L<http://fsf.org>.

SUPPORT

This software is provided as is, per the terms of the GNU Public License. Professional support and customisation services are available from the author.

=cut

