const lib = @import("lib");
const Allocator = lib.Allocator;
const assert = lib.assert;
const ELF = lib.ELF(64);
const log = lib.log;
const Spinlock = lib.Spinlock;
const bootloader = @import("bootloader");
const privileged = @import("privileged");
const panic = cpu.panic;
const PageAllocator = cpu.PageAllocator;
const x86_64 = privileged.arch.x86_64;
const APIC = x86_64.APIC;
const paging = x86_64.paging;
const cr0 = x86_64.registers.cr0;
const cr3 = x86_64.registers.cr3;
const cr4 = x86_64.registers.cr4;
const PhysicalAddress = lib.PhysicalAddress;
const PhysicalMemoryRegion = lib.PhysicalMemoryRegion;
const VirtualAddress = lib.VirtualAddress;
const VirtualMemoryRegion = lib.VirtualMemoryRegion;

const cpu = @import("cpu");
const Heap = cpu.Heap;
const VirtualAddressSpace = cpu.VirtualAddressSpace;

const init = @import("./x86/64/init.zig");
pub const syscall = @import("./x86/64/syscall.zig");
pub const entryPoint = init.entryPoint;

const rise = @import("rise");

var writer_lock: Spinlock = .released;

pub const Registers = extern struct {
    r15: u64,
    r14: u64,
    r13: u64,
    r12: u64,
    rbp: u64,
    rbx: u64,
    r11: u64,
    r10: u64,
    r9: u64,
    r8: u64,
    rax: u64,
    rcx: u64,
    rdx: u64,
    rsi: u64,
    rdi: u64,
    syscall_number_or_error_code: u64,
    rip: u64,
    cs: u64,
    rflags: lib.arch.x86_64.registers.RFLAGS,
    rsp: u64,
    ss: u64,
};

const interrupt_kind: u32 = 0;

export fn interruptHandler(regs: *const InterruptRegisters, interrupt_number: u8) void {
    switch (interrupt_number) {
        local_timer_vector => {
            APIC.write(.eoi, 0);
            nextTimer(10);
        },
        else => cpu.panicFromInstructionPointerAndFramePointer(regs.rip, regs.rbp, "Exception: 0x{x}", .{interrupt_number}),
    }
}

const InterruptRegisters = extern struct {
    r15: u64,
    r14: u64,
    r13: u64,
    r12: u64,
    rbp: u64,
    rbx: u64,
    r11: u64,
    r10: u64,
    r9: u64,
    r8: u64,
    rax: u64,
    rcx: u64,
    rdx: u64,
    rsi: u64,
    rdi: u64,
    error_code: u64,
    rip: u64,
    cs: u64,
    rflags: u64,
    rsp: u64,
    ss: u64,
};

const local_timer_vector = 0xef;
pub export var ticks_per_ms: privileged.arch.x86_64.TicksPerMS = undefined;
pub inline fn nextTimer(ms: u32) void {
    APIC.write(.lvt_timer, local_timer_vector | (1 << 17));
    APIC.write(.timer_initcnt, ticks_per_ms.lapic * ms);
}
pub const kpti = true;
pub const pcid = false;
pub const smap = false;
pub const invariant_tsc = false;
pub const capability_address_space_size = 1 * lib.gb;
pub const capability_address_space_start = capability_address_space_stack_top - capability_address_space_size;
pub const capability_address_space_stack_top = 0xffff_ffff_8000_0000;
pub const capability_address_space_stack_size = privileged.default_stack_size;
pub const capability_address_space_stack_alignment = lib.arch.valid_page_sizes[0];
pub const capability_address_space_stack_address = VirtualAddress.new(capability_address_space_stack_top - capability_address_space_stack_size);
pub const code_64 = @offsetOf(GDT, "code_64");
pub const data_64 = @offsetOf(GDT, "data_64");
pub const user_code_64 = @offsetOf(GDT, "user_code_64");
pub const user_data_64 = @offsetOf(GDT, "user_data_64");
pub const tss_selector = @offsetOf(GDT, "tss_descriptor");
pub const user_code_selector = user_code_64 | user_dpl;
pub const user_data_selector = user_data_64 | user_dpl;
pub const user_dpl = 3;

pub const GDT = extern struct {
    null: Entry = GDT.Entry.null_entry, // 0x00
    code_16: Entry = GDT.Entry.code_16, // 0x08
    data_16: Entry = GDT.Entry.data_16, // 0x10
    code_32: Entry = GDT.Entry.code_32, // 0x18
    data_32: Entry = GDT.Entry.data_32, // 0x20
    code_64: u64 = 0x00A09A0000000000, // 0x28
    data_64: u64 = 0x0000920000000000, // 0x30
    user_data_64: u64 = @as(u64, 0x0000920000000000) | (3 << 45), //GDT.Entry.user_data_64, // 0x38
    user_code_64: u64 = @as(u64, 0x00A09A0000000000) | (3 << 45), //GDT.Entry.user_code_64, // 0x40
    tss_descriptor: TSS.Descriptor = undefined, // 0x48

    const Entry = privileged.arch.x86_64.GDT.Entry;

    pub const Descriptor = privileged.arch.x86_64.GDT.Descriptor;

    comptime {
        const entry_count = 9;
        const target_size = entry_count * @sizeOf(Entry) + @sizeOf(TSS.Descriptor);

        assert(@sizeOf(GDT) == target_size);
        assert(@offsetOf(GDT, "code_64") == 0x28);
        assert(@offsetOf(GDT, "data_64") == 0x30);
        assert(@offsetOf(GDT, "user_data_64") == 0x38);
        assert(@offsetOf(GDT, "user_code_64") == 0x40);
        assert(@offsetOf(GDT, "tss_descriptor") == entry_count * @sizeOf(Entry));
    }

    pub fn getDescriptor(global_descriptor_table: *const GDT) GDT.Descriptor {
        return .{
            .limit = @sizeOf(GDT) - 1,
            .address = @ptrToInt(global_descriptor_table),
        };
    }
};

pub const SystemSegmentDescriptor = extern struct {
    const Type = enum(u4) {
        ldt = 0b0010,
        tss_available = 0b1001,
        tss_busy = 0b1011,
        call_gate = 0b1100,
        interrupt_gate = 0b1110,
        trap_gate = 0b1111,
    };
};

pub const TSS = extern struct {
    reserved0: u32 = 0,
    rsp: [3]u64 align(4) = [3]u64{ 0, 0, 0 },
    reserved1: u64 align(4) = 0,
    IST: [7]u64 align(4) = [7]u64{ 0, 0, 0, 0, 0, 0, 0 },
    reserved3: u64 align(4) = 0,
    reserved4: u16 = 0,
    IO_map_base_address: u16 = 104,

    comptime {
        assert(@sizeOf(TSS) == 104);
    }

    pub const Descriptor = extern struct {
        limit_low: u16,
        base_low: u16,
        base_mid_low: u8,
        access: Access,
        attributes: Attributes,
        base_mid_high: u8,
        base_high: u32,
        reserved: u32 = 0,

        pub const Access = packed struct(u8) {
            type: SystemSegmentDescriptor.Type,
            reserved: u1 = 0,
            dpl: u2,
            present: bool,
        };

        pub const Attributes = packed struct(u8) {
            limit: u4,
            available_for_system_software: bool,
            reserved: u2 = 0,
            granularity: bool,
        };

        comptime {
            assert(@sizeOf(TSS.Descriptor) == 0x10);
        }
    };

    pub fn getDescriptor(tss_struct: *const TSS, offset: u64) Descriptor {
        const address = @ptrToInt(tss_struct) + offset;
        return Descriptor{
            .low = .{
                .limit_low = @truncate(u16, @sizeOf(TSS) - 1),
                .base_low = @truncate(u16, address),
                .base_low_mid = @truncate(u8, address >> 16),
                .type = 0b1001,
                .descriptor_privilege_level = 0,
                .present = 1,
                .limit_high = 0,
                .available_for_system_software = 0,
                .granularity = 0,
                .base_mid = @truncate(u8, address >> 24),
            },
            .base_high = @truncate(u32, address >> 32),
        };
    }
};

pub const IDT = extern struct {
    descriptors: [entry_count]GateDescriptor = undefined,
    pub const Descriptor = privileged.arch.x86_64.SegmentDescriptor;
    pub const GateDescriptor = extern struct {
        offset_low: u16,
        segment_selector: u16,
        flags: packed struct(u16) {
            ist: u3,
            reserved: u5 = 0,
            type: SystemSegmentDescriptor.Type,
            reserved1: u1 = 0,
            dpl: u2,
            present: bool,
        },
        offset_mid: u16,
        offset_high: u32,
        reserved: u32 = 0,

        comptime {
            assert(@sizeOf(@This()) == 0x10);
        }
    };
    pub const entry_count = 256;
};

const ApicPageAllocator = extern struct {
    pages: [4]PhysicalAddress = .{PhysicalAddress.invalid()} ** 4,

    const PageEntry = cpu.VirtualAddressSpace.PageEntry;

    fn allocate(context: ?*anyopaque, size: u64, alignment: u64, options: privileged.PageAllocator.AllocateOptions) Allocator.Allocate.Error!PhysicalMemoryRegion {
        const apic_allocator = @ptrCast(?*ApicPageAllocator, @alignCast(@alignOf(ApicPageAllocator), context)) orelse return Allocator.Allocate.Error.OutOfMemory;
        assert(size == lib.arch.valid_page_sizes[0]);
        assert(alignment == lib.arch.valid_page_sizes[0]);
        assert(options.count == 1);
        assert(options.level_valid);
        const physical_memory = try cpu.page_allocator.allocate(size, alignment);
        apic_allocator.pages[@enumToInt(options.level)] = physical_memory.address;
        return physical_memory;
    }
};

var apic_page_allocator = ApicPageAllocator{};
const apic_page_allocator_interface = privileged.PageAllocator{
    .allocate = ApicPageAllocator.allocate,
    .context = &apic_page_allocator,
    .context_type = .cpu,
};

pub inline fn writerStart() void {
    writer_lock.acquire();
}

pub inline fn writerEnd() void {
    writer_lock.release();
}

/// Architecture-specific implementation of mapping when you already can create user-space virtual address spaces
pub fn map(virtual_address_space: *VirtualAddressSpace, asked_physical_address: PhysicalAddress, asked_virtual_address: VirtualAddress, size: u64, general_flags: privileged.Mapping.Flags) !void {
    if (general_flags.user) {
        assert(!general_flags.secret);
    }

    try virtual_address_space.arch.map(asked_physical_address, asked_virtual_address, size, general_flags, virtual_address_space.getPageAllocatorInterface());
    if (!general_flags.secret) {
        const cpu_pml4 = try virtual_address_space.arch.getCpuPML4Table();
        const user_pml4 = try virtual_address_space.arch.getUserPML4Table();
        const first_indices = paging.computeIndices(asked_virtual_address.value());
        const last_indices = paging.computeIndices(asked_virtual_address.offset(size - lib.arch.valid_page_sizes[0]).value());
        const first_index = first_indices[@enumToInt(paging.Level.PML4)];
        const last_index = @intCast(u9, last_indices[@enumToInt(paging.Level.PML4)]) +| 1;

        for (cpu_pml4[first_index..last_index], user_pml4[first_index..last_index]) |cpu_pml4te, *user_pml4te| {
            user_pml4te.* = cpu_pml4te;
        }
    }
}
pub const PageTableEntry = paging.Level;
pub const root_page_table_entry = @intToEnum(cpu.arch.PageTableEntry, 0);

pub const IOMap = extern struct {
    debug: bool,
};
