const builtin = @import("builtin");
const math = std.math;
const std = @import("std");

pub const cast = math.cast;
pub const fmax = math.floatMax;
pub const fmin = math.floatMin;
pub const imax = math.maxInt;
pub const imin = math.minInt;
pub const inf = math.inf;
pub const nan = math.nan;
pub const next = math.nextAfter;
pub const tmin = math.floatTrueMin;

pub const Gpr = switch (builtin.cpu.arch) {
    else => unreachable,
    .x86 => u32,
    .x86_64 => u64,
};
pub const Sse = if (builtin.cpu.has(.x86, .avx))
    @Vector(32, u8)
else
    @Vector(16, u8);

pub fn Scalar(comptime Type: type) type {
    return switch (@typeInfo(Type)) {
        else => Type,
        .vector => |info| info.child,
    };
}
pub fn ChangeScalar(comptime Type: type, comptime NewScalar: type) type {
    return switch (@typeInfo(Type)) {
        else => NewScalar,
        .vector => |vector| @Vector(vector.len, NewScalar),
    };
}
pub fn AsSignedness(comptime Type: type, comptime signedness: std.builtin.Signedness) type {
    return switch (@typeInfo(Scalar(Type))) {
        .int => |int| ChangeScalar(Type, @Int(signedness, int.bits)),
        .float => Type,
        else => @compileError(@typeName(Type)),
    };
}
pub fn AddOneBit(comptime Type: type) type {
    return ChangeScalar(Type, switch (@typeInfo(Scalar(Type))) {
        .int => |int| @Int(int.signedness, 1 + int.bits),
        .float => Scalar(Type),
        else => @compileError(@typeName(Type)),
    });
}
pub fn DoubleBits(comptime Type: type) type {
    return ChangeScalar(Type, switch (@typeInfo(Scalar(Type))) {
        .int => |int| @Int(int.signedness, int.bits * 2),
        .float => Scalar(Type),
        else => @compileError(@typeName(Type)),
    });
}
pub fn RoundBitsUp(comptime Type: type, comptime multiple: u16) type {
    return ChangeScalar(Type, switch (@typeInfo(Scalar(Type))) {
        .int => |int| @Int(int.signedness, std.mem.alignForward(u16, int.bits, multiple)),
        .float => Scalar(Type),
        else => @compileError(@typeName(Type)),
    });
}
pub fn Log2Int(comptime Type: type) type {
    return ChangeScalar(Type, math.Log2Int(Scalar(Type)));
}
pub fn Log2IntCeil(comptime Type: type) type {
    return ChangeScalar(Type, math.Log2IntCeil(Scalar(Type)));
}
pub fn splat(comptime Type: type, scalar: Scalar(Type)) Type {
    return switch (@typeInfo(Type)) {
        else => scalar,
        .vector => @splat(scalar),
    };
}
pub fn sign(rhs: anytype) ChangeScalar(@TypeOf(rhs), bool) {
    const Int = ChangeScalar(@TypeOf(rhs), switch (@typeInfo(Scalar(@TypeOf(rhs)))) {
        .int, .comptime_int => Scalar(@TypeOf(rhs)),
        .float => |float| @Int(.signed, float.bits),
        else => @compileError(@typeName(@TypeOf(rhs))),
    });
    return @as(Int, @bitCast(rhs)) < splat(Int, 0);
}
pub fn select(cond: anytype, lhs: anytype, rhs: @TypeOf(lhs)) @TypeOf(lhs) {
    return switch (@typeInfo(@TypeOf(cond))) {
        .bool => if (cond) lhs else rhs,
        .vector => @select(Scalar(@TypeOf(lhs)), cond, lhs, rhs),
        else => @compileError(@typeName(@TypeOf(cond))),
    };
}

pub const Compare = enum { strict, relaxed, approx, approx_int, approx_or_overflow };
// noinline for a more helpful stack trace
pub noinline fn checkExpected(expected: anytype, actual: @TypeOf(expected), comptime compare: Compare) !void {
    const Expected = @TypeOf(expected);
    const unexpected = unexpected: switch (@typeInfo(Scalar(Expected))) {
        else => expected != actual,
        .float => switch (compare) {
            .strict, .relaxed => {
                const unequal = (expected != actual) & ((expected == expected) | (actual == actual));
                break :unexpected switch (compare) {
                    .strict => unequal | (sign(expected) != sign(actual)),
                    .relaxed => unequal,
                    .approx, .approx_int, .approx_or_overflow => comptime unreachable,
                };
            },
            .approx, .approx_int, .approx_or_overflow => {
                const epsilon = math.floatEps(Scalar(Expected));
                const tolerance = switch (compare) {
                    .strict, .relaxed => comptime unreachable,
                    .approx, .approx_int => @sqrt(epsilon),
                    .approx_or_overflow => @exp2(@log2(epsilon) * 0.4),
                };
                const approx_unequal = @abs(expected - actual) > @max(
                    @abs(expected) * splat(Expected, tolerance),
                    splat(Expected, switch (compare) {
                        .strict, .relaxed => comptime unreachable,
                        .approx, .approx_or_overflow => tolerance,
                        .approx_int => 1,
                    }),
                );
                break :unexpected switch (compare) {
                    .strict, .relaxed => comptime unreachable,
                    .approx, .approx_int => approx_unequal,
                    .approx_or_overflow => approx_unequal &
                        (((@abs(expected) != splat(Expected, inf(Expected))) &
                            (@abs(actual) != splat(Expected, inf(Expected)))) |
                            (sign(expected) != sign(actual))),
                };
            },
        },
        .@"struct" => |@"struct"| inline for (@"struct".field_names) |field_name| {
            try checkExpected(@field(expected, field_name), @field(actual, field_name), compare);
        } else return,
    };
    if (switch (@typeInfo(Expected)) {
        else => unexpected,
        .vector => @reduce(.Or, unexpected),
    }) return error.Unexpected;
}
test checkExpected {
    if (checkExpected(nan(f16), nan(f16), .strict) == error.Unexpected) return error.Unexpected;
    if (checkExpected(nan(f16), -nan(f16), .strict) != error.Unexpected) return error.Unexpected;
    if (checkExpected(@as(f16, 0.0), @as(f16, 0.0), .strict) == error.Unexpected) return error.Unexpected;
    if (checkExpected(@as(f16, -0.0), @as(f16, -0.0), .strict) == error.Unexpected) return error.Unexpected;
    if (checkExpected(@as(f16, -0.0), @as(f16, 0.0), .strict) != error.Unexpected) return error.Unexpected;
    if (checkExpected(@as(f16, 0.0), @as(f16, -0.0), .strict) != error.Unexpected) return error.Unexpected;

    if (checkExpected(nan(f32), nan(f32), .strict) == error.Unexpected) return error.Unexpected;
    if (checkExpected(nan(f32), -nan(f32), .strict) != error.Unexpected) return error.Unexpected;
    if (checkExpected(@as(f32, 0.0), @as(f32, 0.0), .strict) == error.Unexpected) return error.Unexpected;
    if (checkExpected(@as(f32, -0.0), @as(f32, -0.0), .strict) == error.Unexpected) return error.Unexpected;
    if (checkExpected(@as(f32, -0.0), @as(f32, 0.0), .strict) != error.Unexpected) return error.Unexpected;
    if (checkExpected(@as(f32, 0.0), @as(f32, -0.0), .strict) != error.Unexpected) return error.Unexpected;

    if (checkExpected(nan(f64), nan(f64), .strict) == error.Unexpected) return error.Unexpected;
    if (checkExpected(nan(f64), -nan(f64), .strict) != error.Unexpected) return error.Unexpected;
    if (checkExpected(@as(f64, 0.0), @as(f64, 0.0), .strict) == error.Unexpected) return error.Unexpected;
    if (checkExpected(@as(f64, -0.0), @as(f64, -0.0), .strict) == error.Unexpected) return error.Unexpected;
    if (checkExpected(@as(f64, -0.0), @as(f64, 0.0), .strict) != error.Unexpected) return error.Unexpected;
    if (checkExpected(@as(f64, 0.0), @as(f64, -0.0), .strict) != error.Unexpected) return error.Unexpected;

    if (checkExpected(nan(f80), nan(f80), .strict) == error.Unexpected) return error.Unexpected;
    if (checkExpected(nan(f80), -nan(f80), .strict) != error.Unexpected) return error.Unexpected;
    if (checkExpected(@as(f80, 0.0), @as(f80, 0.0), .strict) == error.Unexpected) return error.Unexpected;
    if (checkExpected(@as(f80, -0.0), @as(f80, -0.0), .strict) == error.Unexpected) return error.Unexpected;
    if (checkExpected(@as(f80, -0.0), @as(f80, 0.0), .strict) != error.Unexpected) return error.Unexpected;
    if (checkExpected(@as(f80, 0.0), @as(f80, -0.0), .strict) != error.Unexpected) return error.Unexpected;

    if (checkExpected(nan(f128), nan(f128), .strict) == error.Unexpected) return error.Unexpected;
    if (checkExpected(nan(f128), -nan(f128), .strict) != error.Unexpected) return error.Unexpected;
    if (checkExpected(@as(f128, 0.0), @as(f128, 0.0), .strict) == error.Unexpected) return error.Unexpected;
    if (checkExpected(@as(f128, -0.0), @as(f128, -0.0), .strict) == error.Unexpected) return error.Unexpected;
    if (checkExpected(@as(f128, -0.0), @as(f128, 0.0), .strict) != error.Unexpected) return error.Unexpected;
    if (checkExpected(@as(f128, 0.0), @as(f128, -0.0), .strict) != error.Unexpected) return error.Unexpected;
}
