/*
 * QEMU MSHV support
 *
 * Copyright Microsoft, Corp. 2025
 *
 * Authors: Magnus Kulke <magnuskulke@microsoft.com>
 *
 * SPDX-License-Identifier: GPL-2.0-or-later
 */

#include "qemu/osdep.h"

#include "cpu.h"
#include "emulate/x86_decode.h"
#include "emulate/x86_emu.h"
#include "emulate/x86_mmu.h"
#include "qemu/error-report.h"
#include "system/mshv.h"

/* RW or Exec segment */
static const uint8_t RWRX_SEGMENT_TYPE        = 0x2;
static const uint8_t CODE_SEGMENT_TYPE        = 0x8;
static const uint8_t EXPAND_DOWN_SEGMENT_TYPE = 0x4;

typedef enum CpuMode {
    REAL_MODE,
    PROTECTED_MODE,
    LONG_MODE,
} CpuMode;

static CpuMode cpu_mode(CPUState *cpu)
{
    enum CpuMode m = REAL_MODE;

    if (x86_is_protected(cpu)) {
        m = PROTECTED_MODE;

        if (x86_is_long_mode(cpu)) {
            m = LONG_MODE;
        }
    }

    return m;
}

static bool segment_type_ro(const x86_segment_descriptor desc)
{
    uint32_t type_ = desc.type;
    return (type_ & (~RWRX_SEGMENT_TYPE)) == 0;
}

static bool segment_type_code(const x86_segment_descriptor desc)
{
    uint32_t type_ = desc.type;
    return (type_ & CODE_SEGMENT_TYPE) != 0;
}

static bool segment_expands_down(const x86_segment_descriptor desc)
{
    uint32_t type_ = desc.type;

    if (segment_type_code(desc)) {
        return false;
    }

    return (type_ & EXPAND_DOWN_SEGMENT_TYPE) != 0;
}

static uint8_t segment_db(const x86_segment_descriptor desc)
{
    return desc.db;
}

static uint32_t segment_max_limit(const x86_segment_descriptor desc)
{
    if (segment_db(desc) != 0) {
        return 0xFFFFFFFF;
    }
    return 0xFFFF;
}

static int linearize(CPUState *cpu,
                     target_ulong logical_addr, target_ulong *linear_addr,
                     X86Seg seg_idx)
{
    enum CpuMode mode;
    struct x86_segment_descriptor desc;
    target_ulong base;
    target_ulong logical_addr_32b;
    uint32_t limit;
    /* TODO: the emulator will not pass us "write" indicator yet */
    bool write = false;

    emul_ops->read_segment_descriptor(cpu, &desc, seg_idx);
    base = x86_segment_base(&desc);
    mode = cpu_mode(cpu);

    switch (mode) {
    case LONG_MODE:
        if (__builtin_add_overflow(logical_addr, base, linear_addr)) {
            error_report("Address overflow");
            return -1;
        }
        break;
    case PROTECTED_MODE:
    case REAL_MODE:
        if (segment_type_ro(desc) && write) {
            error_report("Cannot write to read-only segment");
            return -1;
        }

        logical_addr_32b = logical_addr & 0xFFFFFFFF;
        limit = x86_segment_limit(&desc);

        if (segment_expands_down(desc)) {
            if (logical_addr_32b >= limit) {
                error_report("Address exceeds limit (expands down)");
                return -1;
            }

            limit = segment_max_limit(desc);
        }

        if (logical_addr_32b > limit) {
            error_report("Address exceeds limit %u", limit);
            return -1;
        }
        *linear_addr = logical_addr_32b + base;
        break;
    default:
        error_report("Unknown cpu mode: %d", mode);
        return -1;
    }

    return 0;
}

bool x86_read_segment_descriptor(CPUState *cpu,
                                 struct x86_segment_descriptor *desc,
                                 x86_segment_selector sel)
{
    target_ulong base;
    uint32_t limit;
    X86CPU *x86_cpu = X86_CPU(cpu);
    CPUX86State *env = &x86_cpu->env;
    target_ulong gva;

    memset(desc, 0, sizeof(*desc));

    /* valid gdt descriptors start from index 1 */
    if (!sel.index && GDT_SEL == sel.ti) {
        return false;
    }

    if (GDT_SEL == sel.ti) {
        base = env->gdt.base;
        limit = env->gdt.limit;
    } else {
        base = env->ldt.base;
        limit = env->ldt.limit;
    }

    if (sel.index * 8 >= limit) {
        return false;
    }

    gva = base + sel.index * 8;
    x86_read_mem_priv(cpu, desc, gva, sizeof(*desc));

    return true;
}

bool x86_read_call_gate(CPUState *cpu, struct x86_call_gate *idt_desc,
                        int gate)
{
    target_ulong base;
    uint32_t limit;
    X86CPU *x86_cpu = X86_CPU(cpu);
    CPUX86State *env = &x86_cpu->env;
    target_ulong gva;

    base = env->idt.base;
    limit = env->idt.limit;

    memset(idt_desc, 0, sizeof(*idt_desc));
    if (gate * 8 >= limit) {
        perror("call gate exceeds idt limit");
        return false;
    }

    gva = base + gate * 8;
    x86_read_mem_priv(cpu, idt_desc, gva, sizeof(*idt_desc));

    return true;
}

target_ulong x86_read_cr(CPUState *cpu, int cr)
{
    X86CPU *x86_cpu = X86_CPU(cpu);
    CPUX86State *env = &x86_cpu->env;

    if (emul_ops->read_cr) {
        return emul_ops->read_cr(cpu, cr);
    }
    return env->cr[cr];
}

bool x86_is_protected(CPUState *cpu)
{
    uint64_t cr0;

    if (emul_ops->is_protected_mode) {
        return emul_ops->is_protected_mode(cpu);
    }

    cr0 = x86_read_cr(cpu, 0);
    return cr0 & CR0_PE_MASK;
}

bool x86_is_real(CPUState *cpu)
{
    return !x86_is_protected(cpu);
}

bool x86_is_v8086(CPUState *cpu)
{
    X86CPU *x86_cpu = X86_CPU(cpu);
    CPUX86State *env = &x86_cpu->env;
    return x86_is_protected(cpu) && (env->eflags & VM_MASK);
}

bool x86_is_long_mode(CPUState *cpu)
{
    X86CPU *x86_cpu = X86_CPU(cpu);
    CPUX86State *env = &x86_cpu->env;
    uint64_t efer = env->efer;
    uint64_t lme_lma = (MSR_EFER_LME | MSR_EFER_LMA);

    if (emul_ops->is_long_mode) {
        return emul_ops->is_long_mode(cpu);
    }
    return ((efer & lme_lma) == lme_lma);
}

bool x86_is_la57(CPUState *cpu)
{
    uint64_t is_la57 = x86_read_cr(cpu, 4) & CR4_LA57_MASK;
    return is_la57;
}

bool x86_is_long64_mode(CPUState *cpu)
{
    error_report("unimplemented: is_long64_mode()");
    abort();
}

bool x86_is_paging_mode(CPUState *cpu)
{
    uint64_t cr0 = x86_read_cr(cpu, 0);

    return cr0 & CR0_PG_MASK;
}

bool x86_is_pae_enabled(CPUState *cpu)
{
    uint64_t cr4 = x86_read_cr(cpu, 4);

    return cr4 & CR4_PAE_MASK;
}

target_ulong linear_addr(CPUState *cpu, target_ulong addr, X86Seg seg)
{
    int ret;
    target_ulong linear_addr;

    ret = linearize(cpu, addr, &linear_addr, seg);
    if (ret < 0) {
        error_report("failed to linearize address");
        abort();
    }

    return linear_addr;
}

target_ulong linear_addr_size(CPUState *cpu, target_ulong addr, int size,
                              X86Seg seg)
{
    switch (size) {
    case 2:
        addr = (uint16_t)addr;
        break;
    case 4:
        addr = (uint32_t)addr;
        break;
    default:
        break;
    }
    return linear_addr(cpu, addr, seg);
}

target_ulong linear_rip(CPUState *cpu, target_ulong rip)
{
    return linear_addr(cpu, rip, R_CS);
}
