const std = @import("std");
const mem = std.mem;
const Language = @import("language.zig");
const Tokenizer = @import("tokenizer.zig");
const assert = std.debug.assert;

const Self = @This();

pub const Error = error{TypeError};

language: *Language,
tokenizer: *Tokenizer,

pub fn parse(self: *Self, allocator: mem.Allocator) !usize {
    return self.language.parse(allocator, self.tokenizer);
}

pub fn init(allocator: mem.Allocator, text: []const u8) !Self {
    const self = Self{
        .language = try allocator.create(Language),
        .tokenizer = try allocator.create(Tokenizer),
    };

    self.language.* = .init;
    self.tokenizer.* = try .init(allocator, text);
    return self;
}

pub fn deinit(self: *Self, allocator: mem.Allocator) void {
    self.language.deinit(allocator);
    self.tokenizer.deinit(allocator);
    allocator.destroy(self.language);
    allocator.destroy(self.tokenizer);
}

/// needs an index but useful otherwise
/// root starting from idx
pub fn reflectT(self: *Self, comptime T: type, allocator: mem.Allocator, idx: usize) !T {
    const Schema = @typeInfo(T);

    if (std.meta.hasFn(T, "toJson")) {
        return T.toJson(self, allocator, idx);
    }

    switch (self.language.index.get(idx)) {
        .null => {
            if (Schema == .null or Schema == .optional) return null;
        },
        .bool => |b| switch (Schema) {
            .bool => return b,
            .@"union" => |unionInfo| inline for (unionInfo.fields) |field| {
                var r: T = undefined;
                r = @unionInit(T, field.name, b);
                return r;
            },
            else => unreachable,
        },
        .number => |number| switch (Schema) {
            .int, .comptime_int => return @intCast(number.int),
            .float, .comptime_float => return switch (number) {
                .float => @floatCast(number.float),
                .int => @floatFromInt(number.int),
            },
            .@"enum" => |enumInfo| {
                const int: enumInfo.tag_type = @intCast(number.int);
                return @enumFromInt(int);
            },
            .@"struct" => |structInfo| switch (structInfo.layout) {
                .@"packed" => {
                    const int: structInfo.backing_integer.? = @intCast(number.int);
                    return @bitCast(int);
                },
                else => unreachable, // may only cast packed structs
            },
            .@"union" => |unionInfo| {
                inline for (unionInfo.fields) |field| switch (@typeInfo(field.type)) {
                    .int, .comptime_int => return @unionInit(T, field.name, @intCast(number.int)),
                    .float, .comptime_float => return @unionInit(T, field.name, @floatCast(number.float)),
                    else => {},
                };
                return error.TypeError;
            },
            else => unreachable,
        },
        .string => |string| switch (Schema) {
            .@"enum" => |enumInfo| {
                const strslice = string.slice(&self.language.strings);
                inline for (enumInfo.fields) |field| if (mem.eql(u8, field.name, strslice)) {
                    return std.meta.stringToEnum(T, strslice) orelse error.TypeError;
                };
            },
            .@"union" => |unionInfo| inline for (unionInfo.fields) |field| {
                if (field.type == []const u8) {
                    var r: T = undefined;
                    const strslice = string.slice(&self.language.strings);
                    r = @unionInit(T, field.name, strslice);
                    return r;
                }
            },
            .array => |arrayInfo| {
                assert(arrayInfo.child == u8);

                const strslice = string.slice(&self.language.strings);
                assert(arrayInfo.len == strslice.len - 1);

                var r: T = undefined;

                for (strslice, 0..) |char, i| r[i] = char;
                return r;
            },
            .pointer => |ptrInfo| switch (ptrInfo.size) {
                .slice => {
                    assert(ptrInfo.child == u8);
                    const strslice = string.slice(&self.language.strings);
                    var arraylist: std.ArrayList(u8) = .init(allocator);
                    try arraylist.ensureUnusedCapacity(strslice.len);

                    for (strslice) |char| if (char != 0x00)
                        arraylist.appendAssumeCapacity(char);

                    if (ptrInfo.sentinel_ptr) |some| {
                        const sentinel = @as(*align(1) const ptrInfo.child, @ptrCast(some)).*;
                        return try arraylist.toOwnedSliceSentinel(sentinel);
                    }

                    if (ptrInfo.is_const) {
                        arraylist.deinit();
                        return strslice;
                    } else {
                        arraylist.deinit();
                        const slice = try allocator.dupe(u8, strslice);
                        return @as(T, slice);
                    }
                    return try arraylist.toOwnedSlice();
                },
                .many => {
                    assert(ptrInfo.child == u8);
                    const strslice = string.slice(&self.language.strings);
                    var arraylist: std.ArrayList(u8) = .init(allocator);
                    try arraylist.ensureUnusedCapacity(strslice.len);

                    for (strslice) |char|
                        arraylist.appendAssumeCapacity(char);

                    if (ptrInfo.sentinel_ptr) |some| {
                        return arraylist.toOwnedSliceSentinel(blk: {
                            const sentinel: *align(1) const ptrInfo.child = @ptrCast(some);
                            break :blk sentinel.*;
                        });
                    }
                    return arraylist.toOwnedSlice();
                },
                else => unreachable,
            },
            else => unreachable,
        },
        .array => |slice| switch (Schema) {
            .@"struct" => |structInfo| {
                if (structInfo.is_tuple) {
                    assert(structInfo.fields.len == slice.len);

                    var r: T = undefined;
                    inline for (structInfo.fields, 0..slice.len) |field, i| {
                        if (field.is_comptime)
                            @panic(@typeName(T) ++ "." ++ field.name ++ " may not be a comptime field");

                        @field(r, field.name) = try self.reflectT(field.type, allocator, slice.tip + i);
                    }

                    return r;
                }
                unreachable; // may only reflect tuples
            },
            .array => |arrayInfo| {
                assert(slice.len == arrayInfo.len);
                var r: T = undefined;

                for (0..slice.len) |i|
                    r[i] = try self.reflectT(arrayInfo.child, allocator, slice.tip + i);
                return r;
            },
            .pointer => |ptrInfo| switch (ptrInfo.size) {
                .slice => {
                    var r: T = try allocator.alloc(ptrInfo.child, slice.len);

                    if (ptrInfo.sentinel_ptr) |some| {
                        r[slice.len - 1] = blk: {
                            const sentinel: *align(1) const ptrInfo.child = @ptrCast(some);
                            break :blk sentinel.*;
                        };
                    }

                    for (0..slice.len) |i| {
                        // weird hack to populate the string
                        const rptr = @constCast(r);
                        rptr[i] = try self.reflectT(ptrInfo.child, allocator, slice.tip + i);
                    }
                    return r;
                },
                else => unreachable,
            },
            else => unreachable,
        },
        .object => |object| switch (Schema) {
            .@"struct" => |structInfo| {
                if (structInfo.is_tuple) return error.TypeError;

                var tip = object.tip;

                var map: std.StringArrayHashMapUnmanaged(usize) = .empty;
                try map.ensureTotalCapacity(allocator, object.len);
                defer map.deinit(allocator);

                for (0..object.len) |_| if (self.language.property_map.get(tip)) |pen| {
                    const key = pen.tip.slice(&self.language.properties);
                    map.putAssumeCapacity(key, tip);
                    tip += self.language.skipSlots(tip);
                };

                var r: T = undefined;
                inline for (structInfo.fields) |field| {
                    if (field.is_comptime)
                        @panic(@typeName(T) ++ "." ++ field.name ++ " may not be a comptime field");

                    if (map.get(field.name)) |next_i| {
                        @field(r, field.name) = try self.reflectT(field.type, allocator, next_i);
                    } else switch (@typeInfo(field.type)) {
                        .optional => @field(r, field.name) = null,
                        else => @panic("Unknown property: " ++ field.name),
                    }
                }

                return r;
            },
            else => unreachable,
        },
    }

    unreachable;
}

test reflectT {
    const allocator = std.testing.allocator;

    const text =
        \\{
        \\  "age": 15,
        \\  "name": "Yuzu",
        \\  "admin": true,
        \\  "flags": 0,
        \\  "union": ":D",
        \\  "enum": "world",
        \\  "many": [1,2,3],
        \\  "tuple": [1, 2]
        \\}
    ;
    var self = try allocator.create(Self);
    self.* = try init(allocator, text);

    defer allocator.destroy(self);
    defer self.deinit(allocator);

    const idx: usize = try self.parse(allocator);

    const UserFlags = packed struct {
        is_cool: bool = false,
        is_friendly: bool = false,
    };

    const UserSchema = struct {
        age: f64,
        name: []const u8,
        admin: bool,
        flags: UserFlags,
        @"union": union { hi: bool, bye: f64, n128: []const u8 },
        @"enum": enum { hello, world },
        many: []const u8,
        tuple: struct { u64, u64 },
    };

    const root = try self.reflectT(UserSchema, allocator, idx);

    std.debug.print("hello? {s}\n", .{@tagName(root.@"enum")});
    std.debug.print("friend? {s}\n", .{root.@"union".n128});
    std.debug.print("many: {any}\n", .{root.many});
    std.debug.print("tuple: {any}\n", .{root.tuple});
    allocator.free(root.many);
}

pub const Options = struct {
    max_buffer_size: usize = 4096,
    ruleset: Ruleset = .{},
    indent: u8 = 0,
};

pub const Ruleset = packed struct {
    /// print 'packed struct' as a number
    allow_bitfields: bool = false,
    /// print 'enum' as a number
    allow_data_cast: bool = false,
    /// ignore comments
    allow_comments: bool = false,
    /// whether to print '*T' as int
    allow_derefptrs: bool = false,
    /// prettify the output
    pretty: bool = false,

    pub const Pedantic = Ruleset{};

    pub const Chill = Ruleset{
        .allow_bitfields = true,
        .allow_data_cast = true,
        .allow_comments = true,
        .allow_derefptrs = true,
    };
};

pub fn prettyStringify(raw: anytype, comptime options: Options) ![]const u8 {
    return stringify(raw, comptime blk: {
        var opts = options;
        opts.ruleset.pretty = true;
        break :blk opts;
    });
}

pub fn stringify(raw: anytype, comptime options: Options) ![]const u8 {
    if (options.indent > 0 and !options.ruleset.pretty)
        @compileError("Indentation is only allowed when pretty is enabled");

    var buf: [options.max_buffer_size]u8 = undefined;
    var fba: std.heap.FixedBufferAllocator = .init(&buf);
    const allocator = fba.allocator();

    if (std.meta.hasFn(@TypeOf(raw), "stringify")) {
        // If the type has a custom stringify function, use it.
        return raw.stringify(options);
    }

    switch (@typeInfo(@TypeOf(raw))) {
        .null => return "null",
        .bool => return if (raw) "true" else "false",
        .int, .comptime_int => return std.fmt.bufPrint(&buf, "{d}", .{raw}),
        .float, .comptime_float => return std.fmt.bufPrint(&buf, "{d:.1}", .{raw}),
        .optional => {
            if (raw) |value| {
                return stringify(value, options);
            } else {
                return "null";
            }
        },
        .@"enum" => |enumInfo| {
            if (options.ruleset.allow_data_cast) {
                const i: enumInfo.tag_type = @intFromEnum(raw);
                return std.fmt.bufPrint(&buf, "{d}", .{i});
            }
            return std.fmt.bufPrint(&buf, "{s}", .{@tagName(raw)});
        },
        .@"struct" => |structInfo| switch (structInfo.layout) {
            .@"packed" => {
                if (options.ruleset.allow_bitfields) {
                    const i: structInfo.backing_integer.? = @bitCast(raw);
                    return std.fmt.bufPrint(&buf, "{d}", .{i});
                }
                return error.TypeError;
            },
            .auto, .@"extern" => {
                var string: std.ArrayListUnmanaged(u8) = .empty;
                var writer = string.writer(allocator);
                try writer.writeByte('{');
                if (options.ruleset.pretty)
                    try writer.writeByte('\n');
                inline for (structInfo.fields, 0..) |field, i| {
                    inline for (0..options.indent) |_|
                        try writer.writeByte(' ');
                    try writer.writeByte('"');
                    try writer.writeAll(field.name);
                    try writer.writeByte('"');
                    if (options.ruleset.pretty)
                        try writer.writeAll(": ")
                    else
                        try writer.writeByte(':');
                    const value = @field(raw, field.name);
                    const str = try stringify(value, options);
                    try writer.writeAll(str);
                    if (i < structInfo.fields.len - 1)
                        try writer.writeByte(',');
                    if (options.ruleset.pretty)
                        try writer.writeByte('\n');
                }
                try writer.writeByte('}');
                return string.toOwnedSlice(allocator);
            },
        },
        .array => |arrayInfo| {
            var string: std.ArrayListUnmanaged(u8) = .empty;
            var writer = string.writer(allocator);
            try writer.writeByte('[');
            for (0..arrayInfo.len) |i| {
                if (i > 0) {
                    try writer.writeAll(',');
                    if (options.ruleset.pretty)
                        try writer.writeByte(' ');
                }
                const value = @field(raw, i);
                const str = try stringify(value, options);
                try writer.writeAll(str);
            }
            try writer.writeByte(']');
            return string.toOwnedSlice();
        },
        .pointer => |ptrInfo| switch (ptrInfo.size) {
            .one => {
                if (options.ruleset.allow_derefptrs) {
                    return stringify(blk: {
                        const ptr: *align(ptrInfo.alignment) const ptrInfo.child = @ptrCast(raw);
                        break :blk ptr.*;
                    }, options);
                }

                return std.fmt.bufPrint(&buf, "{d}", .{@intFromPtr(raw)});
            },
            .slice => {
                if (ptrInfo.child == u8) {
                    if (ptrInfo.is_const)
                        return std.fmt.bufPrint(&buf, "\"{s}\"", .{raw});
                    return std.fmt.bufPrint(&buf, "\"{b}\"", .{raw});
                }

                // it is a regular array
                const string: std.ArrayListUnmanaged(u8) = .empty;
                const writer = string.writer(allocator);

                try writer.writeByte('[');
                for (0..raw.len) |i| {
                    if (i > 0) {
                        try writer.writeAll(',');
                        if (options.ruleset.pretty)
                            try writer.writeByte(' ');
                    }
                    const value = raw[i];
                    const str = try stringify(value, options);
                    try writer.writeAll(str);
                }
                try writer.writeByte(']');
                return string.toOwnedSlice();
            },
            else => return error.TypeError,
        },
        .@"union" => |unionInfo| {
            if (unionInfo.tag_type) |tag| {
                inline for (unionInfo.fields) |field| {
                    if (field.name == @tagName(tag)) {
                        const value = @field(raw, field.name);
                        if (@typeInfo(field.type) == .pointer and field.type == []const u8) {
                            // idk
                            return std.fmt.bufPrint(&buf, "\"{s}\"", .{value});
                        } else {
                            return stringify(value, options);
                        }
                    }
                    return error.TypeError;
                }
                unreachable;
            }
            unreachable; // union has no tag
        },
        else => |t| @compileError("Error on " ++ @tagName(t)),
    }
}

test stringify {
    const UserFlags = packed struct {
        is_cool: bool = false,
        is_friendly: bool = false,
    };
    const UserSchema = struct {
        age: f64,
        name: []const u8,
        admin: bool,
        flags: UserFlags,
        @"union": union {
            hi: bool,
            bye: f64,
            n128: []const u8,
            pub fn stringify(_: anytype, comptime options: Options) ![]const u8 {
                _ = options; // unused
                return "anything here";
            }
        },
        @"enum": enum { hello, world },
        many: []const u8,
        epicptr: *const u8,
    };

    const e: []const u8 = "epic pointer";
    const user = UserSchema{
        .age = 15.0,
        .name = "Yuzu",
        .admin = true,
        .flags = UserFlags{ .is_cool = true, .is_friendly = false },
        .@"union" = .{ .hi = true },
        .@"enum" = .world,
        .many = "hello",
        .epicptr = @ptrCast(e[0..1]),
    };

    const options = Options{
        .max_buffer_size = 1024,
        .ruleset = Ruleset.Chill,
        .indent = 2,
    };

    const str = try prettyStringify(user, options);
    std.debug.print("Stringified user: {s}\n", .{str});
}
