/*
 *  This file is part of pgn-extract: a Portable Game Notation (PGN) extractor.
 *  Copyright (C) 1994-2019 David J. Barnes
 *
 *  pgn-extract 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.
 *
 *  pgn-extract 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.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with pgn-extract. If not, see <http://www.gnu.org/licenses/>.
 *
 *  David J. Barnes may be contacted as d.j.barnes@kent.ac.uk
 *  https://www.cs.kent.ac.uk/people/staff/djb/
 */

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <ctype.h>
#include "bool.h"
#include "defs.h"
#include "typedef.h"
#include "taglist.h"
#include "tokens.h"
#include "lex.h"
#include "grammar.h"
#include "apply.h"
#include "output.h"
#include "mymalloc.h"


/* Functions for outputting games in the required format. */

/* Define the width in which to print a CM move and move number. */
#define MOVE_NUMBER_WIDTH 3
#define MOVE_WIDTH 15
#define CM_COMMENT_CHAR ';'
/* Define the width of the moves area before a comment. */
#define COMMENT_INDENT (MOVE_NUMBER_WIDTH+2+2*MOVE_WIDTH)

/* Define a macro to calculate an array's size. */
#define ARRAY_SIZE(arr) (sizeof(arr)/sizeof(*arr))

/* A small size for formatted move numbers. */
#define FORMATTED_NUMBER_SIZE (20)

/* How much text we have output on the current line. */
static size_t line_length = 0;
/* The buffer in which each output line of a game is built. */
static char *output_line = NULL;

static Boolean print_move(FILE *outputfile, unsigned move_number,
        Boolean print_move_number, Boolean white_to_move,
        const Move *move_details);
static Boolean
print_items_following_move(FILE *outputfile, const Move *move_details,
        unsigned move_number, Boolean white_to_move);
static void output_STR(FILE *outfp, char **Tags);
static void show_tags(FILE *outfp, char **Tags, int tags_length);
static char promoted_piece_letter(Piece piece);
static void print_algebraic_game(Game *current_game, FILE *outputfile,
        unsigned move_number, Boolean white_to_move,
        Board *final_board);
static void print_EPD_game(Game *current_game, FILE *outputfile,
        unsigned move_number, Boolean white_to_move,
        Board *final_board);
static void print_EPD_move_list(Game *current_game, FILE *outputfile,
        unsigned move_number, Boolean white_to_move,
        Board *final_board);
static const char *build_FEN_comment(const Board *board);
static void add_hashcode_tag(const Game *game);
static unsigned count_single_move_ply(const Move *move_details, Boolean count_variations);
static unsigned count_move_list_ply(Move *move_list, Boolean count_variations);
static void print_space_separated_str(FILE *outputfile, const char *str);
static void start_comment(FILE *outputfile);
static void end_comment(FILE *outputfile);
static void print_as_comment(FILE *outputfile, const char *str);

/* List, the order in which the tags should be output.
 * The first seven should be the Seven Tag Roster that should
 * be present in every game.
 * The order of the remainder is, I believe, a matter of taste.
 * any PSEUDO_*_TAGs should not appear in this list.
 */

/* Give the array an int type, because a negative value is
 * used as a terminator.
 */
static int DefaultTagOrder[] = {
    EVENT_TAG, SITE_TAG, DATE_TAG, ROUND_TAG, WHITE_TAG, BLACK_TAG, RESULT_TAG,
#if 1
    /* @@@ Consider omitting some of these from the default ordering,
     * and allow the output order to be determined from the
     * input order.
     */
    WHITE_TITLE_TAG, BLACK_TITLE_TAG, WHITE_ELO_TAG, BLACK_ELO_TAG,
    WHITE_USCF_TAG, BLACK_USCF_TAG,
    WHITE_TYPE_TAG, BLACK_TYPE_TAG,
    WHITE_NA_TAG, BLACK_NA_TAG,
    ECO_TAG, NIC_TAG, OPENING_TAG, VARIATION_TAG, SUB_VARIATION_TAG,
    LONG_ECO_TAG,
    TIME_CONTROL_TAG,
    ANNOTATOR_TAG,
    EVENT_DATE_TAG, EVENT_SPONSOR_TAG, SECTION_TAG, STAGE_TAG, BOARD_TAG,
    TIME_TAG, UTC_DATE_TAG, UTC_TIME_TAG,
    VARIANT_TAG,
    SETUP_TAG,
    FEN_TAG,
    TERMINATION_TAG, MODE_TAG, PLY_COUNT_TAG,
    MATERIAL_MATCH_TAG,
#endif
    /* The final value should be negative. */
    -1
};

/* Provision for a user-defined tag ordering.
 * See add_to_output_tag_order().
 * Once allocated, the end of the list must be negative.
 */
static int *TagOrder = NULL;
static int tag_order_space = 0;

void
set_output_line_length(unsigned length)
{
    if (output_line != NULL) {
        (void) free((void *) output_line);
    }
    output_line = (char *) malloc_or_die(length + 1);
    GlobalState.max_line_length = length;
}

/* Which output format does the user require, based upon the
 * given command line argument?
 */
OutputFormat
which_output_format(const char *arg)
{
    int i;

    struct {
        const char *arg;
        OutputFormat format;
    } formats[] = {
        { "san", SAN},
        { "SAN", SAN},
        { "epd", EPD},
        { "EPD", EPD},
        { "lalg", LALG},
        { "halg", HALG},
        { "CM", CM},
        { "LALG", LALG},
        { "HALG", HALG},
        { "ELALG", ELALG},
        { "elalg", ELALG},
        { "XLALG", XLALG},
        { "xlalg", XLALG},
        { "XOLALG", XOLALG},
        { "xolalg", XOLALG},
        { "uci", UCI},
        { "cm", CM},
        { "", SOURCE},
        /* Add others before the terminating NULL. */
        { (const char *) NULL, SAN}
    };

    for (i = 0; formats[i].arg != NULL; i++) {
        const char *format_prefix = formats[i].arg;
        const size_t format_prefix_len = strlen(format_prefix);
        if (strncmp(arg, format_prefix, format_prefix_len) == 0) {
            OutputFormat format = formats[i].format;
            /* Sanity check. */
            if (*format_prefix == '\0' && *arg != '\0') {
                fprintf(GlobalState.logfile,
                        "Unknown output format %s.\n", arg);
                exit(1);
            }
            /* If the format is SAN, it is possible to supply
             * a 6-piece suffix listing language-specific
             * letters to use in the output.
             */
            if ((format == SAN ||
                 format == ELALG ||
                 format == XLALG ||
                 format == XOLALG) &&
                (strlen(arg) > format_prefix_len)) {
                set_output_piece_characters(&arg[format_prefix_len]);
            }
            return format;
        }
    }
    fprintf(GlobalState.logfile, "Unknown output format %s.\n", arg);
    return SAN;
}

/* Which file suffix should be used for this output format. */
const char *
output_file_suffix(OutputFormat format)
{
    /* Define a suffix for the output files. */
    static const char PGN_suffix[] = ".pgn";
    static const char EPD_suffix[] = ".epd";
    static const char CM_suffix[] = ".cm";

    switch (format) {
        case SOURCE:
        case SAN:
        case LALG:
        case HALG:
        case ELALG:
        case XLALG:
        case XOLALG:
        case UCI:
            return PGN_suffix;
        case EPD:
            return EPD_suffix;
        case CM:
            return CM_suffix;
        default:
            return PGN_suffix;
    }
}

static const char *
select_tag_string(TagName tag)
{
    const char *tag_string;

    if ((tag == PSEUDO_PLAYER_TAG) || 
            (tag == PSEUDO_ELO_TAG) || 
            (tag == PSEUDO_FEN_PATTERN_TAG) ||
            (tag == PSEUDO_FEN_PATTERN_I_TAG)) {
        tag_string = NULL;
    }
    else {
        tag_string = tag_header_string(tag);
    }
    return tag_string;
}

static Boolean
is_STR(TagName tag)
{
    switch (tag) {
        case EVENT_TAG:
        case SITE_TAG:
        case DATE_TAG:
        case ROUND_TAG:
        case WHITE_TAG:
        case BLACK_TAG:
        case RESULT_TAG:
            return TRUE;
        default:
            return FALSE;
    }
}

/* Output the tags held in the Tags structure.
 * At least the full Seven Tag Roster is printed.
 */
static void
output_tag(TagName tag, char **Tags, FILE *outfp)
{
    const char *tag_string;

    /* Must print STR elements and other non-NULL tags. */
    if ((is_STR(tag)) || (Tags[tag] != NULL)) {
        tag_string = select_tag_string(tag);

        if (tag_string != NULL) {
            const char *tag_value;
            if (Tags[tag] != NULL) {
                tag_value = Tags[tag];
            }
            else {
                if (tag == DATE_TAG) {
                    tag_value = "????.??.??";
                }
                else {
                    tag_value = "?";
                }
            }
            if (GlobalState.json_format) {
                fprintf(outfp, "\"%s\" : \"%s\",\n", tag_string, tag_value);
            }
            else {
                fprintf(outfp, "[%s \"%s\"]\n", tag_string, tag_value);
            }
        }
    }
}

/* Output the Seven Tag Roster. */
static void
output_STR(FILE *outfp, char **Tags)
{
    unsigned tag_index;

    /* Use the default ordering to ensure that STR is output
     * in the way it should be.
     */
    for (tag_index = 0; tag_index < 7; tag_index++) {
        output_tag(DefaultTagOrder[tag_index], Tags, outfp);
    }
}

/* Print out on outfp the current details.
 * These can be used in the case of an error.
 */
static void
show_tags(FILE *outfp, char **Tags, int tags_length)
{
    int tag_index;
    /* Take a copy of the Tags data, so that we can keep
     * track of what has been printed. This will make
     * it possible to print tags that were identified
     * in the source but are not defined with _TAG values.
     * See lex.c for how these extra tags are handled.
     */
    char **copy_of_tags =
            (char **) malloc_or_die(tags_length * sizeof (*copy_of_tags));
    int i;
    for (i = 0; i < tags_length; i++) {
        copy_of_tags[i] = Tags[i];
    }

    /* Ensure that a tag ordering is available. */
    if (TagOrder == NULL) {
        /* None set by the user - use the default. */
        /* Handle the standard tags.
         * The end of the list is marked with a negative value.
         */
        for (tag_index = 0; DefaultTagOrder[tag_index] >= 0; tag_index++) {
            TagName tag = DefaultTagOrder[tag_index];
            output_tag(tag, copy_of_tags, outfp);
            copy_of_tags[tag] = (char *) NULL;
        }
    }
    else {
        for (tag_index = 0; TagOrder[tag_index] >= 0; tag_index++) {
            TagName tag = TagOrder[tag_index];
            output_tag(tag, copy_of_tags, outfp);
            copy_of_tags[tag] = (char *) NULL;
        }
    }
    /* Handle the remaining tags. */
    if(!GlobalState.only_output_wanted_tags) {
        for (tag_index = 0; tag_index < tags_length; tag_index++) {
            if (copy_of_tags[tag_index] != NULL) {
                output_tag(tag_index, copy_of_tags, outfp);
            }
        }
    }
    (void) free(copy_of_tags);
    putc('\n', outfp);
}

/* Ensure that there is room for len more characters on the
 * current line.
 */
static void
check_line_length(FILE *fp, size_t len)
{
    if ((line_length + len) > GlobalState.max_line_length) {
        terminate_line(fp);
    }
}

/* Print ch to fp and update how much of the line
 * has been printed on.
 */
static void
print_single_char(FILE *fp, char ch)
{
    check_line_length(fp, 1);
    output_line[line_length] = ch;
    line_length++;
}

/* Print a space, unless at the beginning of a line. */
static void
print_separator(FILE *fp)
{
    /* Lines shouldn't have trailing spaces, so ensure that there
     * will be room for at least one more character after the space.
     */
    check_line_length(fp, 2);
    if (line_length != 0 && output_line[line_length - 1] != ' ') {
        output_line[line_length] = ' ';
        line_length++;
    }
}

/* Ensure that what comes next starts on a fresh line. */
void
terminate_line(FILE *fp)
{
    /* Delete any trailing space(s). */
    while (line_length >= 1 && output_line[line_length - 1] == ' ') {
        line_length--;
    }
    if (line_length > 0) {
        output_line[line_length] = '\0';
        fprintf(fp, "%s\n", output_line);
        line_length = 0;
    }
}

/* Print str to fp and update how much of the line
 * has been printed on.
 */
void
print_str(FILE *fp, const char *str)
{
    size_t len = strlen(str);

    check_line_length(fp, len);
    if (len > GlobalState.max_line_length) {
        fprintf(GlobalState.logfile,
                "String length %lu is too long for the line length of %lu:\n",
                (unsigned long) len,
                (unsigned long) GlobalState.max_line_length);
        fprintf(GlobalState.logfile, "%s\n", str);
        report_details(GlobalState.logfile);
        fprintf(fp, "%s\n", str);
    }
    else {
        sprintf(&(output_line[line_length]), "%s", str);
        line_length += len;
    }
}

/* Print the given str in separate space-separated
 * pieces to take account of line-breaks.
 * The str should not contain newline characters.
 */
static void
print_space_separated_str(FILE *fp, const char *str)
{
    char *copy = copy_string(str);
    const char *chunk = strtok(copy, " ");
    while (chunk != NULL) {
        print_str(fp, chunk);
        chunk = strtok((char *) NULL, " ");
        if(chunk != NULL) {
            print_separator(fp);
        }
    }
    (void) free((void *) copy);
}

static void
print_comment_list(FILE *fp, CommentList *comment_list)
{
    CommentList *next_comment;

    for (next_comment = comment_list; next_comment != NULL;
            next_comment = next_comment->next) {
        StringList *comment = next_comment->comment;

        if (comment != NULL) {
            start_comment(fp);
            for (; comment != NULL; comment = comment->next) {
                print_space_separated_str(fp, comment->str);
                if(comment->next != NULL) {
                    print_separator(fp);
                }
            }
            end_comment(fp);
        }
    }
}

static void
print_move_list(FILE *outputfile, unsigned move_number, Boolean white_to_move,
        const Move *move_details, const Board *final_board)
{
    Boolean print_move_number = TRUE;
    const Move *move = move_details;
    Boolean keepPrinting;
    int plies;
    /* Keep track of the number of consecutive quiescent moves:
     * captures, Checks and promotion are non-quiescent.
     * @@@ NB: This does not strictly apply to just the main line
     * and could trigger quiescence in a variation, which is undesirable.
     */
    unsigned quiescense_count = 0;

    /* Work out the ply depth. */
    plies = 2 * (move_number) - 1;
    if (!white_to_move) {
        plies++;
    }
    if (GlobalState.output_ply_limit >= 0 &&
            plies > GlobalState.output_ply_limit) {
        keepPrinting = FALSE;
    }
    else {
        keepPrinting = TRUE;
    }

    while (move != NULL && keepPrinting) {
        if (GlobalState.json_format) {
            fputs("{ ", outputfile);
        }
        /* Reset print_move number if a variation was printed. */
        print_move_number = print_move(outputfile, move_number,
                print_move_number,
                white_to_move, move);

        /* See if there is a result attached.  This may be attached either
         * to a move or a comment.
         */
        if (!GlobalState.check_only && (move != NULL) &&
                (move->terminating_result != NULL)) {
            if (GlobalState.output_FEN_string && 
                    GlobalState.FEN_comment_pattern == NULL &&
                    final_board != NULL) {
                if(GlobalState.json_format) {
                    if(!GlobalState.add_FEN_comments) {
                        char *fen = get_FEN_string(final_board);
                        fprintf(outputfile, ", \"FEN\" : \"%s\" ", fen);
                        (void) free((void *) fen);
                    }
                    else {
                        /* The final FEN position will have been output anyway. */
                    }
                }
                else {
                    print_separator(outputfile);
                    const char *comment = build_FEN_comment(final_board);
                    print_str(outputfile, comment);
                    (void) free((void *) comment);
                }
            }
            if (GlobalState.keep_results) {
                print_separator(outputfile);
                print_str(outputfile, move->terminating_result);
            }
        }
        if (move->move[0] != '\0') {
            /* A genuine move was just printed, rather than a comment. */
            if (white_to_move) {
                white_to_move = FALSE;
            }
            else {
                move_number++;
                white_to_move = TRUE;
            }
            plies++;
            if (move->captured_piece != EMPTY ||
                    move->check_status != NOCHECK ||
                    move->promoted_piece != EMPTY) {
                quiescense_count = 0;
            }
            else {
                quiescense_count++;
            }
            if (GlobalState.output_ply_limit >= 0 &&
                    plies > GlobalState.output_ply_limit &&
                    quiescense_count >= GlobalState.quiescence_threshold) {
                keepPrinting = FALSE;
            }
        }
        if (GlobalState.json_format) {
            fputs(" }", outputfile);
        }
        move = move->next;
        /* The following is slightly inaccurate.
         * If the previous value of move was a comment and
         * we aren't printing comments, then this results in two
         * separators being printed after the move preceding the comment.
         * Not sure how to cleanly fix it, because there might have
         * been nags attached to the comment that were printed, for instance!
         */
        if (move != NULL && keepPrinting) {
            if (GlobalState.json_format) {
                fputs(", ", outputfile);
            }
            else {
                print_separator(outputfile);
            }
        }
    }

    if (move != NULL && !keepPrinting) {
        /* We ended printing the game prematurely.
         *
         * Decide whether to print a result indicator.
         */
        if (GlobalState.keep_results) {
            /* Find the final move to see if there was a result there. */
            while (move->next != NULL) {
                move = move->next;
            }
            if (move->terminating_result != NULL) {
                print_separator(outputfile);
                print_str(outputfile, "*");
            }
        }
    }
}

/* Output the current move along with associated information.
 * Return TRUE if either a variation or comment was printed,
 * FALSE otherwise.
 * This is needed to determine whether a new move number
 * is to be printed after a variation.
 */
/* A length to accommodate move numbers. */
#define SMALL_MOVE_NUMBER_LENGTH (20)

static Boolean
print_move(FILE *outputfile, unsigned move_number, Boolean print_move_number,
        Boolean white_to_move, const Move *move_details)
{
    Boolean something_printed = FALSE;
    OutputFormat output_format = GlobalState.output_format;

    if (move_details == NULL) {
        /* Shouldn't happen. */
        fprintf(GlobalState.logfile,
                "Internal error: NULL move in print_move.\n");
        report_details(GlobalState.logfile);
    }
    else {
        if (GlobalState.check_only) {
            /* Nothing to be output. */
        }
        else {
            const unsigned char *move_text = move_details->move;
            /* What move text to print. */
            char *move_to_print;

            if (*move_text != '\0') {
                if (GlobalState.keep_move_numbers &&
                        (white_to_move || print_move_number)) {
                    static char small_number[SMALL_MOVE_NUMBER_LENGTH];

                    /* @@@ Should 1... be written as 1. ... ? */
                    sprintf(small_number,
                            "%u.%s", move_number,
                            white_to_move ? "" : "..");
                    print_str(outputfile, small_number);
                    print_separator(outputfile);
                }
                switch (output_format) {
                    case SAN:
                    case SOURCE:
                        /* @@@ move_text should be handled as unsigned
                         * char text, as the source may be 8-bit rather
                         * than 7-bit.
                         */
                        move_to_print = copy_string((const char *) move_text);
                        if (!GlobalState.keep_checks) {
                            /* Look for a check or mate symbol. */
                            char *check = strchr((const char *) move_text, '+');
                            if (check == NULL) {
                                check = strchr((const char *) move_text, '#');
                            }
                            if (check != NULL) {
                                /* We need to drop it from move_text. */
                                int len = check - ((char *) move_text);
                                move_to_print[len] = '\0';
                            }
                        }
                        break;
                    case HALG:
                    {
                        char algebraic[MAX_MOVE_LEN + 1];

                        *algebraic = '\0';
                        switch (move_details->class) {
                            case PAWN_MOVE:
                            case ENPASSANT_PAWN_MOVE:
                            case KINGSIDE_CASTLE:
                            case QUEENSIDE_CASTLE:
                            case PIECE_MOVE:
                                sprintf(algebraic,
                                        "%c%c-%c%c",
                                        move_details->from_col,
                                        move_details->from_rank,
                                        move_details->to_col,
                                        move_details->to_rank);
                                break;
                            case PAWN_MOVE_WITH_PROMOTION:
                                sprintf(algebraic,
                                        "%c%c%-c%c%c",
                                        move_details->from_col,
                                        move_details->from_rank,
                                        move_details->to_col,
                                        move_details->to_rank,
                                        promoted_piece_letter(move_details->promoted_piece));
                                break;
                            case NULL_MOVE:
                                strcpy(algebraic, NULL_MOVE_STRING);
                                break;
                            case UNKNOWN_MOVE:
                                strcpy(algebraic, "???");
                                break;
                        }
                        if (GlobalState.keep_checks) {
                            switch (move_details->check_status) {
                                case NOCHECK:
                                    break;
                                case CHECK:
                                    strcat(algebraic, "+");
                                    break;
                                case CHECKMATE:
                                    strcat(algebraic, "#");
                                    break;
                            }
                        }
                        move_to_print = copy_string(algebraic);
                    }
                        break;
                    case LALG:
                    case ELALG:
                    case XLALG:
                    case XOLALG:
                    case UCI:
                    {
                        char algebraic[MAX_MOVE_LEN + 1];
                        size_t ind = 0;

                        if(output_format == XOLALG &&
                           (move_details->class == KINGSIDE_CASTLE ||
                            move_details->class == QUEENSIDE_CASTLE)) {
                            strcpy(algebraic, (char *) move_text);
                            ind += strlen((char *) algebraic);
                        }
                        else {
                            /* Prefix with a piece name if ELALG. */
                            if ((output_format == ELALG ||
                                 output_format == XLALG ||
                                 output_format == XOLALG) &&
                                    move_details->class == PIECE_MOVE) {
                                strcpy(algebraic,
                                        piece_str(move_details->piece_to_move));
                                ind = strlen(algebraic);
                            }
                            /* Format the basics. */
                            if (move_details->class != NULL_MOVE) {
                                sprintf(&algebraic[ind],
                                        "%c%c",
                                        move_details->from_col,
                                        move_details->from_rank);

                                ind += 2;
                                if (output_format == XLALG ||
                                    output_format == XOLALG) {
                                    /* Add a separating - or x. */
                                    char separator;
                                    if (move_details->captured_piece != EMPTY) {
                                        separator = 'x';
                                    }
                                    else {
                                        separator = '-';
                                    }
                                    sprintf(&algebraic[ind],
                                            "%c", separator);
                                    ind++;
                                }
                                sprintf(&algebraic[ind],
                                        "%c%c",
                                        move_details->to_col,
                                        move_details->to_rank);
                                ind += 2;
                            }
                            else {
                                strcpy(algebraic, NULL_MOVE_STRING);
                                ind += strlen(NULL_MOVE_STRING);
                            }
                            switch (move_details->class) {
                                case PAWN_MOVE:
                                case KINGSIDE_CASTLE:
                                case QUEENSIDE_CASTLE:
                                case PIECE_MOVE:
                                case NULL_MOVE:
                                    /* Nothing more to do at this stage. */
                                    break;
                                case ENPASSANT_PAWN_MOVE:
                                    if (output_format == ELALG) {
                                        strcat(algebraic, "ep");
                                        ind += 2;
                                    }
                                    break;
                                case PAWN_MOVE_WITH_PROMOTION:
                                    sprintf(&algebraic[ind],
                                            "%s",
                                            piece_str(move_details->promoted_piece));
                                    ind = strlen(algebraic);
                                    break;
                                case UNKNOWN_MOVE:
                                    strcpy(algebraic, "???");
                                    ind += 3;
                                    break;
                            }
                        }
                        if (GlobalState.keep_checks) {
                            switch (move_details->check_status) {
                                case NOCHECK:
                                    break;
                                case CHECK:
                                    strcat(algebraic, "+");
                                    ind++;
                                    break;
                                case CHECKMATE:
                                    strcat(algebraic, "#");
                                    ind++;
                                    break;
                            }
                        }
                        move_to_print = copy_string(algebraic);
                    }
                        break;
                    default:
                        fprintf(GlobalState.logfile,
                                "Unknown output format %d in print_move()\n",
                                output_format);
                        exit(1);
                        move_to_print = NULL;
                        break;
                }
            }
            else {
                /* An empty move. */
                fprintf(GlobalState.logfile,
                        "Internal error: Empty move in print_move.\n");
                report_details(GlobalState.logfile);
                move_to_print = NULL;
            }
            if (GlobalState.json_format) {
                fputs("\"move\" : ", outputfile);
                fputs("\"", outputfile);
                if (move_to_print != NULL) {
                    fputs(move_to_print, outputfile);
                }
                fputs("\"", outputfile);
            }
            else {
                if (move_to_print != NULL) {
                    print_str(outputfile, move_to_print);
                }
            }
            if (move_to_print != NULL) {
                (void) free(move_to_print);
            }
            if(print_items_following_move(outputfile, move_details, move_number, white_to_move)) {
                something_printed = TRUE;
            }            
        }
    }
    return something_printed;
}

/* Maximum length of a 64-bit unsigned int in decimal. 
 * NB: At the moment, the output is hex, which requires
 * only 16 characters.
 */
#define HASH_64_BIT_SPACE 20

/* Print further information, that may be attached to moves,
 * such as NAGs and comments.
 * Return TRUE if something was printed for non JSON format output.
 */
static Boolean
print_items_following_move(FILE *outputfile, const Move *move_details,
        unsigned move_number, Boolean white_to_move)
{
    Boolean something_printed = FALSE;
    Nag *nags = move_details->NAGs;
    Variation *variants = move_details->Variants;
    if (move_details->comment_list != NULL && GlobalState.keep_comments) {
        print_comment_list(outputfile, move_details->comment_list);
        something_printed = TRUE;
    }
    if(nags != NULL) {
        /* We don't need to output move numbers after just
         * NAGs, so don't set something_printed just for NAGs.
         */
        if (GlobalState.keep_NAGs && GlobalState.json_format) {
            fputs(", \"nags\" : [", outputfile);
        }
        while (nags != NULL) {
            if(GlobalState.keep_NAGs) {
                StringList *text = nags->text;
                while(text != NULL) {
                    if(GlobalState.json_format) {
                        fprintf(outputfile, "\"%s\"", text->str);
                        if(nags->next != NULL) {
                            fputs(", ", outputfile);
                        }
                    }
                    else {
                        print_separator(outputfile);
                        print_str(outputfile, text->str);
                    }
                    text = text->next;
                }
            }
            if(nags->comments != NULL && GlobalState.keep_comments) {
                // @@@ JSON option needed.
                
                print_comment_list(outputfile, nags->comments);
                something_printed = TRUE;
            }
            nags = nags->next;
        }
        if(GlobalState.keep_NAGs && GlobalState.json_format) {
            fputs("] ", outputfile);
        }
    }
    if (GlobalState.output_evaluation) {
        if(GlobalState.json_format) {
            fprintf(outputfile, ", \"evaluation\" : \"%.2f\"", 
                    move_details->evaluation);
        } 
        else {
            const char valueSpace[] = "-012456789.00";
            char *evaluation = (char *) malloc_or_die(sizeof (valueSpace));
            sprintf(evaluation, "%.2f", move_details->evaluation);
            if (strlen(evaluation) > strlen(valueSpace)) {
                fprintf(GlobalState.logfile,
                        "Internal error: Overflow in evaluation space in print_items_following_move()\n");
                exit(1);
            }
            
            print_as_comment(outputfile, evaluation);
            (void) free((void *) evaluation);
            something_printed = TRUE;
        }
    }
    if(GlobalState.add_FEN_comments) {
        if(move_details->epd != NULL && move_details->fen_suffix != NULL) {
            if(GlobalState.json_format) {
                fprintf(outputfile, ", \"FEN\" : \"%s %s\"", 
                        move_details->epd,
                        move_details->fen_suffix);
            }
            else {
                start_comment(outputfile);
                print_space_separated_str(outputfile, move_details->epd);
                print_separator(outputfile);
                print_space_separated_str(outputfile, move_details->fen_suffix);
                end_comment(outputfile);
                something_printed = TRUE;
            }
        }
    }
    if (GlobalState.add_hashcode_comments) {
        if(GlobalState.json_format) {
            fprintf(outputfile, ", \"HashCode\" : \"%llx\"", 
                    move_details->zobrist);
        }
        else {
            char *hashcode = (char *) malloc_or_die(HASH_64_BIT_SPACE + 1);
            sprintf(hashcode, "%llx", move_details->zobrist);
            
            print_as_comment(outputfile, hashcode);
            (void) free((void *) hashcode);
            something_printed = TRUE;
        }
    }
    if (variants != NULL) {
        if(GlobalState.keep_variations) {
            if(!GlobalState.split_variants) {
                while (variants != NULL) {
                    print_separator(outputfile);
                    print_single_char(outputfile, '(');
                    if (GlobalState.keep_comments &&
                            (variants->prefix_comment != NULL)) {
                        print_comment_list(outputfile, variants->prefix_comment);
                        print_separator(outputfile);
                    }
                    /* Always start with a move number.
                     * The final board position is not needed.
                     */
                    print_move_list(outputfile, move_number,
                            white_to_move, variants->moves,
                            (const Board *) NULL);
                    print_single_char(outputfile, ')');
                    if (GlobalState.keep_comments &&
                            (variants->suffix_comment != NULL)) {
                        print_comment_list(outputfile, variants->suffix_comment);
                    }
                    variants = variants->next;
                }
                something_printed = TRUE;
            }
            else {
                /* Variations are being split so don't output them. */
            }
        }
        else if(GlobalState.keep_comments) {
            /* Avoid losing suffix comments of variations.
             * NB: @@@ I am not entirely convinced that comments that
             * follow variations actually belong with the move immediately
             * preceding the variation. However, this is one way to avoid
             * their being lost if variations are not being output.
             */
            while(variants != NULL) {
                if(variants->suffix_comment != NULL) {
                    print_comment_list(outputfile, variants->suffix_comment);
                    something_printed = TRUE;
                }
                variants = variants->next;
            }
        }
    }
    return something_printed;
}

/* Start a comment, taking into account separators
 * and possible placement of comments on separate lines.
 */
static void
start_comment(FILE *outputfile)
{
    if(GlobalState.separate_comment_lines) {
        terminate_line(outputfile);
    }
    else {
        print_separator(outputfile);
    }
    print_single_char(outputfile, '{');
    print_separator(outputfile);
}

/* End a comment, taking into account separators
 * and possible placement of comments on separate lines.
 */
static void
end_comment(FILE *outputfile)
{
    print_separator(outputfile);
    print_single_char(outputfile, '}');
    if(GlobalState.separate_comment_lines) {
        terminate_line(outputfile);
    }
}

/* Print str as a comment. */
static void
print_as_comment(FILE *outputfile, const char *str)
{
    start_comment(outputfile);
    print_str(outputfile, str);
    end_comment(outputfile);
}

/* Return the letter associated with the given piece. */
static char
promoted_piece_letter(Piece piece)
{
    switch (piece) {
        case QUEEN:
            return 'Q';
        case ROOK:
            return 'R';
        case BISHOP:
            return 'B';
        case KNIGHT:
            return 'N';
        default:
            return '?';
    }
}

/* Output a comment in CM format. */
static void
output_cm_comment(CommentList *comment, FILE *outputfile, unsigned indent)
{ /* Don't indent for the first comment line, because
     * we should already be positioned at the correct spot.
     */
    unsigned indent_for_this_line = 0;

    putc(CM_COMMENT_CHAR, outputfile);
    line_length++;
    while (comment != NULL) {
        /* We will use strtok to break up the comment string,
         * with chunk to point to each bit in turn.
         */
        char *chunk;
        StringList *comment_str = comment->comment;

        for (; comment_str != NULL; comment_str = comment_str->next) {
            char *str = copy_string(comment_str->str);
            chunk = strtok(str, " ");
            while (chunk != NULL) {
                size_t len = strlen(chunk);

                if ((line_length + 1 + len) > GlobalState.max_line_length) {
                    /* Start a new line. */
                    fputc('\n', outputfile);
                    indent_for_this_line = indent;
                    for (unsigned in = 0; in < indent_for_this_line; in++) {
                        fputc(' ', outputfile);
                    }
                    fputc(CM_COMMENT_CHAR, outputfile);
                    fputc(' ', outputfile);
                    line_length = indent_for_this_line + 2;
                }
                else {
                    fputc(' ', outputfile);
                    line_length++;
                }
                fprintf(outputfile, "%s", chunk);
                line_length += len;
                chunk = strtok((char *) NULL, " ");
            }
            (void) free((void *) str);
        }
        comment = comment->next;
    }
    fputc('\n', outputfile);
    line_length = 0;
}

static void
output_cm_result(const char *result, FILE *outputfile)
{
    fprintf(outputfile, "%c ", CM_COMMENT_CHAR);
    if (strcmp(result, "1-0") == 0) {
        fprintf(outputfile, "and black resigns");
    }
    else if (strcmp(result, "0-1") == 0) {
        fprintf(outputfile, "and white resigns");
    }
    else if (strncmp(result, "1/2", 3) == 0) {
        fprintf(outputfile, "draw");
    }
    else {
        fprintf(outputfile, "incomplete result");
    }
}

/* Output the game in Chess Master format.
 * This is probably obsolete.
 */
static void
output_cm_game(FILE *outputfile, unsigned move_number,
        Boolean white_to_move, const Game *game)
{
    const Move *move = game->moves;

    if ((move_number != 1) || (!white_to_move)) {
        fprintf(GlobalState.logfile,
                "Unable to output CM games other than from the starting position.\n");
        report_details(GlobalState.logfile);
    }
    fprintf(outputfile, "WHITE: %s\n",
            game->tags[WHITE_TAG] != NULL ? game->tags[WHITE_TAG] : "");
    fprintf(outputfile, "BLACK: %s\n",
            game->tags[BLACK_TAG] != NULL ? game->tags[BLACK_TAG] : "");
    putc('\n', outputfile);

    if (game->prefix_comment != NULL) {
        line_length = 0;
        output_cm_comment(game->prefix_comment, outputfile, 0);
    }
    while (move != NULL) {
        if (move->move[0] != '\0') {
            /* A genuine move. */
            if (white_to_move) {
                fprintf(outputfile, "%*u. ", MOVE_NUMBER_WIDTH, move_number);
                fprintf(outputfile, "%*s", -MOVE_WIDTH, move->move);
                white_to_move = FALSE;
            }
            else {
                fprintf(outputfile, "%*s", -MOVE_WIDTH, move->move);
                move_number++;
                white_to_move = TRUE;
            }
        }
        if ((move->comment_list != NULL) && GlobalState.keep_comments) {
            const char *result = move->terminating_result;

            if (!white_to_move) {
                fprintf(outputfile, "%*s", -MOVE_WIDTH, "...");
            }
            line_length = COMMENT_INDENT;
            output_cm_comment(move->comment_list, outputfile, COMMENT_INDENT);
            if ((result != NULL) && (move->check_status != CHECKMATE)) {
                /* Give some information on the nature of the finish. */
                if (white_to_move) {
                    fprintf(outputfile, "%*s", COMMENT_INDENT, "");
                }
                else {
                    /* Print out a string representing the result. */
                    fprintf(outputfile, "%*s %*s%*s",
                            MOVE_NUMBER_WIDTH + 1, "", -MOVE_WIDTH, "...",
                            MOVE_WIDTH, "");
                }
                output_cm_result(result, outputfile);
                putc('\n', outputfile);
            }
            else {
                if (!white_to_move) {
                    /* Indicate that the next move is Black's. */
                    fprintf(outputfile, "%*s %*s",
                            MOVE_NUMBER_WIDTH + 1, "", -MOVE_WIDTH, "...");
                }
            }
        }
        else {
            if ((move->terminating_result != NULL) &&
                    (move->check_status != CHECKMATE)) {
                /* Give some information on the nature of the finish. */
                const char *result = move->terminating_result;

                if (!white_to_move) {
                    fprintf(outputfile, "%*s", -MOVE_WIDTH, "...");
                }
                output_cm_result(result, outputfile);
                if (!white_to_move) {
                    putc('\n', outputfile);
                    fprintf(outputfile, "%*s %*s",
                            MOVE_NUMBER_WIDTH + 1, "", -MOVE_WIDTH, "...");
                }
                putc('\n', outputfile);
            }
            else {
                if (white_to_move) {
                    /* Terminate the move pair. */
                    putc('\n', outputfile);
                }
            }
        }
        move = move->next;
    }
    putc('\n', outputfile);
}

/* Output the current game according to the required output format. */
void
format_game(Game *current_game, FILE *outputfile)
{
    Boolean white_to_move = TRUE;
    unsigned move_number = 1;
    Board *initial_board;
    /* The final board position, if available. */
    Board *final_board = NULL;

    /* We need a copy of the final board.
     * Combine the generation of this with a rewrite
     * of the moves of the game into
     * SAN (Standard Algebraic Notation) unless the original
     * source form is required.
     */
    final_board = rewrite_game(current_game);
    initial_board = new_game_board(current_game->tags[FEN_TAG]);

    /* If we aren't starting from the initial setup, then we
     * need to know the current move number and whose
     * move it is.
     */
    if (current_game->tags[FEN_TAG] != NULL && initial_board != NULL) {
        move_number = initial_board->move_number;
        white_to_move = initial_board->to_move == WHITE;
    }

    /* Start at the beginning of a line. */
    line_length = 0;
    
    if (final_board != NULL) {
        if (GlobalState.output_plycount) {
            add_plycount(current_game);
        }
        if (GlobalState.output_total_plycount) {
            add_total_plycount(current_game, GlobalState.keep_variations);
        }
        if (GlobalState.add_hashcode_tag || current_game->tags[HASHCODE_TAG] != NULL) {
            add_hashcode_tag(current_game);
        }
        switch (GlobalState.output_format) {
            case SAN:
            case SOURCE:
            case LALG:
            case HALG:
            case ELALG:
            case XLALG:
            case XOLALG:
            case UCI:
                print_algebraic_game(current_game, outputfile, move_number, white_to_move,
                        final_board);
                break;
            case EPD:
                print_EPD_game(current_game, outputfile, move_number, white_to_move,
                        initial_board);
                break;
            case CM:
                output_cm_game(outputfile, move_number, white_to_move, current_game);
                break;
            default:
                fprintf(GlobalState.logfile,
                        "Internal error: unknown output type %d in format_game().\n",
                        GlobalState.output_format);
                break;
        }
        fflush(outputfile);
        free_board(final_board);
    }
    free_board(initial_board);
}

/* Add the given tag to the output ordering. */
void
add_to_output_tag_order(TagName tag)
{
    int tag_index;

    if (TagOrder == NULL) {
        tag_order_space = ARRAY_SIZE(DefaultTagOrder);
        TagOrder = (int *) malloc_or_die(tag_order_space * sizeof (*TagOrder));
        /* Always ensure that there is a negative value at the end. */
        TagOrder[0] = -1;
    }
    /* Check to ensure a position has not already been indicated
     * for this tag.
     */
    for (tag_index = 0; (TagOrder[tag_index] != -1) &&
            (TagOrder[tag_index] != (int) tag); tag_index++) {
    }

    if (TagOrder[tag_index] == -1) {
        /* Make sure there is enough space for another. */
        if (tag_index >= tag_order_space) {
            /* Allocate some more. */
            tag_order_space += 10;
            TagOrder = (int *) realloc_or_die((void *) TagOrder,
                    tag_order_space * sizeof (*TagOrder));
        }
        TagOrder[tag_index] = tag;
        TagOrder[tag_index + 1] = -1;
    }
    else {
        fprintf(GlobalState.logfile, "Duplicate position for tag: %s\n",
                select_tag_string(tag));
    }
}

/* Format EPD comments containing tag details.
 * A c0 comment contains player and event info.
 * A c1 comment contains the game result.
 */
static const char *
format_epd_game_comment(char **Tags)
{
    static char c0_prefix[] = "c0 ";
    static char c1_prefix[] = "c1 ";
    static char player_separator[] = "-";
    static size_t prefix_and_separator_len =
            sizeof (c0_prefix) + sizeof (player_separator);
    size_t space_needed = prefix_and_separator_len;
    char *comment;

    if (Tags[WHITE_TAG] != NULL) {
        space_needed += strlen(Tags[WHITE_TAG]);
    }
    if (Tags[BLACK_TAG] != NULL) {
        space_needed += strlen(Tags[BLACK_TAG]);
    }
    /* Allow a space character before each of the remaining tags. */
    if (Tags[EVENT_TAG] != NULL) {
        space_needed += 1 + strlen(Tags[EVENT_TAG]);
    }
    if (Tags[SITE_TAG] != NULL) {
        space_needed += 1 + strlen(Tags[SITE_TAG]);
    }
    if (Tags[DATE_TAG] != NULL) {
        space_needed += 1 + strlen(Tags[DATE_TAG]);
    }
    /* Allow for a terminating semicolon after the c0 comment. */
    space_needed++;

    /* Allow for a space before the c1 comment. */
    space_needed++;
    space_needed += strlen(c1_prefix);
    if(Tags[RESULT_TAG] != NULL) {
        space_needed += 1 + strlen(Tags[RESULT_TAG]);
    }
    else {
        space_needed += 1 + strlen("*");
    }
    /* Allow for a terminating semicolon after the c1 comment. */
    space_needed++;

    comment = (char *) malloc_or_die(space_needed + 1);

    strcpy(comment, c0_prefix);
    if (Tags[WHITE_TAG] != NULL) {
        strcat(comment, Tags[WHITE_TAG]);
    }
    strcat(comment, player_separator);
    if (Tags[BLACK_TAG] != NULL) {
        strcat(comment, Tags[BLACK_TAG]);
    }
    if (Tags[EVENT_TAG] != NULL) {
        strcat(comment, " ");
        strcat(comment, Tags[EVENT_TAG]);
    }
    if (Tags[SITE_TAG] != NULL) {
        strcat(comment, " ");
        strcat(comment, Tags[SITE_TAG]);
    }
    if (Tags[DATE_TAG] != NULL) {
        strcat(comment, " ");
        strcat(comment, Tags[DATE_TAG]);
    }
    strcat(comment, "; ");
    strcat(comment, c1_prefix);
    if(Tags[RESULT_TAG] != NULL) {
        strcat(comment, Tags[RESULT_TAG]);
    }
    else {
        strcat(comment, "*");
    }
    strcat(comment, ";");

    if (strlen(comment) >= space_needed) {
        fprintf(GlobalState.logfile,
                "Internal error: overflow in format_epd_game_comment\n");
    }
    return comment;
}

static void
print_algebraic_game(Game *current_game, FILE *outputfile,
        unsigned move_number, Boolean white_to_move,
        Board *final_board)
{
    if (GlobalState.json_format) {
        /* Need to take account of splitting output over multiple files. */
        Boolean comma_needed;
        if (GlobalState.games_per_file == 0) {
            comma_needed = GlobalState.num_games_matched > 1;
        }
        else {
            comma_needed = GlobalState.games_per_file > 1 &&
                    (GlobalState.num_games_matched % GlobalState.games_per_file) != 1;
        }
        if (comma_needed) {
            fputs(",\n", outputfile);
        }
        fputs("{\n", outputfile);
    }
    /* Report details on the output. */
    if (GlobalState.tag_output_format == ALL_TAGS) {
        show_tags(outputfile, current_game->tags, current_game->tags_length);
    }
    else if (GlobalState.tag_output_format == SEVEN_TAG_ROSTER) {
        output_STR(outputfile, current_game->tags);
        if (GlobalState.add_ECO && !GlobalState.parsing_ECO_file) {
            /* If ECO classification has been requested, then assume
             * that ECO tags are also required.
             */
            output_tag(ECO_TAG, current_game->tags, outputfile);
            output_tag(OPENING_TAG, current_game->tags, outputfile);
            output_tag(VARIATION_TAG, current_game->tags, outputfile);
            output_tag(SUB_VARIATION_TAG, current_game->tags, outputfile);
        }

        if(current_game->tags[FEN_TAG] != NULL) {
            /* Output any FEN that there might be. */
            /* @@@ NB: Strictly speaking, we should check TagOrder for the
             * preferred order of these, in case it is not the default.
             */
            output_tag(VARIANT_TAG, current_game->tags, outputfile);
            output_tag(SETUP_TAG, current_game->tags, outputfile);
            output_tag(FEN_TAG, current_game->tags, outputfile);
        }
        putc('\n', outputfile);
    }
    else if (GlobalState.tag_output_format == NO_TAGS) {
    }
    else {
        fprintf(GlobalState.logfile,
                "Unknown output form for tags: %d\n",
                GlobalState.tag_output_format);
        exit(1);
    }
    if ((GlobalState.keep_comments) &&
            (current_game->prefix_comment != NULL)) {
        print_comment_list(outputfile,
                current_game->prefix_comment);
        terminate_line(outputfile);
        putc('\n', outputfile);
    }
    if (GlobalState.json_format) {
        fputs("\"Moves\":[", outputfile);
    }
    print_move_list(outputfile, move_number, white_to_move,
            current_game->moves, final_board);
    if (GlobalState.json_format) {
        fputs("]\n", outputfile);
    }
    /* Take account of a possible zero move game. */
    if (current_game->moves == NULL) {
        if (current_game->tags[RESULT_TAG] != NULL) {
            if (!GlobalState.json_format) {
                print_str(outputfile, current_game->tags[RESULT_TAG]);
            }
        }
        else {
            fprintf(GlobalState.logfile,
                    "Internal error: Zero move game with no result\n");
        }
    }
    if (GlobalState.json_format) {
        fputs("\n}", outputfile);
    }
    else {
        terminate_line(outputfile);
        putc('\n', outputfile);
    }
}

static void
print_EPD_move_list(Game *current_game, FILE *outputfile,
        unsigned move_number, Boolean white_to_move,
        Board *initial_board)
{
    const char *game_comment = format_epd_game_comment(current_game->tags);
    const Move *move = current_game->moves;

    if (initial_board != NULL) {
        char epd[FEN_SPACE];
        build_basic_EPD_string(initial_board, epd);
        fprintf(outputfile, "%s %s\n", epd, game_comment);
    }
    while (move != NULL) {
        if (move->epd != NULL) {
            fprintf(outputfile, "%s %s\n", move->epd, game_comment);
        }
        else {
            fprintf(GlobalState.logfile, "Internal error: Missing EPD\n");
            report_details(GlobalState.logfile);
            exit(1);
        }
        move = move->next;
    }
    (void) free((void *) game_comment);
}

static void
print_EPD_game(Game *current_game, FILE *outputfile,
        unsigned move_number, Boolean white_to_move,
        Board *initial_board)
{
    if (!GlobalState.check_only) {
        print_EPD_move_list(current_game, outputfile, move_number, white_to_move,
                initial_board);
        putc('\n', outputfile);
    }
}

/*
 * Build a comment containing a FEN representation of the board.
 */
static const char *
build_FEN_comment(const Board *board)
{
    char *fen = get_FEN_string(board);
    const char *prefix = "{ \"";
    const char *suffix = "\" }";
    
    char *comment = (char *) malloc_or_die(strlen(prefix) + strlen(fen) + strlen(suffix) + 1);
    sprintf(comment, "%s%s%s", prefix, fen, suffix);
    (void) free((void *) fen);
    return comment;
}

/*
 * Count how many ply recorded for the given move.
 * Include variations if count_variations.
 */
static unsigned count_single_move_ply(const Move *move_details,
        Boolean count_variations)
{
    unsigned count = 1;
    if (count_variations) {
        Variation *variant = move_details->Variants;
        while (variant != NULL) {
            count += count_move_list_ply(variant->moves, count_variations);
            variant = variant->next;
        }
    }
    return count;
}

/*
 * Count how many plies in the game in total.
 * Include variations if count_variations.
 */
static unsigned count_move_list_ply(Move *move_list, Boolean count_variations)
{
    unsigned count = 0;
    while (move_list != NULL) {
        count += count_single_move_ply(move_list, count_variations);
        move_list = move_list->next;
    }
    return count;
}

/*
 * Count how many plies in the game in total.
 * Include variations if count_variations.
 */
void add_plycount(const Game *game)
{
    unsigned count = count_move_list_ply(game->moves, FALSE);
    char formatted_count[FORMATTED_NUMBER_SIZE];
    sprintf(formatted_count, "%u", count);

    if (game->tags[PLY_COUNT_TAG] != NULL) {
        (void) free(game->tags[PLY_COUNT_TAG]);
    }
    game->tags[PLY_COUNT_TAG] = copy_string(formatted_count);
}

/*
 * Count how many plies in the game in total.
 * Include variations if count_variations.
 */
void add_total_plycount(const Game *game, Boolean count_variations)
{
    unsigned count = count_move_list_ply(game->moves, count_variations);
    char formatted_count[FORMATTED_NUMBER_SIZE];
    sprintf(formatted_count, "%u", count);

    if (game->tags[TOTAL_PLY_COUNT_TAG] != NULL) {
        (void) free(game->tags[TOTAL_PLY_COUNT_TAG]);
    }
    game->tags[TOTAL_PLY_COUNT_TAG] = copy_string(formatted_count);
}

/*
 * Include a tag containing a hashcode for the game.
 * Use the cumulative hash value.
 */
static void add_hashcode_tag(const Game *game)
{
    HashCode hashcode = game->cumulative_hash_value;
    char formatted_code[FORMATTED_NUMBER_SIZE];
    sprintf(formatted_code, "%08x", (unsigned) hashcode);

    if (game->tags[HASHCODE_TAG] != NULL) {
        (void) free(game->tags[HASHCODE_TAG]);
    }
    game->tags[HASHCODE_TAG] = copy_string(formatted_code);
}
