#include <syslinux/pxe_api.h>
#include <lwip/api.h>
#include <lwip/tcpip.h>
#include <lwip/dns.h>
#include <core.h>
#include <net.h>
#include "pxe.h"

const struct url_scheme url_schemes[] = {
    { "tftp", tftp_open, 0 },
    { "http", http_open, O_DIRECTORY },
    { "ftp",  ftp_open,  O_DIRECTORY },
    { NULL, NULL, 0 },
};

/**
 * Open a socket
 *
 * @param:socket, the socket to open
 *
 * @out: error code, 0 on success, -1 on failure
 */
int core_udp_open(struct pxe_pvt_inode *socket)
{
    struct net_private_lwip *priv = &socket->net.lwip;
    int err;

    priv->conn = netconn_new(NETCONN_UDP);
    if (!priv->conn)
	return -1;

    priv->conn->recv_timeout = 15; /* A 15 ms recv timeout... */
    err = netconn_bind(priv->conn, NULL, 0);
    if (err) {
	printf("netconn_bind error %d\n", err);
	return -1;
    }

    return 0;
}

/**
 * Close a socket
 *
 * @param:socket, the socket to open
 */
void core_udp_close(struct pxe_pvt_inode *socket)
{
    struct net_private_lwip *priv = &socket->net.lwip;

    if (priv->conn) {
	netconn_delete(priv->conn);
	priv->conn = NULL;
    }
}

/**
 * Establish a connection on an open socket
 *
 * @param:socket, the open socket
 * @param:ip, the ip address
 * @param:port, the port number, host-byte order
 */
void core_udp_connect(struct pxe_pvt_inode *socket, uint32_t ip,
		      uint16_t port)
{
    struct net_private_lwip *priv = &socket->net.lwip;
    struct ip_addr addr;

    addr.addr = ip;
    netconn_connect(priv->conn, &addr, port);
}

/**
 * Tear down a connection on an open socket
 *
 * @param:socket, the open socket
 */
void core_udp_disconnect(struct pxe_pvt_inode *socket)
{
    struct net_private_lwip *priv = &socket->net.lwip;
    netconn_disconnect(priv->conn);
}

/**
 * Read data from the network stack
 *
 * @param:socket, the open socket
 * @param:buf, location of buffer to store data
 * @param:buf_len, size of buffer

 * @out: src_ip, ip address of the data source
 * @out: src_port, port number of the data source, host-byte order
 */
int core_udp_recv(struct pxe_pvt_inode *socket, void *buf, uint16_t *buf_len,
		  uint32_t *src_ip, uint16_t *src_port)
{
    struct net_private_lwip *priv = &socket->net.lwip;
    struct netbuf *nbuf;
    u16_t nbuf_len;
    int err;

    err = netconn_recv(priv->conn, &nbuf);
    if (err)
	return err;

    if (!nbuf)
	return -1;

    *src_ip = netbuf_fromaddr(nbuf)->addr;
    *src_port = netbuf_fromport(nbuf);

    netbuf_first(nbuf);		/* XXX needed? */
    nbuf_len = netbuf_len(nbuf);
    if (nbuf_len <= *buf_len)
	netbuf_copy(nbuf, buf, nbuf_len);
    else
	nbuf_len = 0; /* impossible mtu < PKTBUF_SIZE */
    netbuf_delete(nbuf);

    *buf_len = nbuf_len;
    return 0;
}

/**
 * Send a UDP packet.
 *
 * @param:socket, the open socket
 * @param:data, data buffer to send
 * @param:len, size of data bufer
 */
void core_udp_send(struct pxe_pvt_inode *socket, const void *data, size_t len)
{
    struct netconn *conn = socket->net.lwip.conn;
    struct netbuf *nbuf;
    void *pbuf;
    int err;

    nbuf = netbuf_new();
    if (!nbuf) {
	printf("netbuf allocation error\n");
	return;
    }

    pbuf = netbuf_alloc(nbuf, len);
    if (!pbuf) {
	printf("pbuf allocation error\n");
	goto out;
    }

    memcpy(pbuf, data, len);

    err = netconn_send(conn, nbuf);
    if (err) {
	printf("netconn_send error %d\n", err);
	goto out;
    }

out:
    netbuf_delete(nbuf);
}

/**
 * Network stack-specific initialization
 */
void net_core_init(void)
{
    int err;
    int i;

    http_bake_cookies();

    /* Initialize lwip */
    tcpip_init(NULL, NULL);

    /* Start up the undi driver for lwip */
    err = undiif_start(IPInfo.myip, IPInfo.netmask, IPInfo.gateway);
    if (err) {
       printf("undiif driver failed to start: %d\n", err);
       kaboom();
    }

    for (i = 0; i < DNS_MAX_SERVERS; i++) {
	/* Transfer the DNS information to lwip */
	dns_setserver(i, (struct ip_addr *)&dns_server[i]);
    }
}

void probe_undi(void)
{
    /* Probe UNDI information */
    pxe_call(PXENV_UNDI_GET_INFORMATION, &pxe_undi_info);
    pxe_call(PXENV_UNDI_GET_IFACE_INFO,  &pxe_undi_iface);

    printf("UNDI: baseio %04x int %d MTU %d type %d \"%s\" flags 0x%x\n",
	   pxe_undi_info.BaseIo, pxe_undi_info.IntNumber,
	   pxe_undi_info.MaxTranUnit, pxe_undi_info.HwType,
	   pxe_undi_iface.IfaceType, pxe_undi_iface.ServiceFlags);
}

int core_tcp_open(struct pxe_pvt_inode *socket)
{
    socket->net.lwip.conn = netconn_new(NETCONN_TCP);
    if (!socket->net.lwip.conn)
	return -1;

    return 0;
}
int core_tcp_connect(struct pxe_pvt_inode *socket, uint32_t ip, uint16_t port)
{
    struct ip_addr addr;
    err_t err;

    addr.addr = ip;
    err = netconn_connect(socket->net.lwip.conn, &addr, port);
    if (err) {
	printf("netconn_connect error %d\n", err);
	return -1;
    }

    return 0;
}

int core_tcp_write(struct pxe_pvt_inode *socket, const void *data, size_t len,
		   bool copy)
{
    err_t err;
    u8_t flags = copy ? NETCONN_COPY : NETCONN_NOCOPY;

    err = netconn_write(socket->net.lwip.conn, data, len, flags);
    if (err) {
	printf("netconn_write failed: %d\n", err);
	return -1;
    }

    return 0;
}

void core_tcp_close_file(struct inode *inode)
{
    struct pxe_pvt_inode *socket = PVT(inode);

    if (socket->net.lwip.conn) {
	netconn_delete(socket->net.lwip.conn);
	socket->net.lwip.conn = NULL;
    }
    if (socket->net.lwip.buf) {
	netbuf_delete(socket->net.lwip.buf);
        socket->net.lwip.buf = NULL;
    }
}

bool core_tcp_is_connected(struct pxe_pvt_inode *socket)
{
    if (socket->net.lwip.conn)
	return true;

    return false;
}

void core_tcp_fill_buffer(struct inode *inode)
{
    struct pxe_pvt_inode *socket = PVT(inode);
    void *data;
    u16_t len;
    err_t err;

    /* Clean up or advance an inuse netbuf */
    if (socket->net.lwip.buf) {
	if (netbuf_next(socket->net.lwip.buf) < 0) {
	    netbuf_delete(socket->net.lwip.buf);
	    socket->net.lwip.buf = NULL;
	}
    }
    /* If needed get a new netbuf */
    if (!socket->net.lwip.buf) {
	err = netconn_recv(socket->net.lwip.conn, &(socket->net.lwip.buf));
	if (!socket->net.lwip.buf || err) {
	    socket->tftp_goteof = 1;
	    if (inode->size == -1)
		inode->size = socket->tftp_filepos;
	    socket->ops->close(inode);
	    return;
	}
    }
    /* Report the current fragment of the netbuf */
    err = netbuf_data(socket->net.lwip.buf, &data, &len);
    if (err) {
	printf("netbuf_data err: %d\n", err);
	kaboom();
    }
    socket->tftp_dataptr = data;
    socket->tftp_filepos += len;
    socket->tftp_bytesleft = len;
    return;
}
