/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 *
 * Copyright 2026 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 "fen.h"

#include <stdlib.h>
#include <stdio.h>
#include <string.h>

static size_t fen_pieces(char *str, const GameState *gamestate) {
    size_t i = 0;
    Rank rank = 7;
    do {
        unsigned int skip = 0;
        for (File file = 0 ; file < 8 ; file++) {
            if (gamestate->board[rank][file]) {
                if (skip > 0) {
                    str[i++] = '0'+skip;
                    skip = 0;
                }
                switch (piece_at(gamestate, file, rank)) {
                case WKING: str[i++] = 'K'; break;
                case WQUEEN: str[i++] = 'Q'; break;
                case WBISHOP: str[i++] = 'B'; break;
                case WKNIGHT: str[i++] = 'N'; break;
                case WROOK: str[i++] = 'R'; break;
                case WPAWN: str[i++] = 'P'; break;
                case BKING: str[i++] = 'k'; break;
                case BQUEEN: str[i++] = 'q'; break;
                case BBISHOP: str[i++] = 'b'; break;
                case BKNIGHT: str[i++] = 'n'; break;
                case BROOK: str[i++] = 'r'; break;
                case BPAWN: str[i++] = 'p'; break;
                }
            } else {
                skip++;
            }
        }
        if (skip > 0) {
            str[i++] = '0'+skip;
        }
        if (rank > 0) {
            str[i++] = '/';
        }
    } while (rank-- > 0);

    return i;
}

static size_t fen_color(char *str, const GameState *gamestate) {
    str[0] = current_color(gamestate) == WHITE ? 'w' : 'b';
    return 1;
}

static size_t fen_castling(char *str, const GameState *gamestate) {
    size_t i = 0;
    if (!gamestate->castling.K) str[i++] = 'K';
    if (!gamestate->castling.Q) str[i++] = 'Q';
    if (!gamestate->castling.k) str[i++] = 'k';
    if (!gamestate->castling.q) str[i++] = 'q';
    if (!i) str[i++] = '-';

    return i;
}

static size_t fen_enpassant(char *str, const GameState *gamestate) {

    str[0] = '-';

    for (int file = 0 ; file < 8 ; file++) {
        if (enpassant_threat_exists(gamestate, file, 3)) {
            str[0] = filechr(file);
            str[1] = rankchr(2);
        }
        if (enpassant_threat_exists(gamestate, file, 4)) {
            str[0] = filechr(file);
            str[1] = rankchr(5);
        }
    }

    return str[0] == '-' ? 1 : 2;
}

static size_t fen_halfmove(char *str, const GameState *gamestate) {
    // TODO: respect a possible fifty_ctr_start
    unsigned int hm = 0;
    for (unsigned int i = 0; i < gamestate->movecount; i++) {
        if (gamestate->moves[i].capture
            || piece_type(gamestate->moves[i].piece) == PAWN) {
            hm = 0;
        } else {
            hm++;
        }
    }

    return sprintf(str, "%u", hm);
}

static size_t fen_movenr(char *str, const GameState *gamestate) {
    unsigned mc = gamestate->movecount + gamestate->move_start;
    return sprintf(str, "%u", 1 + mc / 2);
}

static size_t fen_space(char *str) {
    *str = ' ';
    return 1;
}

void fen_compute(char *str, const GameState *gamestate) {
    str += fen_pieces(str, gamestate);
    str += fen_space(str);
    str += fen_color(str, gamestate);
    str += fen_space(str);
    str += fen_castling(str, gamestate);
    str += fen_space(str);
    str += fen_enpassant(str, gamestate);
    str += fen_space(str);
    str += fen_halfmove(str, gamestate);
    str += fen_space(str);
    str += fen_movenr(str, gamestate);
    *str = '\0';
}

static unsigned fen_parse_number(const char *str, unsigned *target) {
    unsigned l = 0;
    *target = 0;
    while (str[l] >= '0' && str[l] <= '9') {
        unsigned n = str[l] - '0';
        *target *= 10;
        *target += n;
        l++;
    }
    /* safety precaution - reject unreasonable high numbers */
    if (l > 5) return 0;
    return l;
}

int fen_parse(const char *str, GameState *gamestate) {
    const char * const fen_start = str;
    // TODO: think about error reporting that is as good as for PGNs

    if (str == NULL) return 1;

    /* zero-initialize the game state */
    memset(gamestate, 0,  sizeof(GameState));

    /* parse the board (FEN starts top-left at "a8") */
    Rank r = 7;
    File f = 0;
    while (true) {
        switch (*str) {
        case 'K': gamestate->board[r][f] = WKING; break;
        case 'Q': gamestate->board[r][f] = WQUEEN; break;
        case 'B': gamestate->board[r][f] = WBISHOP; break;
        case 'N': gamestate->board[r][f] = WKNIGHT; break;
        case 'R': gamestate->board[r][f] = WROOK; break;
        case 'P': gamestate->board[r][f] = WPAWN; break;
        case 'k': gamestate->board[r][f] = BKING; break;
        case 'q': gamestate->board[r][f] = BQUEEN; break;
        case 'b': gamestate->board[r][f] = BBISHOP; break;
        case 'n': gamestate->board[r][f] = BKNIGHT; break;
        case 'r': gamestate->board[r][f] = BROOK; break;
        case 'p': gamestate->board[r][f] = BPAWN; break;
        case '1': break;
        case '2': f += 1; break;
        case '3': f += 2; break;
        case '4': f += 3; break;
        case '5': f += 4; break;
        case '6': f += 5; break;
        case '7': f += 6; break;
        case '8': f += 7; break;
        default: return 1;
        }
        f++;
        str++;
        if (f == 8) {
            /* rank complete - test for separator or ending space */
            if (r > 0) {
                if (*str != '/') return 1;
                str++;
                f = 0;
                r--;
            } else {
                if (*str != ' ') return 1;
                str++;
                break;
            }
        }
    }

    /* whose turn is it? */
    bool white_to_move;
    if (str[0] == 'w') {
        white_to_move = true;
    } else if (str[0] == 'b') {
        white_to_move = false;
    } else {
        return 1;
    }
    if (str[1] != ' ') return 1;
    str += 2;

    /* castling rights */
    gamestate->castling.K = gamestate->castling.Q = true;
    gamestate->castling.k = gamestate->castling.q = true;
    if (*str == '-') {
        str++;
    } else {
        char cstl[5] = "KQkq";
        bool found = false;
        for (unsigned i = 0 ; i < 4 ; i++) {
            if (*str == cstl[i]) {
                found = true;
                switch (i) {
                case 0: gamestate->castling.K = false; break;
                case 1: gamestate->castling.Q = false; break;
                case 2: gamestate->castling.k = false; break;
                case 3: gamestate->castling.q = false; break;
                }
                str++;
            }
        }
        if (!found) return 1; /* no castling info found */
    }
    if (*str != ' ') return 1;
    str++;

    /* is there an en-passant threat? */
    if (*str == '-') {
        str++;
    } else {
        if (isfile(str[0]) && isrank(str[1])) {
            f = fileidx(str[0]);
            r = rankidx(str[1]);
            if (r == 2) {
                r = 3;
            } else if (r == 5) {
                r = 4;
            } else {
                return 1;
            }
            /* the threat is applied to the pawn, not the field it passed */
            enpassant_threat_add(gamestate, f, r);
        } else {
            return 1;
        }
    }
    if (*str != ' ') return 1;
    str++;

    /* fifty-moves counter */
    unsigned mnr;
    unsigned mlen;
    mlen = fen_parse_number(str, &mnr);
    if (mlen == 0) return 1;
    if (str[mlen] != ' ') return 1;
    str += mlen+1;
    gamestate->fifty_cntr_start = mnr;

    /* move number */
    mlen = fen_parse_number(str, &mnr);
    if (mlen == 0) return 1;
    if (mnr == 0) return 1;
    if (str[mlen] != '\0') return 1;
    str += mlen+1;
    gamestate->move_start = 2*mnr - 1;
    if (white_to_move) gamestate->move_start--;

    /* only copy the fen string if everything is a success */
    gamestate->fen_start = strdup(fen_start);
    return 0;
}