/*
 * Copyright (c) Qualcomm Technologies, Inc. and/or its subsidiaries.
 *
 * SPDX-License-Identifier: GPL-2.0-or-later
 */

#include "qemu/osdep.h"
#include "qemu/log.h"
#include "qemu/main-loop.h"
#include "qemu/qemu-print.h"
#include "cpu.h"
#include "system/cpus.h"
#include "internal.h"
#include "exec/cpu-interrupt.h"
#include "cpu_helper.h"
#include "exec/cputlb.h"
#include "hex_mmu.h"
#include "macros.h"
#include "sys_macros.h"
#include "hw/hexagon/hexagon_tlb.h"
#include "hw/hexagon/hexagon_globalreg.h"

static inline void hex_log_tlbw(uint32_t index, uint64_t entry)
{
    qemu_log_mask(CPU_LOG_MMU,
                  "tlbw[%03" PRIu32 "]: 0x%016" PRIx64 "\n",
                  index, entry);
}

void hex_tlbw(CPUHexagonState *env, uint32_t index, uint64_t value)
{
    uint32_t myidx = fTLB_NONPOW2WRAP(fTLB_IDXMASK(index));
    HexagonTLBState *tlb = env_archcpu(env)->tlb;
    uint64_t old_entry = hexagon_tlb_read(tlb, myidx);

    bool old_entry_valid = extract64(old_entry, 63, 1);
    if (old_entry_valid && hexagon_cpu_mmu_enabled(env)) {
        CPUState *cs = env_cpu(env);
        tlb_flush(cs);
    }
    hexagon_tlb_write(tlb, myidx, value);
    hex_log_tlbw(myidx, value);
}

void hex_mmu_on(CPUHexagonState *env)
{
    CPUState *cs = env_cpu(env);
    qemu_log_mask(CPU_LOG_MMU, "Hexagon MMU turned on!\n");
    tlb_flush(cs);
}

void hex_mmu_off(CPUHexagonState *env)
{
    CPUState *cs = env_cpu(env);
    qemu_log_mask(CPU_LOG_MMU, "Hexagon MMU turned off!\n");
    tlb_flush(cs);
}

void hex_mmu_mode_change(CPUHexagonState *env)
{
    qemu_log_mask(CPU_LOG_MMU, "Hexagon mode change!\n");
    CPUState *cs = env_cpu(env);
    tlb_flush(cs);
}

bool hex_tlb_find_match(CPUHexagonState *env, uint32_t VA,
                        MMUAccessType access_type, hwaddr *PA, int *prot,
                        uint64_t *size, int32_t *excp, int mmu_idx)
{
    HexagonCPU *cpu = env_archcpu(env);
    uint32_t ssr = env->t_sreg[HEX_SREG_SSR];
    uint8_t asid = GET_SSR_FIELD(SSR_ASID, ssr);
    int cause_code = 0;

    bool found = hexagon_tlb_find_match(cpu->tlb, asid, VA, access_type,
                                        PA, prot, size, excp, &cause_code,
                                        mmu_idx);
    if (cause_code) {
        env->cause_code = cause_code;
    }
    return found;
}

/* Called from tlbp instruction */
uint32_t hex_tlb_lookup(CPUHexagonState *env, uint32_t ssr, uint32_t VA)
{
    HexagonCPU *cpu = env_archcpu(env);
    uint8_t asid = GET_SSR_FIELD(SSR_ASID, ssr);
    int cause_code = 0;

    uint32_t result = hexagon_tlb_lookup(cpu->tlb, asid, VA, &cause_code);
    if (cause_code) {
        env->cause_code = cause_code;
    }
    return result;
}

/*
 * Return codes:
 * 0 or positive             index of match
 * -1                        multiple matches
 * -2                        no match
 */
int hex_tlb_check_overlap(CPUHexagonState *env, uint64_t entry, uint64_t index)
{
    HexagonCPU *cpu = env_archcpu(env);
    return hexagon_tlb_check_overlap(cpu->tlb, entry, index);
}

void dump_mmu(Monitor *mon, CPUHexagonState *env)
{
    HexagonCPU *cpu = env_archcpu(env);
    hexagon_tlb_dump(mon, cpu->tlb);
}

static inline void print_thread(const char *str, CPUState *cs)
{
    g_assert(bql_locked());
    CPUHexagonState *thread = cpu_env(cs);
    bool is_stopped = cpu_is_stopped(cs);
    int exe_mode = get_exe_mode(thread);
    hex_lock_state_t lock_state = thread->tlb_lock_state;
    qemu_log_mask(CPU_LOG_MMU,
           "%s: threadId = %" PRIu32 ": %s,"
           " exe_mode = %s, tlb_lock_state = %s\n",
           str,
           thread->threadId,
           is_stopped ? "stopped" : "running",
           exe_mode == HEX_EXE_MODE_OFF ? "off" :
           exe_mode == HEX_EXE_MODE_RUN ? "run" :
           exe_mode == HEX_EXE_MODE_WAIT ? "wait" :
           exe_mode == HEX_EXE_MODE_DEBUG ? "debug" :
           "unknown",
           lock_state == HEX_LOCK_UNLOCKED ? "unlocked" :
           lock_state == HEX_LOCK_WAITING ? "waiting" :
           lock_state == HEX_LOCK_OWNER ? "owner" :
           "unknown");
}

static inline void print_thread_states(const char *str)
{
    CPUState *cs;
    CPU_FOREACH(cs) {
        print_thread(str, cs);
    }
}

void hex_tlb_lock(CPUHexagonState *env)
{
    qemu_log_mask(CPU_LOG_MMU, "hex_tlb_lock: " TARGET_FMT_ld "\n",
                  env->threadId);
    BQL_LOCK_GUARD();
    g_assert((env->tlb_lock_count == 0) || (env->tlb_lock_count == 1));

    HexagonCPU *cpu = env_archcpu(env);
    uint32_t syscfg = cpu->globalregs ?
        hexagon_globalreg_read(cpu->globalregs, HEX_SREG_SYSCFG,
                               env->threadId) : 0;
    uint8_t tlb_lock = GET_SYSCFG_FIELD(SYSCFG_TLBLOCK, syscfg);
    if (tlb_lock) {
        if (env->tlb_lock_state == HEX_LOCK_QUEUED) {
            env->next_PC += 4;
            env->tlb_lock_count++;
            env->tlb_lock_state = HEX_LOCK_OWNER;
            SET_SYSCFG_FIELD(env, SYSCFG_TLBLOCK, 1);
            return;
        }
        if (env->tlb_lock_state == HEX_LOCK_OWNER) {
            qemu_log_mask(CPU_LOG_MMU | LOG_GUEST_ERROR,
                          "Double tlblock at PC: 0x%" PRIx32
                          ", thread may hang\n",
                          env->next_PC);
            env->next_PC += 4;
            CPUState *cs = env_cpu(env);
            cpu_interrupt(cs, CPU_INTERRUPT_HALT);
            return;
        }
        env->tlb_lock_state = HEX_LOCK_WAITING;
        CPUState *cs = env_cpu(env);
        cpu_interrupt(cs, CPU_INTERRUPT_HALT);
    } else {
        env->next_PC += 4;
        env->tlb_lock_count++;
        env->tlb_lock_state = HEX_LOCK_OWNER;
        SET_SYSCFG_FIELD(env, SYSCFG_TLBLOCK, 1);
    }

    if (qemu_loglevel_mask(CPU_LOG_MMU)) {
        qemu_log_mask(CPU_LOG_MMU, "Threads after hex_tlb_lock:\n");
        print_thread_states("\tThread");
    }
}

void hex_tlb_unlock(CPUHexagonState *env)
{
    BQL_LOCK_GUARD();
    g_assert((env->tlb_lock_count == 0) || (env->tlb_lock_count == 1));

    /* Nothing to do if the TLB isn't locked by this thread */
    HexagonCPU *cpu = env_archcpu(env);
    uint32_t syscfg = cpu->globalregs ?
        hexagon_globalreg_read(cpu->globalregs, HEX_SREG_SYSCFG,
                               env->threadId) : 0;
    uint8_t tlb_lock = GET_SYSCFG_FIELD(SYSCFG_TLBLOCK, syscfg);
    if ((tlb_lock == 0) ||
        (env->tlb_lock_state != HEX_LOCK_OWNER)) {
        qemu_log_mask(LOG_GUEST_ERROR,
                      "thread %" PRIu32 " attempted to tlbunlock"
                      " without having the lock, tlb_lock state = %u\n",
                      env->threadId, (unsigned)env->tlb_lock_state);
        g_assert(env->tlb_lock_state != HEX_LOCK_WAITING);
        return;
    }

    env->tlb_lock_count--;
    env->tlb_lock_state = HEX_LOCK_UNLOCKED;
    SET_SYSCFG_FIELD(env, SYSCFG_TLBLOCK, 0);

    /* Look for a thread to unlock */
    unsigned int this_threadId = env->threadId;
    CPUHexagonState *unlock_thread = NULL;
    CPUState *cs;
    CPU_FOREACH(cs) {
        CPUHexagonState *thread = cpu_env(cs);

        /*
         * The hardware implements round-robin fairness, so we look for threads
         * starting at env->threadId + 1 and incrementing modulo the number of
         * threads.
         *
         * To implement this, we check if thread is a earlier in the modulo
         * sequence than unlock_thread.
         *     if unlock thread is higher than this thread
         *         thread must be between this thread and unlock_thread
         *     else
         *         thread higher than this thread is ahead of unlock_thread
         *         thread must be lower then unlock thread
         */
        if (thread->tlb_lock_state == HEX_LOCK_WAITING) {
            if (!unlock_thread) {
                unlock_thread = thread;
            } else if (unlock_thread->threadId > this_threadId) {
                if (this_threadId < thread->threadId &&
                    thread->threadId < unlock_thread->threadId) {
                    unlock_thread = thread;
                }
            } else {
                if (thread->threadId > this_threadId) {
                    unlock_thread = thread;
                }
                if (thread->threadId < unlock_thread->threadId) {
                    unlock_thread = thread;
                }
            }
        }
    }
    if (unlock_thread) {
        cs = env_cpu(unlock_thread);
        print_thread("\tWaiting thread found", cs);
        unlock_thread->tlb_lock_state = HEX_LOCK_QUEUED;
        SET_SYSCFG_FIELD(unlock_thread, SYSCFG_TLBLOCK, 1);
        cpu_interrupt(cs, CPU_INTERRUPT_TLB_UNLOCK);
    }

    if (qemu_loglevel_mask(CPU_LOG_MMU)) {
        qemu_log_mask(CPU_LOG_MMU, "Threads after hex_tlb_unlock:\n");
        print_thread_states("\tThread");
    }

}
