#!/bin/sh

# Copyright 2024 Loïc Cerf (lcerf@dcc.ufmg.br)

# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or (at
# your option) any later version.

# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# General Public License for more details.

# "Phragmén’s ordered method, formulation 3" on page 22 of "Phragmén’s
# and Thiele’s election methods" by Svante Janson (2016).

export POSIXLY_CORRECT=1	# to have GNU Awk localize numbers

invalid ()
{
    sequence=$(printf " $sequence" | tr -sc 0-9 ' ')
    if [ "$sequence" != " " ]
    then
	if ! printf "$sequence" | awk -v RS=' ' -v n=$n '
NF && ($0 < 1 || $0 > n || $0 in approved) {
    exit 1 }

{
    approved[$0] }'
	then
	    sequence=$(printf "$sequence" | sed -e 's/^ //' -e 's/ $//')
	    printf "Ballot \"$sequence\" invalid: ignored!\n" >&2
	    return 0
	fi
	printf "$sequence\n" >> $TMP/groups/$(printf "$sequence" | cut -d ' ' -f 2)
    fi
    return 1
}

prompt ()
{
    printf "Here are the $n candidates in tie-breaking order:\n\n"
    nl -w 2 -s '. ' $TMP/candidates | column -c 80
    read -p '
Type the ballot, a sequence of numbers in descending order of preference,
or no digit if there is no more votes: ' sequence
    printf '\n'
    if invalid
    then
	printf '\n'
	prompt
    fi
}

if [ -z "$1" -o "$1" = "-h" -o "$1" = "--help" ]
then
    printf "Usage: $0 [-v] candidates [ballots...]

One candidate per non-empty line of \"candidates\".
In case of tie, who is before in the file wins.

One ballot per non-empty line of \"ballots\":
candidate numbers, in descending order of preference.\n"
    exit
fi

if [ "$1" = "-v" -o "$1" = "--verbose" ]
then
    shift
    verbose=1
fi

for file in "$@"
do
    if [ ! -r "$file" ]
    then
	printf "\"$file\" cannot be read!\n" >&2
	exit 66
    fi
done

TMP=$(mktemp -dt seq-phragmen.XXXXXX)
trap "rm -r $TMP 2>/dev/null" 0

mkdir -p $TMP/groups $TMP/new_groups
grep -xv '[[:space:]]*' "$1" > $TMP/candidates
n=$(wc -l < $TMP/candidates)

# Store the ballots, already creating the initial groups, one per
# candidate positioned first
if [ -n "$2" ]
then
    shift
    cat "$@" | while read sequence
    do
	invalid
    done
else
    prompt
    while [ "$sequence" != " " ]
    do
	prompt
    done
fi

if  [ -n "$(ls $TMP/groups)" ]
then
    # Initialize $TMP/descr with, in that order:
    # - the group id
    # - the favorite candidate of the group
    # - how many ballots in the group
    # - the place number of the group, 0 initially
    for group in $TMP/groups/*
    do
	printf "$(basename $group) $(basename $group) $(wc -l < $group) 0\n"
    done > $TMP/descr
fi

while [ -s $TMP/descr ]
do
    # Elect a candidate
    top=$(awk '
{
    v[$2] += $3
    q[$2] += $4 }

END {
    for (candidate in v)
        print v[candidate] / ++q[candidate] "\t" candidate }' $TMP/descr | sort -k 1,1nr -k 2n | head -1)
    elected=$(printf "$top" | cut -f 2)

    if [ -n "$verbose" ]
    then
	awk -v elected=$elected '
FILENAME == ARGV[1] {
    name[NR] = $0 }

FILENAME == ARGV[2] {
    v[$2] += $3
    q[$2] += $4 }

END {
    for (candidate in v)
        print "* " v[candidate] " / (1 + " q[candidate] ") = " v[candidate] / ++q[candidate] " reduced vote(s) for " name[candidate]
    print "\nWith " v[elected] / q[elected] " reduced vote(s) and possible tie break, next in ranking: " name[elected] "\n" }' $TMP/candidates $TMP/descr
    else
	sed -n ${elected}p $TMP/candidates
    fi

    # From the groups that have just been satisfied, create new
    # groups, depending on their next non-elected choices, if any
    satisfied=$(awk -v elected=$elected -v dir=" $TMP/groups/" '$2 == elected { printf dir $1 }' $TMP/descr)
    ranking="$ranking $elected"
    awk -v ranking="$ranking " -v dir=$TMP/new_groups/ '
{
    for (i = 1; ranking ~ " " $i " "; ++i)
        $i = "" }

$i {
    print > dir $i }' $satisfied
    rm $satisfied

    # Remove from $TMP/descr the satisfied groups
    awk -v elected=$elected '$2 != elected' $TMP/descr > $TMP/tmp
    mv $TMP/tmp $TMP/descr

    # If there are new groups, move them with the old ones and define
    # their place numbers
    if [ -n "$(ls $TMP/new_groups)" ]
    then
	w=$(printf "$top" | cut -f 1 | tr , .)
	for group in $TMP/new_groups/*
	do
	    n=$(expr $n + 1)
	    printf "$n $(basename $group) " >> $TMP/descr
	    awk -v w=$w 'END { print NR, NR / w }' $group >> $TMP/descr
	    if [ -n "$verbose" ]
	    then
		awk -v w=$w 'END { printf "* %d satisfied voter(s), now with place number %d / %g = %g prefer(s) ", NR, NR, w, NR / w }' $group
		sed -n $(basename $group)p $TMP/candidates
	    fi
	    mv $group $TMP/groups/$n
	done
    fi
done
