/*
 * 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::svc {

    /* =============================    Common    ============================= */

    namespace {

        Result QueryPhysicalAddress(ams::svc::PhysicalMemoryInfo *out_info, uintptr_t address) {
            /* NOTE: In 10.0.0, Nintendo stubbed this SVC. Should we do so? */
            /* R_UNLESS(GetTargetFirmware() < TargetFirmware_10_0_0, svc::ResultInvalidCurrentMemory()); */

            /* Get reference to page table. */
            auto &pt = GetCurrentProcess().GetPageTable();

            /* Check that the address is valid. */
            R_UNLESS(pt.Contains(address, 1), svc::ResultInvalidCurrentMemory());

            /* Query the physical mapping. */
            R_TRY(pt.QueryPhysicalAddress(out_info, address));

            R_SUCCEED();
        }

        Result QueryMemoryMapping(uintptr_t *out_address, size_t *out_size, uint64_t phys_addr, size_t size) {
            /* Declare variables we'll populate. */
            KProcessAddress found_address = Null<KProcessAddress>;
            size_t          found_size    = 0;

            /* Get reference to page table. */
            auto &pt = GetCurrentProcess().GetPageTable();

            /* Check whether the address is aligned. */
            const bool aligned = util::IsAligned(phys_addr, PageSize);

            auto QueryMappingFromPageTable = [&](uint64_t phys_addr, size_t size) ALWAYS_INLINE_LAMBDA -> Result {
                /* The size must be non-zero. */
                R_UNLESS(size > 0, svc::ResultInvalidSize());

                /* The request must not overflow. */
                R_UNLESS((phys_addr < phys_addr + size), svc::ResultNotFound());

                /* Query the mapping. */
                R_TRY_CATCH(pt.QueryIoMapping(std::addressof(found_address), phys_addr, size)) {
                    R_CATCH(svc::ResultNotFound) {
                        /* If we failed to find an io mapping, check if the address is a static mapping. */
                        R_TRY(pt.QueryStaticMapping(std::addressof(found_address), phys_addr, size));
                    }
                } R_END_TRY_CATCH;

                /* Use the size as the found size. */
                found_size = size;

                R_SUCCEED();
            };

            if (aligned) {
                /* Query the input. */
                R_TRY(QueryMappingFromPageTable(phys_addr, size));
            } else {
                if (kern::GetTargetFirmware() < TargetFirmware_8_0_0 && phys_addr >= PageSize) {
                    /* Query the aligned-down page. */
                    const size_t offset = phys_addr & (PageSize - 1);
                    R_TRY(QueryMappingFromPageTable(phys_addr - offset, size + offset));

                    /* Adjust the output address. */
                    found_address += offset;
                } else {
                    /* Newer kernel only allows unaligned addresses when they're special enum members. */
                    R_UNLESS(phys_addr < PageSize, svc::ResultNotFound());

                    /* Try to find the memory region. */
                    const KMemoryRegion * const region = [] ALWAYS_INLINE_LAMBDA (ams::svc::MemoryRegionType type) -> const KMemoryRegion * {
                        switch (type) {
                            case ams::svc::MemoryRegionType_KernelTraceBuffer: return KMemoryLayout::GetPhysicalKernelTraceBufferRegion();
                            case ams::svc::MemoryRegionType_OnMemoryBootImage: return KMemoryLayout::GetPhysicalOnMemoryBootImageRegion();
                            case ams::svc::MemoryRegionType_DTB:               return KMemoryLayout::GetPhysicalDTBRegion();
                            default:                                           return nullptr;
                        }
                    }(static_cast<ams::svc::MemoryRegionType>(phys_addr));

                    /* Ensure that we found the region. */
                    R_UNLESS(region != nullptr, svc::ResultNotFound());

                    /* Chcek that the region is valid. */
                    MESOSPHERE_ABORT_UNLESS(region->GetEndAddress() != 0);

                    R_TRY(pt.QueryStaticMapping(std::addressof(found_address), region->GetAddress(), region->GetSize()));
                    found_size = region->GetSize();
                }
            }

            /* We succeeded. */
            MESOSPHERE_ASSERT(found_address != Null<KProcessAddress>);
            MESOSPHERE_ASSERT(found_size    != 0);
            if (out_address != nullptr) {
                *out_address = GetInteger(found_address);
            }
            if (out_size != nullptr) {
                *out_size = found_size;
            }
            R_SUCCEED();
        }

    }

    /* =============================    64 ABI    ============================= */

    Result QueryPhysicalAddress64(ams::svc::lp64::PhysicalMemoryInfo *out_info, ams::svc::Address address) {
        R_RETURN(QueryPhysicalAddress(out_info, address));
    }

    Result QueryMemoryMapping64(ams::svc::Address *out_address, ams::svc::Size *out_size, ams::svc::PhysicalAddress physical_address, ams::svc::Size size) {
        static_assert(sizeof(*out_address) == sizeof(uintptr_t));
        static_assert(sizeof(*out_size)    == sizeof(size_t));
        R_RETURN(QueryMemoryMapping(reinterpret_cast<uintptr_t *>(out_address), reinterpret_cast<size_t *>(out_size), physical_address, size));
    }

    Result LegacyQueryIoMapping64(ams::svc::Address *out_address, ams::svc::PhysicalAddress physical_address, ams::svc::Size size) {
        static_assert(sizeof(*out_address) == sizeof(uintptr_t));
        R_RETURN(QueryMemoryMapping(reinterpret_cast<uintptr_t *>(out_address), nullptr, physical_address, size));
    }

    /* ============================= 64From32 ABI ============================= */

    Result QueryPhysicalAddress64From32(ams::svc::ilp32::PhysicalMemoryInfo *out_info, ams::svc::Address address) {
        ams::svc::PhysicalMemoryInfo info = {};
        R_TRY(QueryPhysicalAddress(std::addressof(info), address));

        *out_info = {
            .physical_address = info.physical_address,
            .virtual_address  = static_cast<u32>(info.virtual_address),
            .size             = static_cast<u32>(info.size),
        };
        R_SUCCEED();
    }

    Result QueryMemoryMapping64From32(ams::svc::Address *out_address, ams::svc::Size *out_size, ams::svc::PhysicalAddress physical_address, ams::svc::Size size) {
        static_assert(sizeof(*out_address) == sizeof(uintptr_t));
        static_assert(sizeof(*out_size)    == sizeof(size_t));
        R_RETURN(QueryMemoryMapping(reinterpret_cast<uintptr_t *>(out_address), reinterpret_cast<size_t *>(out_size), physical_address, size));
    }

    Result LegacyQueryIoMapping64From32(ams::svc::Address *out_address, ams::svc::PhysicalAddress physical_address, ams::svc::Size size) {
        static_assert(sizeof(*out_address) == sizeof(uintptr_t));
        R_RETURN(QueryMemoryMapping(reinterpret_cast<uintptr_t *>(out_address), nullptr, physical_address, size));
    }

}
