const std = @import("std");
const builtin = @import("builtin");
const endian = builtin.cpu.arch.endian();
const testing = @import("std").testing;
const ptr_size = @sizeOf(usize);

test "type pun signed and unsigned as single pointer" {
    comptime {
        var x: u32 = 0;
        const y = @as(*i32, @ptrCast(&x));
        y.* = -1;
        try testing.expectEqual(@as(u32, 0xFFFFFFFF), x);
    }
}

test "type pun signed and unsigned as many pointer" {
    comptime {
        var x: u32 = 0;
        const y = @as([*]i32, @ptrCast(&x));
        y[0] = -1;
        try testing.expectEqual(@as(u32, 0xFFFFFFFF), x);
    }
}

test "type pun signed and unsigned as array pointer" {
    comptime {
        var x: u32 = 0;
        const y = @as(*[1]i32, @ptrCast(&x));
        y[0] = -1;
        try testing.expectEqual(@as(u32, 0xFFFFFFFF), x);
    }
}

test "type pun signed and unsigned as offset many pointer" {
    comptime {
        var x: [11]u32 = undefined;
        var y: [*]i32 = @ptrCast(&x[10]);
        y -= 10;
        y[10] = -1;
        try testing.expectEqual(@as(u32, 0xFFFFFFFF), x[10]);
    }
}

test "type pun signed and unsigned as array pointer with pointer arithemtic" {
    comptime {
        var x: [11]u32 = undefined;
        const y = @as([*]i32, @ptrCast(&x[10])) - 10;
        const z: *[15]i32 = y[0..15];
        z[10] = -1;
        try testing.expectEqual(@as(u32, 0xFFFFFFFF), x[10]);
    }
}

test "type pun value and struct" {
    comptime {
        const StructOfU32 = extern struct { x: u32 };
        var inst: StructOfU32 = .{ .x = 0 };
        @as(*i32, @ptrCast(&inst.x)).* = -1;
        try testing.expectEqual(@as(u32, 0xFFFFFFFF), inst.x);
        @as(*i32, @ptrCast(&inst)).* = -2;
        try testing.expectEqual(@as(u32, 0xFFFFFFFE), inst.x);
    }
}

fn bigToNativeEndian(comptime T: type, v: T) T {
    return if (endian == .big) v else @byteSwap(v);
}
test "type pun endianness" {
    if (builtin.zig_backend == .stage2_aarch64) return error.SkipZigTest; // TODO
    if (builtin.zig_backend == .stage2_sparc64) return error.SkipZigTest; // TODO

    comptime {
        const StructOfBytes = extern struct { x: [4]u8 };
        var inst: StructOfBytes = .{ .x = [4]u8{ 0, 0, 0, 0 } };
        const structPtr = @as(*align(1) u32, @ptrCast(&inst));
        const arrayPtr = @as(*align(1) u32, @ptrCast(&inst.x));
        inst.x[0] = 0xFE;
        inst.x[2] = 0xBE;
        try testing.expectEqual(bigToNativeEndian(u32, 0xFE00BE00), structPtr.*);
        try testing.expectEqual(bigToNativeEndian(u32, 0xFE00BE00), arrayPtr.*);
        structPtr.* = bigToNativeEndian(u32, 0xDEADF00D);
        try testing.expectEqual(bigToNativeEndian(u32, 0xDEADF00D), structPtr.*);
        try testing.expectEqual(bigToNativeEndian(u32, 0xDEADF00D), arrayPtr.*);
        try testing.expectEqual(@as(u8, 0xDE), inst.x[0]);
        try testing.expectEqual(@as(u8, 0xAD), inst.x[1]);
        try testing.expectEqual(@as(u8, 0xF0), inst.x[2]);
        try testing.expectEqual(@as(u8, 0x0D), inst.x[3]);
    }
}

const Bits = packed struct {
    // Note: This struct has only single byte words so it
    // doesn't need to be byte swapped.
    p0: u1,
    p1: u4,
    p2: u3,
    p3: u2,
    p4: u6,
    p5: u8,
    p6: u7,
    p7: u1,
};
const ShuffledBits = packed struct {
    p1: u4,
    p3: u2,
    p7: u1,
    p0: u1,
    p5: u8,
    p2: u3,
    p6: u7,
    p4: u6,
};
fn shuffle(ptr: usize, comptime From: type, comptime To: type) usize {
    if (@sizeOf(From) != @sizeOf(To))
        @compileError("Mismatched sizes! " ++ @typeName(From) ++ " and " ++ @typeName(To) ++ " must have the same size!");
    const array_len = @divExact(ptr_size, @sizeOf(From));
    var result: usize = 0;
    const pSource = @as(*align(1) const [array_len]From, @ptrCast(&ptr));
    const pResult = @as(*align(1) [array_len]To, @ptrCast(&result));
    var i: usize = 0;
    while (i < array_len) : (i += 1) {
        inline for (@typeInfo(To).@"struct".fields) |f| {
            @field(pResult[i], f.name) = @field(pSource[i], f.name);
        }
    }
    return result;
}

fn doTypePunBitsTest(as_bits: *Bits) !void {
    const as_u32 = @as(*align(1) u32, @ptrCast(as_bits));
    const as_bytes = @as(*[4]u8, @ptrCast(as_bits));
    as_u32.* = bigToNativeEndian(u32, 0xB0A7DEED);
    try testing.expectEqual(@as(u1, 0x00), as_bits.p0);
    try testing.expectEqual(@as(u4, 0x08), as_bits.p1);
    try testing.expectEqual(@as(u3, 0x05), as_bits.p2);
    try testing.expectEqual(@as(u2, 0x03), as_bits.p3);
    try testing.expectEqual(@as(u6, 0x29), as_bits.p4);
    try testing.expectEqual(@as(u8, 0xDE), as_bits.p5);
    try testing.expectEqual(@as(u7, 0x6D), as_bits.p6);
    try testing.expectEqual(@as(u1, 0x01), as_bits.p7);

    as_bits.p6 = 0x2D;
    as_bits.p1 = 0x0F;
    try testing.expectEqual(bigToNativeEndian(u32, 0xBEA7DEAD), as_u32.*);

    // clobbering one bit doesn't clobber the word
    as_bits.p7 = undefined;
    try testing.expectEqual(@as(u7, 0x2D), as_bits.p6);
    // even when read as a whole
    const u = as_u32.*;
    _ = u; // u is undefined
    try testing.expectEqual(@as(u7, 0x2D), as_bits.p6);
    // or if a field which shares the byte is modified
    as_bits.p6 = 0x6D;
    try testing.expectEqual(@as(u7, 0x6D), as_bits.p6);

    // but overwriting the undefined will clear it
    as_bytes[3] = 0xAF;
    try testing.expectEqual(bigToNativeEndian(u32, 0xBEA7DEAF), as_u32.*);
}

test "type pun bits" {
    if (true) {
        // TODO: currently, marking one bit of `Bits` as `undefined` does
        // mark the whole value as `undefined`, since the pointer interpretation
        // logic reads it back in as a `u32`, which is partially-undef and thus
        // has value `undefined`. We need an improved comptime memory representation
        // to make this work.
        return error.SkipZigTest;
    }
    comptime {
        var v: u32 = undefined;
        try doTypePunBitsTest(@as(*Bits, @ptrCast(&v)));
    }
}

const imports = struct {
    var global_u32: u32 = 0;
};

// Make sure lazy values work on their own, before getting into more complex tests
test "basic pointer preservation" {
    if (true) {
        // TODO https://github.com/ziglang/zig/issues/9646
        return error.SkipZigTest;
    }

    comptime {
        const lazy_address = @intFromPtr(&imports.global_u32);
        try testing.expectEqual(@intFromPtr(&imports.global_u32), lazy_address);
        try testing.expectEqual(&imports.global_u32, @as(*u32, @ptrFromInt(lazy_address)));
    }
}

test "byte copy preserves linker value" {
    if (true) {
        // TODO https://github.com/ziglang/zig/issues/9646
        return error.SkipZigTest;
    }

    const ct_value = comptime blk: {
        const lazy = &imports.global_u32;
        var result: *u32 = undefined;
        const pSource = @as(*const [ptr_size]u8, @ptrCast(&lazy));
        const pResult = @as(*[ptr_size]u8, @ptrCast(&result));
        var i: usize = 0;
        while (i < ptr_size) : (i += 1) {
            pResult[i] = pSource[i];
            try testing.expectEqual(pSource[i], pResult[i]);
        }
        try testing.expectEqual(&imports.global_u32, result);
        break :blk result;
    };

    try testing.expectEqual(&imports.global_u32, ct_value);
}

test "unordered byte copy preserves linker value" {
    if (true) {
        // TODO https://github.com/ziglang/zig/issues/9646
        return error.SkipZigTest;
    }

    const ct_value = comptime blk: {
        const lazy = &imports.global_u32;
        var result: *u32 = undefined;
        const pSource = @as(*const [ptr_size]u8, @ptrCast(&lazy));
        const pResult = @as(*[ptr_size]u8, @ptrCast(&result));
        if (ptr_size > 8) @compileError("This array needs to be expanded for platform with very big pointers");
        const shuffled_indices = [_]usize{ 4, 5, 2, 6, 1, 3, 0, 7 };
        for (shuffled_indices) |i| {
            pResult[i] = pSource[i];
            try testing.expectEqual(pSource[i], pResult[i]);
        }
        try testing.expectEqual(&imports.global_u32, result);
        break :blk result;
    };

    try testing.expectEqual(&imports.global_u32, ct_value);
}

test "shuffle chunks of linker value" {
    if (true) {
        // TODO https://github.com/ziglang/zig/issues/9646
        return error.SkipZigTest;
    }

    const lazy_address = @intFromPtr(&imports.global_u32);
    const shuffled1_rt = shuffle(lazy_address, Bits, ShuffledBits);
    const unshuffled1_rt = shuffle(shuffled1_rt, ShuffledBits, Bits);
    try testing.expectEqual(lazy_address, unshuffled1_rt);
    const shuffled1_ct = comptime shuffle(lazy_address, Bits, ShuffledBits);
    const shuffled1_ct_2 = comptime shuffle(lazy_address, Bits, ShuffledBits);
    try comptime testing.expectEqual(shuffled1_ct, shuffled1_ct_2);
    const unshuffled1_ct = comptime shuffle(shuffled1_ct, ShuffledBits, Bits);
    try comptime testing.expectEqual(lazy_address, unshuffled1_ct);
    try testing.expectEqual(shuffled1_ct, shuffled1_rt);
}

test "dance on linker values" {
    if (true) {
        // TODO https://github.com/ziglang/zig/issues/9646
        return error.SkipZigTest;
    }

    comptime {
        var arr: [2]usize = undefined;
        arr[0] = @intFromPtr(&imports.global_u32);
        arr[1] = @intFromPtr(&imports.global_u32);

        const weird_ptr = @as([*]Bits, @ptrCast(@as([*]u8, @ptrCast(&arr)) + @sizeOf(usize) - 3));
        try doTypePunBitsTest(&weird_ptr[0]);
        if (ptr_size > @sizeOf(Bits))
            try doTypePunBitsTest(&weird_ptr[1]);

        const arr_bytes: *[2][ptr_size]u8 = @ptrCast(&arr);

        var rebuilt_bytes: [ptr_size]u8 = undefined;
        var i: usize = 0;
        while (i < ptr_size - 3) : (i += 1) {
            rebuilt_bytes[i] = arr_bytes[0][i];
        }
        while (i < ptr_size) : (i += 1) {
            rebuilt_bytes[i] = arr_bytes[1][i];
        }

        try testing.expectEqual(&imports.global_u32, @as(*u32, @ptrFromInt(@as(usize, @bitCast(rebuilt_bytes)))));
    }
}

test "offset array ptr by element size" {
    comptime {
        const VirtualStruct = struct { x: u32 };
        var arr: [4]VirtualStruct = .{
            .{ .x = bigToNativeEndian(u32, 0x0004080c) },
            .{ .x = bigToNativeEndian(u32, 0x0105090d) },
            .{ .x = bigToNativeEndian(u32, 0x02060a0e) },
            .{ .x = bigToNativeEndian(u32, 0x03070b0f) },
        };

        const buf: [*]align(@alignOf(VirtualStruct)) u8 = @ptrCast(&arr);

        const second_element: *VirtualStruct = @ptrCast(buf + 2 * @sizeOf(VirtualStruct));
        try testing.expectEqual(bigToNativeEndian(u32, 0x02060a0e), second_element.x);
    }
}

test "offset instance by field size" {
    if (true) {
        // TODO https://github.com/ziglang/zig/issues/9646
        return error.SkipZigTest;
    }

    comptime {
        const VirtualStruct = struct { x: u32, y: u32, z: u32, w: u32 };
        var inst = VirtualStruct{ .x = 0, .y = 1, .z = 2, .w = 3 };

        var ptr = @intFromPtr(&inst);
        ptr -= 4;
        ptr += @offsetOf(VirtualStruct, "x");
        try testing.expectEqual(@as(u32, 0), @as([*]u32, @ptrFromInt(ptr))[1]);
        ptr -= @offsetOf(VirtualStruct, "x");
        ptr += @offsetOf(VirtualStruct, "y");
        try testing.expectEqual(@as(u32, 1), @as([*]u32, @ptrFromInt(ptr))[1]);
        ptr = ptr - @offsetOf(VirtualStruct, "y") + @offsetOf(VirtualStruct, "z");
        try testing.expectEqual(@as(u32, 2), @as([*]u32, @ptrFromInt(ptr))[1]);
        ptr = @intFromPtr(&inst.z) - 4 - @offsetOf(VirtualStruct, "z");
        ptr += @offsetOf(VirtualStruct, "w");
        try testing.expectEqual(@as(u32, 3), @as(*u32, @ptrFromInt(ptr + 4)).*);
    }
}

test "offset field ptr by enclosing array element size" {
    if (true) {
        // TODO https://github.com/ziglang/zig/issues/9646
        return error.SkipZigTest;
    }

    comptime {
        const VirtualStruct = struct { x: u32 };
        var arr: [4]VirtualStruct = .{
            .{ .x = bigToNativeEndian(u32, 0x0004080c) },
            .{ .x = bigToNativeEndian(u32, 0x0105090d) },
            .{ .x = bigToNativeEndian(u32, 0x02060a0e) },
            .{ .x = bigToNativeEndian(u32, 0x03070b0f) },
        };

        var i: usize = 0;
        while (i < 4) : (i += 1) {
            var ptr: [*]u8 = @ptrCast(&arr[0]);
            ptr += i;
            ptr += @offsetOf(VirtualStruct, "x");
            var j: usize = 0;
            while (j < 4) : (j += 1) {
                const base = ptr + j * @sizeOf(VirtualStruct);
                try testing.expectEqual(@as(u8, @intCast(i * 4 + j)), base[0]);
            }
        }
    }
}

test "accessing reinterpreted memory of parent object" {
    if (builtin.zig_backend == .stage2_aarch64) return error.SkipZigTest; // TODO
    if (builtin.zig_backend == .stage2_sparc64) return error.SkipZigTest; // TODO

    const S = extern struct {
        a: f32,
        b: [4]u8,
        c: f32,
    };
    const expected = if (endian == .little) 102 else 38;

    comptime {
        const x = S{
            .a = 1.5,
            .b = [_]u8{ 1, 2, 3, 4 },
            .c = 2.6,
        };
        const ptr = &x.b[0];
        const b = @as([*c]const u8, @ptrCast(ptr))[5];
        try testing.expect(b == expected);
    }
}

test "bitcast packed union to integer" {
    const U = packed union {
        x: i2,
        y: u2,
    };

    comptime {
        const a: U = .{ .x = -1 };
        const b: U = .{ .y = 2 };
        const cast_a: u2 = @bitCast(a);
        const cast_b: u2 = @bitCast(b);

        try testing.expectEqual(@as(u2, 3), cast_a);
        try testing.expectEqual(@as(u2, 2), cast_b);
    }
}

test "mutate entire slice at comptime" {
    comptime {
        var buf: [3]u8 = undefined;
        const x: [2]u8 = .{ 1, 2 }; // Avoid RLS
        buf[1..3].* = x;
    }
}

test "dereference undefined pointer to zero-bit type" {
    const p0: *void = undefined;
    try testing.expectEqual({}, p0.*);

    const p1: *[0]u32 = undefined;
    try testing.expect(p1.*.len == 0);
}

test "type pun extern struct" {
    const S = extern struct { f: u8 };
    comptime var s = S{ .f = 123 };
    @as(*u8, @ptrCast(&s)).* = 72;
    try testing.expectEqual(@as(u8, 72), s.f);
}

test "type pun @ptrFromInt" {
    const p: *u8 = @ptrFromInt(42);
    // note that expectEqual hides the bug
    try testing.expect(@as(*const [*]u8, @ptrCast(&p)).* == @as([*]u8, @ptrFromInt(42)));
}

test "type pun null pointer-like optional" {
    const p: ?*u8 = null;
    // note that expectEqual hides the bug
    try testing.expect(@as(*const ?*i8, @ptrCast(&p)).* == null);
}

test "write empty array to end" {
    comptime var array: [5]u8 = "hello".*;
    array[5..5].* = .{};
    array[5..5].* = [0]u8{};
    array[5..5].* = [_]u8{};
    comptime std.debug.assert(std.mem.eql(u8, "hello", &array));
}

fn doublePtrTest() !void {
    var a: u32 = 0;
    const ptr = &a;
    const double_ptr = &ptr;
    setDoublePtr(double_ptr, 1);
    setDoublePtr(double_ptr, 2);
    setDoublePtr(double_ptr, 1);
    try std.testing.expect(a == 1);
}
fn setDoublePtr(ptr: *const *const u32, value: u32) void {
    setPtr(ptr.*, value);
}
fn setPtr(ptr: *const u32, value: u32) void {
    const mut_ptr: *u32 = @constCast(ptr);
    mut_ptr.* = value;
}
test "double pointer can mutate comptime state" {
    try comptime doublePtrTest();
}

fn GenericIntApplier(
    comptime Context: type,
    comptime applyFn: fn (context: Context, arg: u32) void,
) type {
    return struct {
        context: Context,

        const Self = @This();

        inline fn any(self: *const Self) IntApplier {
            return .{
                .context = @ptrCast(&self.context),
                .applyFn = typeErasedApplyFn,
            };
        }

        fn typeErasedApplyFn(context: *const anyopaque, arg: u32) void {
            const ptr: *const Context = @alignCast(@ptrCast(context));
            applyFn(ptr.*, arg);
        }
    };
}
const IntApplier = struct {
    context: *const anyopaque,
    applyFn: *const fn (context: *const anyopaque, arg: u32) void,

    fn apply(ia: IntApplier, arg: u32) void {
        ia.applyFn(ia.context, arg);
    }
};
const Accumulator = struct {
    value: u32,

    const Applier = GenericIntApplier(*u32, add);

    fn applier(a: *Accumulator) Applier {
        return .{ .context = &a.value };
    }

    fn add(context: *u32, arg: u32) void {
        context.* += arg;
    }
};
fn fieldPtrTest() u32 {
    var a: Accumulator = .{ .value = 0 };
    const applier = a.applier();
    applier.any().apply(1);
    applier.any().apply(1);
    return a.value;
}
test "pointer in aggregate field can mutate comptime state" {
    try comptime std.testing.expect(fieldPtrTest() == 2);
}

test "comptime store of extern struct with void field" {
    comptime {
        var x: extern struct { a: u8, b: void } = undefined;
        x = .{ .a = 123, .b = {} };
        std.debug.assert(x.a == 123);
    }
}

test "comptime store of extern struct with void field into array" {
    comptime {
        var x: [3]extern struct { a: u8, b: void } = undefined;
        x[1] = .{ .a = 123, .b = {} };
        std.debug.assert(x[1].a == 123);
    }
}

test "comptime store of packed struct with void field" {
    comptime {
        var x: packed struct { a: u8, b: void } = undefined;
        x = .{ .a = 123, .b = {} };
        std.debug.assert(x.a == 123);
    }
}

test "comptime store of packed struct with void field into array" {
    comptime {
        var x: [3]packed struct { a: u8, b: void } = undefined;
        x[1] = .{ .a = 123, .b = {} };
        std.debug.assert(x[1].a == 123);
    }
}

test "comptime store of reinterpreted zero-bit type" {
    const S = struct {
        fn doTheTest(comptime T: type) void {
            comptime var buf: T = undefined;
            const ptr: *void = @ptrCast(&buf);
            ptr.* = {};
        }
    };
    S.doTheTest(void);
    S.doTheTest(u0);
    S.doTheTest([0]u8);
    S.doTheTest([1]u0);
    S.doTheTest([5]u0);
    S.doTheTest([5]void);
    S.doTheTest(packed struct(u0) {});
}

test "comptime store to extern struct reinterpreted as byte array" {
    const T = extern struct {
        x: u32,
        y: f32,
        z: [2]void,
    };
    comptime var val: T = undefined;

    const bytes: *[@sizeOf(T)]u8 = @ptrCast(&val);
    @memset(bytes, 0);

    comptime std.debug.assert(val.x == 0);
}
