const std = @import("std");
const fio = @import("fio.zig");
const util = @import("util.zig");

const Self = @This();

const struct_mustache_s = opaque {};
const mustache_s = struct_mustache_s;
const enum_mustache_error_en = c_uint;
const mustache_error_en = enum_mustache_error_en;

extern fn fiobj_mustache_new(args: MustacheLoadArgsFio) ?*mustache_s;
extern fn fiobj_mustache_build(mustache: ?*mustache_s, data: fio.FIOBJ) fio.FIOBJ;
extern fn fiobj_mustache_build2(dest: fio.FIOBJ, mustache: ?*mustache_s, data: fio.FIOBJ) fio.FIOBJ;
extern fn fiobj_mustache_free(mustache: ?*mustache_s) void;

/// Load arguments used when creating a new Mustache instance.
pub const MustacheLoadArgs = struct {
    /// Filename. This enables partial templates on filesystem.
    filename: ?[]const u8 = null,

    /// String data. Should be used if no filename is specified.
    data: ?[]const u8 = null,
};

/// Internal struct used for interfacing with fio.
const MustacheLoadArgsFio = extern struct {
    filename: [*c]const u8,
    filename_len: usize,
    data: [*c]const u8,
    data_len: usize,
    err: [*c]mustache_error_en,
};

/// Handle to the underlying fiobj mustache instance.
handle: *mustache_s,

pub const Error = error{
    MUSTACHE_ERR_TOO_DEEP,
    MUSTACHE_ERR_CLOSURE_MISMATCH,
    MUSTACHE_ERR_FILE_NOT_FOUND,
    MUSTACHE_ERR_FILE_TOO_BIG,
    MUSTACHE_ERR_FILE_NAME_TOO_LONG,
    MUSTACHE_ERR_FILE_NAME_TOO_SHORT,
    MUSTACHE_ERR_EMPTY_TEMPLATE,
    MUSTACHE_ERR_DELIMITER_TOO_LONG,
    MUSTACHE_ERR_NAME_TOO_LONG,
    MUSTACHE_ERR_UNKNOWN,
    MUSTACHE_ERR_USER_ERROR,
};

/// Create a new `Mustache` instance; `deinit()` should be called to free
/// the object after usage.
pub fn init(load_args: MustacheLoadArgs) Error!Self {
    var err: mustache_error_en = undefined;

    const args: MustacheLoadArgsFio = .{
        .filename = filn: {
            if (load_args.filename) |filn| break :filn filn.ptr else break :filn null;
        },
        .filename_len = filn_len: {
            if (load_args.filename) |filn| break :filn_len filn.len else break :filn_len 0;
        },
        .data = data: {
            if (load_args.data) |data| break :data data.ptr else break :data null;
        },
        .data_len = data_len: {
            if (load_args.data) |data| break :data_len data.len else break :data_len 0;
        },
        .err = &err,
    };

    const ret = fiobj_mustache_new(args);
    switch (err) {
        0 => return Self{
            .handle = ret.?,
        },
        1 => return Error.MUSTACHE_ERR_TOO_DEEP,
        2 => return Error.MUSTACHE_ERR_CLOSURE_MISMATCH,
        3 => return Error.MUSTACHE_ERR_FILE_NOT_FOUND,
        4 => return Error.MUSTACHE_ERR_FILE_TOO_BIG,
        5 => return Error.MUSTACHE_ERR_FILE_NAME_TOO_LONG,
        6 => return Error.MUSTACHE_ERR_FILE_NAME_TOO_SHORT,
        7 => return Error.MUSTACHE_ERR_EMPTY_TEMPLATE,
        8 => return Error.MUSTACHE_ERR_DELIMITER_TOO_LONG,
        9 => return Error.MUSTACHE_ERR_NAME_TOO_LONG,
        10 => return Error.MUSTACHE_ERR_UNKNOWN,
        11 => return Error.MUSTACHE_ERR_USER_ERROR,
        else => return Error.MUSTACHE_ERR_UNKNOWN,
    }
    unreachable;
}

/// Convenience function to create a new `Mustache` instance with in-memory data loaded;
/// `deinit()` should be called to free the object after usage..
pub fn fromData(data: []const u8) Error!Self {
    return Self.init(.{ .data = data });
}

/// Convenience function to create a new `Mustache` instance with file-based data loaded;
/// `deinit()` should be called to free the object after usage..
pub fn fromFile(filename: []const u8) Error!Self {
    return Self.init(.{ .filename = filename });
}

/// Free the data backing a `Mustache` instance.
pub fn deinit(self: *Self) void {
    fiobj_mustache_free(self.handle);
}

// TODO: implement these - fiobj_mustache.c
// pub extern fn fiobj_mustache_build(mustache: ?*mustache_s, data: FIOBJ) FIOBJ;
// pub extern fn fiobj_mustache_build2(dest: FIOBJ, mustache: ?*mustache_s, data: FIOBJ) FIOBJ;

/// The result from calling `build`.
const MustacheBuildResult = struct {
    fiobj_result: fio.FIOBJ = 0,

    /// Holds the context converted into a fiobj.
    /// This is used in `build`.
    fiobj_context: fio.FIOBJ = 0,

    /// Free the data backing a `MustacheBuildResult` instance.
    pub fn deinit(m: *const MustacheBuildResult) void {
        fio.fiobj_free_wrapped(m.fiobj_result);
        fio.fiobj_free_wrapped(m.fiobj_context);
    }

    /// Retrieve a string representation of the built template.
    pub fn str(m: *const MustacheBuildResult) ?[]const u8 {
        return util.fio2str(m.fiobj_result);
    }
};

/// Build the Mustache template; `deinit()` should be called on the build
/// result to free the data.
// TODO: The build may be slow because it needs to convert zig types to facil.io
// types. However, this needs to be investigated into.
// See `fiobjectify` for more information.
pub fn build(self: *Self, data: anytype) MustacheBuildResult {
    const T = @TypeOf(data);
    if (@typeInfo(T) != .Struct) {
        @compileError("No struct: '" ++ @typeName(T) ++ "'");
    }

    var result: MustacheBuildResult = .{};

    result.fiobj_context = fiobjectify(data);
    result.fiobj_result = fiobj_mustache_build(self.handle, result.fiobj_context);
    return result;
}

/// Internal function used to convert zig types to facil.io types.
/// Used when providing the context to `fiobj_mustache_build`.
fn fiobjectify(
    value: anytype,
) fio.FIOBJ {
    const T = @TypeOf(value);
    switch (@typeInfo(T)) {
        .Float, .ComptimeFloat => {
            return fio.fiobj_float_new(value);
        },
        .Int, .ComptimeInt => {
            return fio.fiobj_num_new_bignum(value);
        },
        .Bool => {
            return if (value) fio.fiobj_true() else fio.fiobj_false();
        },
        .Null => {
            return 0;
        },
        .Optional => {
            if (value) |payload| {
                return fiobjectify(payload);
            } else {
                return fiobjectify(null);
            }
        },
        .Enum => {
            return fio.fiobj_num_new_bignum(@intFromEnum(value));
        },
        .Union => {
            const info = @typeInfo(T).Union;
            if (info.tag_type) |UnionTagType| {
                inline for (info.fields) |u_field| {
                    if (value == @field(UnionTagType, u_field.name)) {
                        return fiobjectify(@field(value, u_field.name));
                    }
                }
            } else {
                @compileError("Unable to fiobjectify untagged union '" ++ @typeName(T) ++ "'");
            }
        },
        .Struct => |S| {
            // create a new fio hashmap
            const m = fio.fiobj_hash_new();
            // std.debug.print("new struct\n", .{});
            inline for (S.fields) |Field| {
                // don't include void fields
                if (Field.type == void) continue;

                // std.debug.print("    new field: {s}\n", .{Field.name});
                const fname = fio.fiobj_str_new(util.toCharPtr(Field.name), Field.name.len);
                // std.debug.print("    fiobj name : {any}\n", .{fname});
                const v = @field(value, Field.name);
                // std.debug.print("    value: {any}\n", .{v});
                const fvalue = fiobjectify(v);
                // std.debug.print("    fiobj value: {any}\n", .{fvalue});
                _ = fio.fiobj_hash_set(m, fname, fvalue);
                fio.fiobj_free_wrapped(fname);
            }
            return m;
        },
        .ErrorSet => return fiobjectify(@as([]const u8, @errorName(value))),
        .Pointer => |ptr_info| switch (ptr_info.size) {
            .One => switch (@typeInfo(ptr_info.child)) {
                .Array => {
                    const Slice = []const std.meta.Elem(ptr_info.child);
                    return fiobjectify(@as(Slice, value));
                },
                else => {
                    // TODO: avoid loops?
                    return fiobjectify(value.*);
                },
            },
            // TODO: .Many when there is a sentinel (waiting for https://github.com/ziglang/zig/pull/3972)
            .Slice => {
                // std.debug.print("new slice\n", .{});
                if (ptr_info.child == u8 and std.unicode.utf8ValidateSlice(value)) {
                    return fio.fiobj_str_new(util.toCharPtr(value), value.len);
                }

                const arr = fio.fiobj_ary_new2(value.len);
                for (value) |x| {
                    const v = fiobjectify(x);
                    fio.fiobj_ary_push(arr, v);
                }
                return arr;
            },
            else => @compileError("Unable to fiobjectify type '" ++ @typeName(T) ++ "'"),
        },
        .Array => return fiobjectify(&value),
        .Vector => |info| {
            const array: [info.len]info.child = value;
            return fiobjectify(&array);
        },
        else => @compileError("Unable to fiobjectify type '" ++ @typeName(T) ++ "'"),
    }
    unreachable;
}
