    //

    // Adapted from BearSSL's ctmul64 implementation originally written by Thomas Pornin <pornin@bolet.org>

    const std = @import("../std.zig");
    const builtin = @import("builtin");
    const assert = std.debug.assert;
    const math = std.math;
    const mem = std.mem;
    const utils = std.crypto.utils;

    /// GHASH is a universal hash function that features multiplication
    /// by a fixed parameter within a Galois field.
    ///
    /// It is not a general purpose hash function - The key must be secret, unpredictable and never reused.
    ///
    /// GHASH is typically used to compute the authentication tag in the AES-GCM construction.
    pub const Ghash = struct {
        pub const block_length: usize = 16;
        pub const mac_length = 16;
        pub const key_length = 16;

        y0: u64 = 0,
        y1: u64 = 0,
        h0: u64,
        h1: u64,
        h2: u64,
        h0r: u64,
        h1r: u64,
        h2r: u64,

        hh0: u64 = undefined,
        hh1: u64 = undefined,
        hh2: u64 = undefined,
        hh0r: u64 = undefined,
        hh1r: u64 = undefined,
        hh2r: u64 = undefined,

        leftover: usize = 0,
        buf: [block_length]u8 align(16) = undefined,

        pub fn init(key: *const [key_length]u8) Ghash {
            const h1 = mem.readIntBig(u64, key[0..8]);
            const h0 = mem.readIntBig(u64, key[8..16]);
            const h1r = @bitReverse(h1);
            const h0r = @bitReverse(h0);
            const h2 = h0 ^ h1;
            const h2r = h0r ^ h1r;

            if (builtin.mode == .ReleaseSmall) {
                return Ghash{
                    .h0 = h0,
                    .h1 = h1,
                    .h2 = h2,
                    .h0r = h0r,
                    .h1r = h1r,
                    .h2r = h2r,
                };
            } else {
                // Precompute H^2

                var hh = Ghash{
                    .h0 = h0,
                    .h1 = h1,
                    .h2 = h2,
                    .h0r = h0r,
                    .h1r = h1r,
                    .h2r = h2r,
                };
                hh.update(key);
                const hh1 = hh.y1;
                const hh0 = hh.y0;
                const hh1r = @bitReverse(hh1);
                const hh0r = @bitReverse(hh0);
                const hh2 = hh0 ^ hh1;
                const hh2r = hh0r ^ hh1r;

                return Ghash{
                    .h0 = h0,
                    .h1 = h1,
                    .h2 = h2,
                    .h0r = h0r,
                    .h1r = h1r,
                    .h2r = h2r,

                    .hh0 = hh0,
                    .hh1 = hh1,
                    .hh2 = hh2,
                    .hh0r = hh0r,
                    .hh1r = hh1r,
                    .hh2r = hh2r,
                };
            }
        }

        inline fn clmul_pclmul(x: u64, y: u64) u64 {
            const product = asm (
                \\ vpclmulqdq $0x00, %[x], %[y], %[out]

                : [out] "=x" (-> @Vector(2, u64)),
                : [x] "x" (@bitCast(@Vector(2, u64), @as(u128, x))),
                  [y] "x" (@bitCast(@Vector(2, u64), @as(u128, y))),
            );
            return product[0];
        }

        inline fn clmul_pmull(x: u64, y: u64) u64 {
            const product = asm (
                \\ pmull %[out].1q, %[x].1d, %[y].1d

                : [out] "=w" (-> @Vector(2, u64)),
                : [x] "w" (@bitCast(@Vector(2, u64), @as(u128, x))),
                  [y] "w" (@bitCast(@Vector(2, u64), @as(u128, y))),
            );
            return product[0];
        }

        fn clmul_soft(x: u64, y: u64) u64 {
            const x0 = x & 0x1111111111111111;
            const x1 = x & 0x2222222222222222;
            const x2 = x & 0x4444444444444444;
            const x3 = x & 0x8888888888888888;
            const y0 = y & 0x1111111111111111;
            const y1 = y & 0x2222222222222222;
            const y2 = y & 0x4444444444444444;
            const y3 = y & 0x8888888888888888;
            var z0 = (x0 *% y0) ^ (x1 *% y3) ^ (x2 *% y2) ^ (x3 *% y1);
            var z1 = (x0 *% y1) ^ (x1 *% y0) ^ (x2 *% y3) ^ (x3 *% y2);
            var z2 = (x0 *% y2) ^ (x1 *% y1) ^ (x2 *% y0) ^ (x3 *% y3);
            var z3 = (x0 *% y3) ^ (x1 *% y2) ^ (x2 *% y1) ^ (x3 *% y0);
            z0 &= 0x1111111111111111;
            z1 &= 0x2222222222222222;
            z2 &= 0x4444444444444444;
            z3 &= 0x8888888888888888;
            return z0 | z1 | z2 | z3;
        }

        const has_pclmul = std.Target.x86.featureSetHas(builtin.cpu.features, .pclmul);
        const has_avx = std.Target.x86.featureSetHas(builtin.cpu.features, .avx);
        const has_armaes = std.Target.aarch64.featureSetHas(builtin.cpu.features, .aes);
        const clmul = if (builtin.cpu.arch == .x86_64 and has_pclmul and has_avx) impl: {
            break :impl clmul_pclmul;
        } else if (builtin.cpu.arch == .aarch64 and has_armaes) impl: {
            break :impl clmul_pmull;
        } else impl: {
            break :impl clmul_soft;
        };

        fn blocks(st: *Ghash, msg: []const u8) void {
            assert(msg.len % 16 == 0); // GHASH blocks() expects full blocks

            var y1 = st.y1;
            var y0 = st.y0;

            var i: usize = 0;

            // 2-blocks aggregated reduction

            if (builtin.mode != .ReleaseSmall) {
                while (i + 32 <= msg.len) : (i += 32) {
                    // B0 * H^2 unreduced

                    y1 ^= mem.readIntBig(u64, msg[i..][0..8]);
                    y0 ^= mem.readIntBig(u64, msg[i..][8..16]);

                    const y1r = @bitReverse(y1);
                    const y0r = @bitReverse(y0);
                    const y2 = y0 ^ y1;
                    const y2r = y0r ^ y1r;

                    var z0 = clmul(y0, st.hh0);
                    var z1 = clmul(y1, st.hh1);
                    var z2 = clmul(y2, st.hh2) ^ z0 ^ z1;
                    var z0h = clmul(y0r, st.hh0r);
                    var z1h = clmul(y1r, st.hh1r);
                    var z2h = clmul(y2r, st.hh2r) ^ z0h ^ z1h;

                    // B1 * H unreduced

                    const sy1 = mem.readIntBig(u64, msg[i..][16..24]);
                    const sy0 = mem.readIntBig(u64, msg[i..][24..32]);

                    const sy1r = @bitReverse(sy1);
                    const sy0r = @bitReverse(sy0);
                    const sy2 = sy0 ^ sy1;
                    const sy2r = sy0r ^ sy1r;

                    const sz0 = clmul(sy0, st.h0);
                    const sz1 = clmul(sy1, st.h1);
                    const sz2 = clmul(sy2, st.h2) ^ sz0 ^ sz1;
                    const sz0h = clmul(sy0r, st.h0r);
                    const sz1h = clmul(sy1r, st.h1r);
                    const sz2h = clmul(sy2r, st.h2r) ^ sz0h ^ sz1h;

                    // ((B0 * H^2) + B1 * H) (mod M)

                    z0 ^= sz0;
                    z1 ^= sz1;
                    z2 ^= sz2;
                    z0h ^= sz0h;
                    z1h ^= sz1h;
                    z2h ^= sz2h;
                    z0h = @bitReverse(z0h) >> 1;
                    z1h = @bitReverse(z1h) >> 1;
                    z2h = @bitReverse(z2h) >> 1;

                    var v3 = z1h;
                    var v2 = z1 ^ z2h;
                    var v1 = z0h ^ z2;
                    var v0 = z0;

                    v3 = (v3 << 1) | (v2 >> 63);
                    v2 = (v2 << 1) | (v1 >> 63);
                    v1 = (v1 << 1) | (v0 >> 63);
                    v0 = (v0 << 1);

                    v2 ^= v0 ^ (v0 >> 1) ^ (v0 >> 2) ^ (v0 >> 7);
                    v1 ^= (v0 << 63) ^ (v0 << 62) ^ (v0 << 57);
                    y1 = v3 ^ v1 ^ (v1 >> 1) ^ (v1 >> 2) ^ (v1 >> 7);
                    y0 = v2 ^ (v1 << 63) ^ (v1 << 62) ^ (v1 << 57);
                }
            }

            // single block

            while (i + 16 <= msg.len) : (i += 16) {
                y1 ^= mem.readIntBig(u64, msg[i..][0..8]);
                y0 ^= mem.readIntBig(u64, msg[i..][8..16]);

                const y1r = @bitReverse(y1);
                const y0r = @bitReverse(y0);
                const y2 = y0 ^ y1;
                const y2r = y0r ^ y1r;

                const z0 = clmul(y0, st.h0);
                const z1 = clmul(y1, st.h1);
                var z2 = clmul(y2, st.h2) ^ z0 ^ z1;
                var z0h = clmul(y0r, st.h0r);
                var z1h = clmul(y1r, st.h1r);
                var z2h = clmul(y2r, st.h2r) ^ z0h ^ z1h;
                z0h = @bitReverse(z0h) >> 1;
                z1h = @bitReverse(z1h) >> 1;
                z2h = @bitReverse(z2h) >> 1;

                // shift & reduce

                var v3 = z1h;
                var v2 = z1 ^ z2h;
                var v1 = z0h ^ z2;
                var v0 = z0;

                v3 = (v3 << 1) | (v2 >> 63);
                v2 = (v2 << 1) | (v1 >> 63);
                v1 = (v1 << 1) | (v0 >> 63);
                v0 = (v0 << 1);

                v2 ^= v0 ^ (v0 >> 1) ^ (v0 >> 2) ^ (v0 >> 7);
                v1 ^= (v0 << 63) ^ (v0 << 62) ^ (v0 << 57);
                y1 = v3 ^ v1 ^ (v1 >> 1) ^ (v1 >> 2) ^ (v1 >> 7);
                y0 = v2 ^ (v1 << 63) ^ (v1 << 62) ^ (v1 << 57);
            }
            st.y1 = y1;
            st.y0 = y0;
        }

        pub fn update(st: *Ghash, m: []const u8) void {
            var mb = m;

            if (st.leftover > 0) {
                const want = math.min(block_length - st.leftover, mb.len);
                const mc = mb[0..want];
                for (mc) |x, i| {
                    st.buf[st.leftover + i] = x;
                }
                mb = mb[want..];
                st.leftover += want;
                if (st.leftover < block_length) {
                    return;
                }
                st.blocks(&st.buf);
                st.leftover = 0;
            }
            if (mb.len >= block_length) {
                const want = mb.len & ~(block_length - 1);
                st.blocks(mb[0..want]);
                mb = mb[want..];
            }
            if (mb.len > 0) {
                for (mb) |x, i| {
                    st.buf[st.leftover + i] = x;
                }
                st.leftover += mb.len;
            }
        }

        /// Zero-pad to align the next input to the first byte of a block
        pub fn pad(st: *Ghash) void {
            if (st.leftover == 0) {
                return;
            }
            var i = st.leftover;
            while (i < block_length) : (i += 1) {
                st.buf[i] = 0;
            }
            st.blocks(&st.buf);
            st.leftover = 0;
        }

        pub fn final(st: *Ghash, out: *[mac_length]u8) void {
            st.pad();
            mem.writeIntBig(u64, out[0..8], st.y1);
            mem.writeIntBig(u64, out[8..16], st.y0);

            utils.secureZero(u8, @ptrCast([*]u8, st)[0..@sizeOf(Ghash)]);
        }

        pub fn create(out: *[mac_length]u8, msg: []const u8, key: *const [key_length]u8) void {
            var st = Ghash.init(key);
            st.update(msg);
            st.final(out);
        }
    };

    const htest = @import("test.zig");

    test "ghash" {
        const key = [_]u8{0x42} ** 16;
        const m = [_]u8{0x69} ** 256;

        var st = Ghash.init(&key);
        st.update(&m);
        var out: [16]u8 = undefined;
        st.final(&out);
        try htest.assertEqual("889295fa746e8b174bf4ec80a65dea41", &out);

        st = Ghash.init(&key);
        st.update(m[0..100]);
        st.update(m[100..]);
        st.final(&out);
        try htest.assertEqual("889295fa746e8b174bf4ec80a65dea41", &out);
    }