// The rounding logic is inspired by LLVM's APFloat and Go's atofHex
// implementation.

const std = @import("std");
const ascii = std.ascii;
const fmt = std.fmt;
const math = std.math;
const testing = std.testing;

const assert = std.debug.assert;

pub fn parseHexFloat(comptime T: type, s: []const u8) !T {
    assert(@typeInfo(T) == .Float);

    const IntT = std.meta.Int(.unsigned, @typeInfo(T).Float.bits);

    const mantissa_bits = math.floatMantissaBits(T);
    const exponent_bits = math.floatExponentBits(T);

    const sign_shift = mantissa_bits + exponent_bits;

    const exponent_bias = (1 << (exponent_bits - 1)) - 1;
    const exponent_min = 1 - exponent_bias;
    const exponent_max = exponent_bias;

    if (s.len == 0)
        return error.InvalidCharacter;

    if (ascii.eqlIgnoreCase(s, "nan")) {
        return math.nan(T);
    } else if (ascii.eqlIgnoreCase(s, "inf") or ascii.eqlIgnoreCase(s, "+inf")) {
        return math.inf(T);
    } else if (ascii.eqlIgnoreCase(s, "-inf")) {
        return -math.inf(T);
    }

    var negative: bool = false;
    var exp_negative: bool = false;

    var mantissa: u128 = 0;
    var exponent: i16 = 0;
    var frac_scale: i16 = 0;

    const State = enum {
        MaybeSign,
        Prefix,
        LeadingIntegerDigit,
        IntegerDigit,
        MaybeDot,
        LeadingFractionDigit,
        FractionDigit,
        ExpPrefix,
        MaybeExpSign,
        ExpDigit,
    };

    var state = State.MaybeSign;

    var i: usize = 0;
    while (i < s.len) {
        const c = s[i];

        switch (state) {
            .MaybeSign => {
                state = .Prefix;

                if (c == '+') {
                    i += 1;
                } else if (c == '-') {
                    negative = true;
                    i += 1;
                }
            },
            .Prefix => {
                state = .LeadingIntegerDigit;

                // Match both 0x and 0X.
                if (i + 2 > s.len or s[i] != '0' or s[i + 1] | 32 != 'x')
                    return error.InvalidCharacter;
                i += 2;
            },
            .LeadingIntegerDigit => {
                if (c == '0') {
                    // Skip leading zeros.
                    i += 1;
                } else if (c == '_') {
                    return error.InvalidCharacter;
                } else {
                    state = .IntegerDigit;
                }
            },
            .IntegerDigit => {
                if (ascii.isXDigit(c)) {
                    if (mantissa >= math.maxInt(u128) / 16)
                        return error.Overflow;
                    mantissa *%= 16;
                    mantissa += try fmt.charToDigit(c, 16);
                    i += 1;
                } else if (c == '_') {
                    i += 1;
                } else {
                    state = .MaybeDot;
                }
            },
            .MaybeDot => {
                if (c == '.') {
                    state = .LeadingFractionDigit;
                    i += 1;
                } else state = .ExpPrefix;
            },
            .LeadingFractionDigit => {
                if (c == '_') {
                    return error.InvalidCharacter;
                } else state = .FractionDigit;
            },
            .FractionDigit => {
                if (ascii.isXDigit(c)) {
                    if (mantissa < math.maxInt(u128) / 16) {
                        mantissa *%= 16;
                        mantissa +%= try fmt.charToDigit(c, 16);
                        frac_scale += 1;
                    } else if (c != '0') {
                        return error.Overflow;
                    }
                    i += 1;
                } else if (c == '_') {
                    i += 1;
                } else {
                    state = .ExpPrefix;
                }
            },
            .ExpPrefix => {
                state = .MaybeExpSign;
                // Match both p and P.
                if (c | 32 != 'p')
                    return error.InvalidCharacter;
                i += 1;
            },
            .MaybeExpSign => {
                state = .ExpDigit;

                if (c == '+') {
                    i += 1;
                } else if (c == '-') {
                    exp_negative = true;
                    i += 1;
                }
            },
            .ExpDigit => {
                if (ascii.isXDigit(c)) {
                    if (exponent >= math.maxInt(i16) / 10)
                        return error.Overflow;
                    exponent *%= 10;
                    exponent +%= try fmt.charToDigit(c, 10);
                    i += 1;
                } else if (c == '_') {
                    i += 1;
                } else {
                    return error.InvalidCharacter;
                }
            },
        }
    }

    if (exp_negative)
        exponent *= -1;

    // Bring the decimal part to the left side of the decimal dot.
    exponent -= frac_scale * 4;

    if (mantissa == 0) {
        // Signed zero.
        return if (negative) -0.0 else 0.0;
    }

    // Divide by 2^mantissa_bits to right-align the mantissa in the fractional
    // part.
    exponent += mantissa_bits;

    // Keep around two extra bits to correctly round any value that doesn't fit
    // the available mantissa bits. The result LSB serves as Guard bit, the
    // following one is the Round bit and the last one is the Sticky bit,
    // computed by OR-ing all the dropped bits.

    // Normalize by aligning the implicit one bit.
    while (mantissa >> (mantissa_bits + 2) == 0) {
        mantissa <<= 1;
        exponent -= 1;
    }

    // Normalize again by dropping the excess precision.
    // Note that the discarded bits are folded into the Sticky bit.
    while (mantissa >> (mantissa_bits + 2 + 1) != 0) {
        mantissa = mantissa >> 1 | (mantissa & 1);
        exponent += 1;
    }

    // Very small numbers can be possibly represented as denormals, reduce the
    // exponent as much as possible.
    while (mantissa != 0 and exponent < exponent_min - 2) {
        mantissa = mantissa >> 1 | (mantissa & 1);
        exponent += 1;
    }

    // Whenever the guard bit is one (G=1) and:
    //   - we've truncated more than 0.5ULP (R=S=1)
    //   - we've truncated exactly 0.5ULP (R=1 S=0)
    // Were are going to increase the mantissa (round up)
    const guard_bit_and_half_or_more = (mantissa & 0b110) == 0b110;
    mantissa >>= 2;
    exponent += 2;

    if (guard_bit_and_half_or_more) {
        mantissa += 1;
    }

    if (mantissa == (1 << (mantissa_bits + 1))) {
        // Renormalize, if the exponent overflows we'll catch that below.
        mantissa >>= 1;
        exponent += 1;
    }

    if (mantissa >> mantissa_bits == 0) {
        // This is a denormal number, the biased exponent is zero.
        exponent = -exponent_bias;
    }

    if (exponent > exponent_max) {
        // Overflow, return +inf.
        return math.inf(T);
    }

    // Remove the implicit bit.
    mantissa &= @as(u128, (1 << mantissa_bits) - 1);

    const raw: IntT =
        (if (negative) @as(IntT, 1) << sign_shift else 0) |
        @as(IntT, @bitCast(u16, exponent + exponent_bias)) << mantissa_bits |
        @truncate(IntT, mantissa);

    return @bitCast(T, raw);
}

test "special" {
    try testing.expect(math.isNan(try parseHexFloat(f32, "nAn")));
    try testing.expect(math.isPositiveInf(try parseHexFloat(f32, "iNf")));
    try testing.expect(math.isPositiveInf(try parseHexFloat(f32, "+Inf")));
    try testing.expect(math.isNegativeInf(try parseHexFloat(f32, "-iNf")));
}
test "zero" {
    try testing.expectEqual(@as(f32, 0.0), try parseHexFloat(f32, "0x0"));
    try testing.expectEqual(@as(f32, 0.0), try parseHexFloat(f32, "-0x0"));
    try testing.expectEqual(@as(f32, 0.0), try parseHexFloat(f32, "0x0p42"));
    try testing.expectEqual(@as(f32, 0.0), try parseHexFloat(f32, "-0x0.00000p42"));
    try testing.expectEqual(@as(f32, 0.0), try parseHexFloat(f32, "0x0.00000p666"));
}

test "f16" {
    const Case = struct { s: []const u8, v: f16 };
    const cases: []const Case = &[_]Case{
        .{ .s = "0x1p0", .v = 1.0 },
        .{ .s = "-0x1p-1", .v = -0.5 },
        .{ .s = "0x10p+10", .v = 16384.0 },
        .{ .s = "0x10p-10", .v = 0.015625 },
        // Max normalized value.
        .{ .s = "0x1.ffcp+15", .v = math.f16_max },
        .{ .s = "-0x1.ffcp+15", .v = -math.f16_max },
        // Min normalized value.
        .{ .s = "0x1p-14", .v = math.f16_min },
        .{ .s = "-0x1p-14", .v = -math.f16_min },
        // Min denormal value.
        .{ .s = "0x1p-24", .v = math.f16_true_min },
        .{ .s = "-0x1p-24", .v = -math.f16_true_min },
    };

    for (cases) |case| {
        try testing.expectEqual(case.v, try parseHexFloat(f16, case.s));
    }
}
test "f32" {
    const Case = struct { s: []const u8, v: f32 };
    const cases: []const Case = &[_]Case{
        .{ .s = "0x1p0", .v = 1.0 },
        .{ .s = "-0x1p-1", .v = -0.5 },
        .{ .s = "0x10p+10", .v = 16384.0 },
        .{ .s = "0x10p-10", .v = 0.015625 },
        .{ .s = "0x0.ffffffp128", .v = 0x0.ffffffp128 },
        .{ .s = "0x0.1234570p-125", .v = 0x0.1234570p-125 },
        // Max normalized value.
        .{ .s = "0x1.fffffeP+127", .v = math.f32_max },
        .{ .s = "-0x1.fffffeP+127", .v = -math.f32_max },
        // Min normalized value.
        .{ .s = "0x1p-126", .v = math.f32_min },
        .{ .s = "-0x1p-126", .v = -math.f32_min },
        // Min denormal value.
        .{ .s = "0x1P-149", .v = math.f32_true_min },
        .{ .s = "-0x1P-149", .v = -math.f32_true_min },
    };

    for (cases) |case| {
        try testing.expectEqual(case.v, try parseHexFloat(f32, case.s));
    }
}
test "f64" {
    const Case = struct { s: []const u8, v: f64 };
    const cases: []const Case = &[_]Case{
        .{ .s = "0x1p0", .v = 1.0 },
        .{ .s = "-0x1p-1", .v = -0.5 },
        .{ .s = "0x10p+10", .v = 16384.0 },
        .{ .s = "0x10p-10", .v = 0.015625 },
        // Max normalized value.
        .{ .s = "0x1.fffffffffffffp+1023", .v = math.f64_max },
        .{ .s = "-0x1.fffffffffffffp1023", .v = -math.f64_max },
        // Min normalized value.
        .{ .s = "0x1p-1022", .v = math.f64_min },
        .{ .s = "-0x1p-1022", .v = -math.f64_min },
        // Min denormalized value.
        .{ .s = "0x1p-1074", .v = math.f64_true_min },
        .{ .s = "-0x1p-1074", .v = -math.f64_true_min },
    };

    for (cases) |case| {
        try testing.expectEqual(case.v, try parseHexFloat(f64, case.s));
    }
}
test "f128" {
    const Case = struct { s: []const u8, v: f128 };
    const cases: []const Case = &[_]Case{
        .{ .s = "0x1p0", .v = 1.0 },
        .{ .s = "-0x1p-1", .v = -0.5 },
        .{ .s = "0x10p+10", .v = 16384.0 },
        .{ .s = "0x10p-10", .v = 0.015625 },
        // Max normalized value.
        .{ .s = "0xf.fffffffffffffffffffffffffff8p+16380", .v = math.f128_max },
        .{ .s = "-0xf.fffffffffffffffffffffffffff8p+16380", .v = -math.f128_max },
        // Min normalized value.
        .{ .s = "0x1p-16382", .v = math.f128_min },
        .{ .s = "-0x1p-16382", .v = -math.f128_min },
        // // Min denormalized value.
        .{ .s = "0x1p-16494", .v = math.f128_true_min },
        .{ .s = "-0x1p-16494", .v = -math.f128_true_min },
        .{ .s = "0x1.edcb34a235253948765432134674fp-1", .v = 0x1.edcb34a235253948765432134674fp-1 },
    };

    for (cases) |case| {
        try testing.expectEqual(@bitCast(u128, case.v), @bitCast(u128, try parseHexFloat(f128, case.s)));
    }
}
