/*
 * Copyright (c) Atmosphère-NX
 *
 * This program is free software; you can redistribute it and/or modify it
 * under the terms and conditions of the GNU General Public License,
 * version 2, as published by the Free Software Foundation.
 *
 * This program is distributed in the hope it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License for
 * more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
#include <mesosphere.hpp>

namespace ams::kern {

    namespace {

        ALWAYS_INLINE KDebugBase *GetDebugObject(KProcess *process) {
            return static_cast<KDebugBase *>(process->GetDebugObject());
        }

    }

    void KDebugBase::Initialize() {
        /* Clear the continue flags. */
        m_continue_flags = 0;
    }

    bool KDebugBase::Is64Bit() const {
        MESOSPHERE_ASSERT(m_lock.IsLockedByCurrentThread());
        MESOSPHERE_ASSERT(m_is_attached);

        KProcess * const process = this->GetProcessUnsafe();
        MESOSPHERE_ASSERT(process != nullptr);
        return process->Is64Bit();
    }


    Result KDebugBase::QueryMemoryInfo(ams::svc::MemoryInfo *out_memory_info, ams::svc::PageInfo *out_page_info, KProcessAddress address) {
        /* Check that we're attached. */
        R_UNLESS(this->IsAttached(), svc::ResultProcessTerminated());

        /* Open a reference to our process. */
        R_UNLESS(this->OpenProcess(), svc::ResultProcessTerminated());

        /* Close our reference to our process when we're done. */
        ON_SCOPE_EXIT { this->CloseProcess(); };

        /* Lock ourselves. */
        KScopedLightLock lk(m_lock);

        /* Check that we're still attached now that we're locked. */
        R_UNLESS(this->IsAttached(), svc::ResultProcessTerminated());

        /* Get the process pointer. */
        KProcess * const process = this->GetProcessUnsafe();

        /* Check that the process isn't terminated. */
        R_UNLESS(!process->IsTerminated(), svc::ResultProcessTerminated());

        /* Query the mapping's info. */
        KMemoryInfo info;
        R_TRY(process->GetPageTable().QueryInfo(std::addressof(info), out_page_info, address));

        /* Write output. */
        *out_memory_info = info.GetSvcMemoryInfo();

        R_SUCCEED();
    }

    Result KDebugBase::ReadMemory(KProcessAddress buffer, KProcessAddress address, size_t size) {
        /* Check that we're attached. */
        R_UNLESS(this->IsAttached(), svc::ResultProcessTerminated());

        /* Open a reference to our process. */
        R_UNLESS(this->OpenProcess(), svc::ResultProcessTerminated());

        /* Close our reference to our process when we're done. */
        ON_SCOPE_EXIT { this->CloseProcess(); };

        /* Lock ourselves. */
        KScopedLightLock lk(m_lock);

        /* Check that we're still attached now that we're locked. */
        R_UNLESS(this->IsAttached(), svc::ResultProcessTerminated());

        /* Get the process pointer. */
        KProcess * const process = this->GetProcessUnsafe();

        /* Check that the process isn't terminated. */
        R_UNLESS(!process->IsTerminated(), svc::ResultProcessTerminated());

        /* Get the page tables. */
        KProcessPageTable &debugger_pt = GetCurrentProcess().GetPageTable();
        KProcessPageTable &target_pt   = process->GetPageTable();

        /* Verify that the regions are in range. */
        R_UNLESS(target_pt.Contains(address, size),  svc::ResultInvalidCurrentMemory());
        R_UNLESS(debugger_pt.Contains(buffer, size), svc::ResultInvalidCurrentMemory());

        /* Iterate over the target process's memory blocks. */
        KProcessAddress cur_address = address;
        size_t remaining = size;
        while (remaining > 0) {
            /* Get the current memory info. */
            KMemoryInfo info;
            ams::svc::PageInfo pi;
            R_TRY(target_pt.QueryInfo(std::addressof(info), std::addressof(pi), cur_address));

            /* Check that the memory is accessible. */
            R_UNLESS(info.GetState() != static_cast<KMemoryState>(ams::svc::MemoryState_Inaccessible), svc::ResultInvalidAddress());

            /* Get the current size. */
            const size_t cur_size = std::min(remaining, info.GetEndAddress() - GetInteger(cur_address));

            /* Read the memory. */
            if (info.GetState() != KMemoryState_Io) {
                /* The memory is normal memory. */
                R_TRY(target_pt.ReadDebugMemory(GetVoidPointer(buffer), cur_address, cur_size));
            } else {
                /* The memory is IO memory. */
                R_TRY(target_pt.ReadDebugIoMemory(GetVoidPointer(buffer), cur_address, cur_size));
            }

            /* Advance. */
            buffer      += cur_size;
            cur_address += cur_size;
            remaining   -= cur_size;
        }

        R_SUCCEED();
    }

    Result KDebugBase::WriteMemory(KProcessAddress buffer, KProcessAddress address, size_t size) {
        /* Check that we're attached. */
        R_UNLESS(this->IsAttached(), svc::ResultProcessTerminated());

        /* Open a reference to our process. */
        R_UNLESS(this->OpenProcess(), svc::ResultProcessTerminated());

        /* Close our reference to our process when we're done. */
        ON_SCOPE_EXIT { this->CloseProcess(); };

        /* Lock ourselves. */
        KScopedLightLock lk(m_lock);

        /* Check that we're still attached now that we're locked. */
        R_UNLESS(this->IsAttached(), svc::ResultProcessTerminated());

        /* Get the process pointer. */
        KProcess * const process = this->GetProcessUnsafe();

        /* Check that the process isn't terminated. */
        R_UNLESS(!process->IsTerminated(), svc::ResultProcessTerminated());

        /* Get the page tables. */
        KProcessPageTable &debugger_pt = GetCurrentProcess().GetPageTable();
        KProcessPageTable &target_pt   = process->GetPageTable();

        /* Verify that the regions are in range. */
        R_UNLESS(target_pt.Contains(address, size),  svc::ResultInvalidCurrentMemory());
        R_UNLESS(debugger_pt.Contains(buffer, size), svc::ResultInvalidCurrentMemory());

        /* Iterate over the target process's memory blocks. */
        KProcessAddress cur_address = address;
        size_t remaining = size;
        while (remaining > 0) {
            /* Get the current memory info. */
            KMemoryInfo info;
            ams::svc::PageInfo pi;
            R_TRY(target_pt.QueryInfo(std::addressof(info), std::addressof(pi), cur_address));

            /* Check that the memory is accessible. */
            R_UNLESS(info.GetState() != static_cast<KMemoryState>(ams::svc::MemoryState_Inaccessible), svc::ResultInvalidAddress());

            /* Get the current size. */
            const size_t cur_size = std::min(remaining, info.GetEndAddress() - GetInteger(cur_address));

            /* Read the memory. */
            if (info.GetState() != KMemoryState_Io) {
                /* The memory is normal memory. */
                R_TRY(target_pt.WriteDebugMemory(cur_address, GetVoidPointer(buffer), cur_size));
            } else {
                /* The memory is IO memory. */
                R_TRY(target_pt.WriteDebugIoMemory(cur_address, GetVoidPointer(buffer), cur_size));
            }

            /* Advance. */
            buffer      += cur_size;
            cur_address += cur_size;
            remaining   -= cur_size;
        }

        R_SUCCEED();
    }

    Result KDebugBase::GetRunningThreadInfo(ams::svc::LastThreadContext *out_context, u64 *out_thread_id) {
        /* Check that we're attached. */
        R_UNLESS(this->IsAttached(), svc::ResultProcessTerminated());

        /* Open a reference to our process. */
        R_UNLESS(this->OpenProcess(), svc::ResultProcessTerminated());

        /* Close our reference to our process when we're done. */
        ON_SCOPE_EXIT { this->CloseProcess(); };

        /* Get the process pointer. */
        KProcess * const process = this->GetProcessUnsafe();

        /* Get the thread info. */
        {
            KScopedSchedulerLock sl;

            /* Get the running thread. */
            const s32 core_id = GetCurrentCoreId();
            KThread *thread = process->GetRunningThread(core_id);

            /* We want to check that the thread is actually running. */
            /* If it is, then the scheduler will have just switched from the thread to the current thread. */
            /* This implies exactly one switch will have taken place, and the current thread will be on the current core. */
            const auto &scheduler = Kernel::GetScheduler(core_id);
            if (!(thread != nullptr && thread->GetActiveCore() == core_id && process->GetRunningThreadSwitchCount(core_id) + 1 == scheduler.GetSwitchCount())) {
                /* The most recent thread switch was from a thread other than the expected one to the current one. */
                /* We want to use the appropriate result to inform userland about what thread we switched from. */
                if (scheduler.GetIdleCount() + 1 == scheduler.GetSwitchCount()) {
                    /* We switched from the idle thread. */
                    R_THROW(svc::ResultNoThread());
                } else {
                    /* We switched from some other unknown thread. */
                    R_THROW(svc::ResultUnknownThread());
                }
            }

            /* Get the thread's exception context. */
            GetExceptionContext(thread)->GetSvcThreadContext(out_context);

            /* Get the thread's id. */
            *out_thread_id = thread->GetId();
        }

        R_SUCCEED();
    }

    Result KDebugBase::Attach(KProcess *target) {
        /* Check that the process isn't null. */
        MESOSPHERE_ASSERT(target != nullptr);

        /* Clear ourselves as unattached. */
        m_is_attached = false;

        /* Attach to the process. */
        {
            /* Lock both ourselves, the target process, and the scheduler. */
            KScopedLightLock state_lk(target->GetStateLock());
            KScopedLightLock list_lk(target->GetListLock());
            KScopedLightLock this_lk(m_lock);
            KScopedSchedulerLock sl;

            /* Check that the process isn't already being debugged. */
            R_UNLESS(!target->IsAttachedToDebugger(), svc::ResultBusy());

            {
                /* Ensure the process is in a state that allows for debugging. */
                const KProcess::State state = target->GetState();
                switch (state) {
                    case KProcess::State_Created:
                    case KProcess::State_Running:
                    case KProcess::State_Crashed:
                        break;
                    case KProcess::State_CreatedAttached:
                    case KProcess::State_RunningAttached:
                    case KProcess::State_DebugBreak:
                        R_THROW(svc::ResultBusy());
                    case KProcess::State_Terminating:
                    case KProcess::State_Terminated:
                        R_THROW(svc::ResultProcessTerminated());
                    MESOSPHERE_UNREACHABLE_DEFAULT_CASE();
                }

                /* Attach to the target. */
                m_process_holder.Attach(target);
                m_is_attached = true;

                /* Set ourselves as the process's attached object. */
                m_old_process_state = target->SetDebugObject(this);

                /* Send an event for our attaching to the process. */
                this->PushDebugEvent(ams::svc::DebugEvent_CreateProcess, nullptr, 0);

                /* Send events for attaching to each thread in the process. */
                {
                    auto end = target->GetThreadList().end();
                    for (auto it = target->GetThreadList().begin(); it != end; ++it) {
                        /* Request that we suspend the thread. */
                        it->RequestSuspend(KThread::SuspendType_Debug);

                        /* If the thread is in a state for us to do so, generate the event. */
                        if (const auto thread_state = it->GetState(); thread_state == KThread::ThreadState_Runnable || thread_state == KThread::ThreadState_Waiting) {
                            /* Mark the thread as attached to. */
                            it->SetDebugAttached();

                            /* Send the event. */
                            const uintptr_t params[2] = { it->GetId(), GetInteger(it->GetThreadLocalRegionAddress()) };
                            this->PushDebugEvent(ams::svc::DebugEvent_CreateThread, params, util::size(params));
                        }
                    }
                }

                /* Send the process's jit debug info, if relevant. */
                if (KEventInfo *jit_info = target->GetJitDebugInfo(); jit_info != nullptr) {
                    this->EnqueueDebugEventInfo(jit_info);
                }

                /* Send an exception event to represent our attaching. */
                const uintptr_t params[1] = { static_cast<uintptr_t>(ams::svc::DebugException_DebuggerAttached) };
                this->PushDebugEvent(ams::svc::DebugEvent_Exception, params, util::size(params));

                /* Signal. */
                this->NotifyAvailable();
            }
        }

        R_SUCCEED();
    }

    Result KDebugBase::BreakProcess() {
        /* Check that we're attached. */
        R_UNLESS(this->IsAttached(), svc::ResultProcessTerminated());

        /* Open a reference to our process. */
        R_UNLESS(this->OpenProcess(), svc::ResultProcessTerminated());

        /* Close our reference to our process when we're done. */
        ON_SCOPE_EXIT { this->CloseProcess(); };

        /* Get the process pointer. */
        KProcess * const target = this->GetProcessUnsafe();

        /* Lock both ourselves, the target process, and the scheduler. */
        KScopedLightLock state_lk(target->GetStateLock());
        KScopedLightLock list_lk(target->GetListLock());
        KScopedLightLock this_lk(m_lock);
        KScopedSchedulerLock sl;

        /* Check that we're still attached now that we're locked. */
        R_UNLESS(this->IsAttached(), svc::ResultProcessTerminated());

        /* Check that the process isn't terminated. */
        R_UNLESS(!target->IsTerminated(), svc::ResultProcessTerminated());

        /* Get the currently active threads. */
        constexpr u64 ThreadIdNoThread      = -1ll;
        constexpr u64 ThreadIdUnknownThread = -2ll;
        uintptr_t debug_info_params[1 + cpu::NumCores] = { static_cast<uintptr_t>(ams::svc::DebugException_DebuggerBreak), };
        for (size_t i = 0; i < cpu::NumCores; ++i) {
            /* Get the currently running thread. */
            KThread *thread = target->GetRunningThread(i);

            /* Check that the thread's idle count is correct. */
            if (target->GetRunningThreadIdleCount(i) == Kernel::GetScheduler(i).GetIdleCount()) {
                if (thread != nullptr && static_cast<size_t>(thread->GetActiveCore()) == i) {
                    debug_info_params[1 + i] = thread->GetId();
                } else {
                    /* We found an unknown thread. */
                    debug_info_params[1 + i] = ThreadIdUnknownThread;
                }
            } else {
                /* We didn't find a thread. */
                debug_info_params[1 + i] = ThreadIdNoThread;
            }
        }

        /* Suspend all the threads in the process. */
        {
            auto end = target->GetThreadList().end();
            for (auto it = target->GetThreadList().begin(); it != end; ++it) {
                /* Request that we suspend the thread. */
                it->RequestSuspend(KThread::SuspendType_Debug);
            }
        }

        /* Send an exception event to represent our breaking the process. */
        this->PushDebugEvent(ams::svc::DebugEvent_Exception, debug_info_params, util::size(debug_info_params));

        /* Signal. */
        this->NotifyAvailable();

        /* Set the process as breaked. */
        target->SetDebugBreak();

        R_SUCCEED();
    }

    Result KDebugBase::TerminateProcess() {
        /* Check that we're attached. */
        R_UNLESS(this->IsAttached(), ResultSuccess());

        /* Open a reference to our process. */
        R_UNLESS(this->OpenProcess(), ResultSuccess());

        /* Close our reference to our process when we're done. */
        ON_SCOPE_EXIT { this->CloseProcess(); };

        /* Get the process pointer. */
        KProcess * const target = this->GetProcessUnsafe();

        /* Detach from the process. */
        {
            /* Lock both ourselves and the target process. */
            KScopedLightLock state_lk(target->GetStateLock());
            KScopedLightLock list_lk(target->GetListLock());
            KScopedLightLock this_lk(m_lock);

            /* Check that we're still attached. */
            if (this->IsAttached()) {
                /* Lock the scheduler. */
                KScopedSchedulerLock sl;

                /* Get the process's state. */
                const KProcess::State state = target->GetState();

                /* Check that the process is in a state where we can terminate it. */
                R_UNLESS(state != KProcess::State_Created,         svc::ResultInvalidState());
                R_UNLESS(state != KProcess::State_CreatedAttached, svc::ResultInvalidState());

                /* Decide on a new state for the process. */
                KProcess::State new_state;
                if (state == KProcess::State_RunningAttached) {
                    /* If the process is running, transition it accordingly. */
                    new_state = KProcess::State_Running;
                } else if (state == KProcess::State_DebugBreak) {
                    /* If the process is debug breaked, transition it accordingly. */
                    new_state = KProcess::State_Crashed;

                    /* Suspend all the threads in the process. */
                    {
                        auto end = target->GetThreadList().end();
                        for (auto it = target->GetThreadList().begin(); it != end; ++it) {
                            /* Request that we suspend the thread. */
                            it->RequestSuspend(KThread::SuspendType_Debug);
                        }
                    }
                } else {
                    /* Otherwise, don't transition. */
                    new_state = state;
                }

                #if defined(MESOSPHERE_ENABLE_HARDWARE_SINGLE_STEP)
                /* Clear single step on all threads. */
                {
                    auto end = target->GetThreadList().end();
                    for (auto it = target->GetThreadList().begin(); it != end; ++it) {
                        it->ClearHardwareSingleStep();
                    }
                }
                #endif

                /* Detach from the process. */
                target->ClearDebugObject(new_state);
                m_is_attached = false;

                /* Close the initial reference opened to our process. */
                this->CloseProcess();

                /* Clear our continue flags. */
                m_continue_flags = 0;
            }
        }

        /* Terminate the process. */
        target->Terminate();

        R_SUCCEED();
    }

    Result KDebugBase::GetThreadContext(ams::svc::ThreadContext *out, u64 thread_id, u32 context_flags) {
        /* Check that we're attached. */
        R_UNLESS(this->IsAttached(), svc::ResultProcessTerminated());

        /* Open a reference to our process. */
        R_UNLESS(this->OpenProcess(), svc::ResultProcessTerminated());

        /* Close our reference to our process when we're done. */
        ON_SCOPE_EXIT { this->CloseProcess(); };

        /* Lock ourselves. */
        KScopedLightLock lk(m_lock);

        /* Check that we're still attached now that we're locked. */
        R_UNLESS(this->IsAttached(), svc::ResultProcessTerminated());

        /* Get the process pointer. */
        KProcess * const process = this->GetProcessUnsafe();

        /* Get the thread from its id. */
        KThread *thread = KThread::GetThreadFromId(thread_id);
        R_UNLESS(thread != nullptr, svc::ResultInvalidId());
        ON_SCOPE_EXIT { thread->Close(); };

        /* Verify that the thread is owned by our process. */
        R_UNLESS(process == thread->GetOwnerProcess(), svc::ResultInvalidId());

        /* Verify that the thread isn't terminated. */
        R_UNLESS(thread->GetState() != KThread::ThreadState_Terminated, svc::ResultTerminationRequested());

        /* Check that the thread is not the current one. */
        /* NOTE: Nintendo does not check this, and thus the following loop will deadlock. */
        R_UNLESS(thread != GetCurrentThreadPointer(), svc::ResultInvalidId());

        /* Try to get the thread context until the thread isn't current on any core. */
        while (true) {
            KScopedSchedulerLock sl;

            /* The thread needs to be requested for debug suspension. */
            R_UNLESS(thread->IsSuspendRequested(KThread::SuspendType_Debug), svc::ResultInvalidState());

            /* If the thread's raw state isn't runnable, check if it's current on some core. */
            if (thread->GetRawState() != KThread::ThreadState_Runnable) {
                bool current = false;
                for (auto i = 0; i < static_cast<s32>(cpu::NumCores); ++i) {
                    if (thread == Kernel::GetScheduler(i).GetSchedulerCurrentThread()) {
                        current = true;
                        break;
                    }
                }

                /* If the thread is current, retry until it isn't. */
                if (current) {
                    continue;
                }
            }

            /* Get the thread context. */
            static_assert(std::derived_from<KDebug, KDebugBase>);
            R_RETURN(static_cast<KDebug *>(this)->GetThreadContextImpl(out, thread, context_flags));
        }
    }

    Result KDebugBase::SetThreadContext(const ams::svc::ThreadContext &ctx, u64 thread_id, u32 context_flags) {
        /* Check that we're attached. */
        R_UNLESS(this->IsAttached(), svc::ResultProcessTerminated());

        /* Open a reference to our process. */
        R_UNLESS(this->OpenProcess(), svc::ResultProcessTerminated());

        /* Close our reference to our process when we're done. */
        ON_SCOPE_EXIT { this->CloseProcess(); };

        /* Lock ourselves. */
        KScopedLightLock lk(m_lock);

        /* Check that we're still attached now that we're locked. */
        R_UNLESS(this->IsAttached(), svc::ResultProcessTerminated());

        /* Get the process pointer. */
        KProcess * const process = this->GetProcessUnsafe();

        /* Get the thread from its id. */
        KThread *thread = KThread::GetThreadFromId(thread_id);
        R_UNLESS(thread != nullptr, svc::ResultInvalidId());
        ON_SCOPE_EXIT { thread->Close(); };

        /* Verify that the thread is owned by our process. */
        R_UNLESS(process == thread->GetOwnerProcess(), svc::ResultInvalidId());

        /* Verify that the thread isn't terminated. */
        R_UNLESS(thread->GetState() != KThread::ThreadState_Terminated, svc::ResultTerminationRequested());

        /* Check that the thread is not the current one. */
        /* NOTE: Nintendo does not check this, and thus the following loop will deadlock. */
        R_UNLESS(thread != GetCurrentThreadPointer(), svc::ResultInvalidId());

        /* Try to get the thread context until the thread isn't current on any core. */
        while (true) {
            KScopedSchedulerLock sl;

            /* The thread needs to be requested for debug suspension. */
            R_UNLESS(thread->IsSuspendRequested(KThread::SuspendType_Debug), svc::ResultInvalidState());

            /* If the thread's raw state isn't runnable, check if it's current on some core. */
            if (thread->GetRawState() != KThread::ThreadState_Runnable) {
                bool current = false;
                for (auto i = 0; i < static_cast<s32>(cpu::NumCores); ++i) {
                    if (thread == Kernel::GetScheduler(i).GetSchedulerCurrentThread()) {
                        current = true;
                        break;
                    }
                }

                /* If the thread is current, retry until it isn't. */
                if (current) {
                    continue;
                }
            }

            /* Update thread single-step state. */
            #if defined(MESOSPHERE_ENABLE_HARDWARE_SINGLE_STEP)
            {
                if ((context_flags & ams::svc::ThreadContextFlag_SetSingleStep) != 0) {
                    /* Set single step. */
                    thread->SetHardwareSingleStep();

                    /* If no other thread flags are present, we're done. */
                    R_SUCCEED_IF((context_flags & ~ams::svc::ThreadContextFlag_SetSingleStep) == 0);
                } else if ((context_flags & ams::svc::ThreadContextFlag_ClearSingleStep) != 0) {
                    /* Clear single step. */
                    thread->ClearHardwareSingleStep();

                    /* If no other thread flags are present, we're done. */
                    R_SUCCEED_IF((context_flags & ~ams::svc::ThreadContextFlag_ClearSingleStep) == 0);
                }
            }
            #endif

            /* Verify that the thread's svc state is valid. */
            if (thread->IsCallingSvc()) {
                const u8 svc_id         = thread->GetSvcId();

                const bool is_valid_svc = svc_id == svc::SvcId_Break ||
                                          svc_id == svc::SvcId_ReturnFromException;

                R_UNLESS(is_valid_svc, svc::ResultInvalidState());
            }

            /* Set the thread context. */
            static_assert(std::derived_from<KDebug, KDebugBase>);
            R_RETURN(static_cast<KDebug *>(this)->SetThreadContextImpl(ctx, thread, context_flags));
        }
    }


    Result KDebugBase::ContinueDebug(const u32 flags, const u64 *thread_ids, size_t num_thread_ids) {
        /* Check that we're attached. */
        R_UNLESS(this->IsAttached(), svc::ResultProcessTerminated());

        /* Open a reference to our process. */
        R_UNLESS(this->OpenProcess(), svc::ResultProcessTerminated());

        /* Close our reference to our process when we're done. */
        ON_SCOPE_EXIT { this->CloseProcess(); };

        /* Get the process pointer. */
        KProcess * const target = this->GetProcessUnsafe();

        /* Lock both ourselves, the target process, and the scheduler. */
        KScopedLightLock state_lk(target->GetStateLock());
        KScopedLightLock list_lk(target->GetListLock());
        KScopedLightLock this_lk(m_lock);
        KScopedSchedulerLock sl;

        /* Check that we're still attached now that we're locked. */
        R_UNLESS(this->IsAttached(), svc::ResultProcessTerminated());

        /* Check that the process isn't terminated. */
        R_UNLESS(!target->IsTerminated(), svc::ResultProcessTerminated());

        /* Check that we have no pending events. */
        R_UNLESS(m_event_info_list.empty(), svc::ResultBusy());

        /* Clear the target's JIT debug info. */
        target->ClearJitDebugInfo();

        /* Set our continue flags. */
        m_continue_flags = flags;

        /* Iterate over threads, continuing them as we should. */
        bool has_debug_break_thread = false;
        {
            /* Parse our flags. */
            const bool exception_handled = (m_continue_flags & ams::svc::ContinueFlag_ExceptionHandled) != 0;
            const bool continue_all      = (m_continue_flags & ams::svc::ContinueFlag_ContinueAll)      != 0;
            const bool continue_others   = (m_continue_flags & ams::svc::ContinueFlag_ContinueOthers)   != 0;

            /* Update each thread. */
            auto end = target->GetThreadList().end();
            for (auto it = target->GetThreadList().begin(); it != end; ++it) {
                /* Determine if we should continue the thread. */
                bool should_continue;
                {
                    if (continue_all) {
                        /* Continue all threads. */
                        should_continue = true;
                    } else if (continue_others) {
                        /* Continue the thread if it doesn't match one of our target ids. */
                        const u64 thread_id = it->GetId();
                        should_continue = true;
                        for (size_t i = 0; i < num_thread_ids; ++i) {
                            if (thread_ids[i] == thread_id) {
                                should_continue = false;
                                break;
                            }
                        }
                    } else {
                        /* Continue the thread if it matches one of our target ids. */
                        const u64 thread_id = it->GetId();
                        should_continue = false;
                        for (size_t i = 0; i < num_thread_ids; ++i) {
                            if (thread_ids[i] == thread_id) {
                                should_continue = true;
                                break;
                            }
                        }
                    }
                }

                /* Continue the thread if we should. */
                if (should_continue) {
                    if (exception_handled) {
                        it->SetDebugExceptionResult(svc::ResultStopProcessingException());
                    }
                    it->Resume(KThread::SuspendType_Debug);
                }

                /* If the thread has debug suspend requested, note so. */
                if (it->IsSuspendRequested(KThread::SuspendType_Debug)) {
                    has_debug_break_thread = true;
                }
            }
        }

        /* Set the process's state. */
        if (has_debug_break_thread) {
            target->SetDebugBreak();
        } else {
            target->SetAttached();
        }

        R_SUCCEED();
    }

    KEventInfo *KDebugBase::CreateDebugEvent(ams::svc::DebugEvent event, u64 cur_thread_id, const uintptr_t *params, size_t num_params) {
        /* Allocate a new event. */
        KEventInfo *info = KEventInfo::Allocate();

        /* Populate the event info. */
        if (info != nullptr) {
            /* Set common fields. */
            info->event     = event;
            info->thread_id = 0;
            info->flags     = ams::svc::DebugEventFlag_Stopped;

            /* Set event specific fields. */
            switch (event) {
                case ams::svc::DebugEvent_CreateProcess:
                    {
                        /* Check parameters. */
                        MESOSPHERE_ASSERT(params == nullptr);
                        MESOSPHERE_ASSERT(num_params == 0);
                    }
                    break;
                case ams::svc::DebugEvent_CreateThread:
                    {
                        /* Check parameters. */
                        MESOSPHERE_ASSERT(params != nullptr);
                        MESOSPHERE_ASSERT(num_params == 2);

                        /* Set the thread id. */
                        info->thread_id = params[0];

                        /* Set the thread creation info. */
                        info->info.create_thread.thread_id   = params[0];
                        info->info.create_thread.tls_address = params[1];
                    }
                    break;
                case ams::svc::DebugEvent_ExitProcess:
                    {
                        /* Check parameters. */
                        MESOSPHERE_ASSERT(params != nullptr);
                        MESOSPHERE_ASSERT(num_params == 1);

                        /* Set the exit reason. */
                        info->info.exit_process.reason = static_cast<ams::svc::ProcessExitReason>(params[0]);

                        /* Clear the thread id and flags. */
                        info->thread_id = 0;
                        info->flags = 0;
                    }
                    break;
                case ams::svc::DebugEvent_ExitThread:
                    {
                        /* Check parameters. */
                        MESOSPHERE_ASSERT(params != nullptr);
                        MESOSPHERE_ASSERT(num_params == 2);

                        /* Set the thread id. */
                        info->thread_id = params[0];

                        /* Set the exit reason. */
                        info->info.exit_thread.reason = static_cast<ams::svc::ThreadExitReason>(params[1]);
                    }
                    break;
                case ams::svc::DebugEvent_Exception:
                    {
                        /* Check parameters. */
                        MESOSPHERE_ASSERT(params != nullptr);
                        MESOSPHERE_ASSERT(num_params >= 1);

                        /* Set the thread id. */
                        info->thread_id = cur_thread_id;

                        /* Set the exception type, and clear the count. */
                        info->info.exception.exception_type       = static_cast<ams::svc::DebugException>(params[0]);
                        info->info.exception.exception_data_count = 0;
                        switch (static_cast<ams::svc::DebugException>(params[0])) {
                            case ams::svc::DebugException_UndefinedInstruction:
                            case ams::svc::DebugException_BreakPoint:
                            case ams::svc::DebugException_UndefinedSystemCall:
                                {
                                    MESOSPHERE_ASSERT(num_params >= 3);

                                    info->info.exception.exception_address    = params[1];

                                    info->info.exception.exception_data_count = 1;
                                    info->info.exception.exception_data[0]    = params[2];
                                }
                                break;
                            case ams::svc::DebugException_DebuggerAttached:
                                {
                                    info->thread_id = 0;

                                    info->info.exception.exception_address = 0;
                                }
                                break;
                            case ams::svc::DebugException_UserBreak:
                                {
                                    MESOSPHERE_ASSERT(num_params >= 2);

                                    info->info.exception.exception_address    = params[1];

                                    info->info.exception.exception_data_count = 0;
                                    for (size_t i = 2; i < num_params; ++i) {
                                        info->info.exception.exception_data[info->info.exception.exception_data_count++] = params[i];
                                    }
                                }
                                break;
                            case ams::svc::DebugException_DebuggerBreak:
                                {
                                    info->thread_id = 0;

                                    info->info.exception.exception_address    = 0;

                                    info->info.exception.exception_data_count = 0;
                                    for (size_t i = 1; i < num_params; ++i) {
                                        info->info.exception.exception_data[info->info.exception.exception_data_count++] = params[i];
                                    }
                                }
                                break;
                            case ams::svc::DebugException_MemorySystemError:
                                {
                                    info->info.exception.exception_address = 0;
                                }
                                break;
                            case ams::svc::DebugException_InstructionAbort:
                            case ams::svc::DebugException_DataAbort:
                            case ams::svc::DebugException_AlignmentFault:
                            default:
                                {
                                    MESOSPHERE_ASSERT(num_params >= 2);

                                    info->info.exception.exception_address = params[1];
                                }
                                break;
                        }
                    }
                    break;
            }
        }

        return info;
    }

    void KDebugBase::PushDebugEvent(ams::svc::DebugEvent event, const uintptr_t *params, size_t num_params) {
        /* Create and enqueue and event. */
        if (KEventInfo *new_info = CreateDebugEvent(event, GetCurrentThread().GetId(), params, num_params); new_info != nullptr) {
            this->EnqueueDebugEventInfo(new_info);
        }
    }

    void KDebugBase::EnqueueDebugEventInfo(KEventInfo *info) {
        /* Lock the scheduler. */
        KScopedSchedulerLock sl;

        /* Push the event to the back of the list. */
        m_event_info_list.push_back(*info);
    }


    template<typename T> requires (std::same_as<T, ams::svc::lp64::DebugEventInfo> || std::same_as<T, ams::svc::ilp32::DebugEventInfo>)
    Result KDebugBase::GetDebugEventInfoImpl(T *out) {
        /* Check that we're attached. */
        R_UNLESS(this->IsAttached(), svc::ResultProcessTerminated());

        /* Open a reference to our process. */
        R_UNLESS(this->OpenProcess(), svc::ResultProcessTerminated());

        /* Close our reference to our process when we're done. */
        ON_SCOPE_EXIT { this->CloseProcess(); };

        /* Get the process pointer. */
        KProcess * const process = this->GetProcessUnsafe();

        /* Pop an event info from our queue. */
        KEventInfo *info = nullptr;
        {
            KScopedSchedulerLock sl;

            /* Check that we have an event to dequeue. */
            R_UNLESS(!m_event_info_list.empty(), svc::ResultNoEvent());

            /* Pop the event from the front of the queue. */
            info = std::addressof(m_event_info_list.front());
            m_event_info_list.pop_front();
        }
        MESOSPHERE_ASSERT(info != nullptr);

        /* Free the event info once we're done with it. */
        ON_SCOPE_EXIT { KEventInfo::Free(info); };

        /* Set common fields. */
        out->type      = info->event;
        out->thread_id = info->thread_id;
        out->flags     = info->flags;

        /* Set event specific fields. */
        switch (info->event) {
            case ams::svc::DebugEvent_CreateProcess:
                {
                    out->info.create_process.program_id                     = process->GetProgramId();
                    out->info.create_process.process_id                     = process->GetId();
                    out->info.create_process.flags                          = process->GetCreateProcessFlags();
                    out->info.create_process.user_exception_context_address = GetInteger(process->GetProcessLocalRegionAddress());

                    std::memcpy(out->info.create_process.name, process->GetName(), sizeof(out->info.create_process.name));
                }
                break;
            case ams::svc::DebugEvent_CreateThread:
                {
                    out->info.create_thread.thread_id   = info->info.create_thread.thread_id;
                    out->info.create_thread.tls_address = info->info.create_thread.tls_address;
                }
                break;
            case ams::svc::DebugEvent_ExitProcess:
                {
                    out->info.exit_process.reason = info->info.exit_process.reason;
                }
                break;
            case ams::svc::DebugEvent_ExitThread:
                {
                    out->info.exit_thread.reason = info->info.exit_thread.reason;
                }
                break;
            case ams::svc::DebugEvent_Exception:
                {
                    out->info.exception.type    = info->info.exception.exception_type;
                    out->info.exception.address = info->info.exception.exception_address;

                    switch (info->info.exception.exception_type) {
                        case ams::svc::DebugException_UndefinedInstruction:
                            {
                                MESOSPHERE_ASSERT(info->info.exception.exception_data_count == 1);
                                out->info.exception.specific.undefined_instruction.insn = info->info.exception.exception_data[0];
                            }
                            break;
                        case ams::svc::DebugException_BreakPoint:
                            {
                                MESOSPHERE_ASSERT(info->info.exception.exception_data_count == 1);
                                out->info.exception.specific.break_point.type    = static_cast<ams::svc::BreakPointType>(info->info.exception.exception_data[0]);
                                out->info.exception.specific.break_point.address = 0;
                            }
                            break;
                        case ams::svc::DebugException_UserBreak:
                            {
                                MESOSPHERE_ASSERT(info->info.exception.exception_data_count == 3);
                                out->info.exception.specific.user_break.break_reason = static_cast<ams::svc::BreakReason>(info->info.exception.exception_data[0]);
                                out->info.exception.specific.user_break.address      = info->info.exception.exception_data[1];
                                out->info.exception.specific.user_break.size         = info->info.exception.exception_data[2];
                            }
                            break;
                        case ams::svc::DebugException_DebuggerBreak:
                            {
                                /* TODO: How does this work with non-4 cpu count? */
                                static_assert(cpu::NumCores <= 4);

                                MESOSPHERE_ASSERT(info->info.exception.exception_data_count == cpu::NumCores);
                                out->info.exception.specific.debugger_break.active_thread_ids[0] = info->info.exception.exception_data[0];
                                out->info.exception.specific.debugger_break.active_thread_ids[1] = info->info.exception.exception_data[1];
                                out->info.exception.specific.debugger_break.active_thread_ids[2] = info->info.exception.exception_data[2];
                                out->info.exception.specific.debugger_break.active_thread_ids[3] = info->info.exception.exception_data[3];
                            }
                            break;
                        case ams::svc::DebugException_UndefinedSystemCall:
                            {
                                MESOSPHERE_ASSERT(info->info.exception.exception_data_count == 1);
                                out->info.exception.specific.undefined_system_call.id = info->info.exception.exception_data[0];
                            }
                            break;
                        default:
                            {
                                /* ... */
                            }
                            break;
                    }
                }
                break;
        }

        R_SUCCEED();
    }

    Result KDebugBase::GetDebugEventInfo(ams::svc::lp64::DebugEventInfo *out) {
        R_RETURN(this->GetDebugEventInfoImpl(out));
    }

    Result KDebugBase::GetDebugEventInfo(ams::svc::ilp32::DebugEventInfo *out) {
        R_RETURN(this->GetDebugEventInfoImpl(out));
    }

    void KDebugBase::Finalize() {
        /* Perform base finalization. */
        KSynchronizationObject::Finalize();

        /* Perform post-synchronization finalization. */
        this->OnFinalizeSynchronizationObject();
    }

    void KDebugBase::OnFinalizeSynchronizationObject() {
        /* Detach from our process, if we have one. */
        if (this->IsAttached() && this->OpenProcess()) {
            /* Close the process when we're done with it. */
            ON_SCOPE_EXIT { this->CloseProcess(); };

            /* Get the process pointer. */
            KProcess * const process = this->GetProcessUnsafe();

            /* Lock both ourselves and the target process. */
            KScopedLightLock state_lk(process->GetStateLock());
            KScopedLightLock list_lk(process->GetListLock());
            KScopedLightLock this_lk(m_lock);

            /* Check that we're still attached. */
            if (m_is_attached) {
                KScopedSchedulerLock sl;

                /* Detach ourselves from the process. */
                process->ClearDebugObject(m_old_process_state);

                /* Release all threads. */
                const bool resume = (process->GetState() != KProcess::State_Crashed);
                {
                    auto end = process->GetThreadList().end();
                    for (auto it = process->GetThreadList().begin(); it != end; ++it) {
                        #if defined(MESOSPHERE_ENABLE_HARDWARE_SINGLE_STEP)
                        /* Clear the thread's single-step state. */
                        it->ClearHardwareSingleStep();
                        #endif

                        if (resume) {
                            /* If the process isn't crashed, resume threads. */
                            it->Resume(KThread::SuspendType_Debug);
                        } else {
                            /* Otherwise, suspend them. */
                            it->RequestSuspend(KThread::SuspendType_Debug);
                        }
                    }
                }

                /* Note we're now unattached. */
                m_is_attached = false;

                /* Close the initial reference opened to our process. */
                this->CloseProcess();
            }
        }

        /* Free any pending events. */
        {
            KScopedSchedulerLock sl;

            while (!m_event_info_list.empty()) {
                KEventInfo *info = std::addressof(m_event_info_list.front());
                m_event_info_list.pop_front();
                KEventInfo::Free(info);
            }
        }
    }

    bool KDebugBase::IsSignaled() const {
        bool empty;
        {
            KScopedSchedulerLock sl;

            empty = m_event_info_list.empty();
        }

        return !empty || !m_is_attached || this->GetProcessUnsafe()->IsTerminated();
    }

    Result KDebugBase::ProcessDebugEvent(ams::svc::DebugEvent event, const uintptr_t *params, size_t num_params) {
        /* Get the current process. */
        KProcess *process = GetCurrentProcessPointer();

        /* If the event is CreateThread and we've already attached, there's nothing to do. */
        if (event == ams::svc::DebugEvent_CreateThread) {
            R_SUCCEED_IF(GetCurrentThread().IsAttachedToDebugger());
        }

        while (true) {
            /* Lock the process and the scheduler. */
            KScopedLightLock state_lk(process->GetStateLock());
            KScopedLightLock list_lk(process->GetListLock());
            KScopedSchedulerLock sl;

            /* If the current thread is terminating, we can't process an event. */
            R_SUCCEED_IF(GetCurrentThread().IsTerminationRequested());

            /* Get the debug object. If we have none, there's nothing to process. */
            KDebugBase *debug = GetDebugObject(process);
            R_SUCCEED_IF(debug == nullptr);

            /* If the event is an exception and we don't have exception events enabled, we can't handle the event. */
            if (event == ams::svc::DebugEvent_Exception && (debug->m_continue_flags & ams::svc::ContinueFlag_EnableExceptionEvent) == 0) {
                GetCurrentThread().SetDebugExceptionResult(ResultSuccess());
                R_THROW(svc::ResultNotHandled());
            }

            /* If the current thread is suspended, retry. */
            if (GetCurrentThread().IsSuspended()) {
                continue;
            }

            /* Suspend all the process's threads. */
            {
                auto end = process->GetThreadList().end();
                for (auto it = process->GetThreadList().begin(); it != end; ++it) {
                    it->RequestSuspend(KThread::SuspendType_Debug);
                }
            }

            /* Push the event. */
            debug->PushDebugEvent(event, params, num_params);
            debug->NotifyAvailable();

            /* Set the process as breaked. */
            process->SetDebugBreak();

            /* If the event is an exception, set the result and clear single step. */
            if (event == ams::svc::DebugEvent_Exception) {
                GetCurrentThread().SetDebugExceptionResult(ResultSuccess());
            }

            /* Exit our retry loop. */
            break;
        }

        /* If the event is an exception, get the exception result. */
        if (event == ams::svc::DebugEvent_Exception) {
            /* Lock the scheduler. */
            KScopedSchedulerLock sl;

            /* If the thread is terminating, we can't process the exception. */
            R_UNLESS(!GetCurrentThread().IsTerminationRequested(), svc::ResultStopProcessingException());

            /* Get the debug object. */
            if (KDebugBase *debug = GetDebugObject(process); debug != nullptr) {
                /* If we have one, check the debug exception. */
                R_RETURN(GetCurrentThread().GetDebugExceptionResult());
            } else {
                /* We don't have a debug object, so stop processing the exception. */
                R_THROW(svc::ResultStopProcessingException());
            }
        }

        R_SUCCEED();
    }

    Result KDebugBase::OnDebugEvent(ams::svc::DebugEvent event, const uintptr_t *params, size_t num_params) {
        if (KProcess *process = GetCurrentProcessPointer(); process != nullptr && process->IsAttachedToDebugger()) {
            R_RETURN(ProcessDebugEvent(event, params, num_params));
        }
        R_SUCCEED();
    }

    Result KDebugBase::OnExitProcess(KProcess *process) {
        MESOSPHERE_ASSERT(process != nullptr);

        /* Check if we're attached to a debugger. */
        if (process->IsAttachedToDebugger()) {
            /* If we are, lock the scheduler. */
            KScopedSchedulerLock sl;

            /* Push the event. */
            if (KDebugBase *debug = GetDebugObject(process); debug != nullptr) {
                const uintptr_t params[1] = { static_cast<uintptr_t>(ams::svc::ProcessExitReason_ExitProcess) };
                debug->PushDebugEvent(ams::svc::DebugEvent_ExitProcess, params, util::size(params));
                debug->NotifyAvailable();
            }
        }

        R_SUCCEED();
    }

    Result KDebugBase::OnTerminateProcess(KProcess *process) {
        MESOSPHERE_ASSERT(process != nullptr);

        /* Check if we're attached to a debugger. */
        if (process->IsAttachedToDebugger()) {
            /* If we are, lock the scheduler. */
            KScopedSchedulerLock sl;

            /* Push the event. */
            if (KDebugBase *debug = GetDebugObject(process); debug != nullptr) {
                const uintptr_t params[1] = { static_cast<uintptr_t>(ams::svc::ProcessExitReason_TerminateProcess) };
                debug->PushDebugEvent(ams::svc::DebugEvent_ExitProcess, params, util::size(params));
                debug->NotifyAvailable();
            }
        }

        R_SUCCEED();
    }

    Result KDebugBase::OnExitThread(KThread *thread) {
        MESOSPHERE_ASSERT(thread != nullptr);

        /* Check if we're attached to a debugger. */
        if (KProcess *process = thread->GetOwnerProcess(); process != nullptr && process->IsAttachedToDebugger()) {
            /* If we are, submit the event. */
            const uintptr_t params[2] = { thread->GetId(), static_cast<uintptr_t>(thread->IsTerminationRequested() ? ams::svc::ThreadExitReason_TerminateThread : ams::svc::ThreadExitReason_ExitThread) };
            R_TRY(OnDebugEvent(ams::svc::DebugEvent_ExitThread, params, util::size(params)));
        }

        R_SUCCEED();
    }

}