#include "user.h"
#include "embfmt/embformat.h"
#include "lwip/apps/fs.h"
#include "lwip/apps/httpd.h"
#include "lwip/igmp.h"
#include "lwip/tcp.h"
#include "lwip/udp.h"
#include "netif/ethernet.h"
#include "standard_output/standard_output.h"
#include <string.h>

#define MAX(a, b) (((a) > (b)) ? (a) : (b))
#define MIN(a, b) (((a) < (b)) ? (a) : (b))

// -----------------------------------

#define RECV_BUF_LEN (511)
static char recv_buf[RECV_BUF_LEN + 1];
static char *msg = "Üdvözlet a H735-ös kártyáról!";

// ----------- Custom EtherType client

// TODO: 0x9000 fölött kb. bármit lehet választani

#define CUSTOM_RX_ETHERTYPE (0x88F7)

err_t hook_unknown_ethertype(struct pbuf *pbuf, struct netif *netif) {
    // aquire ethertype
    uint16_t etherType = 0;
    memcpy(&etherType, ((uint8_t *)pbuf->payload) + 12, 2);
    etherType = ntohs(etherType);

    uint8_t *shwa = ((uint8_t *)pbuf->payload) + 6;     // get source hardware address
    uint16_t len = pbuf->len - 14;                      // get payload length
    uint8_t *payload = ((uint8_t *)pbuf->payload) + 14; // actual payload

    // check for match
    if (etherType == CUSTOM_RX_ETHERTYPE) {
        // copy payload
        uint16_t copy_len = MIN(RECV_BUF_LEN, len);
        recv_buf[copy_len] = '\0';
        memcpy(recv_buf, payload, copy_len);

        // print it
        MSG("[%02X:%02X:%02X:%02X:%02X:%02X] %u\n%s\n", shwa[0], shwa[1], shwa[2], shwa[3], shwa[4], shwa[5], len, recv_buf);
    }

    pbuf_free(pbuf);

    return ERR_OK;
}

#define CUSTOM_TX_ETHERTYPE (0x9101)
static struct eth_addr dest_hwa = {.addr = {0x00, 0x1b, 0x21, 0x91, 0x9E, 0x64}};

void send_frame() {
    uint16_t len = strlen(msg);
    struct pbuf *p = pbuf_alloc(PBUF_LINK, len, PBUF_RAM);
    if (p == NULL) {
        MSG("Nem sikerült pbuf-ot foglalni az Ethernet-keret küldéséhez!\n");
        return;
    }

    memcpy(p->payload, msg, len);
    if (ethernet_output(netif_default, p, (struct eth_addr *)netif_default->hwaddr, &dest_hwa, CUSTOM_TX_ETHERTYPE) != ERR_OK) {
        MSG("Sikertelen volt az Ethernet-keret küldése!\n");
        return;
    }

    pbuf_free(p);
}

// ----------- UDP client ------------

static struct udp_pcb *udpc;
static ip4_addr_t dest_addr;
static uint16_t dest_port;

void udp_client_init() {
    // create new PCB
    udpc = udp_new();
    if (udpc == NULL) {
        MSG("Hiba az UDP-kliens létrehozásánál!\n");
    }

    // set destination IP address and port
    IP4_ADDR(&dest_addr, 10, 42, 0, 1);
    dest_port = 4000;

    // TODO: connect?
}

void udp_client_send() {
    uint16_t len = strlen(msg);

    // allocate pbuf and fill with contents
    struct pbuf *buf = pbuf_alloc(PBUF_TRANSPORT, len, PBUF_RAM);
    if (buf == NULL) {
        MSG("Nem lehetett újabb pbuf-ot foglalni!\n");
        return;
    }
    memcpy(buf->payload, msg, len);

    // send UDP datagram to defined destination
    if (udp_sendto(udpc, buf, &dest_addr, dest_port) != ERR_OK) {
        MSG("Nem sikerült elküldeni az UDP datagramot!\n");
    }

    // decrease reference count of the buf
    pbuf_free(buf);
}

// -----------------------------------

static struct udp_pcb *udps;
static uint16_t server_port;

static void udp_server_recv(void *arg, struct udp_pcb *pcb, struct pbuf *p, const ip_addr_t *addr, u16_t port) {
    uint16_t copy_len = MIN(RECV_BUF_LEN, p->len);
    recv_buf[copy_len] = '\0';
    memcpy(recv_buf, p->payload, copy_len);
    MSG("[%s:%u] %ub\n%s\n", ipaddr_ntoa(addr), port, p->len, recv_buf);
    pbuf_free(p);
}

void udp_server_init() {
    // create new PCB
    udps = udp_new();
    if (udps == NULL) {
        MSG("Hiba az UDP-szerver létrehozásánál!\n");
    }

    // set server port
    server_port = 3000;

    // bind it to a local address
    if (udp_bind(udps, IP4_ADDR_ANY, server_port) != ERR_OK) {
        MSG("Nem sikerült az UDP-szervert felcsatolni a %u portra!\n", server_port);
        udp_remove(udps);
    }

    // register receive callback
    udp_recv(udps, udp_server_recv, NULL);
}

// -----------------------------------

static struct udp_pcb *udpmc;
static uint16_t mc_port;
static ip4_addr_t mc_group;

static void udp_mcast_recv(void *arg, struct udp_pcb *pcb, struct pbuf *p, const ip_addr_t *addr, u16_t port) {
    uint16_t copy_len = MIN(RECV_BUF_LEN, p->len);
    recv_buf[copy_len] = '\0';
    memcpy(recv_buf, p->payload, copy_len);
    MSG("[%s:%u] %ub\n%s\n", ipaddr_ntoa(addr), port, p->len, recv_buf);
    pbuf_free(p);
}

void udp_mcast_init() {
    // create new PCB
    udpmc = udp_new();
    if (udps == NULL) {
        MSG("Hiba a multicast UDP szolgáltatás létrehozásánál!\n");
    }

    // set multicast group's address and port
    IP4_ADDR(&mc_group, 224, 0, 2, 5);
    mc_port = 2000;

    // join the relevant multicast group
    igmp_joingroup(&netif_default->ip_addr, &mc_group);

    // bind it to a local address
    if (udp_bind(udpmc, &mc_group, mc_port) != ERR_OK) {
        MSG("Nem sikerült az UDP-szervert felcsatolni a %u portra!\n", mc_port);
        goto cleanup;
    }

    // connect to the IGMP group
    if (udp_connect(udpmc, &mc_group, mc_port) != ERR_OK) {
        MSG("Nem sikerült az UDP multicast szolgáltatást becsatlakoztatni a multicast-csoportba!\n");
        goto cleanup;
    }

    // register receive callback
    udp_recv(udpmc, udp_mcast_recv, NULL);

    return;

// ---
cleanup:
    udp_remove(udpmc);
    return;
}

void udp_mcast_send() {
    uint16_t len = strlen(msg);

    // allocate pbuf and fill with contents
    struct pbuf *buf = pbuf_alloc(PBUF_TRANSPORT, len, PBUF_RAM);
    if (buf == NULL) {
        MSG("Nem lehetett újabb pbuf-ot foglalni!\n");
        return;
    }
    memcpy(buf->payload, msg, len);

    // send UDP datagram to defined destination
    if (udp_send(udpmc, buf) != ERR_OK) {
        MSG("Nem sikerült elküldeni az UDP datagramot!\n");
    }

    // decrease reference count of the buf
    pbuf_free(buf);
}

// -----------------------------------

static struct tcp_pcb *tcps;
static uint16_t tcps_port;
static ip_addr_t tcps_addr;

static err_t tcp_server_conn_recv(void *arg, struct tcp_pcb *tpcb, struct pbuf *p, err_t err) {
    if (p != NULL) {
        uint16_t copy_len = MIN(RECV_BUF_LEN, p->len);
        recv_buf[copy_len] = '\0';
        memcpy(recv_buf, p->payload, copy_len);
        MSG("[%s:%u] %ub\n%s\n", ipaddr_ntoa(&tpcb->remote_ip), tpcb->remote_port, p->len, recv_buf);
        pbuf_free(p);
        tcp_recved(tpcb, copy_len);
    } else {
        MSG("A kliens lekapcsolódott %s:%u\n", ipaddr_ntoa(&tpcb->remote_ip), tpcb->remote_port);
    }
    return ERR_OK;
}

static void tcp_server_conn_err(void *arg, err_t err) {
    MSG("TCP-kapcsolat hiba! (%u)\n", err);
}

static err_t tcp_server_accept(void *arg, struct tcp_pcb *newpcb, err_t err) {
    tcp_recv(newpcb, tcp_server_conn_recv);
    tcp_err(newpcb, tcp_server_conn_err);
    MSG("Bejövő TCP-kapcsolat érkezett %s:%u!\n", ipaddr_ntoa(&newpcb->remote_ip), newpcb->remote_port);

    return ERR_OK;
}

static void tcp_server_error(void *arg, err_t err) {
    MSG("TCP-szerver hiba! (%u)\n", err);
}

void tcp_server_init() {
    // create new TCP service
    tcps = tcp_new();
    if (tcps == NULL) {
        MSG("Nem sikerült létrehozni a TCP szerver szolgáltatást!\n");
        return;
    }

    // set port
    tcps_port = 6000;
    IP4_ADDR(&tcps_addr, 10, 42, 0, 137);

    // bind to local port
    if (tcp_bind(tcps, &tcps_addr, tcps_port) != ERR_OK) {
        MSG("Nem sikerült a TCP-szervert a %u portra csatolni!\n", tcps_port);
        goto cleanup;
    }

    // enable server functions
    tcps = tcp_listen(tcps);

    // assign error handling function
    tcp_err(tcps, tcp_server_error);

    // register accept callback
    tcp_accept(tcps, tcp_server_accept);

    return;

// -----
cleanup:
    tcp_close(tcps);
    return;
}

static struct tcp_pcb *tcpc;
static ip4_addr_t tcpc_remote_addr;
static uint16_t tcpc_remote_port;

static void tcp_client_error(void *arg, err_t err) {
    MSG("TCP-kliens hiba! (%u)\n", err);
}

static err_t tcp_client_recv(void *arg, struct tcp_pcb *tpcb, struct pbuf *p, err_t err) {
    if (p != NULL) {
        uint16_t copy_len = MIN(RECV_BUF_LEN, p->len);
        recv_buf[copy_len] = '\0';
        memcpy(recv_buf, p->payload, copy_len);
        MSG("[%s:%u] %ub\n%s\n", ipaddr_ntoa(&tpcb->remote_ip), tpcb->remote_port, p->len, recv_buf);
        pbuf_free(p);
        tcp_recved(tpcb, copy_len);
    } else {
        MSG("A szerver bontotta a kapcsolatot %s:%u\n", ipaddr_ntoa(&tpcb->remote_ip), tpcb->remote_port);
    }
    return ERR_OK;
}

static err_t tcp_client_connected(void *arg, struct tcp_pcb *tpcb, err_t err) {
    MSG("Sikeresen fölkapcsolódtunk a TCP-szerverre: %s:%u!\n", ipaddr_ntoa(&tpcb->remote_ip), tpcb->remote_port);

    tcp_write(tcpc, msg, strlen(msg), 0);

    tcp_close(tcpc);

    return ERR_OK;
}

void tcp_client_init() {
    // create new TCP service
    tcpc = tcp_new();
    if (tcpc == NULL) {
        MSG("Nem sikerült létrehozni a TCP szerver szolgáltatást!\n");
        return;
    }

    // set remote address and port
    IP4_ADDR(&tcpc_remote_addr, 10, 42, 0, 1);
    tcpc_remote_port = 9000;

    // assign error handling function
    tcp_err(tcpc, tcp_client_error);

    // assign reception callback
    tcp_recv(tcpc, tcp_client_recv);

    // connect to remote host
    tcp_connect(tcpc, &tcpc_remote_addr, tcpc_remote_port, tcp_client_connected);
}

// -----------------------------------

#define GENDATA_LEN (31)
static char generated_data[GENDATA_LEN + 1];

int fs_open_custom(struct fs_file *file, const char *name) {
    /* this example only provides one file */
    if (!strcmp(name, "/generated.html")) {
        /* generate a random number */
        uint32_t random = rand();
        embfmt(generated_data, GENDATA_LEN, "%u is random!", random);

        /* initialize fs_file correctly */
        memset(file, 0, sizeof(struct fs_file));
        uint32_t len = strlen(generated_data);
        file->pextension = mem_malloc(len);
        if (file->pextension != NULL) {
            /* instead of doing memcpy, you would generate e.g. a JSON here */
            memcpy(file->pextension, generated_data, len);
            file->data = (const char *)file->pextension;
            file->len = len; /* don't send the trailing 0 */
            file->index = file->len;
            /* allow persisting connections */
            file->flags = FS_FILE_FLAGS_HEADER_PERSISTENT;
            return 1;
        }
    }
    return 0;
}

void fs_close_custom(struct fs_file *file) {
    if (file && file->pextension) {
        mem_free(file->pextension);
        file->pextension = NULL;
    }
}

// -----------------------------------

// void btn_cb() {
//     MSG("Küldés!\n");

//     //udp_client_send();
//     // send_frame();
//     // udp_mcast_send();
//     tcp_client_init();
// }

void user_init() {
    udp_client_init();
    // udp_server_init();
    // udp_mcast_init();
    tcp_server_init();

    httpd_init();
}