#ifdef HAVE_EIP712_FULL_SUPPORT

#include <stdint.h>
#include <string.h>
#include "path.h"
#include "mem.h"
#include "context_712.h"
#include "commands_712.h"
#include "type_hash.h"
#include "shared_context.h"
#include "mem_utils.h"
#include "ui_logic.h"
#include "apdu_constants.h"  // APDU response codes
#include "typed_data.h"

static s_path *path_struct = NULL;

/**
 * Get the field pointer to by the first N depths of the path.
 *
 * @param[out] fields_count_ptr the number of fields in the last evaluated depth
 * @param[in] n the number of depths to evaluate
 * @return the field which the first Nth depths points to
 */
static const void *get_nth_field(uint8_t *const fields_count_ptr, uint8_t n) {
    const void *struct_ptr = NULL;
    const void *field_ptr = NULL;
    const char *typename;
    uint8_t length;
    uint8_t fields_count;

    if (path_struct == NULL) {
        return NULL;
    }

    struct_ptr = path_struct->root_struct;

    if (n > path_struct->depth_count)  // sanity check
    {
        return NULL;
    }
    for (uint8_t depth = 0; depth < n; ++depth) {
        field_ptr = get_struct_fields_array(struct_ptr, &fields_count);

        if (fields_count_ptr != NULL) {
            *fields_count_ptr = fields_count;
        }
        // check if the index at this depth makes sense
        if (path_struct->depths[depth] > fields_count) {
            return NULL;
        }

        for (uint8_t index = 0; index < path_struct->depths[depth]; ++index) {
            field_ptr = get_next_struct_field(field_ptr);
        }
        if (struct_field_type(field_ptr) == TYPE_CUSTOM) {
            typename = get_struct_field_typename(field_ptr, &length);
            if ((struct_ptr = get_structn(typename, length)) == NULL) {
                return NULL;
            }
        }
    }
    return field_ptr;
}

/**
 * Get the element the path is pointing to.
 *
 * @param[out] the number of fields in the depth of the returned field
 * @return the field which the path points to
 */
static inline const void *get_field(uint8_t *const fields_count) {
    return get_nth_field(fields_count, path_struct->depth_count);
}

/**
 * Get Nth struct field from path
 *
 * @param[in] n nth depth requested
 * @return pointer to the matching field, \ref NULL otherwise
 */
const void *path_get_nth_field(uint8_t n) {
    return get_nth_field(NULL, n);
}

/**
 * Get Nth to last struct field from path
 *
 * @param[in] n nth to last depth requested
 * @return pointer to the matching field, \ref NULL otherwise
 */
const void *path_get_nth_field_to_last(uint8_t n) {
    const char *typename;
    uint8_t typename_len;
    const void *field_ptr;
    const void *struct_ptr = NULL;

    field_ptr = get_nth_field(NULL, path_struct->depth_count - n);
    if (field_ptr != NULL) {
        typename = get_struct_field_typename(field_ptr, &typename_len);
        struct_ptr = get_structn(typename, typename_len);
    }
    return struct_ptr;
}

/**
 * Get the element the path is pointing to
 *
 * @return the field which the path points to
 */
const void *path_get_field(void) {
    return get_field(NULL);
}

/**
 * Go down (add) a depth level.
 *
 * @return whether the push was successful
 */
static bool path_depth_list_push(void) {
    if (path_struct == NULL) {
        return false;
    }
    if (path_struct->depth_count == MAX_PATH_DEPTH) {
        return false;
    }
    path_struct->depths[path_struct->depth_count] = 0;
    path_struct->depth_count += 1;
    return true;
}

/**
 * Get the last hashing context (corresponding to the current path depth)
 *
 * @return pointer to the hashing context
 */
static cx_sha3_t *get_last_hash_ctx(void) {
    return ((cx_sha3_t *) mem_alloc(0)) - 1;
}

/**
 * Finalize the last hashing context
 *
 * @param[out] hash pointer to buffer where the hash will be stored
 * @return whether there was anything hashed at this depth
 */
static bool finalize_hash_depth(uint8_t *hash) {
    const cx_sha3_t *hash_ctx;
    size_t hashed_bytes;
    cx_err_t error = CX_INTERNAL_ERROR;

    hash_ctx = get_last_hash_ctx();
    hashed_bytes = hash_ctx->blen;
    // finalize hash
    CX_CHECK(
        cx_hash_no_throw((cx_hash_t *) hash_ctx, CX_LAST, NULL, 0, hash, KECCAK256_HASH_BYTESIZE));
    mem_dealloc(sizeof(*hash_ctx));  // remove hash context
    return hashed_bytes > 0;
end:
    return false;
}

/**
 * Continue last progressive hashing context with given hash
 *
 * @param[in] hash pointer to given hash
 */
static void feed_last_hash_depth(const uint8_t *const hash) {
    const cx_sha3_t *hash_ctx;

    hash_ctx = get_last_hash_ctx();
    // continue progressive hash with the array hash
    CX_ASSERT(cx_hash_no_throw((cx_hash_t *) hash_ctx, 0, hash, KECCAK256_HASH_BYTESIZE, NULL, 0));
}

/**
 * Create a new hashing context depth in memory
 *
 * @param[in] init if the hashing context should be initialized
 * @return whether the memory allocation of the hashing context was successful
 */
static bool push_new_hash_depth(bool init) {
    cx_sha3_t *hash_ctx;
    cx_err_t error = CX_INTERNAL_ERROR;

    // allocate new hash context
    if ((hash_ctx = MEM_ALLOC_AND_ALIGN_TYPE(*hash_ctx)) == NULL) {
        return false;
    }
    if (init) {
        CX_CHECK(cx_keccak_init_no_throw(hash_ctx, 256));
    }
    return true;
end:
    return false;
}

/**
 * Go up (remove) a depth level.
 *
 * @return whether the pop was successful
 */
static bool path_depth_list_pop(void) {
    uint8_t hash[KECCAK256_HASH_BYTESIZE];
    bool to_feed;

    if (path_struct == NULL) {
        return false;
    }
    if (path_struct->depth_count == 0) {
        return false;
    }
    path_struct->depth_count -= 1;

    to_feed = finalize_hash_depth(hash);
    if (path_struct->depth_count > 0) {
        if (to_feed) {
            feed_last_hash_depth(hash);
        }
    } else {
        switch (path_struct->root_type) {
            case ROOT_DOMAIN:
                memcpy(tmpCtx.messageSigningContext712.domainHash, hash, KECCAK256_HASH_BYTESIZE);
                break;
            case ROOT_MESSAGE:
                memcpy(tmpCtx.messageSigningContext712.messageHash, hash, KECCAK256_HASH_BYTESIZE);
                break;
            default:
                break;
        }
    }

    return true;
}

/**
 * Go down (add) an array depth level.
 *
 * @param[in] path_idx the index in the path list
 * @param[in] the number of elements contained in that depth
 * @return whether the push was successful
 */
static bool array_depth_list_push(uint8_t path_idx, uint8_t size) {
    s_array_depth *arr;

    if (path_struct == NULL) {
        apdu_response_code = APDU_RESPONSE_CONDITION_NOT_SATISFIED;
        return false;
    }
    if (path_struct->array_depth_count == MAX_ARRAY_DEPTH) {
        apdu_response_code = APDU_RESPONSE_CONDITION_NOT_SATISFIED;
        return false;
    }

    arr = &path_struct->array_depths[path_struct->array_depth_count];
    arr->path_index = path_idx;
    arr->size = size;
    path_struct->array_depth_count += 1;
    return true;
}

/**
 * Go up (remove) an array depth level.
 *
 * @return whether the pop was successful
 */
static bool array_depth_list_pop(void) {
    uint8_t hash[KECCAK256_HASH_BYTESIZE];

    if (path_struct == NULL) {
        return false;
    }
    if (path_struct->array_depth_count == 0) {
        return false;
    }

    finalize_hash_depth(hash);  // return value not checked on purpose
    feed_last_hash_depth(hash);

    path_struct->array_depth_count -= 1;
    return true;
}

/**
 * Updates the path so that it doesn't point to a struct-type field, but rather
 * only to actual fields.
 *
 * @return whether the path update worked or not
 */
static bool path_update(void) {
    uint8_t fields_count;
    const void *struct_ptr;
    const void *field_ptr;
    const char *typename;
    uint8_t typename_len;
    uint8_t hash[KECCAK256_HASH_BYTESIZE];

    if (path_struct == NULL) {
        return false;
    }
    if ((field_ptr = get_field(NULL)) == NULL) {
        return false;
    }
    while (struct_field_type(field_ptr) == TYPE_CUSTOM) {
        typename = get_struct_field_typename(field_ptr, &typename_len);
        if ((struct_ptr = get_structn(typename, typename_len)) == NULL) {
            return false;
        }
        if ((field_ptr = get_struct_fields_array(struct_ptr, &fields_count)) == NULL) {
            return false;
        }

        if (push_new_hash_depth(true) == false) {
            return false;
        }
        // get the struct typehash
        if (type_hash(typename, typename_len, hash) == false) {
            return false;
        }
        feed_last_hash_depth(hash);

        // TODO: Find a better way to show inner structs in verbose mode when it might be
        //       an empty array of structs in which case we don't want to show it but the
        //       size is only known later
        // ui_712_queue_struct_to_review();
        path_depth_list_push();
    }
    return true;
}

/**
 * Set a new struct as the path root type
 *
 * @param[in] struct_name the root struct name
 * @param[in] name_length the root struct name length
 * @return boolean indicating if it was successful or not
 */
bool path_set_root(const char *const struct_name, uint8_t name_length) {
    uint8_t hash[KECCAK256_HASH_BYTESIZE];

    if (path_struct == NULL) {
        apdu_response_code = APDU_RESPONSE_INVALID_DATA;
        return false;
    }

    path_struct->root_struct = get_structn(struct_name, name_length);

    if (path_struct->root_struct == NULL) {
        PRINTF("Struct name not found (");
        for (int i = 0; i < name_length; ++i) {
            PRINTF("%c", struct_name[i]);
        }
        PRINTF(")!\n");
        apdu_response_code = APDU_RESPONSE_INVALID_DATA;
        return false;
    }

    if (push_new_hash_depth(true) == false) {
        return false;
    }
    if (type_hash(struct_name, name_length, hash) == false) {
        return false;
    }
    feed_last_hash_depth(hash);
    //

    // init depth, at 0 : empty path
    path_struct->depth_count = 0;
    path_depth_list_push();

    // init array levels at 0
    path_struct->array_depth_count = 0;

    if ((name_length == strlen(DOMAIN_STRUCT_NAME)) &&
        (strncmp(struct_name, DOMAIN_STRUCT_NAME, name_length) == 0)) {
        path_struct->root_type = ROOT_DOMAIN;
    } else {
        path_struct->root_type = ROOT_MESSAGE;
    }

    struct_state = DEFINED;

    // because the first field could be a struct type
    path_update();
    return true;
}

/**
 * Checks the new array depth and adds it to the list
 *
 * @param[in] depth pointer to the array depth definition
 * @param[in] total_count number of array depth contained down to this array depth
 * @param[in] pidx path index
 * @param[in] size requested array depth size
 * @return whether the checks and add were successful or not
 */
static bool check_and_add_array_depth(const void *depth,
                                      uint8_t total_count,
                                      uint8_t pidx,
                                      uint8_t size) {
    uint8_t expected_size;
    uint8_t arr_idx;
    e_array_type expected_type;

    arr_idx = (total_count - path_struct->array_depth_count) - 1;
    // we skip index 0, since we already have it
    for (uint8_t idx = 1; idx < (arr_idx + 1); ++idx) {
        if ((depth = get_next_struct_field_array_lvl(depth)) == NULL) {
            return false;
        }
    }
    expected_type = struct_field_array_depth(depth, &expected_size);
    if ((expected_type == ARRAY_FIXED_SIZE) && (expected_size != size)) {
        apdu_response_code = APDU_RESPONSE_INVALID_DATA;
        PRINTF("Unexpected array depth size. (expected %d, got %d)\n", expected_size, size);
        return false;
    }
    // add it
    if (!array_depth_list_push(pidx, size)) {
        return false;
    }
    return true;
}

/**
 * Add a new array depth with a given size (number of elements).
 *
 * @param[in] data pointer to the number of elements
 * @param[in] length length of data
 * @return whether the add was successful or not
 */
bool path_new_array_depth(const uint8_t *const data, uint8_t length) {
    const void *field_ptr = NULL;
    const void *depth = NULL;
    uint8_t depth_count;
    uint8_t total_count = 0;
    uint8_t pidx;
    bool is_custom;
    uint8_t array_size;
    uint8_t array_depth_count_bak;
    cx_err_t error = CX_INTERNAL_ERROR;

    if (path_struct == NULL) {
        apdu_response_code = APDU_RESPONSE_CONDITION_NOT_SATISFIED;
        return false;
    } else if (length != 1) {
        apdu_response_code = APDU_RESPONSE_INVALID_DATA;
        return false;
    }

    array_size = *data;
    array_depth_count_bak = path_struct->array_depth_count;
    for (pidx = 0; pidx < path_struct->depth_count; ++pidx) {
        if ((field_ptr = get_nth_field(NULL, pidx + 1)) == NULL) {
            apdu_response_code = APDU_RESPONSE_CONDITION_NOT_SATISFIED;
            return false;
        }
        if (struct_field_is_array(field_ptr)) {
            if ((depth = get_struct_field_array_lvls_array(field_ptr, &depth_count)) == NULL) {
                apdu_response_code = APDU_RESPONSE_CONDITION_NOT_SATISFIED;
                return false;
            }
            total_count += depth_count;
            if (total_count > path_struct->array_depth_count) {
                if (!check_and_add_array_depth(depth, total_count, pidx, array_size)) {
                    return false;
                }
                break;
            }
        }
    }

    if (pidx == path_struct->depth_count) {
        apdu_response_code = APDU_RESPONSE_INVALID_DATA;
        PRINTF("Did not find a matching array type.\n");
        return false;
    }
    is_custom = struct_field_type(field_ptr) == TYPE_CUSTOM;
    if (push_new_hash_depth(!is_custom) == false) {
        return false;
    }
    if (is_custom) {
        cx_sha3_t *hash_ctx = get_last_hash_ctx();
        cx_sha3_t *old_ctx = hash_ctx - 1;

        if (array_size > 0) {
            memcpy(hash_ctx, old_ctx, sizeof(*old_ctx));
        } else {
            CX_CHECK(cx_keccak_init_no_throw(hash_ctx, 256));
        }
        CX_CHECK(cx_keccak_init_no_throw(old_ctx, 256));
    }
    if (array_size == 0) {
        do {
            path_advance();
        } while (path_struct->array_depth_count != array_depth_count_bak);
    }

    return true;
end:
    return false;
}

/**
 * Advance within the struct that contains the field the path points to.
 *
 * @return whether the end of the struct has been reached.
 */
static bool path_advance_in_struct(void) {
    bool end_reached = true;
    uint8_t *depth = &path_struct->depths[path_struct->depth_count - 1];
    uint8_t fields_count;

    if (path_struct == NULL) {
        return false;
    }
    if ((get_field(&fields_count)) == NULL) {
        return false;
    }
    if (path_struct->depth_count > 0) {
        *depth += 1;
        ui_712_notify_filter_change();
        end_reached = (*depth == fields_count);
    }
    if (end_reached) {
        path_depth_list_pop();
    }
    return end_reached;
}

/**
 * Advance within the array levels of the current field the path points to.
 *
 * @return whether the end of the array levels has been reached.
 */
static bool path_advance_in_array(void) {
    bool end_reached;
    s_array_depth *arr_depth;

    if (path_struct == NULL) {
        return false;
    }
    do {
        end_reached = false;
        arr_depth = &path_struct->array_depths[path_struct->array_depth_count - 1];

        if ((path_struct->array_depth_count > 0) &&
            (arr_depth->path_index == (path_struct->depth_count - 1))) {
            if (arr_depth->size > 0) arr_depth->size -= 1;
            if (arr_depth->size == 0) {
                array_depth_list_pop();
                end_reached = true;
            } else {
                return false;
            }
        }
    } while (end_reached);
    return true;
}

/**
 * Updates the path to point to the next field in order (DFS).
 *
 * @return whether the advancement was successful or not
 */
bool path_advance(void) {
    bool end_reached;

    do {
        if (path_advance_in_array()) {
            end_reached = path_advance_in_struct();
        } else {
            end_reached = false;
        }
    } while (end_reached);
    path_update();
    return true;
}

/**
 * Get root structure type from path (domain or message)
 *
 * @return enum representing root type
 */
e_root_type path_get_root_type(void) {
    if (path_struct == NULL) {
        return ROOT_DOMAIN;
    }
    return path_struct->root_type;
}

/**
 * Get root structure from path
 *
 * @return pointer to the root structure definition
 */
const void *path_get_root(void) {
    if (path_struct == NULL) {
        return NULL;
    }
    return path_struct->root_struct;
}

/**
 * Get the current amount of depth
 *
 * @return depth count
 */
uint8_t path_get_depth_count(void) {
    if (path_struct == NULL) {
        return 0;
    }
    return path_struct->depth_count;
}

/**
 * Initialize the path context with its indexes in memory and sets it with a depth of 0.
 *
 * @return whether the memory allocation were successful.
 */
bool path_init(void) {
    if (path_struct == NULL) {
        if ((path_struct = MEM_ALLOC_AND_ALIGN_TYPE(*path_struct)) == NULL) {
            apdu_response_code = APDU_RESPONSE_INSUFFICIENT_MEMORY;
        } else {
            path_struct->depth_count = 0;
        }
    }
    return path_struct != NULL;
}

/**
 * De-initialize the path context
 */
void path_deinit(void) {
    path_struct = NULL;
}

#endif  // HAVE_EIP712_FULL_SUPPORT
