// Copyright 2018 yuzu emulator team
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.

#include "common/assert.h"
#include "core/arm/exclusive_monitor.h"
#include "core/core.h"
#include "core/core_cpu.h"
#include "core/cpu_core_manager.h"
#include "core/gdbstub/gdbstub.h"
#include "core/settings.h"

namespace Core {
namespace {
void RunCpuCore(const System& system, Cpu& cpu_state) {
    while (system.IsPoweredOn()) {
        cpu_state.RunLoop(true);
    }
}
} // Anonymous namespace

CpuCoreManager::CpuCoreManager() = default;
CpuCoreManager::~CpuCoreManager() = default;

void CpuCoreManager::Initialize(System& system) {
    barrier = std::make_unique<CpuBarrier>();
    exclusive_monitor = Cpu::MakeExclusiveMonitor(cores.size());

    for (std::size_t index = 0; index < cores.size(); ++index) {
        cores[index] =
            std::make_unique<Cpu>(system.CoreTiming(), *exclusive_monitor, *barrier, index);
    }

    // Create threads for CPU cores 1-3, and build thread_to_cpu map
    // CPU core 0 is run on the main thread
    thread_to_cpu[std::this_thread::get_id()] = cores[0].get();
    if (!Settings::values.use_multi_core) {
        return;
    }

    for (std::size_t index = 0; index < core_threads.size(); ++index) {
        core_threads[index] = std::make_unique<std::thread>(RunCpuCore, std::cref(system),
                                                            std::ref(*cores[index + 1]));
        thread_to_cpu[core_threads[index]->get_id()] = cores[index + 1].get();
    }
}

void CpuCoreManager::Shutdown() {
    barrier->NotifyEnd();
    if (Settings::values.use_multi_core) {
        for (auto& thread : core_threads) {
            thread->join();
            thread.reset();
        }
    }

    thread_to_cpu.clear();
    for (auto& cpu_core : cores) {
        cpu_core.reset();
    }

    exclusive_monitor.reset();
    barrier.reset();
}

Cpu& CpuCoreManager::GetCore(std::size_t index) {
    return *cores.at(index);
}

const Cpu& CpuCoreManager::GetCore(std::size_t index) const {
    return *cores.at(index);
}

ExclusiveMonitor& CpuCoreManager::GetExclusiveMonitor() {
    return *exclusive_monitor;
}

const ExclusiveMonitor& CpuCoreManager::GetExclusiveMonitor() const {
    return *exclusive_monitor;
}

Cpu& CpuCoreManager::GetCurrentCore() {
    if (Settings::values.use_multi_core) {
        const auto& search = thread_to_cpu.find(std::this_thread::get_id());
        ASSERT(search != thread_to_cpu.end());
        ASSERT(search->second);
        return *search->second;
    }

    // Otherwise, use single-threaded mode active_core variable
    return *cores[active_core];
}

const Cpu& CpuCoreManager::GetCurrentCore() const {
    if (Settings::values.use_multi_core) {
        const auto& search = thread_to_cpu.find(std::this_thread::get_id());
        ASSERT(search != thread_to_cpu.end());
        ASSERT(search->second);
        return *search->second;
    }

    // Otherwise, use single-threaded mode active_core variable
    return *cores[active_core];
}

void CpuCoreManager::RunLoop(bool tight_loop) {
    // Update thread_to_cpu in case Core 0 is run from a different host thread
    thread_to_cpu[std::this_thread::get_id()] = cores[0].get();

    if (GDBStub::IsServerEnabled()) {
        GDBStub::HandlePacket();

        // If the loop is halted and we want to step, use a tiny (1) number of instructions to
        // execute. Otherwise, get out of the loop function.
        if (GDBStub::GetCpuHaltFlag()) {
            if (GDBStub::GetCpuStepFlag()) {
                tight_loop = false;
            } else {
                return;
            }
        }
    }

    for (active_core = 0; active_core < NUM_CPU_CORES; ++active_core) {
        cores[active_core]->RunLoop(tight_loop);
        if (Settings::values.use_multi_core) {
            // Cores 1-3 are run on other threads in this mode
            break;
        }
    }

    if (GDBStub::IsServerEnabled()) {
        GDBStub::SetCpuStepFlag(false);
    }
}

void CpuCoreManager::InvalidateAllInstructionCaches() {
    for (auto& cpu : cores) {
        cpu->ArmInterface().ClearInstructionCache();
    }
}

} // namespace Core
