/*  DreamChess
**
**  DreamChess is the legal property of its developers, whose names are too
**  numerous to list here. Please refer to the COPYRIGHT file distributed
**  with this source distribution.
**
**  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.
**
**  You should have received a copy of the GNU General Public License
**  along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/

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

#include "makebook.h"
#include "board.h"
#include "move.h"
#include "commands.h"
#include "pgn_scanner.h"

typedef struct
{
    move_t move;
    int count;
} makebook_move_data;

typedef struct
{
    unsigned long long hash;
    makebook_move_data *move;
    int moves;
} makebook_entry;

static makebook_entry *table;
static int table_entries;
static int table_size;

static board_t board;

static int moves_done;

static unsigned short move_to_short(move_t move)
{
    unsigned short m;

    m = MOVE_GET(move, DEST) | (MOVE_GET(move, SOURCE) << 6);

    switch (MOVE_GET(move, TYPE) & MOVE_PROMOTION_MASK)
    {
    case PROMOTION_MOVE_KNIGHT:
        m |= MAKEBOOK_KNIGHT << 12;
        break;
    case PROMOTION_MOVE_BISHOP:
        m |= MAKEBOOK_BISHOP << 12;
        break;
    case PROMOTION_MOVE_ROOK:
        m |= MAKEBOOK_ROOK << 12;
        break;
    case PROMOTION_MOVE_QUEEN:
        m |= MAKEBOOK_QUEEN << 12;
    }

    return m;
}

static int makebook_find(long long hash)
{
    int low = 0;
    int high = table_entries;

    while (low < high) {
        int mid = (low + high) / 2;
        if (table[mid].hash < hash)
            low = mid + 1;
        else
            high = mid;
    }

    return low;
}

static void makebook_add_move(makebook_entry *entry, move_t move)
{
    int i;

    for (i = 0; i < entry->moves; i++)
        if (entry->move[i].move == move)
        {
            entry->move[i].count++;
            return;
        }

    entry->move = realloc(entry->move, sizeof(makebook_move_data) * (entry->moves + 1));
    entry->move[entry->moves].move = move;
    entry->move[entry->moves++].count = 1;
}

static void makebook_add(unsigned long long hash, move_t move)
{
    int index = makebook_find(hash);

    if (table[index].hash != hash)
    {
        if (table_entries == table_size)
        {
            table_size *= 2;
            table = realloc(table, sizeof(makebook_entry) * table_size);

            if (!table)
            {
                fprintf(stderr, "Out of memory\n");
                exit(1);
            }
        }

        if (index < table_entries)
            memmove(&table[index + 1], &table[index], (table_entries - index) * sizeof(makebook_entry));

        table[index].hash = hash;
        table[index].move = NULL;
        table[index].moves = 0;
        table_entries++;
    }

    makebook_add_move(&table[index], move);
   
}

void makebook_reset(void)
{
    setup_board(&board);
    moves_done = 0;
}

void makebook_move(char *str)
{
    move_t move;

    if (!parse_move(&board, 0, str, &move))
    {
        if (moves_done++ < 20)
        {
            makebook_add(board.hash_key, move);
            execute_move(&board, move);
        }
    }
    else
    {
        fprintf(stderr, "move %s is invalid\n", str);
        exit(1);
    }
}

static int compare(const void *p1, const void *p2)
{
    if (((makebook_move_data *) p1)->count > ((makebook_move_data *) p2)->count)
        return -1;

    if (((makebook_move_data *) p1)->count < ((makebook_move_data *) p2)->count)
        return 1;

    return 0;
}

static void makebook_normalize_moves(makebook_entry *entry)
{
    int i;
    int max = 0;

    for (i = 0; i < entry->moves; i++)
        if (entry->move[i].count > max)
            max = entry->move[i].count;

    for (i = 0; i < entry->moves; i++)
        entry->move[i].count = entry->move[i].count * 255 / max;

    qsort(entry->move, entry->moves, sizeof(makebook_move_data), compare);

    for (i = 0; i < entry->moves; i++)
        if (entry->move[i].count == 0)
            break;

    entry->moves = i;
}

static void makebook_cleanup_moves(void)
{
    int i;

    for (i = 0; i < table_entries; i++)
    {
        makebook_normalize_moves(&table[i]);
    }
}

#if 0
static void makebook_print(long long hash)
{
    int index = makebook_find(hash);
    int i;

    if (table[index].hash != hash)
        return;

    for (i = 0; i < table[index].moves; i++)
    {
        char *s = coord_move_str(table[index].move[i].move);

        fprintf(stderr, "%s (%i)\n", s, table[index].move[i].count);
    }
}
#endif

static void write_uint8(FILE *f, unsigned char c)
{
    if (fwrite(&c, 1, 1, f) < 1)
    {
        fprintf(stderr, "Error writing to opening book\n");
        fclose(f);
        exit(1);
    }
}

static void write_uint16(FILE *f, unsigned short i)
{
    write_uint8(f, i >> 8);
    write_uint8(f, i);
}

static void write_uint32(FILE *f, unsigned int i)
{
    write_uint16(f, i >> 16);
    write_uint16(f, i);
}

static void write_uint64(FILE *f, unsigned long long ll)
{
    write_uint32(f, ll >> 32);
    write_uint32(f, ll);
}

static void makebook_write(char *file)
{
    FILE *f;
    int i;
    int offset;

    f = fopen(file, "wb+");

    if (!f)
    {
        fprintf(stderr, "Failed to open %s for writing\n", file);
        exit(1);
    }

    fprintf(f, "DCB 0000");

    /* Write number of entries */
    write_uint32(f, table_entries);

    offset = 12 + table_entries * 12;
    for (i = 0; i < table_entries; i++)
    {
        write_uint64(f, table[i].hash);
        write_uint32(f, offset);
        offset += table[i].moves * 3;
    }

    for (i = 0; i < table_entries; i++)
    {
        int j;
        for (j = 0; j < table[i].moves; j++)
        {
            unsigned short move = move_to_short(table[i].move[j].move);

            if (j == table[i].moves - 1)
                move |= MAKEBOOK_LAST;
            write_uint16(f, move);
            write_uint8(f, table[i].move[j].count);
        }
    }

    fclose(f);
}

void makebook(char *pgnfile, char *binfile)
{
    makebook_reset();
    table_size = 256;
    table_entries = 0;
    table = malloc(sizeof(makebook_entry) * table_size);

    pgn_parse_file(pgnfile);
    makebook_cleanup_moves();
    makebook_write(binfile);
    return;
}
