/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 *
 * Copyright 2016 Mike Becker. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 *   1. Redistributions of source code must retain the above copyright
 *      notice, this list of conditions and the following disclaimer.
 *
 *   2. Redistributions in binary form must reproduce the above copyright
 *      notice, this list of conditions and the following disclaimer in the
 *      documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 *
 */

#include "pgn.h"

#include <stdlib.h>
#include <ctype.h>
#include <string.h>
#include <sys/time.h>
#include <time.h>

enum {
    pgn_error_missing_quote = 1,
    pgn_error_missing_bracket,
    pgn_error_missing_brace,
    pgn_error_missing_dot,
    pgn_error_move_syntax,
    pgn_error_move_semantics,
    pgn_error_result_syntax,
};

static const char* pgn_error_strings[] = {
    "No Error.",
    "Tag values must be enclosed in double-quotes.",
    "Missing closing brace '}' for comment.",
    "Tags must be enclosed in square brackets: '[Key \"Value\"]'.",
    "Move numbers must be terminated with a dot (e.g. '13.' - not '13').",
    "Move is syntactically incorrect.",
    "Move is not valid according to chess rules.",
    "Result syntax is incorrect. Expected 1-0, 0-1, 1/2-1/2, or *.",
};

const char* pgn_error_str(int code) {
    return pgn_error_strings[code];
}

int read_pgn(FILE* stream, GameState *gamestate) {
    int c, i;
    
    char result[8];
    
    char tagkey[32];
    char tagvalue[128];
    
    /* read tag pairs */
    while (true) {
        while (isspace(c = fgetc(stream)));
        if (c == '1') {
            break;
        }
        if (c != '[') {
            return pgn_error_missing_bracket;
        }
        while (isspace(c = fgetc(stream)));
        i = 0;
        do {
            tagkey[i++] = c;
        } while (!isspace(c = fgetc(stream)));
        tagkey[i] = '\0';
        while (isspace(c = fgetc(stream)));
        if (c != '"') {
            return pgn_error_missing_quote;
        }
        i = 0;
        while ((c = fgetc(stream)) != '"') {
            if (c == '\n' || c == EOF) {
                return pgn_error_missing_quote;
            }
            tagvalue[i++] = c;
        }
        tagvalue[i] = '\0';
        if (fgetc(stream) != ']') {
            return pgn_error_missing_bracket;
        }

        // TODO: read clock info
        if (strcmp("Result", tagkey) == 0) {
            // TODO: why are we parsing this? it is never used!
            memcpy(result, tagvalue, 8);
        }
    }
    
    /* read moves */
    if (fgetc(stream) != '.') {
        return pgn_error_missing_dot;
    }
    
    char movestr[10];
    Move move;
    uint8_t curcol = WHITE;
    
    while (true) {
        /* move */
        while (isspace(c = fgetc(stream)));
        i = 0;
        do {
            movestr[i++] = c;
            if (i >= 10) {
                return pgn_error_move_syntax;
            }
        } while (!isspace(c = fgetc(stream)));
        movestr[i] = '\0';
        if (eval_move(gamestate, movestr, &move, curcol)
                != VALID_MOVE_SYNTAX) {
            return pgn_error_move_syntax;
        }
        if (validate_move(gamestate, &move) != VALID_MOVE_SEMANTICS) {
            return pgn_error_move_semantics;
        }
        apply_move(gamestate, &move);
        
        /* skip spaces */
        while (isspace(c = fgetc(stream)));
        
        /* parse possible comment */
        if (c == '{') {
            // TODO: interpret comment (may contain clock info)
            do {
                c = fgetc(stream);
            } while (c != '}' && c != EOF);
            if (c == EOF) {
                return pgn_error_missing_brace;
            }
            /* skip spaces */
            while (isspace(c = fgetc(stream)));
        }
        
        /* end of game data encountered */
        if (c == EOF) {
            break;
        }
        if (c == '1' || c == '0') {
            c = fgetc(stream);
            // TODO: #842 - allow games to be continued if possible
            if (c == '-') {
                if (!gamestate->checkmate) {
                    // TODO: maybe we should not parse this here;
                    //       there is an unused result string from the STR
                    c = fgetc(stream);
                    if (c == '1') {
                        gamestate->wresign = true;
                    } else if (c == '0') {
                        gamestate->bresign = true;
                    } else {
                        return pgn_error_result_syntax;
                    }
                }
                break;
            } else if (c == '/') {
                gamestate->remis = !gamestate->stalemate;
                break;
            } else {
                /* oops, it was a move number, go back! */
                fseek(stream, -1, SEEK_CUR);
            }
        }
        
        /* we have eaten the next valuable byte, so go back */
        fseek(stream, -1, SEEK_CUR);
        
        /* skip move number after black move */
        if (curcol == BLACK) {
            while (isdigit(c = fgetc(stream)));
            if (c != '.') {
                return pgn_error_missing_dot;
            }
        }
        curcol = opponent_color(curcol);
    }
    
    return 0;
}

static void pgn_insert_newlines(char *block) {
    size_t pos = 0;
    size_t last_space_pos = 0;
    size_t line_len = 0;
    while (block[pos] != '\0') {
        if (block[pos] == 0x1f) {
            block[pos++] = ' ';
            continue;
        }
        if (block[pos] == ' ') {
            last_space_pos = pos;
        }
        line_len++;
        if (line_len > 80) {
            block[last_space_pos] = '\n';
            line_len = pos - last_space_pos;
        }
        pos++;
    }
}

static const char *pgn_date(GameState *gamestate) {
    static char date[16];
    time_t dateseconds;
    if (gamestate->movecount == 0) {
        struct timeval curtimestamp;
        gettimeofday(&curtimestamp, NULL);
        dateseconds = curtimestamp.tv_sec;
    } else {
        dateseconds = (time_t) gamestate->moves[0].timestamp.tv_sec;
    }
    struct tm *tm = gmtime(&dateseconds);
    if (tm == NULL) {
        /* fallback - should never happen in reality */
        strncpy(date, "????.??.??", sizeof(date));
    } else {
        int year = tm->tm_year + 1900;
        int month = tm->tm_mon + 1;
        int day = tm->tm_mday;
#ifdef __GNUC__
        /* this is required to tell GCC that it does not need to warn
         * about the small buffer size of the date string.
         */
        if (year < 1900 || year > 9999) __builtin_unreachable();
        if (month < 1 || month > 12) __builtin_unreachable();
        if (day < 1 || day > 31) __builtin_unreachable();
#endif
        snprintf(date, sizeof(date), "%04d.%02d.%02d",
            year, month, day);
    }
    return date;
}

const char *pgn_player_name(GameState *gamestate, uint8_t color) {
    const char *name = color == WHITE ? gamestate->wname : gamestate->bname;
    return name[0] != '\0' ? name : "Anonymous";
}

void write_pgn(FILE* stream, GameState *gamestate, bool export_comments) {
    /* STR tag pairs */
    fprintf(stream, "[Event \"%s\"]\n", "terminal-chess game");
    fprintf(stream, "[Site \"%s\"]\n", "Somewhere on Earth");
    fprintf(stream, "[Date \"%s\"]\n", pgn_date(gamestate));
    fprintf(stream, "[Round \"%s\"]\n", "-");
    fprintf(stream, "[White \"%s\"]\n", pgn_player_name(gamestate, WHITE));
    fprintf(stream, "[Black \"%s\"]\n", pgn_player_name(gamestate, BLACK));

    char *result;
    if (gamestate->stalemate || gamestate->remis) {
        result = "1/2-1/2";
    } else if (gamestate->wresign) {
        result = "0-1";
    } else if (gamestate->bresign) {
        result = "1-0";
    } else if (gamestate->checkmate) {
        if (gamestate->movecount > 0) {
            result = (last_move(gamestate).piece & COLOR_MASK) == WHITE ?
                "1-0" : "0-1";
        } else {
            result = "0-1";
        }
    } else {
        result = "*";
    }
    fprintf(stream, "[Result \"%s\"]\n\n", result);

    // TODO: add optional clock info
    
    /* moves */
    size_t moveblkcap = 4096;
    char *moveblk = malloc(moveblkcap);
    char *moveblkptr = moveblk;
    if (moveblk == NULL) {
        // TODO: error handling (for the entire function actually)
        abort();
    }
    for (unsigned i = 0 ; i < gamestate->movecount ; i++) {
        /* reallocate move block buffer if needed */
        {
            size_t moveblksize = moveblkptr - moveblk;
            if (moveblksize + 128 > moveblkcap) {
                moveblkcap *= 2;
                char *newmoveblk = realloc(moveblk, moveblkcap);
                if (newmoveblk == NULL) {
                    free(moveblk);
                    abort();
                }
                moveblk = newmoveblk;
                moveblkptr = moveblk + moveblksize;
            }
        }

        int snpr; /* return value of printf calls */

        if (i % 2 == 0) {
            snpr = snprintf(moveblkptr, 16, "%d.\x1f%s",
                1+i/2, gamestate->moves[i].string);
        } else {
            snpr = snprintf(moveblkptr, 16, "%s",
                gamestate->moves[i].string);
        }
        moveblkptr += snpr;

        if (!export_comments) {
            *(moveblkptr++) = ' ';
            continue;
        }

        /* add clock times when the game was under time control */
        if (gamestate->info.timecontrol) {
            memcpy(moveblkptr, " {[%clk ", 8);
            moveblkptr += 8;
            unsigned clkmv = i + 2; /* time for the next move! */
            uint16_t clk = remaining_movetime2(gamestate, clkmv);
            snpr = print_clk(clk, moveblkptr, true);
            moveblkptr += snpr;
            *(moveblkptr++) = ']';
            *(moveblkptr++) = '}';
            
            /* elapsed move time */
            memcpy(moveblkptr, " {[%emt ", 8);
            moveblkptr += 8;
            uint16_t emt = gamestate->moves[i].movetime.tv_sec;
            snpr = print_clk(emt, moveblkptr, true);
            moveblkptr += snpr;
            *(moveblkptr++) = ']';
            *(moveblkptr++) = '}';
        }
        
        *(moveblkptr++) = ' ';
    }
    
    if (result[0] != '*') {
        size_t rlen = strlen(result);
        memcpy(moveblkptr, result, rlen);
        moveblkptr += rlen;
    }
    *(moveblkptr++) = '\n';
    *moveblkptr = 0;

    pgn_insert_newlines(moveblk);
    fputs(moveblk, stream);

    free(moveblk);
}
