const c = @cImport({
    @cInclude("stdlib.h");
});

const std = @import("std");

const allocator = std.heap.page_allocator;

const common = @import("common.zig");

const cpu = @cImport({
    @cInclude("concrete-cpu.h");
});

fn test3(csprng: *cpu.Csprng) !void {
    const polynomial_size: usize = 1024;
    const glwe_dim: usize = 1;
    const small_dim: usize = 585;
    const level_bsk: usize = 2;
    const base_log_bsk: usize = 10;
    const level_ksk: usize = 7;
    const base_log_ksk: usize = 4;

    const variance = std.math.pow(f64, 2, -2 * 60);

    const number_of_bits_of_message = 5;
    var raw_fft = c.aligned_alloc(cpu.CONCRETE_FFT_ALIGN, cpu.CONCRETE_FFT_SIZE);
    const fft = @ptrCast(*cpu.Fft, raw_fft);

    cpu.concrete_cpu_construct_concrete_fft(fft, polynomial_size);

    const big_dim = glwe_dim * polynomial_size;

    const small_sk = try allocator.alloc(u64, small_dim);
    cpu.concrete_cpu_init_secret_key_u64(small_sk.ptr, small_dim, csprng, &cpu.CONCRETE_CSPRNG_VTABLE);

    const big_sk = try allocator.alloc(u64, big_dim);
    cpu.concrete_cpu_init_secret_key_u64(big_sk.ptr, big_dim, csprng, &cpu.CONCRETE_CSPRNG_VTABLE);

    const bsk_f = try common.new_bsk(
        csprng,
        small_dim,
        glwe_dim,
        polynomial_size,
        level_bsk,
        base_log_bsk,
        variance,
        small_sk,
        big_sk,
        fft,
    );
    defer allocator.free(bsk_f);

    const ksk_size = cpu.concrete_cpu_keyswitch_key_size_u64(level_ksk, base_log_ksk, big_dim, small_dim);

    const ksk = try allocator.alloc(u64, ksk_size);
    defer allocator.free(ksk);

    cpu.concrete_cpu_init_lwe_keyswitch_key_u64(
        ksk.ptr,
        big_sk.ptr,
        small_sk.ptr,
        big_dim,
        small_dim,
        level_ksk,
        base_log_ksk,
        variance,
        csprng,
        &cpu.CONCRETE_CSPRNG_VTABLE,
    );

    const delta_log = 64 - number_of_bits_of_message;

    // 19 in binary is 10011, so has the high bit, low bit set and is not symetrical
    const val: u64 = 19;

    std.debug.assert(1 << number_of_bits_of_message > val);
    const message = val << delta_log;

    // We will extract all bits
    const number_of_bits_to_extract = number_of_bits_of_message;

    const in_ct = try allocator.alloc(u64, big_dim + 1);
    defer allocator.free(in_ct);

    cpu.concrete_cpu_encrypt_lwe_ciphertext_u64(
        big_sk.ptr,
        in_ct.ptr,
        message,
        big_dim,
        variance,
        csprng,
        &cpu.CONCRETE_CSPRNG_VTABLE,
    );

    const out_cts = try allocator.alloc(u64, (small_dim + 1) * number_of_bits_to_extract);
    defer allocator.free(out_cts);

    var stack_align: usize = 0;
    var stack_size: usize = 0;

    try std.testing.expect(cpu.concrete_cpu_extract_bit_lwe_ciphertext_u64_scratch(
        &stack_size,
        &stack_align,
        small_dim,
        big_dim,
        glwe_dim,
        polynomial_size,
        fft,
    ) == 0);

    const stack = @ptrCast([*]u8, c.aligned_alloc(stack_align, stack_size) orelse unreachable)[0..stack_size];
    defer c.free(stack.ptr);

    cpu.concrete_cpu_extract_bit_lwe_ciphertext_u64(
        out_cts.ptr,
        in_ct.ptr,
        bsk_f.ptr,
        ksk.ptr,
        small_dim,
        number_of_bits_to_extract,
        big_dim,
        number_of_bits_to_extract,
        delta_log,
        level_bsk,
        base_log_bsk,
        glwe_dim,
        polynomial_size,
        small_dim,
        level_ksk,
        base_log_ksk,
        big_dim,
        small_dim,
        fft,
        stack.ptr,
        stack.len,
    );

    var i: u64 = 0;
    while (i < number_of_bits_to_extract) {
        const expected = (val >> @intCast(u6, number_of_bits_of_message - 1 - i)) & 1;
        var decrypted: u64 = 0;
        cpu.concrete_cpu_decrypt_lwe_ciphertext_u64(small_sk.ptr, out_cts[(small_dim + 1) * i ..].ptr, small_dim, &decrypted);

        const rounded = common.closest_representable(decrypted, 1, 1);
        const decoded = rounded >> 63;

        std.debug.assert(decoded == expected);
        i += 1;
    }
}

test "bit_extract" {
    var raw_csprng = c.aligned_alloc(cpu.CONCRETE_CSPRNG_ALIGN, cpu.CONCRETE_CSPRNG_SIZE);
    defer c.free(raw_csprng);
    const csprng = @ptrCast(*cpu.Csprng, raw_csprng);
    cpu.concrete_cpu_construct_concrete_csprng(
        csprng,
        cpu.Uint128{ .little_endian_bytes = [_]u8{1} ** 16 },
    );
    defer cpu.concrete_cpu_destroy_concrete_csprng(csprng);

    try test3(csprng);
}
