#include "libcflat.h"
#include "processor.h"
#include "msr.h"
#include "isr.h"
#include "vm.h"
#include "apic.h"
#include "desc.h"
#include "smp.h"
#include "atomic.h"
#include "hyperv.h"
#include "asm/barrier.h"
#include "alloc_page.h"

#define MAX_CPUS 4

#define SINT1_VEC 0xF1
#define SINT2_VEC 0xF2

#define APIC_VEC1 0xF3
#define APIC_VEC2 0xF4

#define SINT1_NUM 2
#define SINT2_NUM 3
#define ONE_MS_IN_100NS 10000

static struct spinlock g_synic_alloc_lock;

struct stimer {
    int sint;
    int index;
    bool direct;
    int apic_vec;
    atomic_t fire_count;
};

struct svcpu {
    int vcpu;
    void *msg_page;
    void *evt_page;
    struct stimer timer[HV_SYNIC_STIMER_COUNT];
};

static struct svcpu g_synic_vcpu[MAX_CPUS];

static void *synic_alloc_page(void)
{
    void *page;

    spin_lock(&g_synic_alloc_lock);
    page = alloc_page();
    spin_unlock(&g_synic_alloc_lock);
    return page;
}

static void synic_free_page(void *page)
{
    spin_lock(&g_synic_alloc_lock);
    free_page(page);
    spin_unlock(&g_synic_alloc_lock);
}

static void stimer_init(struct stimer *timer, int index)
{
    memset(timer, 0, sizeof(*timer));
    timer->index = index;
}

static void synic_enable(void)
{
    int vcpu = smp_id(), i;
    struct svcpu *svcpu = &g_synic_vcpu[vcpu];

    memset(svcpu, 0, sizeof(*svcpu));
    svcpu->vcpu = vcpu;
    svcpu->msg_page = synic_alloc_page();
    for (i = 0; i < ARRAY_SIZE(svcpu->timer); i++) {
        stimer_init(&svcpu->timer[i], i);
    }
    wrmsr(HV_X64_MSR_SIMP, (u64)virt_to_phys(svcpu->msg_page) |
            HV_SYNIC_SIMP_ENABLE);
    wrmsr(HV_X64_MSR_SCONTROL, HV_SYNIC_CONTROL_ENABLE);
}

static void stimer_shutdown(struct stimer *timer)
{
    wrmsr(HV_X64_MSR_STIMER0_CONFIG + 2*timer->index, 0);
}

static void process_stimer_expired(struct stimer *timer)
{
    atomic_inc(&timer->fire_count);
}

static void process_stimer_msg(struct svcpu *svcpu,
                              struct hv_message *msg, int sint)
{
    struct hv_timer_message_payload *payload =
                        (struct hv_timer_message_payload *)msg->u.payload;
    struct stimer *timer;

    if (msg->header.message_type != HVMSG_TIMER_EXPIRED &&
        msg->header.message_type != HVMSG_NONE) {
        report_fail("invalid Hyper-V SynIC msg type");
        report_summary();
        abort();
    }

    if (msg->header.message_type == HVMSG_NONE) {
        return;
    }

    if (msg->header.payload_size < sizeof(*payload)) {
        report_fail("invalid Hyper-V SynIC msg payload size");
        report_summary();
        abort();
    }

    /* Now process timer expiration message */

    if (payload->timer_index >= ARRAY_SIZE(svcpu->timer)) {
        report_fail("invalid Hyper-V SynIC timer index");
        report_summary();
        abort();
    }
    timer = &svcpu->timer[payload->timer_index];

    if (timer->direct) {
        report(false, "VMBus message in direct mode received");
        report_summary();
        abort();
    }

    process_stimer_expired(timer);

    msg->header.message_type = HVMSG_NONE;
    mb();
    if (msg->header.message_flags.msg_pending) {
        wrmsr(HV_X64_MSR_EOM, 0);
    }
}

static void __stimer_isr(int vcpu)
{
    struct svcpu *svcpu = &g_synic_vcpu[vcpu];
    struct hv_message_page *msg_page;
    struct hv_message *msg;
    int i;


    msg_page = (struct hv_message_page *)svcpu->msg_page;
    for (i = 0; i < ARRAY_SIZE(msg_page->sint_message); i++) {
        msg = &msg_page->sint_message[i];
        process_stimer_msg(svcpu, msg, i);
    }
}

static void stimer_isr(isr_regs_t *regs)
{
    int vcpu = smp_id();

    __stimer_isr(vcpu);
    eoi();
}

static void stimer_isr_auto_eoi(isr_regs_t *regs)
{
    int vcpu = smp_id();

    __stimer_isr(vcpu);
}

static void __stimer_isr_direct(int vcpu, int timer_index)
{
    struct svcpu *svcpu = &g_synic_vcpu[vcpu];
    struct stimer *timer = &svcpu->timer[timer_index];

    process_stimer_expired(timer);
}

static void stimer_isr_direct1(isr_regs_t *regs)
{
    int vcpu = smp_id();

    __stimer_isr_direct(vcpu, 0);

    eoi();
}

static void stimer_isr_direct2(isr_regs_t *regs)
{
    int vcpu = smp_id();

    __stimer_isr_direct(vcpu, 1);

    eoi();
}

static void stimer_start(struct stimer *timer,
                         bool auto_enable, bool periodic,
                         u64 tick_100ns)
{
    u64 count;
    union hv_stimer_config config = {.as_uint64 = 0};

    atomic_set(&timer->fire_count, 0);

    config.periodic = periodic;
    config.enable = 1;
    config.auto_enable = auto_enable;
    if (!timer->direct) {
        config.sintx = timer->sint;
    } else {
        config.direct_mode = 1;
        config.apic_vector = timer->apic_vec;
    }

    if (periodic) {
        count = tick_100ns;
    } else {
        count = rdmsr(HV_X64_MSR_TIME_REF_COUNT) + tick_100ns;
    }

    if (!auto_enable) {
        wrmsr(HV_X64_MSR_STIMER0_COUNT + timer->index*2, count);
        wrmsr(HV_X64_MSR_STIMER0_CONFIG + timer->index*2, config.as_uint64);
    } else {
        wrmsr(HV_X64_MSR_STIMER0_CONFIG + timer->index*2, config.as_uint64);
        wrmsr(HV_X64_MSR_STIMER0_COUNT + timer->index*2, count);
    }
}

static void stimers_shutdown(void)
{
    int vcpu = smp_id(), i;
    struct svcpu *svcpu = &g_synic_vcpu[vcpu];

    for (i = 0; i < ARRAY_SIZE(svcpu->timer); i++) {
        stimer_shutdown(&svcpu->timer[i]);
    }
}

static void synic_disable(void)
{
    int vcpu = smp_id();
    struct svcpu *svcpu = &g_synic_vcpu[vcpu];

    wrmsr(HV_X64_MSR_SCONTROL, 0);
    wrmsr(HV_X64_MSR_SIMP, 0);
    wrmsr(HV_X64_MSR_SIEFP, 0);
    synic_free_page(svcpu->msg_page);
}


static void stimer_test_prepare(void *ctx)
{
    int vcpu = smp_id();
    struct svcpu *svcpu = &g_synic_vcpu[vcpu];
    struct stimer *timer1, *timer2;

    write_cr3((ulong)ctx);
    synic_enable();

    synic_sint_create(SINT1_NUM, SINT1_VEC, false);
    synic_sint_create(SINT2_NUM, SINT2_VEC, true);

    timer1 = &svcpu->timer[0];
    timer2 = &svcpu->timer[1];

    timer1->sint = SINT1_NUM;
    timer2->sint = SINT2_NUM;
}

static void stimer_test_prepare_direct(void *ctx)
{
    int vcpu = smp_id();
    struct svcpu *svcpu = &g_synic_vcpu[vcpu];
    struct stimer *timer1, *timer2;

    write_cr3((ulong)ctx);

    timer1 = &svcpu->timer[0];
    timer2 = &svcpu->timer[1];

    stimer_init(timer1, 0);
    stimer_init(timer2, 1);

    timer1->apic_vec = APIC_VEC1;
    timer2->apic_vec = APIC_VEC2;

    timer1->direct = true;
    timer2->direct = true;
}


static void stimer_test_periodic(int vcpu, struct stimer *timer1,
                                 struct stimer *timer2)
{
    /* Check periodic timers */
    stimer_start(timer1, false, true, ONE_MS_IN_100NS);
    stimer_start(timer2, false, true, ONE_MS_IN_100NS);
    while ((atomic_read(&timer1->fire_count) < 1000) ||
           (atomic_read(&timer2->fire_count) < 1000)) {
        pause();
    }
    report_pass("Hyper-V SynIC periodic timers test vcpu %d", vcpu);
    stimer_shutdown(timer1);
    stimer_shutdown(timer2);
}

static void stimer_test_one_shot(int vcpu, struct stimer *timer)
{
    /* Check one-shot timer */
    stimer_start(timer, false, false, ONE_MS_IN_100NS);
    while (atomic_read(&timer->fire_count) < 1) {
        pause();
    }
    report_pass("Hyper-V SynIC one-shot test vcpu %d", vcpu);
    stimer_shutdown(timer);
}

static void stimer_test_auto_enable_one_shot(int vcpu, struct stimer *timer)
{
    /* Check auto-enable one-shot timer */
    stimer_start(timer, true, false, ONE_MS_IN_100NS);
    while (atomic_read(&timer->fire_count) < 1) {
        pause();
    }
    report_pass("Hyper-V SynIC auto-enable one-shot timer test vcpu %d", vcpu);
    stimer_shutdown(timer);
}

static void stimer_test_auto_enable_periodic(int vcpu, struct stimer *timer)
{
    /* Check auto-enable periodic timer */
    stimer_start(timer, true, true, ONE_MS_IN_100NS);
    while (atomic_read(&timer->fire_count) < 1000) {
        pause();
    }
    report_pass("Hyper-V SynIC auto-enable periodic timer test vcpu %d", vcpu);
    stimer_shutdown(timer);
}

static void stimer_test_one_shot_busy(int vcpu, struct stimer *timer)
{
    struct hv_message_page *msg_page;
    struct hv_message *msg;

    /* Skipping msg slot busy test in direct mode */
    if (timer->direct)
        return;

    msg_page = g_synic_vcpu[vcpu].msg_page;
    msg = &msg_page->sint_message[timer->sint];

    msg->header.message_type = HVMSG_TIMER_EXPIRED;
    wmb();

    stimer_start(timer, false, false, ONE_MS_IN_100NS);

    do
        rmb();
    while (!msg->header.message_flags.msg_pending);

    report(!atomic_read(&timer->fire_count),
           "no timer fired while msg slot busy: vcpu %d", vcpu);

    msg->header.message_type = HVMSG_NONE;
    wmb();
    wrmsr(HV_X64_MSR_EOM, 0);

    while (atomic_read(&timer->fire_count) < 1) {
        pause();
    }
    report_pass("timer resumed when msg slot released: vcpu %d", vcpu);

    stimer_shutdown(timer);
}

static void stimer_test(void *ctx)
{
    int vcpu = smp_id();
    struct svcpu *svcpu = &g_synic_vcpu[vcpu];
    struct stimer *timer1, *timer2;

    sti();

    timer1 = &svcpu->timer[0];
    timer2 = &svcpu->timer[1];

    stimer_test_periodic(vcpu, timer1, timer2);
    stimer_test_one_shot(vcpu, timer1);
    stimer_test_auto_enable_one_shot(vcpu, timer2);
    stimer_test_auto_enable_periodic(vcpu, timer1);
    stimer_test_one_shot_busy(vcpu, timer1);

    cli();
}

static void stimer_test_cleanup(void *ctx)
{
    stimers_shutdown();
    synic_sint_destroy(SINT1_NUM);
    synic_sint_destroy(SINT2_NUM);
    synic_disable();
}

static void stimer_test_cleanup_direct(void *ctx)
{
    stimers_shutdown();
}

static void stimer_test_all(bool direct)
{
    int ncpus;

    setup_vm();
    enable_apic();

    ncpus = cpu_count();
    if (ncpus > MAX_CPUS)
        report_abort("number cpus exceeds %d", MAX_CPUS);
    printf("cpus = %d\n", ncpus);

    if (!direct) {
        printf("Starting Hyper-V SynIC timers tests: message mode\n");

        handle_irq(SINT1_VEC, stimer_isr);
        handle_irq(SINT2_VEC, stimer_isr_auto_eoi);

	on_cpus(stimer_test_prepare, (void *)read_cr3());
	on_cpus(stimer_test, NULL);
	on_cpus(stimer_test_cleanup, NULL);
    } else {
        printf("Starting Hyper-V SynIC timers tests: direct mode\n");

        handle_irq(APIC_VEC1, stimer_isr_direct1);
        handle_irq(APIC_VEC2, stimer_isr_direct2);

        on_cpus(stimer_test_prepare_direct, (void *)read_cr3());
        on_cpus(stimer_test, NULL);
        on_cpus(stimer_test_cleanup_direct, NULL);
    }
}

int main(int argc, char **argv)
{
    bool direct = argc >= 2 && !strcmp(argv[1], "direct");

    if (!hv_synic_supported()) {
        report_skip("Hyper-V SynIC is not supported");
        goto done;
    }

    if (!hv_stimer_supported()) {
        report_skip("Hyper-V SynIC timers are not supported");
        goto done;
    }

    if (!hv_time_ref_counter_supported()) {
        report_skip("Hyper-V time reference counter is not supported");
        goto done;
    }

    if (direct && !stimer_direct_supported()) {
	report_skip("Hyper-V SinIC timer direct mode is not supported");
    }

    stimer_test_all(direct);
done:
    return report_summary();
}