/*
 * Copyright (c) 2024 Raspberry Pi (Trading) Ltd.
 *
 * SPDX-License-Identifier: BSD-3-Clause
 */

#include <algorithm>
#include <cassert>
#include <cstdio>
#include <cstdarg>
#include <cstring>
#include <fstream>
#include <iostream>
#include <iterator>
#include <sstream>
#include <string>
#include <vector>
#include <memory>

#include "elf.h"
#include "elf_file.h"
#include "errors.h"

#include "portable_endian.h"

// tsk namespace is polluted on windows
#ifdef _WIN32
#undef min
#undef max

#define _CRT_SECURE_NO_WARNINGS
#endif

void eh_he(elf32_header &eh) {
    // Swap to host endianness
    eh.common.magic    = le32toh(eh.common.magic);
    eh.common.type     = le16toh(eh.common.type);
    eh.common.machine  = le16toh(eh.common.machine);
    eh.common.version2 = le32toh(eh.common.version2);
    eh.entry           = le32toh(eh.entry);
    eh.ph_offset       = le32toh(eh.ph_offset);
    eh.sh_offset       = le32toh(eh.sh_offset);
    eh.flags           = le32toh(eh.flags);
    eh.eh_size         = le16toh(eh.eh_size);
    eh.ph_entry_size   = le16toh(eh.ph_entry_size);
    eh.ph_num          = le16toh(eh.ph_num);
    eh.sh_entry_size   = le16toh(eh.sh_entry_size);
    eh.sh_num          = le16toh(eh.sh_num);
    eh.sh_str_index    = le16toh(eh.sh_str_index);
}
void eh_le(elf32_header &eh) {
    // Swap to little endianness
    eh.common.magic    = htole32(eh.common.magic);
    eh.common.type     = htole16(eh.common.type);
    eh.common.machine  = htole16(eh.common.machine);
    eh.common.version2 = htole32(eh.common.version2);
    eh.entry           = htole32(eh.entry);
    eh.ph_offset       = htole32(eh.ph_offset);
    eh.sh_offset       = htole32(eh.sh_offset);
    eh.flags           = htole32(eh.flags);
    eh.eh_size         = htole16(eh.eh_size);
    eh.ph_entry_size   = htole16(eh.ph_entry_size);
    eh.ph_num          = htole16(eh.ph_num);
    eh.sh_entry_size   = htole16(eh.sh_entry_size);
    eh.sh_num          = htole16(eh.sh_num);
    eh.sh_str_index    = htole16(eh.sh_str_index);
}
void ph_he(elf32_ph_entry &ph) {
    // Swap to host endianness
    ph.type     = le32toh(ph.type);
    ph.offset   = le32toh(ph.offset);
    ph.vaddr    = le32toh(ph.vaddr);
    ph.paddr    = le32toh(ph.paddr);
    ph.filez    = le32toh(ph.filez);
    ph.memsz    = le32toh(ph.memsz);
    ph.flags    = le32toh(ph.flags);
    ph.align    = le32toh(ph.align);
}
void ph_le(elf32_ph_entry &ph) {
    // Swap to little endianness
    ph.type     = htole32(ph.type);
    ph.offset   = htole32(ph.offset);
    ph.vaddr    = htole32(ph.vaddr);
    ph.paddr    = htole32(ph.paddr);
    ph.filez    = htole32(ph.filez);
    ph.memsz    = htole32(ph.memsz);
    ph.flags    = htole32(ph.flags);
    ph.align    = htole32(ph.align);
}
void sh_he(elf32_sh_entry &sh) {
    // Swap to host endianness
    sh.name         = le32toh(sh.name);
    sh.type         = le32toh(sh.type);
    sh.flags        = le32toh(sh.flags);
    sh.addr         = le32toh(sh.addr);
    sh.offset       = le32toh(sh.offset);
    sh.size         = le32toh(sh.size);
    sh.link         = le32toh(sh.link);
    sh.info         = le32toh(sh.info);
    sh.addralign    = le32toh(sh.addralign);
    sh.entsize      = le32toh(sh.entsize);
}
void sh_le(elf32_sh_entry &sh) {
    // Swap to little endianness
    sh.name         = htole32(sh.name);
    sh.type         = htole32(sh.type);
    sh.flags        = htole32(sh.flags);
    sh.addr         = htole32(sh.addr);
    sh.offset       = htole32(sh.offset);
    sh.size         = htole32(sh.size);
    sh.link         = htole32(sh.link);
    sh.info         = htole32(sh.info);
    sh.addralign    = htole32(sh.addralign);
    sh.entsize      = htole32(sh.entsize);
}
void sym_he(elf32_sym_entry &sym) {
    // Swap to host endianness
    sym.name    = le32toh(sym.name);
    sym.value   = le32toh(sym.value);
    sym.size    = le32toh(sym.size);
    sym.info    = le32toh(sym.info);
    sym.other   = le32toh(sym.other);
    sym.shndx   = le32toh(sym.shndx);
}
void sym_le(elf32_sym_entry &sym) {
    // Swap to little endianness
    sym.name    = htole32(sym.name);
    sym.value   = htole32(sym.value);
    sym.size    = htole32(sym.size);
    sym.info    = htole32(sym.info);
    sym.other   = htole32(sym.other);
    sym.shndx   = htole32(sym.shndx);
}

// Checks whether an ELF header is compatible with RP2040 / RP3050
// Returns zero on success
int rp_check_elf_header(const elf32_header &eh) {
    if (eh.common.magic != ELF_MAGIC) {
        fail(ERROR_FORMAT, "Not an ELF file");
    }
    if (eh.common.version != 1 || eh.common.version2 != 1) {
        fail(ERROR_FORMAT, "Unrecognized ELF version");
    }
    if (eh.common.arch_class != 1 || eh.common.endianness != 1) {
        fail(ERROR_INCOMPATIBLE, "Require 32 bit little-endian ELF");
    }
    if (eh.eh_size != sizeof(struct elf32_header)) {
        fail(ERROR_FORMAT, "Invalid ELF32 format");
    }
    if (eh.common.machine != EM_ARM && eh.common.machine != EM_RISCV) {
        fail(ERROR_FORMAT, "Not an Arm or RISC-V executable");
    }
    // Accept either ELFOSABI_NONE or ELFOSABI_GNU for EI_OSABI. Compilers may
    // set the OS/ABI field to ELFOSABI_GNU when they use GNU features, such as
    // the SHF_GNU_RETAIN section flag, but the binary is still compatible.
    if (eh.common.abi != 0 /* NONE */ && eh.common.abi != 3 /* GNU */) {
        fail(ERROR_INCOMPATIBLE, "Unrecognized ABI");
    }
    // todo amy not sure if this should be expected or not - we have HARD float in clang only for now
    if (eh.flags & EF_ARM_ABI_FLOAT_HARD) {
//        fail(ERROR_INCOMPATIBLE, "HARD-FLOAT not supported");
    }
    return 0;
}

// Determine binary type (flash or ram)
int rp_determine_binary_type(const elf32_header &eh, const std::vector<elf32_ph_entry>& entries, address_ranges flash_range, address_ranges ram_range, bool *ram_style) {
    for(const auto &entry : entries) {
        if (entry.type == PT_LOAD && entry.memsz) {
            unsigned int mapped_size = std::min(entry.filez, entry.memsz);
            if (mapped_size) {
                // we back convert the entrypoint from a VADDR to a PADDR to see if it originates in flash, and if
                // so call THAT a flash binary.
                if (eh.entry >= entry.vaddr && eh.entry < entry.vaddr + mapped_size) {
                    uint32_t effective_entry = eh.entry + entry.paddr - entry.vaddr;
                    if (is_address_initialized(ram_range, effective_entry)) {
                        *ram_style = true;
                        return 0;
                    } else if (is_address_initialized(flash_range, effective_entry)) {
                        *ram_style = false;
                        return 0;
                    }
                }
            }
        }
    }
    fail(ERROR_INCOMPATIBLE, "entry point is not in mapped part of file");
    return ERROR_INCOMPATIBLE;
}

void elf_file::read_bytes(unsigned offset, unsigned length, void *dest) {
    if (offset + length > elf_bytes.size()) {
        fail(ERROR_FORMAT, "ELF File Read from 0x%x with size 0x%x exceeds the file size 0x%zx", offset, length, elf_bytes.size());
    }
    memcpy(dest, &elf_bytes[offset], length);
}

int elf_file::read_header(void) {
    read_bytes(0, sizeof(eh), &eh);
    eh_he(eh);  // swap to Host for processing
    return rp_check_elf_header(eh);
}

// Flattens the data in the section array the elf_bytes blob
void elf_file::flatten(void) {
    elf_bytes.resize(sizeof(eh));
    auto eh_out = eh;
    eh_le(eh_out);    // swap to LE for writing
    memcpy(&elf_bytes[0], &eh_out, sizeof(eh_out));

    elf_bytes.resize(std::max(eh.ph_offset + sizeof(elf32_ph_entry) * eh.ph_num, elf_bytes.size()));
    auto ph_entries_out = ph_entries;
    for (auto ph : ph_entries_out) {
        ph_le(ph);  // swap to LE for writing
    }
    memcpy(&elf_bytes[eh.ph_offset], &ph_entries_out[0], sizeof(elf32_ph_entry) * eh.ph_num);

    elf_bytes.resize(std::max(eh.sh_offset + sizeof(elf32_sh_entry) * eh.sh_num, elf_bytes.size()));
    auto sh_entries_out = sh_entries;
    for (auto sh : sh_entries_out) {
        sh_le(sh);  // swap to LE for writing
    }
    memcpy(&elf_bytes[eh.sh_offset], &sh_entries_out[0], sizeof(elf32_sh_entry) * eh.sh_num);

    int idx = 0;
    for (const auto &sh : sh_entries) {
        if (sh.size && sh.type != SHT_NOBITS) {
            elf_bytes.resize(std::max(sh.offset + sh.size, (uint32_t)elf_bytes.size()));
            memcpy(&elf_bytes[sh.offset], &sh_data[idx][0], sh.size);
        }
        idx++;
    }

    idx = 0;
    for (const auto &ph : ph_entries) {
        if (ph.filez) {
            elf_bytes.resize(std::max(ph.offset + ph.filez, (uint32_t)elf_bytes.size()));
            memcpy(&elf_bytes[ph.offset], &ph_data[idx][0], ph.filez);
        }
        idx++;
    }
    if (verbose) printf("Elf file size %zu\n", elf_bytes.size());
}

void elf_file::write(std::shared_ptr<std::iostream> out) {
    flatten();
    out->exceptions(std::iostream::failbit | std::iostream::badbit);
    if (verbose) printf("Writing %lu bytes to file\n", elf_bytes.size());
    out->write(reinterpret_cast<const char*>(&elf_bytes[0]), elf_bytes.size());
}

void elf_file::read_sh(void) {
    if (verbose) printf("%s sh offset %u #entries %d\n", __func__, eh.sh_offset, eh.sh_num);
    if (eh.sh_num) {
        sh_entries.resize(eh.sh_num);
        read_bytes(eh.sh_offset, sizeof(elf32_sh_entry) * eh.sh_num, &sh_entries[0]);
        for (auto sh : sh_entries) {
            sh_he(sh);  // swap to Host for processing
        }
    }
}

// Read the section data from the internal byte array into discrete sections.
// This is used after modifying segments but before inserting new segments
void elf_file::read_sh_data(void) {
    int sh_idx = 0;
    sh_data.resize(eh.sh_num);
    for (const auto &sh: sh_entries) {
        if (sh.size && sh.type != SHT_NOBITS) {
            sh_data[sh_idx].resize(sh.size);
            read_bytes(sh.offset, sh.size, &sh_data[sh_idx][0]);
        }
        sh_idx++;
    }
}

void elf_file::read_ph_data(void) {
    int ph_idx = 0;
    ph_data.resize(eh.ph_num);
    for (const auto &ph: ph_entries) {
        if (ph.filez) {
            ph_data[ph_idx].resize(ph.filez);
            read_bytes(ph.offset, ph.filez, &ph_data[ph_idx][0]);
        }
        ph_idx++;
    }
}

const std::string elf_file::section_name(uint32_t sh_name) const {
    if (!eh.sh_str_index || eh.sh_str_index > eh.sh_num)
        return "";

    if (sh_name > sh_data[eh.sh_str_index].size())
        return "";

    const char * str =(const char *) &sh_data[eh.sh_str_index][0];
    return &str[sh_name];
}

const elf32_sh_entry* elf_file::get_section(const std::string &sh_name) {
    for (unsigned int i = 0; i < sh_entries.size(); i++) {
        if (section_name(sh_entries[i].name) == sh_name) {
            return &sh_entries[i];
        }
    }
    return NULL;
}

uint32_t elf_file::get_symbol(const std::string &sym_name) {
    auto sym_tab = get_section(".symtab");
    auto str_tab = get_section(".strtab");
    if (!sym_tab || !str_tab) {
        return 0;
    }
    auto data = content(*sym_tab);
    auto strings = content(*str_tab);
    const char * str =(const char *) strings.data();
    for (unsigned int i=0; i < sym_tab->size / sizeof(elf32_sym_entry); i++) {
        elf32_sym_entry sym;
        memcpy(&sym, data.data() + i*sizeof(elf32_sym_entry), sizeof(elf32_sym_entry));
        sym_he(sym);    // swap to Host for processing
        if (&str[sym.name] == sym_name) {
            return sym.value;
        }
    }
    return 0;
}

uint32_t elf_file::append_section_name(const std::string &sh_name_str) {
    // Create byte array with new section name
    std::vector<uint8_t> name_bytes(sh_name_str.begin(), sh_name_str.end());
    name_bytes.push_back(0);

    // Append the byte array to section header table remembering the offset
    // of the start of the string for the new section
    elf32_sh_entry &shstrtab = sh_entries[eh.sh_str_index];
    std::vector<uint8_t> &shstrtab_data = sh_data[eh.sh_str_index];
    sh_entries[eh.sh_str_index].size += name_bytes.size();
    uint32_t sh_name = shstrtab_data.size();
    shstrtab_data.insert(shstrtab_data.end(), name_bytes.begin(), name_bytes.end());

    // Move offsets for anything stored after the resized section header table
    for (auto &sh: sh_entries) {
        if (sh.offset > shstrtab.offset)
            sh.offset += name_bytes.size();
    }
    for (auto &ph: ph_entries) {
        if (ph.offset > shstrtab.offset)
            ph.offset += name_bytes.size();
    }
    return sh_name;
}

void elf_file::dump(void) const {
    for (const auto &ph: ph_entries) {
        printf("PH offset %08x vaddr %08x paddr %08x size %08x type %08x\n",
            ph.offset, ph.vaddr, ph.paddr, ph.memsz, ph.type);
    }

    int sh_idx = 0;
    for (const auto &sh: sh_entries) {
        printf("SH[%d] %20s addr %08x offset %08x size %08x type %08x\n",
            sh_idx, section_name(sh.name).c_str(), sh.addr, sh.offset, sh.size, sh.type);
        sh_idx++;
    }
}

void elf_file::move_all(int dist) {
    if (verbose) printf("Incrementing all paddr by %d\n", dist);
    for (auto &ph: ph_entries) {
        ph.paddr += dist;
    }
}

void elf_file::read_ph(void) {
    if (verbose) printf("%s ph offset %u #entries %d\n", __func__, eh.ph_offset, eh.ph_num);
    if (eh.ph_num) {
        ph_entries.resize(eh.ph_num);
        read_bytes(eh.ph_offset, sizeof(elf32_ph_entry) * eh.ph_num, &ph_entries[0]);
        for (auto ph : ph_entries) {
            ph_he(ph);  // swap to Host for processing
        }
    }
}

int elf_file::read_file(std::shared_ptr<std::iostream> file) {
    int rc = 0;
    try {
        elf_bytes = read_binfile(file);
        int rc = read_header();
        if (!rc) {
            read_ph();
            read_sh();
        }
        read_sh_data();
        read_ph_data();
    }
    catch (const std::ios_base::failure &e) {
        std::cerr << "Failed to read elf file" << std::endl;
        rc = -1;
    }
    return rc;
}

uint32_t elf_file::lowest_section_offset(void) const {
    uint32_t offset = eh.sh_offset; // Section header offset is after the data
    for (const auto &sh: sh_entries) {
        if (sh.type != SHT_NULL && sh.offset > 0 && sh.offset < offset) {
            offset = sh.offset;
        }
    }
    return offset;
}

uint32_t elf_file::highest_section_offset(void) const {
    uint32_t offset = 0; // Section header offset is after the data
    for (const auto &sh: sh_entries) {
        if (sh.type != SHT_NULL && sh.offset > 0 && sh.offset >= offset) {
            offset = sh.offset + sh.size;
        }
    }
    return offset;
}

std::vector<uint8_t> elf_file::content(const elf32_ph_entry &ph) const {
    std::vector<uint8_t> content;
    std::copy(elf_bytes.begin() + ph.offset, elf_bytes.begin() + ph.offset + ph.filez, std::back_inserter(content));
    return content;
}

std::vector<uint8_t> elf_file::content(const elf32_sh_entry &sh) const {
    std::vector<uint8_t> content;
    std::copy(elf_bytes.begin() + sh.offset, elf_bytes.begin() + sh.offset + sh.size, std::back_inserter(content));
    return content;
}

void elf_file::content(const elf32_ph_entry &ph, const std::vector<uint8_t> &content) {
    if (!editable) return;
    assert(content.size() <= ph.filez);
    if (verbose) printf("Update segment content offset %x content size %zx physical size %x\n", ph.offset, content.size(), ph.filez);
    memcpy(&elf_bytes[ph.offset], &content[0], std::min(content.size(), (size_t) ph.filez));
    read_sh_data(); // Extract the sections after modifying the content
    read_ph_data();
}

void elf_file::content(const elf32_sh_entry &sh, const std::vector<uint8_t> &content) {
    if (!editable) return;
    assert(content.size() <= sh.size);
    if (verbose) printf("Update section content offset %x content size %zx section size %x\n", sh.offset, content.size(), sh.size);
    memcpy(&elf_bytes[sh.offset], &content[0], std::min(content.size(), (size_t) sh.size));
    read_sh_data();  // Extract the sections after modifying the content
    read_ph_data();
}

const elf32_ph_entry* elf_file::segment_from_physical_address(uint32_t paddr) {
    for (int i = 0; i < eh.ph_num; i++) {
        if (paddr >= ph_entries[i].paddr && paddr < ph_entries[i].paddr + ph_entries[i].filez) {
            if (verbose) printf("segment %d contains physical address %x\n", i, paddr);
            return &ph_entries[i];
        }
    }
    return nullptr;
}

const elf32_ph_entry* elf_file::segment_from_virtual_address(uint32_t vaddr) {
    for (int i = 0; i < eh.ph_num; i++) {
        if (vaddr >= ph_entries[i].vaddr && vaddr < ph_entries[i].vaddr + ph_entries[i].memsz) {
            if (verbose) printf("segment %d contains virtual address %x\n", i, vaddr);
            return &ph_entries[i];
        }
    }
    return NULL;
}

// Appends a new segment and section - filled with zeros
// Use content to replace the content
const elf32_ph_entry& elf_file::append_segment(uint32_t vaddr, uint32_t paddr, uint32_t size, const std::string &name) {
    elf32_ph_entry ph;
    read_sh_data(); // Convert the section data back into discreet chunks
    uint32_t sh_name = append_section_name(name);

    ph.type = PT_LOAD;
    ph.flags = PF_R; // Readable segment
    ph.paddr = paddr;
    ph.vaddr = vaddr;
    ph.filez = size;
    ph.memsz = size;
    ph.align = 2;

    if (verbose) {
        std::cout << "new segment " << name <<
            " paddr " << std::hex << paddr <<
            " vaddr " << std::hex << vaddr <<
            " size " << std::hex << size << std::endl;
    }
    ph_entries.push_back(ph);
    eh.ph_num++;

    // There's normally space between the end of the program header table and the start of data to
    // squeeze in another program header. If not, shuffle everything by 4K;
    uint32_t lso = lowest_section_offset();
    if (lso < eh.ph_offset + eh.ph_entry_size * eh.ph_num) {

        // Move the segment offsets
        for (auto &ph: ph_entries) {
            ph.offset += 0x1000;
        }

        // Move section header table and each section offset
        eh.sh_offset += 0x1000;
        for (auto &sh : sh_entries) {
            sh.offset += 0x1000;
        }
    }

    // Append the new signature section
    elf32_sh_entry sh = {};
    sh.name = sh_name;
    sh.type = SHT_PROGBITS;
    sh.flags = SHF_ALLOC;
    sh.addr = ph.vaddr;
    sh.size = size;
    uint32_t hso = highest_section_offset();
    sh.offset = hso;

    // Add the new segment for the signature and point to offset in file for data
    sh_entries.push_back(sh);
    sh_data.push_back(std::vector<uint8_t>(size));
    ph_entries.back().offset = sh.offset;
    ph_data.push_back(std::vector<uint8_t>(size));

    eh.sh_offset = sh.offset + sh.size;
    eh.sh_num++;

    if (verbose) printf("%s sig offset %08x num sections %u\n", __func__, sh.offset, eh.sh_num);
    flatten();
    return ph_entries.back();
}

 std::vector<uint8_t> elf_file::read_binfile(std::shared_ptr<std::iostream> in) {
    std::vector<uint8_t> data;
    in->exceptions(std::iostream::failbit | std::iostream::badbit);
    in->seekg(0, in->end);
    data.resize(in->tellg());
    in->seekg(0, in->beg);
    in->read(reinterpret_cast<char *>(&data[0]), data.size());
    return data;
}
