const std = @import("std");

const debug = @import("debug.zig");
const compileError = debug.compileError;

pub fn isStruct(comptime T: type) bool {
    return switch (@typeInfo(T)) {
        .@"struct" => true,
        else => false,
    };
}

pub fn isTuple(comptime T: type) bool {
    return switch (@typeInfo(T)) {
        .@"struct" => |info| info.is_tuple,
        else => false,
    };
}

pub fn isStructOf(comptime T: type, comptime Elem: type) bool {
    return switch (@typeInfo(T)) {
        .@"struct" => |info| blk: {
            inline for (info.fields) |field| {
                if (field.type != Elem) {
                    break :blk false;
                }
            }
            break :blk true;
        },
        else => false,
    };
}

pub fn isStructOfAny(comptime T: type, comptime f: fn (comptime type) bool) bool {
    return switch (@typeInfo(T)) {
        .@"struct" => |info| blk: {
            inline for (info.fields) |field| {
                if (f(field.type) == false) {
                    break :blk false;
                }
            }
            break :blk true;
        },
        else => false,
    };
}

pub fn isTupleOf(comptime T: type, comptime Elem: type) bool {
    return isTuple(T) and isStructOf(T, Elem);
}

pub fn isTupleOfAny(comptime T: type, comptime f: fn (comptime type) bool) bool {
    return isTuple(T) and isStructOfAny(T, f);
}

pub fn isSliceOf(comptime T: type, comptime Elem: type) bool {
    return switch (@typeInfo(T)) {
        .pointer => |info| switch (info.size) {
            .slice => info.child == Elem,
            .one => switch (@typeInfo(info.child)) {
                // As Zig, convert pointer to Array as a slice.
                .array => |arr_info| arr_info.child == Elem,
                else => false,
            },
            else => false,
        },
        else => false,
    };
}

pub fn isInteger(comptime T: type) bool {
    return switch (@typeInfo(T)) {
        .int, .comptime_int => true,
        else => false,
    };
}

pub fn isSliceOfAny(comptime T: type, comptime f: fn (comptime type) bool) bool {
    return switch (@typeInfo(T)) {
        .pointer => |info| switch (info.size) {
            .one => info.child == @TypeOf(.{}),
            .slice => f(info.child),
            else => false,
        },
        else => false,
    };
}

pub fn DeclEnum(comptime T: type) type {
    const field_infos = std.meta.declarations(T);
    if (field_infos.len == 0) {
        compileError("Struct {} has no declarations", .{T});
    }
    return std.meta.DeclEnum(UnwrapPtr(T));
}

pub fn UnwrapPtr(comptime T: type) type {
    return switch (@typeInfo(T)) {
        .pointer => |info| switch (info.size) {
            .one => info.child,
            else => T,
        },
        else => T,
    };
}

pub fn asSlice(comptime T: type) type {
    const err_msg = "Type " ++ @typeName(T) ++ " can't be interpreted as a slice";
    return switch (@typeInfo(T)) {
        .pointer => |info| switch (info.size) {
            .slice => info.child,
            .one => switch (@typeInfo(info.child)) {
                // As Zig, convert pointer to Array as a slice.
                .array => |arr_info| arr_info.child,
                else => @compileError(err_msg),
            },
            else => @compileError(err_msg),
        },
        else => @compileError(err_msg),
    };
}

pub fn TupleRange(comptime T: type, comptime start: ?usize, comptime end: ?usize) type {
    const fields = std.meta.fields(T);
    const start_ = start orelse 0;
    const end_ = end orelse fields.len;

    if (start_ == end_) {
        return @Tuple(&.{});
    }

    var field_types: [end_ - start_]type = undefined;
    inline for (start_..end_, 0..) |i, j| {
        field_types[j] = fields[i].type;
    }
    return @Tuple(&field_types);
}

pub fn FnParam(comptime func: anytype, comptime n: comptime_int) type {
    const params = @typeInfo(@TypeOf(func)).@"fn".params;
    if (n >= params.len) {
        @compileError("param doesn't exist in func");
    }
    return params[n].type orelse @compileError("anytype is not supported");
}

pub fn FnReturn(comptime func: anytype) type {
    return @typeInfo(@TypeOf(func)).@"fn".return_type orelse @compileError("anytype is not supported");
}

pub fn FnReturnPayload(comptime func: anytype) type {
    const rt = @typeInfo(@TypeOf(func)).@"fn".return_type orelse @compileError("anytype is not supported");
    return switch (@typeInfo(rt)) {
        .error_union => |u| u.payload,
        else => rt,
    };
}

pub fn FnReturnErrorSet(comptime func: anytype) ?type {
    const rt = @typeInfo(@TypeOf(func)).@"fn".return_type orelse @compileError("anytype is not supported");
    return switch (@typeInfo(rt)) {
        .error_union => |u| u.error_set,
        else => null,
    };
}

pub fn Head(comptime Tuple: type) type {
    return switch (@typeInfo(Tuple)) {
        .@"struct" => |struct_info| {
            if (struct_info.fields.len == 0) @compileError("Can't tail empty tuple");
            return struct_info.fields[0].type;
        },
        else => @compileError("Head works on tuple type"),
    };
}

pub fn Tail(comptime Tuple: type) type {
    return switch (@typeInfo(Tuple)) {
        .@"struct" => |struct_info| {
            if (struct_info.fields.len == 0) @compileError("Can't tail empty tuple");
            var types: [struct_info.fields.len - 1]type = undefined;
            for (struct_info.fields[1..], &types) |field, *type_| {
                type_.* = field.type;
            }
            return @Tuple(&types);
        },
        else => @compileError("Tail works on tuple type"),
    };
}
