const std = @import("std");
const Linenoise = @import("linenoize").Linenoise;
const Scanner = @import("./Scanner.zig");
const Token = @import("./Token.zig");
const Error = @import("./Error.zig");

const readline = @cImport({
    @cInclude("stdio.h");
    @cInclude("stdlib.h");

    @cInclude("readline/readline.h");
    @cInclude("readline/history.h");
});

pub fn main() !u8 {
    var gen_alloc = std.heap.GeneralPurposeAllocator(.{
        .stack_trace_frames = 128,
    }){};
    defer std.debug.assert(!gen_alloc.deinit());

    const gpa = gen_alloc.allocator();
    const args = try std.process.argsAlloc(gpa);
    defer std.process.argsFree(gpa, args);

    switch (args.len) {
        1 => {
            try runPrompt(gpa);
        },
        2 => {
            return runFile(gpa, args[1]);
        },
        else => {
            std.debug.print("Usage: jlox [script]\n", .{});
        },
    }
    return 0;
}

// No Mutex shenanigans, as there is no async yet.
// var stdout_mutex: std.event.Lock = .{};
pub fn print(comptime fmt: []const u8, args: anytype) !void {
    var bw = std.io.bufferedWriter(std.io.getStdOut().writer());
    const stdout = bw.writer();
    stdout.print(fmt, args) catch return;
    try bw.flush();
}

pub fn readLine(allocator: std.mem.Allocator) !?[]const u8 {
    var br = std.io.bufferedReader(std.io.getStdIn().reader());
    const stdin = br.reader();
    return stdin.readUntilDelimiterOrEofAlloc(allocator, '\n', std.math.maxInt(u8));
}

fn init_readline() !void {
    readline.rl_completion_entry_function = &lox_completion;
    // if (readline.rl_bind_key('\t', readline.rl_insert) != 0) {
    //     return error.ReadlineInitError;
    // }
}

fn lox_completion(text: [*c]const u8, state: c_int) callconv(.C) [*c]u8 {
    const alloc = std.heap.c_allocator;
    const textptr = @ptrCast(?[*:0]const u8, text);
    if (textptr) |curr| {
        var slice = lox_completion_internal(alloc, std.mem.span(curr), state) catch return null;
        if (slice) |comp| {
            // Assume that C will properly deallocate our given pointers?
            var ret = alloc.allocSentinel(u8, comp.len, 0) catch @panic("OOM");
            std.mem.copy(u8, ret, comp);
            return ret;
        }
        return null;
    } else {
        // Could not cast Readline text to proper pointer, so it's unsafe, so exit.
        return null;
    }
}

const CompletionDbGoal = enum {
    // General
    Name,
    Value,

    // Syntax
    Equal,
};

const CompletionDbEntry = struct {
    token: Token.Type,
    goal: CompletionDbGoal,
};

const completion_db = [_]CompletionDbEntry{
    .{ .token = .kw_var, .goal = .Name }, // recent names that have been used
    .{ .token = .identifier, .goal = .Equal },
    .{ .token = .equal, .goal = .Value }, // recent values that have been used
};

var completion_db_index: usize = 0;
fn lox_completion_internal(allocator: std.mem.Allocator, text: []const u8, state: c_int) !?[]const u8 {
    // So we are only given a single
    // std.log.debug("{d}", .{state});
    if (state == 0) {
        completion_db_index = 0;
    }

    // Lex the text, then check the last token
    // Can't init Scanner? No completion.
    var scanner = Scanner.init(allocator, text) catch return null;
    defer scanner.deinit();

    Error.can_output = false;
    defer Error.can_output = true;
    // Error in scanning tokens? No completion.
    const tokens = scanner.scanTokens() catch return null;
    defer {
        for (tokens) |token| {
            allocator.free(token.lexeme);
        }
        allocator.free(tokens);
    }
    const lastToken = tokens[tokens.len -| 2]; // Last Token before EOF

    var output = std.ArrayList(u8).init(allocator);
    defer output.deinit();
    const writer = output.writer();
    while (completion_db_index < completion_db.len) : (completion_db_index += 1) {
        const entry = completion_db[completion_db_index];
        if (lastToken.token_type == entry.token) {
            completion_db_index += 1; // Bump so that next call doesn't find us.
            switch (entry.goal) {
                .Name => @panic("TODO get identifiers in scope?"),
                .Equal => try writer.print("{s} =", .{text}),
                .Value => @panic("TODO get values that have been used"),
            }

            return try output.toOwnedSlice();
        }
    }
    return null;
}

var line_read: ?[*:0]u8 = null;
fn rl_gets(prompt: []const u8) !?[]const u8 {
    if (line_read) |_| {
        std.c.free(@ptrCast(?*anyopaque, line_read));
        line_read = null;
    }
    line_read = readline.readline(@ptrCast([*c]const u8, prompt));

    if (line_read) |read_line| {
        readline.add_history(read_line);
        return std.mem.span(read_line);
    }
    return null;
}

// TODO Make environment static, so that completion can access
fn runPrompt(allocator: std.mem.Allocator) !void {
    try init_readline();
    while (true) {
        // Quit on errors
        var line = try rl_gets("> ");
        // Handle EOF gracefully, and dealloc each line when done.
        // TODO Maybe store lines if they don't form a full statement, until they do.
        if (line) |uline| {
            if (uline.len == 0) {
                break;
            }
            try run(allocator, uline);
        } else {
            break;
        }
    }
}

fn runFile(allocator: std.mem.Allocator, file_name: [:0]u8) !u8 {
    std.debug.print("Running file {s}\n", .{file_name});
    const cwd = std.fs.cwd();
    const file = try cwd.openFileZ(file_name, .{ .mode = .read_only });
    const contents = try file.readToEndAlloc(allocator, std.math.maxInt(u32));
    defer allocator.free(contents);
    try run(allocator, contents);

    if (Error.errored) {
        return 65;
    }
    return 0;
}

fn run(allocator: std.mem.Allocator, source: []const u8) !void {
    // std.debug.print("{s}\n", .{source});

    var scanner = try Scanner.init(allocator, source);
    defer scanner.deinit();

    const tokens = try scanner.scanTokens();
    defer {
        for (tokens) |token| {
            allocator.free(token.lexeme);
        }
        allocator.free(tokens);
    }
    for (tokens) |token| {
        std.debug.print("[line {d}] {any}\n", .{ token.line, token });
    }
}

// test "simple test" {
//     var list = std.ArrayList(i32).init(std.testing.allocator);
//     defer list.deinit(); // try commenting this out and see if zig detects the memory leak!
//     try list.append(42);
//     try std.testing.expectEqual(@as(i32, 42), list.pop());
// }