/*
 * SPDX-FileCopyrightText: 2010-2024 Espressif Systems (Shanghai) CO LTD
 *
 * SPDX-License-Identifier: Apache-2.0
 */

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <stdbool.h>

#include "esp_attr.h"
#include "esp_err.h"
#include "esp_log.h"
#include "ulp.h"
#include "ulp_common.h"

#include "soc/soc.h"
#include "soc/rtc_cntl_reg.h"
#include "soc/sens_reg.h"

#include "sdkconfig.h"

static const char* TAG = "ulp";

typedef struct {
    uint32_t label : 16;
    uint32_t addr : 11;
    uint32_t unused : 1;
    uint32_t type : 4;
} reloc_info_t;

#define RELOC_TYPE_LABEL   0
#define RELOC_TYPE_BRANCH  1
#define RELOC_TYPE_LABELPC 2

/* This record means: there is a label at address
 * insn_addr, with number label_num.
 */
#define RELOC_INFO_LABEL(label_num, insn_addr) (reloc_info_t) { \
    .label = label_num, \
    .addr = insn_addr, \
    .unused = 0, \
    .type = RELOC_TYPE_LABEL }

/* This record means: there is a branch instruction at
 * insn_addr, it needs to be changed to point to address
 * of label label_num.
 */
#define RELOC_INFO_BRANCH(label_num, insn_addr) (reloc_info_t) { \
    .label = label_num, \
    .addr = insn_addr, \
    .unused = 0, \
    .type = RELOC_TYPE_BRANCH }

/* This record means: there is a move instruction at insn_addr,
 * imm needs to be changed to the program counter of the instruction
 * at label label_num.
 */
#define RELOC_INFO_LABELPC(label_num, insn_addr) (reloc_info_t) { \
    .label = label_num, \
    .addr = insn_addr, \
    .unused = 0, \
    .type = RELOC_TYPE_LABELPC }

/* Comparison function used to sort the relocations array */
static int reloc_sort_func(const void* p_lhs, const void* p_rhs)
{
    const reloc_info_t lhs = *(const reloc_info_t*) p_lhs;
    const reloc_info_t rhs = *(const reloc_info_t*) p_rhs;
    if (lhs.label < rhs.label) {
        return -1;
    } else if (lhs.label > rhs.label) {
        return 1;
    }
    // label numbers are equal
    if (lhs.type < rhs.type) {
        return -1;
    } else if (lhs.type > rhs.type) {
        return 1;
    }

    // both label number and type are equal
    return 0;
}

/* Processing branch and label macros involves four steps:
 *
 * 1. Iterate over program and count all instructions
 *    with "macro" opcode. Allocate relocations array
 *    with number of entries equal to number of macro
 *    instructions.
 *
 * 2. Remove all fake instructions with "macro" opcode
 *    and record their locations into relocations array.
 *    Removal is done using two pointers. Instructions
 *    are read from read_ptr, and written to write_ptr.
 *    When a macro instruction is encountered,
 *    its contents are recorded into the appropriate
 *    table, and then read_ptr is advanced again.
 *    When a real instruction is encountered, it is
 *    read via read_ptr and written to write_ptr.
 *    In the end, all macro instructions are removed,
 *    size of the program (expressed in words) is
 *    reduced by the total number of macro instructions
 *    which were present.
 *
 * 3. Sort relocations array by label number, and then
 *    by type ("label" or "branch") if label numbers
 *    match. This is done to simplify lookup on the next
 *    step.
 *
 * 4. Iterate over entries of relocations table.
 *    For each label number, label entry comes first
 *    because the array was sorted at the previous step.
 *    Label address is recorded, and all subsequent
 *    entries which point to the same label number
 *    are processed. For each entry, correct offset
 *    or absolute address is calculated, depending on
 *    type and subtype, and written into the appropriate
 *    field of the instruction.
 *
 */

static esp_err_t do_single_reloc(ulp_insn_t* program, uint32_t load_addr,
                                 reloc_info_t label_info, reloc_info_t the_reloc)
{
    size_t insn_offset = the_reloc.addr - load_addr;
    ulp_insn_t* insn = &program[insn_offset];

    switch (the_reloc.type) {
    case RELOC_TYPE_BRANCH: {
        // B, BS and BX have the same layout of opcode/sub_opcode fields,
        // and share the same opcode. B and BS also have the same layout of
        // offset and sign fields.
        assert(insn->b.opcode == OPCODE_BRANCH
               && "branch macro was applied to a non-branch instruction");
        switch (insn->b.sub_opcode) {
        case SUB_OPCODE_B:
        case SUB_OPCODE_BS: {
            int32_t offset = ((int32_t) label_info.addr) - ((int32_t) the_reloc.addr);
            uint32_t abs_offset = abs(offset);
            uint32_t sign = (offset >= 0) ? 0 : 1;
            if (abs_offset > 127) {
                ESP_LOGW(TAG, "target out of range: branch from %x to %x",
                         the_reloc.addr, label_info.addr);
                return ESP_ERR_ULP_BRANCH_OUT_OF_RANGE;
            }
            insn->b.offset = abs_offset; //== insn->bs.offset = abs_offset;
            insn->b.sign = sign;         //== insn->bs.sign = sign;
            break;
        }
        case SUB_OPCODE_BX: {
            assert(insn->bx.reg == 0 &&
                   "relocation applied to a jump with offset in register");
            insn->bx.addr = label_info.addr;
            break;
        }
        default:
            assert(false && "unexpected branch sub-opcode");
        }
        break;
    }
    case RELOC_TYPE_LABELPC: {
        assert((insn->alu_imm.opcode == OPCODE_ALU && insn->alu_imm.sub_opcode == SUB_OPCODE_ALU_IMM && insn->alu_imm.sel == ALU_SEL_MOV)
               && "pc macro was applied to an incompatible instruction");
        insn->alu_imm.imm = label_info.addr;
        break;
    }
    default:
        assert(false && "unknown reloc type");
    }
    return ESP_OK;
}

esp_err_t ulp_process_macros_and_load(uint32_t load_addr, const ulp_insn_t* program, size_t* psize)
{
    const ulp_insn_t* read_ptr = program;
    const ulp_insn_t* end = program + *psize;
    size_t macro_count = 0;
    // step 1: calculate number of macros
    while (read_ptr < end) {
        ulp_insn_t r_insn = *read_ptr;
        if (r_insn.macro.opcode == OPCODE_MACRO) {
            ++macro_count;
        }
        ++read_ptr;
    }
    size_t real_program_size = *psize - macro_count;
    const size_t ulp_mem_end = CONFIG_ULP_COPROC_RESERVE_MEM / sizeof(ulp_insn_t);
    if (load_addr > ulp_mem_end) {
        ESP_LOGW(TAG, "invalid load address %"PRIx32", max is %x",
                 load_addr, ulp_mem_end);
        return ESP_ERR_ULP_INVALID_LOAD_ADDR;
    }
    if (real_program_size + load_addr > ulp_mem_end) {
        ESP_LOGE(TAG, "program too big: %d words, max is %d words",
                 real_program_size, ulp_mem_end);
        return ESP_ERR_ULP_SIZE_TOO_BIG;
    }
    // If no macros found, copy the program and return.
    if (macro_count == 0) {
        memcpy(((ulp_insn_t*) RTC_SLOW_MEM) + load_addr, program, *psize * sizeof(ulp_insn_t));
        return ESP_OK;
    }
    reloc_info_t* reloc_info =
        (reloc_info_t*) malloc(sizeof(reloc_info_t) * macro_count);
    if (reloc_info == NULL) {
        return ESP_ERR_NO_MEM;
    }

    // step 2: record macros into reloc_info array
    // and remove them from then program
    read_ptr = program;
    ulp_insn_t* output_program = ((ulp_insn_t*) RTC_SLOW_MEM) + load_addr;
    ulp_insn_t* write_ptr = output_program;
    uint32_t cur_insn_addr = load_addr;
    reloc_info_t* cur_reloc = reloc_info;
    while (read_ptr < end) {
        ulp_insn_t r_insn = *read_ptr;
        if (r_insn.macro.opcode == OPCODE_MACRO) {
            switch (r_insn.macro.sub_opcode) {
            case SUB_OPCODE_MACRO_LABEL:
                *cur_reloc = RELOC_INFO_LABEL(r_insn.macro.label,
                                              cur_insn_addr);
                break;
            case SUB_OPCODE_MACRO_BRANCH:
                *cur_reloc = RELOC_INFO_BRANCH(r_insn.macro.label,
                                               cur_insn_addr);
                break;
            case SUB_OPCODE_MACRO_LABELPC:
                *cur_reloc = RELOC_INFO_LABELPC(r_insn.macro.label,
                                                cur_insn_addr);
                break;
            default:
                assert(0 && "invalid sub_opcode for macro insn");
            }
            ++read_ptr;
            assert(read_ptr != end && "program can not end with macro insn");
            ++cur_reloc;
        } else {
            // normal instruction (not a macro)
            *write_ptr = *read_ptr;
            ++read_ptr;
            ++write_ptr;
            ++cur_insn_addr;
        }
    }

    // step 3: sort relocations array
    qsort(reloc_info, macro_count, sizeof(reloc_info_t),
          reloc_sort_func);

    // step 4: walk relocations array and fix instructions
    reloc_info_t* reloc_end = reloc_info + macro_count;
    cur_reloc = reloc_info;
    while (cur_reloc < reloc_end) {
        reloc_info_t label_info = *cur_reloc;
        assert(label_info.type == RELOC_TYPE_LABEL);
        ++cur_reloc;
        while (cur_reloc < reloc_end) {
            if (cur_reloc->type == RELOC_TYPE_LABEL) {
                if (cur_reloc->label == label_info.label) {
                    ESP_LOGE(TAG, "duplicate label definition: %d",
                             label_info.label);
                    free(reloc_info);
                    return ESP_ERR_ULP_DUPLICATE_LABEL;
                }
                break;
            }
            if (cur_reloc->label != label_info.label) {
                ESP_LOGE(TAG, "branch to an inexistent label: %d",
                         cur_reloc->label);
                free(reloc_info);
                return ESP_ERR_ULP_UNDEFINED_LABEL;
            }
            esp_err_t rc = do_single_reloc(output_program, load_addr,
                                           label_info, *cur_reloc);
            if (rc != ESP_OK) {
                free(reloc_info);
                return rc;
            }
            ++cur_reloc;
        }
    }
    free(reloc_info);
    *psize = real_program_size;
    return ESP_OK;
}