/*
 * Copyright (c) 2024, Sönke Holz <soenke.holz@serenityos.org>
 *
 * SPDX-License-Identifier: BSD-2-Clause
 */

#include <Kernel/EFIPrekernel/Panic.h>
#include <Kernel/EFIPrekernel/Relocation.h>

#include <LibELF/Arch/GenericDynamicRelocationType.h>
#include <LibELF/ELFABI.h>
#include <LibELF/Image.h>

namespace Kernel {

// This function only works on ELF addresses that are not in a zero-padded area of a program header.
static ErrorOr<Bytes> get_data_at_kernel_elf_virtual_address(ELF::Image const& kernel_elf_image, Bytes kernel_elf_image_data, FlatPtr start_vaddr, size_t size)
{
    ErrorOr<Bytes> result = EINVAL;
    static Optional<ELF::Image::ProgramHeader> cached_program_header;

    static auto is_range_in_program_header = [](FlatPtr start_vaddr, size_t size, ELF::Image::ProgramHeader const& program_header) {
        return start_vaddr >= program_header.vaddr().get() && start_vaddr + size <= program_header.vaddr().get() + program_header.size_in_memory();
    };

    if (cached_program_header.has_value() && is_range_in_program_header(start_vaddr, size, cached_program_header.value()))
        return kernel_elf_image_data.slice(cached_program_header->offset() + start_vaddr - cached_program_header->vaddr().get(), size);

    kernel_elf_image.for_each_program_header([&result, kernel_elf_image_data, start_vaddr, size](ELF::Image::ProgramHeader const& program_header) {
        if (program_header.type() == PT_LOAD && is_range_in_program_header(start_vaddr, size, program_header)) {
            result = kernel_elf_image_data.slice(program_header.offset() + start_vaddr - program_header.vaddr().get(), size);
            cached_program_header = program_header;
            return IterationDecision::Break;
        }
        return IterationDecision::Continue;
    });

    return result;
}

void perform_kernel_relocations(ELF::Image const& kernel_elf_image, Bytes kernel_elf_image_data, FlatPtr base_address)
{
    size_t dynamic_section_offset { 0 };
    kernel_elf_image.for_each_program_header([&dynamic_section_offset](ELF::Image::ProgramHeader const& program_header) {
        if (program_header.type() == PT_DYNAMIC) {
            dynamic_section_offset = program_header.offset();
            return IterationDecision::Break;
        }
        return IterationDecision::Continue;
    });

    if (dynamic_section_offset == 0)
        PANIC("Kernel image does not have a PT_DYNAMIC program header; can't perform relocations");

    FlatPtr relocation_table_vaddr = 0;
    size_t relocation_table_size = 0;
    size_t relocation_entry_size = 0;
    size_t number_of_relative_relocs = 0;

    FlatPtr relr_relocation_table_vaddr = 0;
    size_t relr_relocation_table_size = 0;

    auto const* dynamic_section_entries = bit_cast<Elf_Dyn const*>(bit_cast<FlatPtr>(kernel_elf_image_data.data()) + dynamic_section_offset);
    for (size_t i = 0;; i++) {
        auto const& entry = dynamic_section_entries[i];

        if (entry.d_tag == DT_NULL)
            break;

        switch (entry.d_tag) {
        case DT_REL:
        case DT_RELSZ:
        case DT_RELENT:
        case DT_RELCOUNT:
            PANIC("DT_REL relocation tables are not supported");

        case DT_RELA:
            relocation_table_vaddr = entry.d_un.d_ptr;
            break;
        case DT_RELASZ:
            relocation_table_size = entry.d_un.d_val;
            break;
        case DT_RELAENT:
            relocation_entry_size = entry.d_un.d_val;
            break;
        case DT_RELACOUNT:
            number_of_relative_relocs = entry.d_un.d_val;
            break;

        case DT_RELR:
            relr_relocation_table_vaddr = entry.d_un.d_ptr;
            break;
        case DT_RELRSZ:
            relr_relocation_table_size = entry.d_un.d_val;
            break;
        case DT_RELRENT:
            VERIFY(entry.d_un.d_val == sizeof(FlatPtr));
            break;
        }
    }

    VERIFY((relocation_table_vaddr != 0 && relocation_table_size != 0 && relocation_entry_size != 0 && number_of_relative_relocs != 0)
        || (relr_relocation_table_vaddr != 0 && relr_relocation_table_size != 0));

    if (relocation_table_vaddr != 0) {
        // We are still identity mapped, so translate the address of the relocation table.
        auto relocation_table_data = MUST(get_data_at_kernel_elf_virtual_address(kernel_elf_image, kernel_elf_image_data, relocation_table_vaddr, relocation_table_size));

        for (size_t i = 0; i < number_of_relative_relocs; i++) {
            auto const& raw_relocation = *bit_cast<Elf_Rela const*>(bit_cast<FlatPtr>(relocation_table_data.data()) + (i * relocation_entry_size));
            ELF::Image::Relocation relocation(kernel_elf_image, raw_relocation, true);

            VERIFY(relocation.type() == to_underlying(ELF::GenericDynamicRelocationType::RELATIVE));

            // Elf_Rela::r_offset is a virtual address for executables, so we need to translate it as well.
            auto* patch_ptr = bit_cast<FlatPtr*>(MUST(get_data_at_kernel_elf_virtual_address(kernel_elf_image, kernel_elf_image_data, relocation.offset(), sizeof(FlatPtr))).data());

            FlatPtr relocated_address = base_address + relocation.addend();
            __builtin_memcpy(patch_ptr, &relocated_address, sizeof(FlatPtr));
        }
    }

    if (relr_relocation_table_vaddr != 0) {
        auto patch_relr = [&kernel_elf_image, kernel_elf_image_data, base_address](FlatPtr patch_vaddr) {
            auto* patch_ptr = bit_cast<FlatPtr*>(MUST(get_data_at_kernel_elf_virtual_address(kernel_elf_image, kernel_elf_image_data, patch_vaddr, sizeof(FlatPtr))).data());

            FlatPtr relocated_address;
            __builtin_memcpy(&relocated_address, patch_ptr, sizeof(FlatPtr));
            relocated_address += base_address;
            __builtin_memcpy(patch_ptr, &relocated_address, sizeof(FlatPtr));
        };

        auto relr_relocation_table_data = MUST(get_data_at_kernel_elf_virtual_address(kernel_elf_image, kernel_elf_image_data, relr_relocation_table_vaddr, relr_relocation_table_size));
        auto* entries = bit_cast<Elf_Relr*>(relr_relocation_table_data.data());
        FlatPtr patch_vaddr = 0;

        for (size_t i = 0; i < relr_relocation_table_size / sizeof(FlatPtr); i++) {
            if ((entries[i] & 1) == 0) {
                patch_vaddr = entries[i];
                patch_relr(patch_vaddr);
                patch_vaddr += sizeof(FlatPtr);
            } else {
                auto bitmap = entries[i];
                for (size_t j = 0; (bitmap >>= 1) != 0; j++)
                    if ((bitmap & 1) != 0)
                        patch_relr(patch_vaddr + (j * sizeof(FlatPtr)));

                patch_vaddr += (8 * sizeof(FlatPtr) - 1) * sizeof(FlatPtr);
            }
        }
    }
}

}
