const std = @import("../index.zig");
const builtin = @import("builtin");
const debug = std.debug;
const mem = std.mem;
const math = std.math;
const testing = std.testing;

pub const trait = @import("trait.zig");

const TypeId = builtin.TypeId;
const TypeInfo = builtin.TypeInfo;

pub fn tagName(v: var) []const u8 {
    const T = @typeOf(v);
    switch (@typeInfo(T)) {
        TypeId.Enum => |info| {
            const Tag = info.tag_type;
            inline for (info.fields) |field| {
                if (field.value == @enumToInt(v)) return field.name;
            }

            unreachable;
        },
        TypeId.Union => |info| {
            const UnionTag = if (info.tag_type) |UT| UT else @compileError("union is untagged");
            const Tag = @typeInfo(UnionTag).Enum.tag_type;
            inline for (info.fields) |field| {
                if (field.enum_field.?.value == @enumToInt(UnionTag(v)))
                    return field.name;
            }

            unreachable;
        },
        TypeId.ErrorSet => |info| {
            inline for (info.errors) |err| {
                if (err.value == @errorToInt(v)) return err.name;
            }

            unreachable;
        },
        else => @compileError("expected enum, error set or union type, found '" ++ @typeName(T) ++ "'"),
    }
}

test "std.meta.tagName" {
    const E1 = enum {
        A,
        B,
    };
    const E2 = enum(u8) {
        C = 33,
        D,
    };
    const U1 = union(enum) {
        G: u8,
        H: u16,
    };
    const U2 = union(E2) {
        C: u8,
        D: u16,
    };

    var u1g = U1{ .G = 0 };
    var u1h = U1{ .H = 0 };
    var u2a = U2{ .C = 0 };
    var u2b = U2{ .D = 0 };

    testing.expect(mem.eql(u8, tagName(E1.A), "A"));
    testing.expect(mem.eql(u8, tagName(E1.B), "B"));
    testing.expect(mem.eql(u8, tagName(E2.C), "C"));
    testing.expect(mem.eql(u8, tagName(E2.D), "D"));
    testing.expect(mem.eql(u8, tagName(error.E), "E"));
    testing.expect(mem.eql(u8, tagName(error.F), "F"));
    testing.expect(mem.eql(u8, tagName(u1g), "G"));
    testing.expect(mem.eql(u8, tagName(u1h), "H"));
    testing.expect(mem.eql(u8, tagName(u2a), "C"));
    testing.expect(mem.eql(u8, tagName(u2b), "D"));
}

pub fn stringToEnum(comptime T: type, str: []const u8) ?T {
    inline for (@typeInfo(T).Enum.fields) |enumField| {
        if (std.mem.eql(u8, str, enumField.name)) {
            return @field(T, enumField.name);
        }
    }
    return null;
}

test "std.meta.stringToEnum" {
    const E1 = enum {
        A,
        B,
    };
    testing.expect(E1.A == stringToEnum(E1, "A").?);
    testing.expect(E1.B == stringToEnum(E1, "B").?);
    testing.expect(null == stringToEnum(E1, "C"));
}

pub fn bitCount(comptime T: type) comptime_int {
    return switch (@typeInfo(T)) {
        TypeId.Int => |info| info.bits,
        TypeId.Float => |info| info.bits,
        else => @compileError("Expected int or float type, found '" ++ @typeName(T) ++ "'"),
    };
}

test "std.meta.bitCount" {
    testing.expect(bitCount(u8) == 8);
    testing.expect(bitCount(f32) == 32);
}

pub fn alignment(comptime T: type) comptime_int {
    //@alignOf works on non-pointer types
    const P = if (comptime trait.is(TypeId.Pointer)(T)) T else *T;
    return @typeInfo(P).Pointer.alignment;
}

test "std.meta.alignment" {
    testing.expect(alignment(u8) == 1);
    testing.expect(alignment(*align(1) u8) == 1);
    testing.expect(alignment(*align(2) u8) == 2);
    testing.expect(alignment([]align(1) u8) == 1);
    testing.expect(alignment([]align(2) u8) == 2);
}

pub fn Child(comptime T: type) type {
    return switch (@typeInfo(T)) {
        TypeId.Array => |info| info.child,
        TypeId.Pointer => |info| info.child,
        TypeId.Optional => |info| info.child,
        TypeId.Promise => |info| if (info.child) |child| child else null,
        else => @compileError("Expected promise, pointer, optional, or array type, " ++ "found '" ++ @typeName(T) ++ "'"),
    };
}

test "std.meta.Child" {
    testing.expect(Child([1]u8) == u8);
    testing.expect(Child(*u8) == u8);
    testing.expect(Child([]u8) == u8);
    testing.expect(Child(?u8) == u8);
    testing.expect(Child(promise->u8) == u8);
}

pub fn containerLayout(comptime T: type) TypeInfo.ContainerLayout {
    return switch (@typeInfo(T)) {
        TypeId.Struct => |info| info.layout,
        TypeId.Enum => |info| info.layout,
        TypeId.Union => |info| info.layout,
        else => @compileError("Expected struct, enum or union type, found '" ++ @typeName(T) ++ "'"),
    };
}

test "std.meta.containerLayout" {
    const E1 = enum {
        A,
    };
    const E2 = packed enum {
        A,
    };
    const E3 = extern enum {
        A,
    };
    const S1 = struct {};
    const S2 = packed struct {};
    const S3 = extern struct {};
    const U1 = union {
        a: u8,
    };
    const U2 = packed union {
        a: u8,
    };
    const U3 = extern union {
        a: u8,
    };

    testing.expect(containerLayout(E1) == TypeInfo.ContainerLayout.Auto);
    testing.expect(containerLayout(E2) == TypeInfo.ContainerLayout.Packed);
    testing.expect(containerLayout(E3) == TypeInfo.ContainerLayout.Extern);
    testing.expect(containerLayout(S1) == TypeInfo.ContainerLayout.Auto);
    testing.expect(containerLayout(S2) == TypeInfo.ContainerLayout.Packed);
    testing.expect(containerLayout(S3) == TypeInfo.ContainerLayout.Extern);
    testing.expect(containerLayout(U1) == TypeInfo.ContainerLayout.Auto);
    testing.expect(containerLayout(U2) == TypeInfo.ContainerLayout.Packed);
    testing.expect(containerLayout(U3) == TypeInfo.ContainerLayout.Extern);
}

pub fn definitions(comptime T: type) []TypeInfo.Definition {
    return switch (@typeInfo(T)) {
        TypeId.Struct => |info| info.defs,
        TypeId.Enum => |info| info.defs,
        TypeId.Union => |info| info.defs,
        else => @compileError("Expected struct, enum or union type, found '" ++ @typeName(T) ++ "'"),
    };
}

test "std.meta.definitions" {
    const E1 = enum {
        A,

        fn a() void {}
    };
    const S1 = struct {
        fn a() void {}
    };
    const U1 = union {
        a: u8,

        fn a() void {}
    };

    const defs = comptime [][]TypeInfo.Definition{
        definitions(E1),
        definitions(S1),
        definitions(U1),
    };

    inline for (defs) |def| {
        testing.expect(def.len == 1);
        testing.expect(comptime mem.eql(u8, def[0].name, "a"));
    }
}

pub fn definitionInfo(comptime T: type, comptime def_name: []const u8) TypeInfo.Definition {
    inline for (comptime definitions(T)) |def| {
        if (comptime mem.eql(u8, def.name, def_name))
            return def;
    }

    @compileError("'" ++ @typeName(T) ++ "' has no definition '" ++ def_name ++ "'");
}

test "std.meta.definitionInfo" {
    const E1 = enum {
        A,

        fn a() void {}
    };
    const S1 = struct {
        fn a() void {}
    };
    const U1 = union {
        a: u8,

        fn a() void {}
    };

    const infos = comptime []TypeInfo.Definition{
        definitionInfo(E1, "a"),
        definitionInfo(S1, "a"),
        definitionInfo(U1, "a"),
    };

    inline for (infos) |info| {
        testing.expect(comptime mem.eql(u8, info.name, "a"));
        testing.expect(!info.is_pub);
    }
}

pub fn fields(comptime T: type) switch (@typeInfo(T)) {
    TypeId.Struct => []TypeInfo.StructField,
    TypeId.Union => []TypeInfo.UnionField,
    TypeId.ErrorSet => []TypeInfo.Error,
    TypeId.Enum => []TypeInfo.EnumField,
    else => @compileError("Expected struct, union, error set or enum type, found '" ++ @typeName(T) ++ "'"),
} {
    return switch (@typeInfo(T)) {
        TypeId.Struct => |info| info.fields,
        TypeId.Union => |info| info.fields,
        TypeId.Enum => |info| info.fields,
        TypeId.ErrorSet => |info| info.errors,
        else => @compileError("Expected struct, union, error set or enum type, found '" ++ @typeName(T) ++ "'"),
    };
}

test "std.meta.fields" {
    const E1 = enum {
        A,
    };
    const E2 = error{A};
    const S1 = struct {
        a: u8,
    };
    const U1 = union {
        a: u8,
    };

    const e1f = comptime fields(E1);
    const e2f = comptime fields(E2);
    const sf = comptime fields(S1);
    const uf = comptime fields(U1);

    testing.expect(e1f.len == 1);
    testing.expect(e2f.len == 1);
    testing.expect(sf.len == 1);
    testing.expect(uf.len == 1);
    testing.expect(mem.eql(u8, e1f[0].name, "A"));
    testing.expect(mem.eql(u8, e2f[0].name, "A"));
    testing.expect(mem.eql(u8, sf[0].name, "a"));
    testing.expect(mem.eql(u8, uf[0].name, "a"));
    testing.expect(comptime sf[0].field_type == u8);
    testing.expect(comptime uf[0].field_type == u8);
}

pub fn fieldInfo(comptime T: type, comptime field_name: []const u8) switch (@typeInfo(T)) {
    TypeId.Struct => TypeInfo.StructField,
    TypeId.Union => TypeInfo.UnionField,
    TypeId.ErrorSet => TypeInfo.Error,
    TypeId.Enum => TypeInfo.EnumField,
    else => @compileError("Expected struct, union, error set or enum type, found '" ++ @typeName(T) ++ "'"),
} {
    inline for (comptime fields(T)) |field| {
        if (comptime mem.eql(u8, field.name, field_name))
            return field;
    }

    @compileError("'" ++ @typeName(T) ++ "' has no field '" ++ field_name ++ "'");
}

test "std.meta.fieldInfo" {
    const E1 = enum {
        A,
    };
    const E2 = error{A};
    const S1 = struct {
        a: u8,
    };
    const U1 = union {
        a: u8,
    };

    const e1f = comptime fieldInfo(E1, "A");
    const e2f = comptime fieldInfo(E2, "A");
    const sf = comptime fieldInfo(S1, "a");
    const uf = comptime fieldInfo(U1, "a");

    testing.expect(mem.eql(u8, e1f.name, "A"));
    testing.expect(mem.eql(u8, e2f.name, "A"));
    testing.expect(mem.eql(u8, sf.name, "a"));
    testing.expect(mem.eql(u8, uf.name, "a"));
    testing.expect(comptime sf.field_type == u8);
    testing.expect(comptime uf.field_type == u8);
}

pub fn TagType(comptime T: type) type {
    return switch (@typeInfo(T)) {
        TypeId.Enum => |info| info.tag_type,
        TypeId.Union => |info| if (info.tag_type) |Tag| Tag else null,
        else => @compileError("expected enum or union type, found '" ++ @typeName(T) ++ "'"),
    };
}

test "std.meta.TagType" {
    const E = enum(u8) {
        C = 33,
        D,
    };
    const U = union(E) {
        C: u8,
        D: u16,
    };

    testing.expect(TagType(E) == u8);
    testing.expect(TagType(U) == E);
}

///Returns the active tag of a tagged union
pub fn activeTag(u: var) @TagType(@typeOf(u)) {
    const T = @typeOf(u);
    return @TagType(T)(u);
}

test "std.meta.activeTag" {
    const UE = enum {
        Int,
        Float,
    };

    const U = union(UE) {
        Int: u32,
        Float: f32,
    };

    var u = U{ .Int = 32 };
    testing.expect(activeTag(u) == UE.Int);

    u = U{ .Float = 112.9876 };
    testing.expect(activeTag(u) == UE.Float);
}

///Given a tagged union type, and an enum, return the type of the union
/// field corresponding to the enum tag.
pub fn TagPayloadType(comptime U: type, tag: var) type {
    const Tag = @typeOf(tag);
    testing.expect(trait.is(builtin.TypeId.Union)(U));
    testing.expect(trait.is(builtin.TypeId.Enum)(Tag));

    const info = @typeInfo(U).Union;

    inline for (info.fields) |field_info| {
        if (field_info.enum_field.?.value == @enumToInt(tag)) return field_info.field_type;
    }
    unreachable;
}

test "std.meta.TagPayloadType" {
    const Event = union(enum) {
        Moved: struct {
            from: i32,
            to: i32,
        },
    };
    const MovedEvent = TagPayloadType(Event, Event.Moved);
    var e: Event = undefined;
    testing.expect(MovedEvent == @typeOf(e.Moved));
}

///Compares two of any type for equality. Containers are compared on a field-by-field basis,
/// where possible. Pointers are not followed.
pub fn eql(a: var, b: @typeOf(a)) bool {
    const T = @typeOf(a);

    switch (@typeId(T)) {
        builtin.TypeId.Struct => {
            const info = @typeInfo(T).Struct;

            inline for (info.fields) |field_info| {
                if (!eql(@field(a, field_info.name), @field(b, field_info.name))) return false;
            }
            return true;
        },
        builtin.TypeId.ErrorUnion => {
            if (a) |a_p| {
                if (b) |b_p| return eql(a_p, b_p) else |_| return false;
            } else |a_e| {
                if (b) |_| return false else |b_e| return a_e == b_e;
            }
        },
        builtin.TypeId.Union => {
            const info = @typeInfo(T).Union;

            if (info.tag_type) |_| {
                const tag_a = activeTag(a);
                const tag_b = activeTag(b);
                if (tag_a != tag_b) return false;

                inline for (info.fields) |field_info| {
                    const enum_field = field_info.enum_field.?;
                    if (enum_field.value == @enumToInt(tag_a)) {
                        return eql(@field(a, enum_field.name), @field(b, enum_field.name));
                    }
                }
                return false;
            }

            @compileError("cannot compare untagged union type " ++ @typeName(T));
        },
        builtin.TypeId.Array => {
            if (a.len != b.len) return false;
            for (a) |e, i|
                if (!eql(e, b[i])) return false;
            return true;
        },
        builtin.TypeId.Pointer => {
            const info = @typeInfo(T).Pointer;
            switch (info.size) {
                builtin.TypeInfo.Pointer.Size.One,
                builtin.TypeInfo.Pointer.Size.Many,
                builtin.TypeInfo.Pointer.Size.C,
                => return a == b,
                builtin.TypeInfo.Pointer.Size.Slice => return a.ptr == b.ptr and a.len == b.len,
            }
        },
        builtin.TypeId.Optional => {
            if (a == null and b == null) return true;
            if (a == null or b == null) return false;
            return eql(a.?, b.?);
        },
        else => return a == b,
    }
}

test "std.meta.eql" {
    const S = struct {
        a: u32,
        b: f64,
        c: [5]u8,
    };

    const U = union(enum) {
        s: S,
        f: ?f32,
    };

    const s_1 = S{
        .a = 134,
        .b = 123.3,
        .c = "12345",
    };

    const s_2 = S{
        .a = 1,
        .b = 123.3,
        .c = "54321",
    };

    const s_3 = S{
        .a = 134,
        .b = 123.3,
        .c = "12345",
    };

    const u_1 = U{ .f = 24 };
    const u_2 = U{ .s = s_1 };
    const u_3 = U{ .f = 24 };

    testing.expect(eql(s_1, s_3));
    testing.expect(eql(&s_1, &s_1));
    testing.expect(!eql(&s_1, &s_3));
    testing.expect(eql(u_1, u_3));
    testing.expect(!eql(u_1, u_2));

    var a1 = "abcdef";
    var a2 = "abcdef";
    var a3 = "ghijkl";

    testing.expect(eql(a1, a2));
    testing.expect(!eql(a1, a3));
    testing.expect(!eql(a1[0..], a2[0..]));

    const EU = struct {
        fn tst(err: bool) !u8 {
            if (err) return error.Error;
            return u8(5);
        }
    };

    testing.expect(eql(EU.tst(true), EU.tst(true)));
    testing.expect(eql(EU.tst(false), EU.tst(false)));
    testing.expect(!eql(EU.tst(false), EU.tst(true)));
}

test "intToEnum with error return" {
    const E1 = enum {
        A,
    };
    const E2 = enum {
        A,
        B,
    };

    var zero: u8 = 0;
    var one: u16 = 1;
    testing.expect(intToEnum(E1, zero) catch unreachable == E1.A);
    testing.expect(intToEnum(E2, one) catch unreachable == E2.B);
    testing.expectError(error.InvalidEnumTag, intToEnum(E1, one));
}

pub const IntToEnumError = error{InvalidEnumTag};

pub fn intToEnum(comptime Tag: type, tag_int: var) IntToEnumError!Tag {
    comptime var i = 0;
    inline while (i != @memberCount(Tag)) : (i += 1) {
        const this_tag_value = @field(Tag, @memberName(Tag, i));
        if (tag_int == @enumToInt(this_tag_value)) {
            return this_tag_value;
        }
    }
    return error.InvalidEnumTag;
}
