/*
 * 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 .
 */
#include 
#include 
#include "diag_symbol_impl.hpp"
#include 
extern "C" {
    void __module_offset_helper() { /* ... */ }
}
namespace ams::diag::impl {
    namespace {
        class CurrentExecutableHelper {
            private:
                struct SymbolInfo {
                    uintptr_t address;
                    const char *name;
                };
            private:
                os::NativeHandle m_file_handle;
                os::NativeHandle m_map_handle;
                void *m_file_map;
                size_t m_file_size;
                SymbolInfo *m_symbols;
                size_t m_num_symbol;
                uintptr_t m_module_address;
                size_t m_module_size;
                uintptr_t m_module_displacement;
            private:
                CurrentExecutableHelper() : m_file_handle(INVALID_HANDLE_VALUE), m_map_handle(INVALID_HANDLE_VALUE), m_file_map(nullptr), m_file_size(0), m_symbols(nullptr), m_num_symbol(0), m_module_address(0), m_module_size(0), m_module_displacement(0) {
                    /* Open the current executable. */
                    auto exe_handle = OpenExecutableFile();
                    if (exe_handle == INVALID_HANDLE_VALUE) {
                        return;
                    }
                    ON_SCOPE_EXIT { if (exe_handle != INVALID_HANDLE_VALUE) { ::CloseHandle(exe_handle); } };
                    /* Get the exe size. */
                    LARGE_INTEGER exe_size_li;
                    if (::GetFileSizeEx(exe_handle, std::addressof(exe_size_li)) == 0) {
                        return;
                    }
                    /* Check the exe size. */
                    s64 exe_size = exe_size_li.QuadPart;
                    if (exe_size == 0) {
                        return;
                    }
                    /* Create a file mapping. */
                    auto map_handle = ::CreateFileMappingW(exe_handle, nullptr, PAGE_READONLY, 0, 0, nullptr);
                    if (map_handle == INVALID_HANDLE_VALUE) {
                        return;
                    }
                    ON_SCOPE_EXIT { if (map_handle != INVALID_HANDLE_VALUE) { ::CloseHandle(map_handle); } };
                    /* Map the file. */
                    void *exe_map = ::MapViewOfFile(map_handle, FILE_MAP_READ, 0, 0, 0);
                    if (exe_map == nullptr) {
                        return;
                    }
                    ON_SCOPE_EXIT { if (exe_map != nullptr) { ::UnmapViewOfFile(exe_map); } };
                    /* Get/parse the DOS header. */
                    const uintptr_t exe_start = reinterpret_cast(exe_map);
                    const auto *dos_header = reinterpret_cast(exe_start);
                    if (dos_header->e_magic != IMAGE_DOS_SIGNATURE) {
                        return;
                    }
                    /* Set up the nt headers. */
                    const auto *nt32 = reinterpret_cast(exe_start + dos_header->e_lfanew);
                    const auto *nt64 = reinterpret_cast(exe_start + dos_header->e_lfanew);
                    if (nt32->FileHeader.Machine == IMAGE_FILE_MACHINE_AMD64 || nt32->FileHeader.Machine == IMAGE_FILE_MACHINE_IA64) {
                        nt32 = nullptr;
                    } else {
                        nt64 = nullptr;
                    }
                    #define EXE_NT_HEADER(x) ((nt64 != nullptr) ? nt64->x : nt32->x)
                    if (EXE_NT_HEADER(Signature) != IMAGE_NT_SIGNATURE) {
                        return;
                    }
                    /* Check that the optional header is really present. */
                    if (EXE_NT_HEADER(FileHeader.SizeOfOptionalHeader) < sizeof(IMAGE_OPTIONAL_HEADER32)) {
                        return;
                    }
                    /* Get sections. */
                    const auto *sec   = nt64 != nullptr ? IMAGE_FIRST_SECTION(nt64) : IMAGE_FIRST_SECTION(nt32);
                    /* Get the symbol table. */
                    const auto *symtab = reinterpret_cast(exe_start + EXE_NT_HEADER(FileHeader.PointerToSymbolTable));
                    const size_t nsym  = EXE_NT_HEADER(FileHeader.NumberOfSymbols);
                    const char *strtab = reinterpret_cast(symtab + nsym);
                    /* Find the section containing our code. */
                    int text_section = -1;
                    for (size_t i = 0; i < nsym; ++i) {
                        if (ISFCN(symtab[i].Type)) {
                            const char *name = symtab[i].N.Name.Short == 0 ? strtab + symtab[i].N.Name.Long : reinterpret_cast(symtab[i].N.ShortName);
                            if (std::strcmp(name, "__module_offset_helper") == 0) {
                                AMS_ASSERT(text_section == -1);
                                AMS_ASSERT(m_module_displacement == 0);
                                m_module_displacement = reinterpret_cast(&__module_offset_helper) - symtab[i].Value;
                                text_section          = symtab[i].SectionNumber;
                                AMS_ASSERT(m_module_displacement != 0);
                                AMS_ASSERT(text_section != -1);
                                break;
                            }
                        }
                    }
                    /* Determine the number of functions. */
                    size_t funcs = 0;
                    for (size_t i = 0; i < nsym; ++i) {
                        if (ISFCN(symtab[i].Type) && symtab[i].SectionNumber == text_section) {
                            ++funcs;
                        }
                    }
                    /* Allocate functions. */
                    m_symbols = reinterpret_cast(std::malloc(sizeof(SymbolInfo) * funcs));
                    if (m_symbols == nullptr) {
                        return;
                    }
                    /* Set all symbols. */
                    m_num_symbol = 0;
                    for (size_t i = 0; i < nsym; ++i) {
                        if (ISFCN(symtab[i].Type) && symtab[i].SectionNumber == text_section) {
                            m_symbols[m_num_symbol].address = symtab[i].Value;
                            m_symbols[m_num_symbol].name    = symtab[i].N.Name.Short == 0 ? strtab + symtab[i].N.Name.Long : reinterpret_cast(symtab[i].N.ShortName);
                            ++m_num_symbol;
                        }
                    }
                    AMS_ASSERT(m_num_symbol == funcs);
                    /* Sort the symbols. */
                    std::sort(m_symbols + 0, m_symbols + m_num_symbol, [] (const SymbolInfo &lhs, const SymbolInfo &rhs) {
                        return lhs.address < rhs.address;
                    });
                    m_module_address = 0;
                    m_module_size    = sec[text_section - 1].Misc.VirtualSize;
                    if (m_module_displacement != 0 && m_module_size > 0 && m_num_symbol > 0) {
                        std::swap(m_file_handle, exe_handle);
                        std::swap(m_map_handle, map_handle);
                        std::swap(m_file_map, exe_map);
                        m_file_size = exe_size;
                    }
                }
                ~CurrentExecutableHelper() {
                    if (m_file_map != nullptr) {
                        ::UnmapViewOfFile(m_file_map);
                    }
                    if (m_map_handle != nullptr) {
                        ::CloseHandle(m_map_handle);
                    }
                    if (m_file_handle != nullptr) {
                        ::CloseHandle(m_file_handle);
                    }
                }
            public:
                static CurrentExecutableHelper &GetInstance() {
                    AMS_FUNCTION_LOCAL_STATIC(CurrentExecutableHelper, s_current_executable_helper_instance);
                    return s_current_executable_helper_instance;
                }
            private:
                size_t GetSymbolSizeImpl(const SymbolInfo *symbol) const {
                    /* Do our best to guess. */
                    if (symbol != m_symbols + m_num_symbol - 1) {
                        return (symbol + 1)->address - symbol->address;
                    } else if (m_module_address + m_module_size >= symbol->address) {
                        return m_module_address + m_module_size - symbol->address;
                    } else {
                        return 0;
                    }
                }
                const SymbolInfo *GetBestSymbol(uintptr_t address) const {
                    address -= m_module_displacement;
                    const SymbolInfo *best_symbol = std::lower_bound(m_symbols + 0, m_symbols + m_num_symbol, address, [](const SymbolInfo &lhs, uintptr_t rhs) {
                        return lhs.address < rhs;
                    });
                    if (best_symbol == m_symbols + m_num_symbol) {
                        return nullptr;
                    }
                    if (best_symbol->address != address && best_symbol > m_symbols) {
                        --best_symbol;
                    }
                    const auto vma = best_symbol->address;
                    const auto end = vma + this->GetSymbolSizeImpl(best_symbol);
                    if (vma <= address && address < end) {
                        return best_symbol;
                    } else {
                        return nullptr;
                    }
                }
            public:
                uintptr_t GetSymbolName(char *dst, size_t dst_size, uintptr_t address) const {
                    if (m_file_handle == INVALID_HANDLE_VALUE) {
                        return 0;
                    }
                    /* Get the symbol. */
                    const auto *symbol = this->GetBestSymbol(address);
                    if (symbol == nullptr) {
                        return 0;
                    }
                    /* Print the symbol. */
                    const char *name = symbol->name;
                    int cpp_name_status = 0;
                    if (char *demangled = abi::__cxa_demangle(name, nullptr, 0, std::addressof(cpp_name_status)); cpp_name_status == 0) {
                        AMS_ASSERT(demangled != nullptr);
                        util::TSNPrintf(dst, dst_size, "%s", demangled);
                        std::free(demangled);
                    } else {
                        util::TSNPrintf(dst, dst_size, "%s", name);
                    }
                    return symbol->address + m_module_displacement;
                }
                size_t GetSymbolSize(uintptr_t address) const {
                    if (m_file_handle == INVALID_HANDLE_VALUE) {
                        return 0;
                    }
                    /* Get the symbol. */
                    const auto *symbol = this->GetBestSymbol(address);
                    if (symbol == nullptr) {
                        return 0;
                    }
                    return this->GetSymbolSizeImpl(symbol);
                }
            private:
                static os::NativeHandle OpenExecutableFile() {
                    /* Get the module file name. */
                    wchar_t module_file_name[0x1000];
                    if (::GetModuleFileNameW(0, module_file_name, util::size(module_file_name)) == 0) {
                        return INVALID_HANDLE_VALUE;
                    }
                    return ::CreateFileW(module_file_name, GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
                }
        };
    }
    uintptr_t GetSymbolNameImpl(char *dst, size_t dst_size, uintptr_t address) {
        return CurrentExecutableHelper::GetInstance().GetSymbolName(dst, dst_size, address);
    }
    size_t GetSymbolSizeImpl(uintptr_t address) {
        return CurrentExecutableHelper::GetInstance().GetSymbolSize(address);
    }
}