/* Copyright 2018 Romain "Artefact2" Dal Maso <artefact2@gmail.com>
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <assert.h>
#include <time.h>
#include <pthread.h>
#include "gumble.h"

typedef struct {
	cch_board_t b;
	cch_hashtable_t* tt;
	cch_search_context_t sctx;
} searcher_context_t;

static const char delim[] = " \t\n";
static bool searcher_running = false;
static cch_board_t b;
static cch_hashtable_t tt;
static bool has_tt = false;
static pthread_t searcher;
static pthread_attr_t attr;
static searcher_context_t ctx;



static void* search(void*);
static void go(unsigned int);
static void stop();



static void* search(void* arg) {
	unsigned int tnodes = 0;
	char alg[SAFE_ALG_LENGTH];
	char score[16];
	bool mate = false;
	searcher_context_t* ctx = (searcher_context_t*)arg;
	cch_search_context_t sctx;
	int oldstate;
	clock_t t1, t2, start = clock();

	cch_init_search_context(&sctx, &(ctx->b), ctx->tt);

	for(unsigned char depth = 1; depth < 255; ++depth) {
		t1 = clock();
		cch_search_best_move(depth, &sctx);
		t2 = clock();
		tnodes += sctx.nodes;

		pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &oldstate);
		ctx->sctx = sctx;
		pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, &oldstate);

		if(sctx.eval < CCH_CHECKMATE_SCORE && sctx.eval > -CCH_CHECKMATE_SCORE) {
			snprintf(score, 16, "cp %d", sctx.eval / 10);
		} else if(!mate) {
			int sign = sctx.eval / CCH_CHECKMATE_SCORE;
			snprintf(score, 16, "mate %d", sign * (depth + 1) / 2);
			mate = true;
		}
		printf(
			"info depth %u score %s time %lu nodes %u nps %lu pv",
			depth,
			score,
			1000 * (clock() - start) / CLOCKS_PER_SEC,
			tnodes,
			CLOCKS_PER_SEC * sctx.nodes / (t2 - t1));
		assert(sctx.pv_len <= depth);
		for(unsigned char pvi = 0; pvi < sctx.pv_len; ++pvi) {
			cch_format_lan_move(&(sctx.pv[pvi]), alg, SAFE_ALG_LENGTH);
			printf(" %s", alg);
		}
		putchar('\n');
		fflush(stdout); /* XXX: do this in main thread */
	}

	return arg;
}

static void go(unsigned int msecs) {
	stop();

	/* XXX: the thread is usually cancelled mid-search, leaving the
	 * board in a cluttered state where moves aren't undone; it's
	 * easier to have the thread work on its own copy of it */
	ctx.b = b;

	if(!has_tt) {
		/* XXX: tweak TT size as UCI option */
		cch_hashtable_init(&tt, 26, sizeof(cch_transposition_entry_t));
		has_tt = true;
	}

	cch_return_t ret = pthread_create(&searcher, &attr, search, &ctx);
	assert(!ret);
	searcher_running = true;

	if(!msecs) return;
	struct timespec t;
	t.tv_sec = msecs / 1000;
	t.tv_nsec = (msecs % 1000) * 1000000;
	while(nanosleep(&t, &t)); /* XXX */
	stop();
}

static void stop(void) {
	if(!searcher_running) return;
	cch_return_t ret;
	char alg[SAFE_ALG_LENGTH];

	ret = pthread_cancel(searcher);
	assert(!ret);

	ret = pthread_join(searcher, 0);
	assert(!ret);
	searcher_running = false;

	cch_format_lan_move(&(ctx.sctx.pv[0]), alg, SAFE_ALG_LENGTH);
	printf("bestmove %s\n", alg);
	fflush(stdout);
}

int main(void) {
	char* line = 0;
	size_t n;
	char* tok;
	int ret;

	ret = pthread_attr_init(&attr);
	assert(!ret);
	cch_init_board(&b);
	ctx.tt = &tt;

	while(getline(&line, &n, stdin) != -1) {
		tok = strtok(line, delim);

		if(!strcmp("uci", tok)) {
			puts("id name Gumble " GIT_VERSION);
			puts("id author Romain \"Artefact2\" Dal Maso");
			puts("uciok");
			fflush(stdout);
		} else if(!strcmp("isready", tok)) {
			puts("readyok");
			fflush(stdout);
		} else if(!strcmp("ucinewgame", tok)) {
			cch_init_board(&b);
		} else if(!strcmp("position", tok)) {
			char* arg = strtok(0, delim);
			if(!strcmp("startpos", arg)) {
				cch_init_board(&b);
			} else {
				assert(!strcmp("fen", arg));
				char fen[SAFE_FEN_LENGTH];
				/* XXX: find prettier way to do this */
				snprintf(fen, SAFE_FEN_LENGTH, "%s %s %s %s %s %s",
						 strtok(0, delim), strtok(0, delim), strtok(0, delim),
						 strtok(0, delim), strtok(0, delim), strtok(0, delim));
				cch_return_t ret = cch_load_fen(&b, fen);
				assert(ret == CCH_OK);
			}

			arg = strtok(0, delim);
			if(arg) {
				assert(!strcmp("moves", arg));
				cch_move_t m;
				cch_return_t ret;
				while((arg = strtok(0, delim))) {
					ret = cch_parse_lan_move(arg, &m);
					assert(ret == CCH_OK);
					assert(cch_is_move_legal(&b, &m));
					cch_play_legal_move(&b, &m, 0);
				}
			}
		} else if(!strcmp("go", tok)) {
			unsigned int msecs = 0, time = 0, inc = 0;

			while((tok = strtok(0, delim))) {
				if(!strcmp("infinite", tok)) {
					msecs = 0;
				} else if(!strcmp("movetime", tok)) {
					tok = strtok(0, delim);
					assert(tok);
					msecs = strtoul(tok, 0, 10);
				} else if(!strcmp("wtime", tok)) {
					tok = strtok(0, delim);
					assert(tok);
					if(b.side) time = strtoul(tok, 0, 10);
				} else if(!strcmp("btime", tok)) {
					tok = strtok(0, delim);
					assert(tok);
					if(!b.side) time = strtoul(tok, 0, 10);
				} else if(!strcmp("winc", tok)) {
					tok = strtok(0, delim);
					assert(tok);
					if(b.side) inc = strtoul(tok, 0, 10);
				} else if(!strcmp("binc", tok)) {
					tok = strtok(0, delim);
					assert(tok);
					if(!b.side) inc = strtoul(tok, 0, 10);
				} else {
					assert(0); /* Unsupported and potentially game-breaking */
				}
			}

			if(!msecs && (time || inc)) {
				/* XXX: this is very crude and flawed */
				if(time > inc) {
					msecs = inc + 2 * (time - inc) / 100;
				} else msecs = time;
			}

			go(msecs);
		} else if(!strcmp("stop", tok)) {
			stop();
			assert(!strtok(0, delim));
		} else if(!strcmp("exit", tok) || !strcmp("quit", tok)) {
			stop();
			assert(!strtok(0, delim));
			break;
		} else if(!strcmp("fen", tok)) {
			assert(!strtok(0, delim));
			char fen[SAFE_FEN_LENGTH];
			cch_return_t ret = cch_save_fen(&b, fen, SAFE_FEN_LENGTH);
			assert(ret == CCH_OK);
			printf("info fen %s\n", fen);
			fflush(stdout);
		} else if(!strcmp("lan", tok)) {
			tok = strtok(0, delim);
			assert(tok);
			assert(!strtok(0, delim));
			cch_move_t m;
			cch_return_t ret = cch_parse_san_move(&b, tok, &m);
			assert(ret == CCH_OK);
			char move[SAFE_ALG_LENGTH];
			ret = cch_format_lan_move(&m, move, SAFE_ALG_LENGTH);
			assert(ret == CCH_OK);
			printf("info lan %s\n", move);
			fflush(stdout);
		} else if(!strcmp("san", tok)) {
			tok = strtok(0, delim);
			assert(tok);
			assert(!strtok(0, delim));
			cch_move_t m;
			cch_return_t ret = cch_parse_lan_move(tok, &m);
			assert(ret == CCH_OK);
			char move[SAFE_ALG_LENGTH];
			ret = cch_format_san_move(&b, &m, move, SAFE_ALG_LENGTH, true);
			assert(ret == CCH_OK);
			printf("info san %s\n", move);
			fflush(stdout);
		} else if(!strcmp("moves", tok)) {
			assert(!strtok(0, delim));
			cch_movelist_t ml;
			unsigned char stop = cch_generate_moves(&b, ml, CCH_LEGAL, 0, 64);
			char move[SAFE_ALG_LENGTH];
			fputs("info moves", stdout);
			for(unsigned char i = 0; i < stop; ++i) {
				cch_format_lan_move(&ml[i], move, SAFE_ALG_LENGTH);
				putchar(' ');
				fputs(move, stdout);
			}
			putchar('\n');
			fflush(stdout);
		} else {
			fprintf(stderr, "Unknown command: %s\n", tok);
			continue;
		}
	}

	if(has_tt) cch_hashtable_free(&tt);
	return 0;
}
