#include <string.h>
#include "eth_plugin_handler.h"
#include "eth_plugin_internal.h"
#include "plugin_utils.h"
#include "shared_context.h"
#include "network.h"

void eth_plugin_prepare_init(ethPluginInitContract_t *init,
                             const uint8_t *selector,
                             uint32_t dataSize) {
    memset((uint8_t *) init, 0, sizeof(ethPluginInitContract_t));
    init->selector = selector;
    init->dataSize = dataSize;
}

void eth_plugin_prepare_provide_parameter(ethPluginProvideParameter_t *provideParameter,
                                          uint8_t *parameter,
                                          uint32_t parameterOffset) {
    memset((uint8_t *) provideParameter, 0, sizeof(ethPluginProvideParameter_t));
    provideParameter->parameter = parameter;
    provideParameter->parameterOffset = parameterOffset;
}

void eth_plugin_prepare_finalize(ethPluginFinalize_t *finalize) {
    memset((uint8_t *) finalize, 0, sizeof(ethPluginFinalize_t));
}

void eth_plugin_prepare_provide_info(ethPluginProvideInfo_t *provideToken) {
    memset((uint8_t *) provideToken, 0, sizeof(ethPluginProvideInfo_t));
}

void eth_plugin_prepare_query_contract_ID(ethQueryContractID_t *queryContractID,
                                          char *name,
                                          uint32_t nameLength,
                                          char *version,
                                          uint32_t versionLength) {
    memset((uint8_t *) queryContractID, 0, sizeof(ethQueryContractID_t));
    queryContractID->name = name;
    queryContractID->nameLength = nameLength;
    queryContractID->version = version;
    queryContractID->versionLength = versionLength;
}

void eth_plugin_prepare_query_contract_UI(ethQueryContractUI_t *queryContractUI,
                                          uint8_t screenIndex,
                                          char *title,
                                          uint32_t titleLength,
                                          char *msg,
                                          uint32_t msgLength) {
    uint64_t chain_id;

    memset((uint8_t *) queryContractUI, 0, sizeof(ethQueryContractUI_t));

    // If no extra information was found, set the pointer to NULL
    if (NO_EXTRA_INFO(tmpCtx, 1)) {
        queryContractUI->item1 = NULL;
    } else {
        queryContractUI->item1 = &tmpCtx.transactionContext.extraInfo[1];
    }

    // If no extra information was found, set the pointer to NULL
    if (NO_EXTRA_INFO(tmpCtx, 0)) {
        queryContractUI->item2 = NULL;
    } else {
        queryContractUI->item2 = &tmpCtx.transactionContext.extraInfo[0];
    }

    queryContractUI->screenIndex = screenIndex;
    chain_id = get_tx_chain_id();
    strlcpy(queryContractUI->network_ticker,
            get_displayable_ticker(&chain_id, chainConfig),
            sizeof(queryContractUI->network_ticker));
    queryContractUI->title = title;
    queryContractUI->titleLength = titleLength;
    queryContractUI->msg = msg;
    queryContractUI->msgLength = msgLength;
}

static void eth_plugin_perform_init_default(uint8_t *contractAddress,
                                            ethPluginInitContract_t *init) {
    // check if the registered external plugin matches the TX contract address / selector
    if (memcmp(contractAddress,
               dataContext.tokenContext.contractAddress,
               sizeof(dataContext.tokenContext.contractAddress)) != 0) {
        PRINTF("Got contract: %.*H\n", ADDRESS_LENGTH, contractAddress);
        PRINTF("Expected contract: %.*H\n",
               ADDRESS_LENGTH,
               dataContext.tokenContext.contractAddress);
        os_sched_exit(0);
    }
    if (memcmp(init->selector,
               dataContext.tokenContext.methodSelector,
               sizeof(dataContext.tokenContext.methodSelector)) != 0) {
        PRINTF("Got selector: %.*H\n", SELECTOR_SIZE, init->selector);
        PRINTF("Expected selector: %.*H\n", SELECTOR_SIZE, dataContext.tokenContext.methodSelector);
        os_sched_exit(0);
    }
    PRINTF("Plugin will be used\n");
    dataContext.tokenContext.pluginStatus = ETH_PLUGIN_RESULT_OK;
}

static bool eth_plugin_perform_init_old_internal(uint8_t *contractAddress,
                                                 ethPluginInitContract_t *init) {
    uint8_t i, j;
    const uint8_t **selectors;

    // Search internal plugin list
    for (i = 0;; i++) {
        selectors = (const uint8_t **) PIC(INTERNAL_ETH_PLUGINS[i].selectors);
        if (selectors == NULL) {
            break;
        }
        for (j = 0; ((j < INTERNAL_ETH_PLUGINS[i].num_selectors) && (contractAddress != NULL));
             j++) {
            if (memcmp(init->selector, (const void *) PIC(selectors[j]), SELECTOR_SIZE) == 0) {
                if ((INTERNAL_ETH_PLUGINS[i].availableCheck == NULL) ||
                    ((PluginAvailableCheck) PIC(INTERNAL_ETH_PLUGINS[i].availableCheck))()) {
                    strlcpy(dataContext.tokenContext.pluginName,
                            INTERNAL_ETH_PLUGINS[i].alias,
                            PLUGIN_ID_LENGTH);
                    dataContext.tokenContext.pluginStatus = ETH_PLUGIN_RESULT_OK;
                    return true;
                }
            }
        }
    }

    return false;
}

eth_plugin_result_t eth_plugin_perform_init(uint8_t *contractAddress,
                                            ethPluginInitContract_t *init) {
    dataContext.tokenContext.pluginStatus = ETH_PLUGIN_RESULT_UNAVAILABLE;

    PRINTF("Selector %.*H\n", 4, init->selector);
    switch (pluginType) {
#ifdef HAVE_NFT_SUPPORT
        case ERC1155:
        case ERC721:
#endif  // HAVE_NFT_SUPPORT
        case EXTERNAL:
            eth_plugin_perform_init_default(contractAddress, init);
            contractAddress = NULL;
            break;
        case OLD_INTERNAL:
            if (eth_plugin_perform_init_old_internal(contractAddress, init)) {
                contractAddress = NULL;
            }
            break;
        default:
            PRINTF("Unsupported pluginType %d\n", pluginType);
            os_sched_exit(0);
            break;
    }

    // Do not handle a plugin if running in swap mode
    if (G_called_from_swap && (contractAddress != NULL)) {
        PRINTF("eth_plug_init aborted in swap mode\n");
        return 0;
    }

    eth_plugin_result_t status = ETH_PLUGIN_RESULT_UNAVAILABLE;

    if (contractAddress != NULL) {
        PRINTF("No plugin available for %.*H\n", 20, contractAddress);
        return status;
    }

    PRINTF("eth_plugin_init\n");
    PRINTF("Trying plugin %s\n", dataContext.tokenContext.pluginName);
    status = eth_plugin_call(ETH_PLUGIN_INIT_CONTRACT, (void *) init);

    if (status <= ETH_PLUGIN_RESULT_UNSUCCESSFUL) {
        return status;
    }
    PRINTF("eth_plugin_init ok %s\n", dataContext.tokenContext.pluginName);
    dataContext.tokenContext.pluginStatus = ETH_PLUGIN_RESULT_OK;
    return ETH_PLUGIN_RESULT_OK;
}

eth_plugin_result_t eth_plugin_call(int method, void *parameter) {
    ethPluginSharedRW_t pluginRW;
    ethPluginSharedRO_t pluginRO;
    char *alias;
    uint8_t i;

    pluginRW.sha3 = &global_sha3;
    pluginRO.txContent = &tmpContent.txContent;

    if (dataContext.tokenContext.pluginStatus <= ETH_PLUGIN_RESULT_UNSUCCESSFUL) {
        PRINTF("Cached plugin call but no plugin available\n");
        return dataContext.tokenContext.pluginStatus;
    }
    alias = dataContext.tokenContext.pluginName;

    // Prepare the call

    switch (method) {
        case ETH_PLUGIN_INIT_CONTRACT:
            PRINTF("-- PLUGIN INIT CONTRACT --\n");
            ((ethPluginInitContract_t *) parameter)->interfaceVersion =
                ETH_PLUGIN_INTERFACE_VERSION_LATEST;
            ((ethPluginInitContract_t *) parameter)->result = ETH_PLUGIN_RESULT_UNAVAILABLE;
            ((ethPluginInitContract_t *) parameter)->pluginSharedRW = &pluginRW;
            ((ethPluginInitContract_t *) parameter)->pluginSharedRO = &pluginRO;
            ((ethPluginInitContract_t *) parameter)->pluginContext =
                (uint8_t *) &dataContext.tokenContext.pluginContext;
            ((ethPluginInitContract_t *) parameter)->pluginContextLength =
                sizeof(dataContext.tokenContext.pluginContext);
            ((ethPluginInitContract_t *) parameter)->alias = dataContext.tokenContext.pluginName;
            break;
        case ETH_PLUGIN_PROVIDE_PARAMETER:
            PRINTF("-- PLUGIN PROVIDE PARAMETER --\n");
            ((ethPluginProvideParameter_t *) parameter)->result = ETH_PLUGIN_RESULT_UNAVAILABLE;
            ((ethPluginProvideParameter_t *) parameter)->pluginSharedRW = &pluginRW;
            ((ethPluginProvideParameter_t *) parameter)->pluginSharedRO = &pluginRO;
            ((ethPluginProvideParameter_t *) parameter)->pluginContext =
                (uint8_t *) &dataContext.tokenContext.pluginContext;
            break;
        case ETH_PLUGIN_FINALIZE:
            PRINTF("-- PLUGIN FINALIZE --\n");
            ((ethPluginFinalize_t *) parameter)->result = ETH_PLUGIN_RESULT_UNAVAILABLE;
            ((ethPluginFinalize_t *) parameter)->pluginSharedRW = &pluginRW;
            ((ethPluginFinalize_t *) parameter)->pluginSharedRO = &pluginRO;
            ((ethPluginFinalize_t *) parameter)->pluginContext =
                (uint8_t *) &dataContext.tokenContext.pluginContext;
            break;
        case ETH_PLUGIN_PROVIDE_INFO:
            PRINTF("-- PLUGIN PROVIDE INFO --\n");
            ((ethPluginProvideInfo_t *) parameter)->result = ETH_PLUGIN_RESULT_UNAVAILABLE;
            ((ethPluginProvideInfo_t *) parameter)->pluginSharedRW = &pluginRW;
            ((ethPluginProvideInfo_t *) parameter)->pluginSharedRO = &pluginRO;
            ((ethPluginProvideInfo_t *) parameter)->pluginContext =
                (uint8_t *) &dataContext.tokenContext.pluginContext;
            break;
        case ETH_PLUGIN_QUERY_CONTRACT_ID:
            PRINTF("-- PLUGIN QUERY CONTRACT ID --\n");
            ((ethQueryContractID_t *) parameter)->result = ETH_PLUGIN_RESULT_UNAVAILABLE;
            ((ethQueryContractID_t *) parameter)->pluginSharedRW = &pluginRW;
            ((ethQueryContractID_t *) parameter)->pluginSharedRO = &pluginRO;
            ((ethQueryContractID_t *) parameter)->pluginContext =
                (uint8_t *) &dataContext.tokenContext.pluginContext;
            break;
        case ETH_PLUGIN_QUERY_CONTRACT_UI:
            PRINTF("-- PLUGIN QUERY CONTRACT UI --\n");
            ((ethQueryContractUI_t *) parameter)->pluginSharedRW = &pluginRW;
            ((ethQueryContractUI_t *) parameter)->pluginSharedRO = &pluginRO;
            ((ethQueryContractUI_t *) parameter)->pluginContext =
                (uint8_t *) &dataContext.tokenContext.pluginContext;
            break;
        default:
            PRINTF("Unknown plugin method %d\n", method);
            return ETH_PLUGIN_RESULT_UNAVAILABLE;
    }

    switch (pluginType) {
        case EXTERNAL: {
            uint32_t params[3];
            params[0] = (uint32_t) alias;
            params[1] = method;
            params[2] = (uint32_t) parameter;
            BEGIN_TRY {
                TRY {
                    os_lib_call(params);
                }
                CATCH_OTHER(e) {
                    PRINTF("Plugin call exception for %s\n", alias);
                }
                FINALLY {
                }
            }
            END_TRY;
            break;
        }
#ifdef HAVE_NFT_SUPPORT
        case ERC721: {
            erc721_plugin_call(method, parameter);
            break;
        }
        case ERC1155: {
            erc1155_plugin_call(method, parameter);
            break;
        }
#endif  // HAVE_NFT_SUPPORT
        case OLD_INTERNAL: {
            // Perform the call
            for (i = 0;; i++) {
                if (INTERNAL_ETH_PLUGINS[i].alias[0] == 0) {
                    break;
                }
                if (strcmp(alias, INTERNAL_ETH_PLUGINS[i].alias) == 0) {
                    ((PluginCall) PIC(INTERNAL_ETH_PLUGINS[i].impl))(method, parameter);
                    break;
                }
            }
            break;
        }
        default: {
            PRINTF("Error with pluginType: %d\n", pluginType);
            return ETH_PLUGIN_RESULT_ERROR;
        }
    }

    // Check the call result
    PRINTF("method: %d\n", method);
    switch (method) {
        case ETH_PLUGIN_INIT_CONTRACT:
            switch (((ethPluginInitContract_t *) parameter)->result) {
                case ETH_PLUGIN_RESULT_OK:
                    break;
                case ETH_PLUGIN_RESULT_ERROR:
                    return ETH_PLUGIN_RESULT_ERROR;
                default:
                    return ETH_PLUGIN_RESULT_UNAVAILABLE;
            }
            break;
        case ETH_PLUGIN_PROVIDE_PARAMETER:
            switch (((ethPluginProvideParameter_t *) parameter)->result) {
                case ETH_PLUGIN_RESULT_OK:
                case ETH_PLUGIN_RESULT_FALLBACK:
                    break;
                case ETH_PLUGIN_RESULT_ERROR:
                    return ETH_PLUGIN_RESULT_ERROR;
                default:
                    return ETH_PLUGIN_RESULT_UNAVAILABLE;
            }
            break;
        case ETH_PLUGIN_FINALIZE:
            switch (((ethPluginFinalize_t *) parameter)->result) {
                case ETH_PLUGIN_RESULT_OK:
                case ETH_PLUGIN_RESULT_FALLBACK:
                    break;
                case ETH_PLUGIN_RESULT_ERROR:
                    return ETH_PLUGIN_RESULT_ERROR;
                default:
                    return ETH_PLUGIN_RESULT_UNAVAILABLE;
            }
            break;
        case ETH_PLUGIN_PROVIDE_INFO:
            PRINTF("RESULT: %d\n", ((ethPluginProvideInfo_t *) parameter)->result);
            switch (((ethPluginProvideInfo_t *) parameter)->result) {
                case ETH_PLUGIN_RESULT_OK:
                case ETH_PLUGIN_RESULT_FALLBACK:
                    break;
                case ETH_PLUGIN_RESULT_ERROR:
                    return ETH_PLUGIN_RESULT_ERROR;
                default:
                    return ETH_PLUGIN_RESULT_UNAVAILABLE;
            }
            break;
        case ETH_PLUGIN_QUERY_CONTRACT_ID:
            if (((ethQueryContractID_t *) parameter)->result != ETH_PLUGIN_RESULT_OK) {
                return ETH_PLUGIN_RESULT_ERROR;
            }
            break;
        case ETH_PLUGIN_QUERY_CONTRACT_UI:
            if (((ethQueryContractUI_t *) parameter)->result != ETH_PLUGIN_RESULT_OK) {
                return ETH_PLUGIN_RESULT_ERROR;
            }
            break;
        default:
            return ETH_PLUGIN_RESULT_UNAVAILABLE;
    }

    return ETH_PLUGIN_RESULT_OK;
}
