Chess engine in zig
const std = @import("std");
const Str = []const u8;

const SIZE = @import("Board.zig").SIZE;
const BitMove = @import("Board.zig").BitMove;
const GameState = @import("Board.zig").GameState;
const MoveList = @import("Board.zig").MoveList;
const Search = @import("Search.zig").Search;
const SearchOptions = @import("Search.zig").SearchOptions;

var log_level = std.log.default_level;

const MIN_HASH_SIZE = 4;
const MAX_HASH_SIZE = 256;

pub const EngineOptions = struct {
    hash_size: usize = 128,
};

// https://backscattering.de/chess/uci/
pub fn loop(allocator: std.mem.Allocator) !void {
    var gs = try GameState.init(allocator, null);
    defer gs.deinit();

    var search: ?Search = null;
    defer {
        if (search != null) {
            search.?.deinit();
        }
    }

    var buf: [4 * 1024]u8 = undefined;
    const stdin = std.io.getStdIn().reader();
    const stdout = std.io.getStdOut();

    std.log.info("UCI engine started...", .{});

    var engine_options = EngineOptions{};

    while (true) {
        const input = try stdin.readUntilDelimiterOrEof(&buf, '\n') orelse break;

        if (std.mem.eql(u8, input, "quit")) {
            std.log.debug("received UCI quit, quitting...", .{});
            break;
        } else if (std.mem.eql(u8, input, "debug on")) {
            log_level = std.log.Level.debug;
            std.log.debug("log level set to debug", .{});
        } else if (std.mem.eql(u8, input, "debug off")) {
            log_level = std.log.default_level;
        } else if (std.mem.eql(u8, input, "stop")) {
            std.log.debug("received UCI stop command", .{});
            if (search != null) {
                @atomicStore(bool, &search.?.stop, true, std.builtin.AtomicOrder.Unordered);
                std.log.debug("searching stopped", .{});
            }
        } else if (std.mem.eql(u8, input, "isready")) {
            if (search == null) {
                search = Search.init(&gs, engine_options) catch {
                    std.log.err("Unable to initialize Search interface, quitting...", .{});
                    std.os.exit(1);
                };
            }
            _ = try stdout.write("readyok\n");
        } else {
            if (input.len >= 9) {
                if (std.mem.eql(u8, input[0..9], "setoption")) {
                    // setoption name Hash value <val>
                    if (std.mem.containsAtLeast(u8, input, 1, "Hash")) {
                        const start = std.mem.lastIndexOfLinear(u8, input, " ") orelse continue;
                        engine_options.hash_size =
                            std.fmt.parseUnsigned(usize, input[start + 1 ..], 10) catch {
                            std.log.debug("unable to parse Hash size: `{s}`", .{input[start..]});
                            continue;
                        };
                        if (engine_options.hash_size < MIN_HASH_SIZE) {
                            engine_options.hash_size = MIN_HASH_SIZE;
                        } else if (engine_options.hash_size > MAX_HASH_SIZE) {
                            engine_options.hash_size = MAX_HASH_SIZE;
                        }
                        std.log.debug("Hash size set to {d}", .{engine_options.hash_size});
                    }
                }
            }

            if (input.len >= 8) {
                if (std.mem.eql(u8, input[0..8], "position")) {
                    try parsePosition(&gs, input);
                    gs.show();
                }
            }

            if (input.len >= 3) {
                if (std.mem.eql(u8, input[0..3], "uci")) {
                    if (std.mem.eql(u8, input, "ucinewgame")) {
                        std.log.debug("initializing new uci game", .{});
                        if (search != null) {
                            std.log.debug("clearing transposition table", .{});
                            search.?.tt.clear();
                        }
                        std.log.debug("empty moves history", .{});
                        gs.history.clearRetainingCapacity();
                        try parsePosition(&gs, "position startpos");
                        std.log.debug("new uci game initialized", .{});
                    } else {
                        try hello();
                    }
                }
            }

            if (input.len >= 2) {
                if (std.mem.eql(u8, input[0..2], "go")) {
                    if (search == null) {
                        std.log.err("search is not initialized, call isready first!", .{});
                        continue;
                    }

                    search.?.stop = false;
                    search.?.timer = try std.time.Timer.start();

                    const srch = try std.Thread.spawn(
                        std.Thread.SpawnConfig{},
                        parseGo,
                        .{ &search.?, input },
                    );
                    srch.detach();
                }
            }
        }
    }
}

fn hello() !void {
    const stdout = std.io.getStdOut();
    _ = try stdout.write("id name pistike\n");
    _ = try stdout.write("id author voroskoi\n");
    try std.fmt.format(
        stdout.writer(),
        "option name Hash type spin default 64 min {d} max {d}\n",
        .{ MIN_HASH_SIZE, MAX_HASH_SIZE },
    );
    _ = try stdout.write("uciok\n");
}

pub fn parseGo(search: *Search, in: Str) !void {
    var options: SearchOptions = .{};

    // INFINITE
    if (std.mem.containsAtLeast(u8, in, 1, "infinite")) {
        options.depth = std.math.maxInt(usize);
    }

    inline for ([_]Str{ "depth", "movetime", "nodes", "movestogo", "wtime", "btime", "winc", "binc" }) |keyword| {
        const start = std.mem.indexOf(u8, in, keyword);
        if (start != null) {
            const pos = keyword.len + 1 + start.?;
            const end = std.mem.indexOfPos(u8, in, pos, " ") orelse in.len;
            const value = try std.fmt.parseUnsigned(usize, in[pos..end], 10);
            @field(options, keyword) = value;
        }
    }

    std.log.debug("{any}", .{options});
    try search.bestMove(options);
}

pub fn parsePosition(gs: *GameState, in: Str) !void {
    var words = std.mem.tokenize(u8, in, " ");
    if (words.next()) |position| {
        if (position[0] != 'p') {
            return error.InvalidPosition;
        }
    } else return error.InvalidPosition;

    if (words.next()) |start| {
        switch (start[0]) {
            's' => {
                // startpos
                try gs.parseFEN("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1");
            },
            'f' => {
                // fen
                const end = std.mem.indexOf(u8, in, "moves");
                if (end) |e| {
                    try gs.parseFEN(in[13 .. e - 1]);
                    for ([_]u3{ 0, 1, 2, 3, 4, 5 }) |_| {
                        _ = words.next();
                    }
                } else {
                    try gs.parseFEN(in[13..]);
                    return;
                }
            },
            else => return error.InvalidPosition,
        }
    } else return error.InvalidPosition;

    if (words.next()) |moves| {
        if (moves[0] != 'm') return error.InvalidPosition;
        while (words.next()) |move| {
            const m = try parseMove(gs.*, move);
            _ = gs.makeMove(m, .all);
            try gs.history.put(gs.hash, {});
        }
    }
}

fn parseMove(gs: GameState, in: Str) !BitMove {
    var move: BitMove = .{ .source = undefined, .target = undefined, .piece = undefined };

    move.source = @as(Square, @enumFromInt((in[0] - 'a' + (8 - (in[1] - '0')) * SIZE)));
    move.target = @as(Square, @enumFromInt((in[2] - 'a' + (8 - (in[3] - '0')) * SIZE)));

    var ml = try MoveList.init(0);
    try gs.generateMoves(&ml);

    for (ml.slice()) |mp| {
        if (move.source == mp.move.source and move.target == mp.move.target) {
            move = mp.move;

            // if there is a promotion, check if it is the right one
            if (move.prom != .none) {
                switch (move.prom) {
                    .N, .n => if (in[4] != 'n') continue,
                    .B, .b => if (in[4] != 'b') continue,
                    .R, .r => if (in[4] != 'r') continue,
                    .Q, .q => if (in[4] != 'q') continue,
                    else => unreachable,
                }
            }

            return move;
        }
    }

    std.log.debug("Illegal move found: \n\texpected: {any}\n\tgot: {s}\n", .{ move, in });
    return error.IllegalMove;
}

const Square = @import("Board.zig").Square;
const Chess = @import("Chess.zig");

test "UCI - parseMove" {
    var gs = try GameState.init(
        std.testing.allocator,
        "rnbq1k1r/pp1Pbppp/2p5/8/2B5/8/PPP1NnPP/RNBQK2R w KQ - 1 8",
    );
    defer gs.deinit();

    {
        // gs.show();

        const got = try parseMove(gs, "d7c8b");

        try std.testing.expectEqual(Square.d7, got.source);
        try std.testing.expectEqual(Square.c8, got.target);
        try std.testing.expectEqual(Chess.PE.B, got.prom);
        try std.testing.expectEqual(true, got.capture);
        try std.testing.expectEqual(Chess.PE.P, got.piece);

        // _ = gs.makeMove(got, .all);

        // gs.show();
    }

    {
        // gs.show();
        const got = parseMove(gs, "d7d8");
        try std.testing.expectError(error.IllegalMove, got);
    }
}

test "UCI - parsePosition" {
    var gs = try GameState.init(std.testing.allocator, null);
    defer gs.deinit();

    {
        try parsePosition(&gs, "position startpos");
        // gs.show();
    }

    {
        try parsePosition(&gs, "position startpos moves e2e4 e7e5");
        // gs.show();
    }

    {
        try parsePosition(&gs, "position fen 8/2p5/3p4/KP5r/1R3p1k/8/4P1P1/8 w - -");
        // gs.show();
    }

    {
        try parsePosition(&gs, "position fen 8/2p5/3p4/KP5r/1R3p1k/8/4P1P1/8 w - - 0 1 moves g2g4 c7c5");
        // gs.show();
    }
}