#!/usr/bin/env bash
# Copyright 2014 Vladimir Ivanov <ivvl82@gmail.com>
# Distributed under the terms of the GNU General Public License v2

REPO_DIR=

# Specify the tournament here
TOURNAMENT=

# Version information
VERSION="0.4"

# Firefox User Agent
FIREFOX_UA="Mozilla/5.0 (X11; Linux x86_64; rv:24.0) Gecko/20100101 Firefox/24.0"

argv0=${0##*/}

function usage {
    cat <<EOF
Store chess games played on lichess.org

Usage:
  $argv0 [options] -t <num> <url>...
  $argv0 -h
  $argv0 -v

  Put the script under the root directory of your repository or set
  inner variable REPO_DIR to it. If the tournament is not the last one
  (default), store its sub-directory in inner variable TOURNAMENT.

  The first form fills the results of chess games and stores their PGN
  files, assuming that all the games were played by the same pair of
  players on tour <num> of the tournament. The PGN files are sorted by
  their timestamps, unless '-u' is set. Each game should be available
  at the corresponding <url> (lichess.org).

  The second form shows this help output. The third form shows version
  information.

Options:
  -a            Add current games to existing games
  -c            Clean up existing games before adding
  -u            Don't sort games by their timestamps
EOF

    exit "${1:-0}"
}

function version {
    exec echo "${argv0}-${VERSION}"
}

function game_setup {
    date_re="[0-9?]{2}\.[0-9?]{2}\.[0-9?]{4}"
    : ${REPO_DIR:=`dirname "$0"`}
    # Synchronize the repository
    git --git-dir=${REPO_DIR}/.git pull

    # If no tournament given, set it to the last one
    if [[ -z $TOURNAMENT ]]; then
        local year_dir=$(ls -1 -d ${REPO_DIR}/[0-9][0-9][0-9][0-9]/ | tail -1)
        TOURNAMENT=$(ls -1 -d ${year_dir}[0-9]-*/ | tail -1 \
            | sed -E "s|${REPO_DIR}/(.*)/|\1|")
    fi

    # Configuration file for players
    ply_ini=${REPO_DIR}/${TOURNAMENT}/players.ini
    [[ -f $ply_ini ]] || die "File ${ply_ini} not found."
    # Pairs of players on the tour
    tour_info=${REPO_DIR}/${TOURNAMENT}/tours/${TOUR}/tour_info
    [[ -f $tour_info ]] || die "File ${tour_info} not found."
}

function game_tmp_pgns {
    TMP_PGN_FILES=
    # Don't sort just one game
    [[ $# == 1 ]] && SORT_GAMES=false

    for url in $@; do
        local timestamp=
        [[ $url =~ ^(http://[^/]*)/([^/]*) ]]
        # Link to annotated game PGN
        local game_url=${BASH_REMATCH[1]}/game/export/${BASH_REMATCH[2]::8}.pgn

        # Get the timestamp of game for sorting
        if $SORT_GAMES; then
            local game_api=${BASH_REMATCH[1]}/api/game/${BASH_REMATCH[2]::8}
            local api_response=$(curl -q --fail --location --silent "$game_api")
            [[ -z $api_response ]] && die "Unreachable game API ${game_api}"
            timestamp=$(sed -En "s/.*\"timestamp\":([0-9]+).*/\1/p" <<< "$api_response")
        fi

        # Store PGN file in a temporal location
        local tmp_pgn=$(mktemp -t ${timestamp:-1}.pgn-XXXXXX)
        TMP_PGN_FILES+=" $tmp_pgn"
        trap "rm $TMP_PGN_FILES" EXIT
        wget -q -U "$FIREFOX_UA" -O $tmp_pgn "$game_url" \
            || die "Unreachable game PGN ${game_url}"
    done

    $SORT_GAMES && TMP_PGN_FILES=$(xargs -n1 <<< "$TMP_PGN_FILES" | sort -n | xargs)
}

function game_get_info {
    # Get the names of players, whose games are in PGN files
    local ply_names=
    game_get_names

    # Select the names of two players
    local players=( $(echo -en "$ply_names" | sort -u) )
    [[ ${#players[@]} == 2 ]] || die "Players of the games are not the same."

    # Find the white and black players
    local line0=$(grep " ${players[0]}" $tour_info)
    local line1=$(grep " ${players[1]}" $tour_info)
    [[ -z $line0 || -z $line1 || $line0 != $line1 ]] \
        && die "No game between ${players[0]} and ${players[1]} found in ${tour_info}."
    [[ $line0 =~ ^((${date_re})\ +([^\ ]+)\ +-\ +([^\ ]+))(.*)$ ]]
    white=${BASH_REMATCH[3]}
    black=${BASH_REMATCH[4]}

    # Additional information about the game record
    rec_length=${#BASH_REMATCH[1]}
    game_date=${BASH_REMATCH[2]}
    res_old=$(xargs <<< ${BASH_REMATCH[5]})
    game_validate

    if ! $ADD_GAMES || [[ $game_date =~ \? ]]; then
        local fst_pgn=$(awk '{print $1}' <<< "$TMP_PGN_FILES")
        local date=$(sed -En "s/\[Date \"([^\"]*)\"\]/\1/p" $fst_pgn)
        game_date=${date:8:2}.${date:5:2}.${date::4}
    fi
}

function game_get_names {
    # Make an associative array from Lichess nicks to players' names
    game_parse_config
    local sections=$(grep -o "config_section_player[0-9]*" $tmp_ini)
    declare -A NAMES=()
    for sect in $sections; do
        eval $sect
        NAMES+=( [$lichess]=$name )
    done

    for pgn in $TMP_PGN_FILES; do
        # Extract players on Lichess
        local wt_lichess=$(sed -En "s/\[White \"([^\"]*)\"\]/\1/p" $pgn)
        local bk_lichess=$(sed -En "s/\[Black \"([^\"]*)\"\]/\1/p" $pgn)

        game_add_player $wt_lichess
        game_add_player $bk_lichess
    done
}

function game_parse_config {
    # Temporary files
    tmp_ini=$(mktemp -t `basename $ply_ini`.XXXXXX)
    TMP_INI_FILES="${tmp_ini} ${tmp_ini}.prev"
    trap "rm $TMP_INI_FILES $TMP_PGN_FILES" EXIT

    # Copy player INI file to the temporary location
    # NOTE: an empty line is added to the file beginning in order to
    # match the only first occurrence for non-GNU sed
    echo > $tmp_ini
    cat "$ply_ini" >> $tmp_ini

    # Remove tabs or spaces around the `='
    sed -E -i.prev "s/[[:blank:]]*=[[:blank:]]*/=/" $tmp_ini

    # Transform section labels into function declaration
    sed -E -i.prev "1,/^\[.*\]/s/^\[([^]]*)\]/config_section_\1() {/" $tmp_ini
    sed -E -i.prev "s/^\[([^]]*)\]/}\\"$'\n'"config_section_\1() {/" $tmp_ini
    echo -e "\n}" >> $tmp_ini

    # Source the file
    source $tmp_ini
}

function game_add_player {
    local lichess_ply=$1
    local ply=${NAMES[$lichess_ply]}
    while [[ ! " ${NAMES[@]} " =~ \ $ply\  ]]; do
        echo -n "The list of players:"
        sed "s/ /\\"$'\n'"$(tput setaf 6)*$(tput sgr0) /g" <<< " ${NAMES[@]}"
        echo -n "Type the name of ${lichess_ply}> "
        read ply
    done
    ply_names+="${ply}\n"
}

function game_validate {
    # By default, other games between the players are not allowed
    if ! $ADD_GAMES && ! $CLEANUP_GAMES; then
        if ! [[ $game_date =~ \? && -z $res_old ]]; then
            die "Some games between ${white} and ${black} already recorded."
        fi
    fi

    # Players' sides should interchange
    local length=$(echo -en "$ply_names" | wc -l)
    if $ADD_GAMES && [[ -n $res_old ]]; then
        local residue=$(( $(wc -w <<< "$res_old") % 2 ))
    else
        local residue=0
    fi
    local ply_ordered=
    for ((i = 0; i < length; i++)); do
        if (( i % 2 == residue )); then
            ply_ordered+="${white}\n"
        else
            ply_ordered+="${black}\n"
        fi
    done
    if [[ "$ply_names" != "$ply_ordered" ]]; then
        local answer
        echo -n "Approve games with wrong players' sides? (Y/n)> "
        read answer
        [[ $answer =~ ^(Y|y|Yes|yes)$ ]] || exit 1
    fi
}

function game_store_pgns {
    game_dir=$(ls -1 -d 2>/dev/null \
        ${REPO_DIR}/${TOURNAMENT}/tours/${TOUR}/*-${white}-vs-${black})
    local game_dir_old=
    if $CLEANUP_GAMES && [[ -n $game_dir ]]; then
        git --git-dir=${REPO_DIR}/.git rm --ignore-unmatch ${game_dir}/*.pgn
        [[ -d $game_dir ]] && game_dir_old=$game_dir
        game_dir=
    fi

    local pgn_index=0
    if [[ -z $game_dir ]]; then
        local date=${game_date:6:4}-${game_date:3:2}-${game_date::2}
        game_dir=${REPO_DIR}/${TOURNAMENT}/tours/${TOUR}/${date}-${white}-vs-${black}

        if [[ $game_dir != $game_dir_old ]]; then
            echo "Creating directory ${game_dir}..."
            mkdir $game_dir
            if $CLEANUP_GAMES && [[ -n $game_dir_old ]]; then
                git --git-dir=${REPO_DIR}/.git mv ${game_dir_old}/* ${game_dir}
                rm -r $game_dir_old
            fi
        fi
    else
        $ADD_GAMES || die "Directory ${game_dir} already exist."
        local old_pgns=$(ls -1 -p 2>/dev/null ${game_dir}/*.pgn)
        [[ -n $old_pgns ]] && pgn_index=$(wc -l <<< "$old_pgns")
    fi

    for pgn in $TMP_PGN_FILES; do
        (( pgn_index += 1 ))
        echo "Storing file ${game_dir}/${pgn_index}.pgn..."
        cp $pgn ${game_dir}/${pgn_index}.pgn
    done
}

function game_update_info {
    # Get the maximal length of game records, excepting results
    local length_max=$(grep -Eo "^${date_re} +[^ ]+ +- +[^ ]+" $tour_info \
                              | awk '{print length}' | sort -nr | head -1)

    local result=
    for pgn in $TMP_PGN_FILES; do
        local res=$(sed -En "s/\[Result \"([^\"]*)\"\]/\1/p" $pgn)
        [[ $res == 1/2-1/2 ]] && res=1/2 # representation of a draw
        result+=" $res"
    done
    $ADD_GAMES && [[ -n $res_old ]] && result=" ${res_old}${result}"

    local spaces=$(( length_max - rec_length ))
    local sep=$(printf "%${spaces}s" " ")
    echo "Updating file ${tour_info}..."
    sed -E -i.orig \
        "s|^${date_re}( +${white} +- +${black}).*|${game_date}\1${sep}${result}|" \
        $tour_info
    rm ${tour_info}.orig
}

function game_git_commit {
    git --git-dir=${REPO_DIR}/.git add ${game_dir} $tour_info
    local title=$(awk '{print toupper(substr($0,1,1)) tolower(substr($0,2))}' \
                      <<< ${TOURNAMENT##*-})
    git --git-dir=${REPO_DIR}/.git commit -m \
        "Tournament ${title}, tour ${TOUR#0}: ${white} vs. ${black}."
    git --git-dir=${REPO_DIR}/.git push
}

function die {
    echo "$@" >&2
    exit 1
}

function checkargs {
    [[ $OPTARG =~ ^[0-9]+$ ]] || die "Incorrect tour number."
    TOUR=$(printf "%02g" $OPTARG)
}

ADD_GAMES=false
CLEANUP_GAMES=false
SORT_GAMES=true
while getopts act:uhv opt; do
    case $opt in
        a) ADD_GAMES=true     ;;
        c) CLEANUP_GAMES=true ;;
        t) checkargs          ;;
        u) SORT_GAMES=false   ;;
        h) usage              ;;
        v) version            ;;
        *) usage 1            ;;
    esac
done

shift $(($OPTIND - 1))
# For now, tour number should be given explicitly
[[ -z $TOUR || $# == 0 ]] && usage 1
# Don't add and clean up games simultaneously
$ADD_GAMES && $CLEANUP_GAMES && usage 1

game_setup
game_tmp_pgns $@
game_get_info
game_store_pgns
game_update_info
game_git_commit

exit 0