/*
 * CDDL HEADER START
 *
 * The contents of this file are subject to the terms of the
 * Common Development and Distribution License (the "Licence").
 * You may not use this file except in compliance with the Licence.
 *
 * You can obtain a copy of the licence at RISC OS path @.^.LICENCE
 * or  http://www.riscosdev.com/lanman98/LICENCE.CDDL
 * See the Licence for the specific language governing permissions
 * and limitations under the Licence.
 *
 * When distributing Covered Code, include this CDDL HEADER in each
 * file and include the Licence file. If applicable, add the
 * following below this CDDL HEADER, with the fields enclosed by
 * brackets "[]" replaced with your own identifying information:
 * Portions Copyright [yyyy] [name of copyright owner]
 *
 * CDDL HEADER END
 */
 
/*
 *   Copyright 1996 Warm Silence Software Ltd.  All rights reserved.
 *   Use is subject to license terms.
 */

/*
 *   PHBG: 20/12/96: Initial (termite) version
 *   RJW: Completly rewritten for BSD
 *   PHBG: Fix a bug or three and add listening sockets.
 *   PHBG 5/2/97:   Added UDP support
 *   PHBG 27/7/98:  Threaded version
 */

#include "MemCheck:MemCheck.h"
#include <string.h>
#include "TCPIPLibs.errno.h"
#include "socklib.h"
#include "sys/ioctl.h"
#include "netinet.tcp.h"
#include "netdb.h"
#include "unixlib.h"
#include "inetlib.h"
#include "gethost.h"

#include "swis.h"
#include "kernel.h"
#include "debug.h"
#include "error.h"
#include "memory.h"
#include "thread.h"

#include "tcp.h"


struct tcp_conn_s
{
    int                bleep;
    int                socket;
    int		       ready;
};

struct tcp_bleeper_s
{
    int socket;
    int close_on_destruct;
};

struct tcp_addr_s
{
    int            family;
    int            length;
    struct in_addr addr;
    char           zeropad[8];
};

struct udp_conn_s
{
    int                socket;
    struct sockaddr_in *peer;
};

void TcpInitialise(void)
{
}

void TcpFinalise(void)
{
}

char *TcpHostName(void)
{
    static char buf[65];

    gethostname(buf, 65);
    return buf;
}

typedef struct
{
    char *name;
    tcp_addr_t addr;
    int done;
} *resolution_t;

static int resolve(resolution_t res)
{
    struct hostent *host;
    int error;

    if(!res->done)
    {
        host = gethost(res->name, &error);
        switch(error)
        {
            case 0:
                res->done = 1;
                if(host->h_length > 12)
                    Error("Oversized address returned by gethost");
                res->addr = Malloc(sizeof(*res->addr));
                res->addr->length = host->h_length;
                res->addr->family = host->h_addrtype;
                memcpy((char *) &res->addr->addr, host->h_addr, res->addr->length);
                break;
            case EINPROGRESS:
                break;
            default:
                res->done = 1;
                break;
        }
    }
    return res->done;
}

tcp_addr_t TcpResolve(char *name)
{
    resolution_t res;
    tcp_addr_t addr;

    res = Malloc(sizeof(*res) + strlen(name) + 1);
    res->name = (char *)(res + 1);
    strcpy(res->name, name);
    res->addr = NULL;
    res->done = 0;
    ExceptTry
    {
        while(!resolve(res))
            ThreadWait((int (*)(void *)) resolve, res);
    }
    ExceptCatch
    {
        Free(res->addr);
        Free(res);
        ExceptRethrow();
    }
    addr = res->addr;
    Free(res);
    return addr;
}

tcp_addr_t TcpAddrFromQuad(char *quad)
{
    tcp_addr_t addr;

    addr = Malloc(sizeof(*addr));
    memset(addr, 0, sizeof(*addr));
    addr->family = AF_INET;
    addr->length = 4;
    memcpy(&addr->addr, quad, 4);
    return addr;
}

void TcpAddrToQuad(tcp_addr_t addr, char *quad)
{
    memcpy(quad, &addr->addr, 4);
}

tcp_addr_t TcpPeer(tcp_conn_t conn)
{
    tcp_addr_t result;
    struct sockaddr_in sockaddr;
    int len;

    len = sizeof(sockaddr);
    if(getpeername(conn->socket, (struct sockaddr *) &sockaddr, &len))
        ErrorErr(&_inet_error);
    len -= 4;
    result = Malloc(sizeof(*result));
    result->length = len;
    result->family = sockaddr.sin_family;
    if(len > 12)
        Error("Oversized address returned from getpeername");
    memcpy((char *) &result->addr, (char *) &sockaddr.sin_addr, len);
    return result;
}

void TcpAddrDestruct(tcp_addr_t ip)
{
    Free(ip);
}

static int conn_ready(tcp_conn_t conn)
{
    static fd_set write_fds;
    struct timeval timeout;

    conn->ready = 0;
    FD_ZERO(&write_fds);
    FD_SET(conn->socket, &write_fds);
    timeout.tv_sec = 0;
    timeout.tv_usec = 0;
    if(select(FD_SETSIZE, NULL, &write_fds, NULL, &timeout) < 0)
        ErrorErr(&_inet_error);
    if(FD_ISSET(conn->socket, &write_fds))
    {
        int error, size;

        size = sizeof(int);
        if(getsockopt(conn->socket, SOL_SOCKET, SO_ERROR, &error, &size) < 0)
            ErrorErr(&_inet_error);
        if(error)
            Error("Connection failed (%d)\n", error);
        conn->ready = 1;
    }
    return conn->ready;
}

tcp_conn_t TcpCall(tcp_port_t sp, tcp_port_t dp, tcp_addr_t dip)
{
    tcp_conn_t conn;
    int on = 1;
    struct sockaddr_in server;

    conn = Malloc(sizeof(*conn));
    conn->socket = -1;
    ExceptTry
    {
        conn->socket = socket(AF_INET, SOCK_STREAM, 0);
        if(conn->socket < 0)
            ErrorErr(&_inet_error);

        /* Mark Socket non-blocking */
        if(socketioctl(conn->socket,FIONBIO, &on) < 0)
            ErrorErr(&_inet_error);

        /* First lets bind the socket to the right local port */
        memset(&server, 0, sizeof(server));
        server.sin_addr.s_addr = (unsigned int)htonl(INADDR_ANY);
        server.sin_family = AF_INET;
        server.sin_port = htons((u_short)sp);
        if(bind(conn->socket, (struct sockaddr *) &server, sizeof(server)))
            ErrorErr(&_inet_error);

        /* Now prepare the remote port details */
        if(dip->length > 12)
            Error("Oversized address in TcpCall");
        memset(&server, 0, sizeof(server));
        memcpy((char *)&server.sin_addr,&dip->addr,dip->length);
        server.sin_family = dip->family;
        server.sin_port = htons((u_short)dp);

        /* Now we do the connect */
        if(connect(conn->socket, (struct sockaddr *) &server, sizeof(server)) == -1)
        {
            switch(errno)
            {
                case EINPROGRESS:
                    break;
                default:
                    ErrorErr(&_inet_error);
                    break;
            }
        }
        conn_ready(conn);
        while(!conn->ready)
            ThreadWait((int (*)(void *)) conn_ready, conn);
    }
    ExceptCatch
    {
        TcpConnDestruct(conn);
        ExceptRethrow();
    }
    return conn;
}

tcp_addr_t TcpLocalAddr(tcp_conn_t conn)
{
    tcp_addr_t result;
    struct sockaddr_in sockaddr;
    int len;

    len = sizeof(sockaddr);
    if(getsockname(conn->socket, (struct sockaddr *) &sockaddr, &len))
        ErrorErr(&_inet_error);
    len -= 4;
    result = Malloc(sizeof(*result));
    result->length = len;
    result->family = sockaddr.sin_family;
    if(len > 12)
        Error("Oversized address returned from getpeername");
    memcpy((char *) &result->addr, (char *) &sockaddr.sin_addr, len);
    return result;
}

tcp_port_t TcpLocalPort(tcp_conn_t conn)
{
    int len;
    struct sockaddr_in name;

    len = sizeof(name);
    if(getsockname(conn->socket, (struct sockaddr *) &name, &len))
        ErrorErr(&_inet_error);
    return htons((u_short)name.sin_port);
}

tcp_bleeper_t TcpListen(tcp_port_t port)
{
    tcp_bleeper_t bleep;
    int on = 1;
    struct sockaddr_in server;

    bleep = Malloc(sizeof(*bleep));
    bleep->socket = -1;
    bleep->close_on_destruct = 1;
    ExceptTry
    {
        bleep->socket = socket(AF_INET, SOCK_STREAM, 0);
        if(bleep->socket < 0)
            ErrorErr(&_inet_error);

        /* Mark Socket non-blocking */
        if(socketioctl(bleep->socket ,FIONBIO, &on) < 0)
            ErrorErr(&_inet_error);

        /* First lets bind the socket to the right local port */
        memset(&server, 0, sizeof(server));
        server.sin_addr.s_addr = (unsigned int)htonl(INADDR_ANY);
        server.sin_family = AF_INET;
        server.sin_port = htons((u_short)port);
        if(bind(bleep->socket, (struct sockaddr *) &server, sizeof(server)))
            ErrorErr(&_inet_error);
        if(listen(bleep->socket, 5))
            ErrorErr(&_inet_error);
    }
    ExceptCatch
    {
        TcpBleeperDestruct(bleep);
        ExceptRethrow();
    }
    return bleep;
}

tcp_bleeper_t TcpListenToConn(tcp_conn_t conn)
{
    tcp_bleeper_t bleep;

    bleep = Malloc(sizeof(*bleep));
    bleep->socket = conn->socket;
    bleep->close_on_destruct = 0;
    if(listen(bleep->socket, 5))
        ErrorErr(&_inet_error);

    return bleep;
}

tcp_port_t TcpBleeperPort(tcp_bleeper_t bleeper)
{
    int len;
    struct sockaddr_in name;

    len = sizeof(name);
    if(getsockname(bleeper->socket, (struct sockaddr *) &name, &len))
        ErrorErr(&_inet_error);
    return htons((u_short)name.sin_port);
}

void TcpBleeperDestruct(tcp_bleeper_t bleep)
{
    if(bleep)
    {
        if(bleep->socket != -1 && bleep->close_on_destruct)
            socketclose(bleep->socket);
        Free(bleep);
    }
}

static int answer_bleep(tcp_conn_t conn)
{
    conn->socket = accept(conn->bleep, NULL, NULL);
    if(conn->socket < 0)
    {
        switch(errno)
        {
            case EWOULDBLOCK:
                return NULL;
            default:
                ErrorErr(&_inet_error);
        }
    }
    return conn->socket >= 0;
}

tcp_conn_t TcpAnswer(tcp_bleeper_t bleep)
{
    tcp_conn_t conn;

    conn = Malloc(sizeof(*conn));
    conn->bleep = bleep->socket;
    conn->socket = -1;
    conn->ready = 0;
    ExceptTry
    {
        answer_bleep(conn);
        while(conn->socket < 0)
            ThreadWait((int (*)(void *)) answer_bleep, conn);
    }
    ExceptCatch
    {
        TcpConnDestruct(conn);
        ExceptRethrow();
    }
    return conn;
}

void TcpSetNoDelay(tcp_conn_t conn)
{
    int flag;

    flag = 1;
    if(setsockopt(conn->socket, IPPROTO_TCP, TCP_NODELAY, &flag, sizeof(flag)) == -1)
        ErrorErr(&_inet_error);
}

void TcpConnDestruct(tcp_conn_t conn)
{
    if(conn)
    {
        if(conn->socket >= 0)
        {
            socketclose(conn->socket);
        }
        Free(conn);
    }
}

static int conn_readable(tcp_conn_t conn)
{
    static fd_set read_fds;
    struct timeval timeout;

    FD_ZERO(&read_fds);
    FD_SET(conn->socket, &read_fds);
    timeout.tv_sec = 0;
    timeout.tv_usec = 0;
    if(select(FD_SETSIZE, &read_fds, NULL, NULL, &timeout) < 0)
        ErrorErr(&_inet_error);
    return (FD_ISSET(conn->socket, &read_fds) != 0);
}

int TcpRead(void *buf, int size, tcp_conn_t conn)
{
    char *cbuf;
    int read, rcvd;

#ifdef MemCheck_MEMCHECK
    memmove(buf, buf, size);
#endif
    cbuf = buf;
    read = 0;
    rcvd = -1;
    while(size && rcvd)
    {
        while((rcvd = recv(conn->socket, cbuf, size, 0)) < 0)
        {
            switch(errno)
            {
                case EWOULDBLOCK:
                    ThreadWait((int (*)(void *))conn_readable, conn);
                    break;
                default:
                    ErrorErr(&_inet_error);
                    break;
            }
        }
        cbuf += rcvd;
        read += rcvd;
        size -= rcvd;
        if(size < 0) Error("Overflow in TCPRead");
    }
    return read;
}

int TcpReadSome(void *buf, int size, tcp_conn_t conn)
{
    int rcvd;

#ifdef MemCheck_MEMCHECK
    memmove(buf, buf, size);
#endif
    while((rcvd = recv(conn->socket, buf, size, 0)) < 0)
    {
        switch(errno)
        {
            case EWOULDBLOCK:
                ThreadWait((int (*)(void *))conn_readable, conn);
                break;
            default:
                ErrorErr(&_inet_error);
                break;
        }
    }
    return rcvd;
}

static int conn_writeable(tcp_conn_t conn)
{
    static fd_set write_fds;
    struct timeval timeout;

    FD_ZERO(&write_fds);
    FD_SET(conn->socket, &write_fds);
    timeout.tv_sec = 0;
    timeout.tv_usec = 0;
    if(select(FD_SETSIZE, NULL, &write_fds, NULL, &timeout) < 0)
        ErrorErr(&_inet_error);
    return (FD_ISSET(conn->socket, &write_fds) != 0);
}

void TcpWrite(void *buf, int size, tcp_conn_t conn)
{
    char *cbuf;
    int sent;

#ifdef MemCheck_MEMCHECK
    memmove(buf, buf, size);
#endif
    cbuf = buf;
    sent = -1;
    while(size && sent)
    {
        while((sent = send(conn->socket, cbuf, size, 0)) < 0)
        {
            switch(errno)
            {
                case EWOULDBLOCK:
                    ThreadWait((int (*)(void *))conn_writeable, conn);
                    break;
                default:
                    ErrorErr(&_inet_error);
                    break;
            }
        }
        cbuf += sent;
        size -= sent;
        if(size < 0) Error("Overflow in TCPWrite");
    }
    if(size) Error("Incomplete write to TCP socket");
}

void TcpClose(tcp_conn_t conn)
{
    if(shutdown(conn->socket, 1) != 0)
        ErrorErr(&_inet_error);
}

udp_conn_t UdpCreate(tcp_port_t local)
{
    udp_conn_t conn;
    int on = 1;
    struct sockaddr_in server;

    conn = Malloc(sizeof(*conn));
    conn->peer   = NULL;
    conn->socket = -1;
    ExceptTry
    {
        conn->socket = socket(AF_INET, SOCK_DGRAM, 0);
        if(conn->socket < 0)
            ErrorErr(&_inet_error);

        /* Mark Socket non-blocking */
        if(socketioctl(conn->socket, FIONBIO, &on) < 0)
            ErrorErr(&_inet_error);

        /* First lets bind the socket to the right local port */
        memset(&server, 0, sizeof(server));
        server.sin_addr.s_addr = (unsigned int)htonl(INADDR_ANY);
        server.sin_family = AF_INET;
        server.sin_port = htons((u_short)local);
        if(bind(conn->socket, (struct sockaddr *) &server, sizeof(server)))
            ErrorErr(&_inet_error);
    }
    ExceptCatch
    {
        UdpDestruct(conn);
        ExceptRethrow();
    }
    return conn;
}

void UdpSetPeer(tcp_port_t port, tcp_addr_t addr, udp_conn_t conn)
{
    int on = 1;

    conn->peer = Malloc(sizeof(struct sockaddr_in));
    memset(conn->peer, 0, sizeof(struct sockaddr_in));
    if(addr)
    {
        memcpy((char *)&conn->peer->sin_addr, &addr->addr, addr->length);
        conn->peer->sin_family = addr->family;
    }
    else
    {
        if(setsockopt(conn->socket, SOL_SOCKET, SO_BROADCAST, &on, sizeof(on)) == -1)
            ErrorErr(&_inet_error);
        conn->peer->sin_addr.s_addr = (unsigned int)htonl(INADDR_BROADCAST);
        conn->peer->sin_family = AF_INET;
    }
    conn->peer->sin_port = htons((u_short)port);
}

void UdpDestruct(udp_conn_t conn)
{
    if(conn)
    {
        if(conn->socket != -1)
            socketclose(conn->socket);
        if(conn->peer)
            Free(conn->peer);
        Free(conn);
    }
}

tcp_port_t UdpLocalPort(udp_conn_t conn)
{
    int len;
    struct sockaddr_in name;

    len = sizeof(name);
    if(getsockname(conn->socket, (struct sockaddr *) &name, &len))
        ErrorErr(&_inet_error);
    return htons((u_short)name.sin_port);
}

static int udp_conn_readable(udp_conn_t conn)
{
    static fd_set read_fds;
    struct timeval timeout;

    FD_ZERO(&read_fds);
    FD_SET(conn->socket, &read_fds);
    timeout.tv_sec = 0;
    timeout.tv_usec = 0;
    if(select(FD_SETSIZE, &read_fds, NULL, NULL, &timeout) < 0)
        ErrorErr(&_inet_error);
    return (FD_ISSET(conn->socket, &read_fds) != 0);
}

int UdpRead(void *buf, int size, udp_conn_t conn)
{
    int read;

#ifdef MemCheck_MEMCHECK
    memmove(buf, buf, size);
#endif
    while((read = recv(conn->socket, buf, size, 0)) < 0)
    {
        switch(errno)
        {
            case EWOULDBLOCK:
                ThreadWait((int (*)(void *))udp_conn_readable, conn);
                break;
            default:
                ErrorErr(&_inet_error);
                break;
        }
    }
    return read;
}

static int udp_conn_writeable(udp_conn_t conn)
{
    static fd_set write_fds;
    struct timeval timeout;

    FD_ZERO(&write_fds);
    FD_SET(conn->socket, &write_fds);
    timeout.tv_sec = 0;
    timeout.tv_usec = 0;
    if(select(FD_SETSIZE, NULL, &write_fds, NULL, &timeout) < 0)
        ErrorErr(&_inet_error);
    return (FD_ISSET(conn->socket, &write_fds) != 0);
}

void UdpWrite(void *buf, int size, udp_conn_t conn)
{
#ifdef MemCheck_MEMCHECK
    memmove(buf, buf, size);
#endif
    if(conn->peer == NULL)
        Error("Write to unconnected UDP socket");
    while(sendto(conn->socket, buf, size, 0, (struct sockaddr *) conn->peer, sizeof(struct sockaddr_in)) < 0)
    {
        switch(errno)
        {
            case EWOULDBLOCK:
                ThreadWait((int (*)(void *))udp_conn_writeable, conn);
                break;
            default:
                ErrorErr(&_inet_error);
                break;
        }
    }
}
