#include "argtable3/argtable3.h"
#include "esp_console.h"
#include "esp_event.h"
#include "esp_log.h"
#include "esp_netif.h"
#include "hal/eth_types.h"

#include "ethernet.h"

#define ETHERNET_PHY_ADDR       0x04
#define ETHERNET_PHY_RESET_GPIO -1
#define ETHERNET_PHY_MDC_GPIO   23
#define ETHERNET_PHY_MDIO_GPIO  18
#define ETHERNET_PHY_TXCLK_GPIO 0

#define ETHERNET_MAX_PHYADDR     (1 << 5)
#define ETHERNET_MAX_REGADDR     (1 << 5)

// DEBUG - this should really be set via Kconfig
#define CONFIG_SMI_DEBUG    1

static const char *TAG = "ETH";

static char *ip = "172.28.200";
static char *gateway = "172.28.1.1";
static char *netmask = "255.255.255.0";

static struct {
    struct arg_int *reg_addr;
    struct arg_lit *dump;
    struct arg_lit *help;
    struct arg_int *phy_addr;
    struct arg_lit *read;
    struct arg_int *reg_val;
    struct arg_lit *write;
    struct arg_end *end;        // WARNING: Must be last member of struct
} smi_args;

static esp_eth_mac_t *mac;
static esp_eth_phy_t *phy;
static esp_netif_t *netif;

#if CONFIG_SMI_DEBUG
static const char* smi_progname = "smi";

static void cmd_smi_showhelp(void)
{
    printf("Usage: %s", smi_progname);
    arg_print_syntax(stdout, (void *)&smi_args, "\n");
    printf("\n");
    arg_print_glossary(stdout, (void *)&smi_args, "  %-25s %s\n");

}

static int cmd_smi_do_dump(void)
{
    size_t phy_addr;
    size_t reg_addr;

    // Print column headers
    printf("REG                                                               PHY ADDR\nADDR");
    for (phy_addr = 0; phy_addr < ETHERNET_MAX_PHYADDR; phy_addr++) {
        printf("  %02X ", (uint8_t)phy_addr);
    }
    printf("\n");

    // Print table
    for (reg_addr = 0; reg_addr < ETHERNET_MAX_PHYADDR; reg_addr++) {

        // Row header
        printf("%02X: ", (uint8_t)reg_addr);
        for (phy_addr = 0; phy_addr < ETHERNET_MAX_REGADDR; phy_addr++) {
            uint32_t regval;
            esp_err_t err;

            // Read and print reg value
            err = mac->read_phy_reg(mac, phy_addr, reg_addr, &regval);
            if (err == ESP_OK)
                printf(" %04X", regval);
            else
                printf("  ERR");
        }
        printf("\n");
    }

    return ESP_OK;
}

static int cmd_smi_do_read(void)
{
    uint32_t regval;
    esp_err_t err;

    printf("%s: phy_addr=0x%02X, reg_addr=0x%02X\n", __func__, *smi_args.phy_addr->ival, *smi_args.reg_addr->ival);
    printf("%s: EMACCONFIG_REG=0x%08X, EMACDEBUG_REG=0x%08X\n", __func__,
            *((uint32_t *)0x3FF6A000), *((uint32_t *)0x3FF6A024));
    printf("%s: EMACCSTATUS_REG=0x%08X, EMACWDOGTO_REG=0x%08X\n", __func__,
            *((uint32_t *)0x3FF6A0D8), *((uint32_t *)0x3FF6A0DC));
    printf("%s: EMAC_EX_CLKOUT_CONF_REG=0x%08X, EMAC_EX_OSCCLK_CONF_REG=0x%08X, EMAC_EX_CLK_CTRL_REG=0x%08X\n", __func__,
            *((uint32_t *)0x3FF69800), *((uint32_t *)0x3FF69804), *((uint32_t *)0x3FF69808));
    printf("%s: EMAC_EX_PHYINF_CONF_REG=0x%08X, EMAC_PD_SEL_REG=0x%08X\n", __func__,
            *((uint32_t *)0x3FF6980C), *((uint32_t *)0x3FF69810));

    // Read and print reg value
    err = mac->read_phy_reg(mac, *smi_args.phy_addr->ival, *smi_args.reg_addr->ival, &regval);
    //err = mac->read_phy_reg(mac, 0x04, 0x01, &regval);
    if (err) {
        char buf[64];

        printf("%s: Error: %s (%d)", smi_progname, esp_err_to_name_r(err, buf, sizeof(buf)), err);
        return ESP_FAIL;
    } else
        printf("0x%04X\n", regval);

    return ESP_OK;
}

static int cmd_smi_do_write(void)
{
    esp_err_t err;

    // Read and print reg value
    err = mac->write_phy_reg(mac, *smi_args.phy_addr->ival, *smi_args.reg_addr->ival, *smi_args.reg_val->ival);
    if (err) {
        char buf[64];

        printf("%s: Error: %s (%d)", smi_progname, esp_err_to_name_r(err, buf, sizeof(buf)), err);
        return ESP_FAIL;
    }

    return ESP_OK;
}

static int cmd_smi_main(int argc, char **argv)
{
    int cli_errors;
    int retval = ESP_FAIL;

    // Parse arguments
    cli_errors = arg_parse(argc, argv, (void *)&smi_args);
    if (cli_errors > 0) {
        arg_print_errors(stdout, smi_args.end, smi_progname);
        return ESP_FAIL;
    }

    // Special case: handle help subcommand
    if (smi_args.help->count > 0) {
        cmd_smi_showhelp();
        return ESP_OK;
    }

    // Check for errors in passed values
    if (cli_errors > 0) {
        // Display the error details contained in the arg_end struct
        arg_print_errors(stdout, (void *)&smi_args.end, smi_progname);
        printf("Try '%s --help' for more information.\n", smi_progname);
        return ESP_FAIL;
    }

    // Check for required arguments - exactly one of -d, -r or -w
    if ((smi_args.dump->count <= 0) && (smi_args.read->count <= 0) && (smi_args.write->count <= 0)) {
        printf("%s: Error: Exactly one of -d, -r or -w required", smi_progname);
        cmd_smi_showhelp();
        return ESP_FAIL;
    }

    // Check for required arguments -r and -w require -a and -p
    if (((smi_args.read->count > 0)      || (smi_args.write->count > 0)) &&
        ((smi_args.phy_addr->count <= 0) || (smi_args.reg_addr->count <= 0))) {
        printf("%s: Error: -r and -w require -a and -p", smi_progname);
        cmd_smi_showhelp();
        return ESP_FAIL;
    }

    // Check for illegal option combinations
    if (((smi_args.dump->count > 0) && (smi_args.read->count  > 0)) ||
        ((smi_args.dump->count > 0) && (smi_args.write->count > 0)) ||
        ((smi_args.read->count > 0) && (smi_args.write->count > 0))) {
        printf("%s: Error: Cannot specify -d, -r or -w concurrently", smi_progname);
        cmd_smi_showhelp();
        return ESP_FAIL;
    }

    // Check for invalid phy_addr and reg_addr values
    if (((smi_args.phy_addr->count > 0) && (*smi_args.phy_addr->ival >= ETHERNET_MAX_PHYADDR)) ||
        ((smi_args.reg_addr->count > 0) && (*smi_args.reg_addr->ival >= ETHERNET_MAX_REGADDR))) {
        printf("%s: Error: phy and reg address must be between 0 and  31, inclusive", smi_progname);
        cmd_smi_showhelp();
        return ESP_FAIL;
    }

    // Check for invalid reg_val value
    if ((smi_args.phy_addr->count > 0) && (*smi_args.phy_addr->ival >= ETHERNET_MAX_PHYADDR)) {
        printf("%s: Error: reg value must be between 0 and 0xFFFF, inclusive", smi_progname);
        cmd_smi_showhelp();
        return ESP_FAIL;
    }

    // Execute sub-command
    if (smi_args.dump->count > 0)
        retval = cmd_smi_do_dump();
    else if (smi_args.read->count > 0)
        retval = cmd_smi_do_read();
    else if (smi_args.write->count > 0)
        retval = cmd_smi_do_write();
    else {
        printf("BUG: None of -d, -r or -w specified, this needs to be fixed in source");
        retval = ESP_FAIL;
    }

    return retval;
}

static int cmd_smi_register(void)
{
    const esp_console_cmd_t cmd = {
        .command = smi_progname,
        .help = "SMI read / write commands for debugging",
        .hint = NULL,
        .func = &cmd_smi_main,
        .argtable = &smi_args,
    };

    // Initialize command arguments
    smi_args.reg_addr = arg_int0("a", "reg", "<addr>", "PHY register address (0~1F)");
    smi_args.dump = arg_litn("d", "dump", 0, 1, "Dump entire Clause-22 register space (mutually exclusive with -r and -w)");
    smi_args.help = arg_litn("h", "help", 0, 1, "Show this help and exit");
    smi_args.phy_addr = arg_int0("p", "phy", "<addr>", "PHY device address (0~1F)");
    smi_args.read = arg_litn("r", "read", 0, 1, "Read SMI register (requires -p and -a, mutually exclusive with -d and -w)");
    smi_args.reg_val = arg_int0("v", "val", "<value>", "PHY register value to write (0~FFFF)");
    smi_args.write = arg_litn("w", "write", 0, 1, "Write SMI register (requires -p and -a, mutually exclusive with -d and -r)");
    smi_args.end = arg_end(5);

    // Check that argtable allocations were successful
    if (arg_nullcheck((void *)&smi_args) != 0) {
        ESP_LOGE(TAG, "insufficient memory while initializing smi command argtable");
        return ESP_FAIL;
    }

    ESP_ERROR_CHECK( esp_console_cmd_register(&cmd) );

    return ESP_OK;
}
#endif // CONFIG_SMI_DEBUG

static void eth_event_handler(void *arg, esp_event_base_t event_base,
                              int32_t event_id, void *event_data)
{
    uint8_t mac_addr[6] = {0};
    esp_eth_handle_t eth_handle = *(esp_eth_handle_t *)event_data;

    switch (event_id) {
    case ETHERNET_EVENT_CONNECTED:
        esp_eth_ioctl(eth_handle, ETH_CMD_G_MAC_ADDR, mac_addr);
        ESP_LOGI(TAG, "Ethernet Link Up");
        ESP_LOGI(TAG, "Ethernet HW Addr %02x:%02x:%02x:%02x:%02x:%02x",
                 mac_addr[0], mac_addr[1], mac_addr[2], mac_addr[3], mac_addr[4], mac_addr[5]);
        break;
    case ETHERNET_EVENT_DISCONNECTED:
        ESP_LOGI(TAG, "Ethernet Link Down");
        break;
    case ETHERNET_EVENT_START:
        ESP_LOGI(TAG, "Ethernet Started");
        break;
    case ETHERNET_EVENT_STOP:
        ESP_LOGI(TAG, "Ethernet Stopped");
        break;
    default:
        break;
    }
}

static void got_ip_event_handler(void *arg, esp_event_base_t event_base,
                                 int32_t event_id, void *event_data)
{
    ip_event_got_ip_t *event = (ip_event_got_ip_t *) event_data;
    const esp_netif_ip_info_t *ip_info = &event->ip_info;

    ESP_LOGI(TAG, "Ethernet Got IP Address");
    ESP_LOGI(TAG, "~~~~~~~~~~~");
    ESP_LOGI(TAG, "ETHIP:" IPSTR, IP2STR(&ip_info->ip));
    ESP_LOGI(TAG, "ETHMASK:" IPSTR, IP2STR(&ip_info->netmask));
    ESP_LOGI(TAG, "ETHGW:" IPSTR, IP2STR(&ip_info->gw));
    ESP_LOGI(TAG, "~~~~~~~~~~~");
}

int32_t ethernet_init(void)
{
    // Initialize TCP/IP network interface (should be called only once in application)
    ESP_ERROR_CHECK(esp_netif_init());

    // Create default background event loop
    ESP_ERROR_CHECK(esp_event_loop_create_default());

    // Create new default instance of esp-netif for Ethernet
    esp_netif_config_t cfg = ESP_NETIF_DEFAULT_ETH();
    netif = esp_netif_new(&cfg);

    // Configure PHY driver
    eth_phy_config_t phy_config = ETH_PHY_DEFAULT_CONFIG();
    phy_config.phy_addr = ETHERNET_PHY_ADDR;
    phy_config.reset_gpio_num = ETHERNET_PHY_RESET_GPIO;
    phy = esp_eth_phy_new_ksz8081(&phy_config);

    // Configure MAC driver
    eth_mac_config_t mac_config = ETH_MAC_DEFAULT_CONFIG();
    mac_config.smi_mdc_gpio_num = ETHERNET_PHY_MDC_GPIO;
    mac_config.smi_mdio_gpio_num = ETHERNET_PHY_MDIO_GPIO;
    mac_config.interface = EMAC_DATA_INTERFACE_RMII;
    mac_config.clock_config.rmii.clock_gpio = ETHERNET_PHY_TXCLK_GPIO;
    //mac_config.clock_config.rmii.clock_gpio = EMAC_CLK_IN_GPIO;
    mac_config.clock_config.rmii.clock_mode = EMAC_CLK_EXT_IN;
    mac = esp_eth_mac_new_esp32(&mac_config);

    // Configure top-level Etehrnet driver
    esp_eth_config_t config = ETH_DEFAULT_CONFIG(mac, phy);
    esp_eth_handle_t eth_handle = NULL;
    ESP_ERROR_CHECK(esp_eth_driver_install(&config, &eth_handle));

    // Attach Ethernet driver to TCP/IP stack
    ESP_ERROR_CHECK(esp_netif_attach(netif, esp_eth_new_netif_glue(eth_handle)));

    // Register event handers
    ESP_ERROR_CHECK(esp_event_handler_register(ETH_EVENT, ESP_EVENT_ANY_ID, &eth_event_handler, NULL));
    ESP_ERROR_CHECK(esp_event_handler_register(IP_EVENT, IP_EVENT_ETH_GOT_IP, &got_ip_event_handler, NULL));

    // Start Ethernet state machine
    ESP_ERROR_CHECK(esp_eth_start(eth_handle));

    // DHCP must be stopped before static IP will take effect
    // TODO: there has GOT to be some way to disable this before bringing up the interface.  As it is now,
    // we technically have a timing hole where DHCP is active.  Any settings should immediately be replaced
    // with the static values so I don't see a bug here but, still, this is ugly.
    esp_netif_dhcps_stop(netif);
    esp_netif_dhcpc_stop(netif);

    // Configure IP address
    esp_netif_ip_info_t info_t = {0};
    ip4addr_aton((const char *)ip, (ip4_addr_t *)&info_t.ip.addr);
    ip4addr_aton((const char *)gateway, (ip4_addr_t *)&info_t.gw.addr);
    ip4addr_aton((const char *)netmask, (ip4_addr_t *)&info_t.netmask.addr);
    esp_netif_set_ip_info(netif, &info_t);

#if CONFIG_SMI_DEBUG
    // Register smi command for console-based debugging
    ESP_LOGI(TAG, "Registering %s command", smi_progname);
    if (cmd_smi_register() < 0)
        ESP_LOGE(TAG, "Failed to initialize smi console command");
#endif // CONFIG_SMI_DEBUG

    return 0;
}
