// TODO: Add `listen` and `accept` for if the socket has a `STREAM` `socktype`.
// TODO: Compare to the example in `getaddrinfo(3)`.
// TODO: Mention `getaddrinfo` and the RFC 3484 sorting order in readme.
// TODO: Change client store from queue to binary tree?

/// Headers

//// POSIX 2004
#define _XOPEN_SOURCE 600

//// C standard library
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <ctype.h>
#include <limits.h>

//// POSIX
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/select.h>
#include <netdb.h>
#include <search.h>
#include <termios.h>

// Other
#include "args.h"
#include "report.h"


/// Parameters

//// Application
#define PROGNAME "sockchat"
#define VERSION_STR "1.0"
#define DESCRIPTION \
    "Chat with unlimited number of peers through a variety of sockets"

//// Arguments
// TODO
// #define DEFAULT_FAMILY   "UNSPEC"
// #define DEFAULT_SOCKTYPE "0"
#define DEFAULT_FAMILY   "INET"
#define DEFAULT_SOCKTYPE "DGRAM"
#define DEFAULT_PROTOCOL "0"
#define DEFAULT_NODE     ""
#define DEFAULT_SERVICE  "3200"

//// Command line interface
#define VERSION \
    PROGNAME " " VERSION_STR " - " DESCRIPTION "\n"
#define USAGE \
    "Usage:\n" \
    "  " PROGNAME " (server|client) [options]\n" \
    "  " PROGNAME " -h|--help\n" \
    "  " PROGNAME " --version\n"
#define OPTIONS \
    "Options:\n" \
    "  -f <family>    [default: " DEFAULT_FAMILY   "]\n" \
    "  -t <socktype>  [default: " DEFAULT_SOCKTYPE "]\n" \
    "  -p <protocol>  [default: " DEFAULT_PROTOCOL "]\n" \
    "  -n <node>      [default: " DEFAULT_NODE     "]\n" \
    "  -s <service>   [default: " DEFAULT_SERVICE  "]\n"

//// Implementation
#define BUF_SIZE _POSIX_MAX_CANON


/// Forward declarations

//// Implementation
int getsockfd(
    char const * action_name,
    int (*action)(int sockfd, struct sockaddr const * addr, socklen_t addrlen),
    int flags,
    int family,
    int socktype,
    int protocol,
    char const * node,
    char const * service
);
void run_server(int sockfd);
void run_client(int sockfd);


/// Data

//// Arguments
struct role {
    char const * name;
    char const * action_name;
    int (*action)(int sockfd, struct sockaddr const * addr, socklen_t addrlen);
    int flags;
    void (*run)(int sockfd);
};
struct option {
    char const * name;
    int value;
};
#define ROLE(NAME, ACTION, FLAGS) { #NAME, #ACTION, ACTION, FLAGS, run_##NAME }
static struct role roles[] = {
    ROLE(server, bind, AI_PASSIVE),
    ROLE(client, connect, 0),
};
#define OPTION_FAMILY(FAMILY) { #FAMILY, AF_##FAMILY }
static struct option families[] = {
    OPTION_FAMILY(UNSPEC),
    OPTION_FAMILY(UNIX),
    OPTION_FAMILY(LOCAL),
    OPTION_FAMILY(INET),
    OPTION_FAMILY(INET6),
    OPTION_FAMILY(PACKET),
};
#define OPTION_SOCKTYPE(SOCKTYPE) { #SOCKTYPE, SOCK_##SOCKTYPE }
static struct option socktypes[] = {
    { "0", 0 },
    OPTION_SOCKTYPE(STREAM),
    OPTION_SOCKTYPE(DGRAM),
    OPTION_SOCKTYPE(SEQPACKET),
    OPTION_SOCKTYPE(RAW),
    OPTION_SOCKTYPE(RDM),
    OPTION_SOCKTYPE(PACKET),
};
#define OPTION_PROTOCOL(PROTOCOL) { #PROTOCOL, IPPROTO_##PROTOCOL }
static struct option protocols[] = {
    { "0", 0 },
    OPTION_PROTOCOL(IP),
    OPTION_PROTOCOL(TCP),
    OPTION_PROTOCOL(UDP),
    OPTION_PROTOCOL(UDPLITE),
    OPTION_PROTOCOL(SCTP),
    OPTION_PROTOCOL(ICMP),
};


/// Command line interface
void version(FILE * stream) { fprintf(stream, "%s", VERSION); }
void usage  (FILE * stream) { fprintf(stream, "%s", USAGE);   }
void options(FILE * stream) { fprintf(stream, "%s", OPTIONS); }
void args   (FILE * stream) {
    ARGS_PRINT(stream, family,   families ); fprintf(stream, "\n");
    ARGS_PRINT(stream, socktype, socktypes); fprintf(stream, "\n");
    ARGS_PRINT(stream, protocol, protocols);
}
void help(FILE * stream) {
    version(stream); fprintf(stream, "\n");
    usage  (stream); fprintf(stream, "\n");
    options(stream); fprintf(stream, "\n");
    args   (stream);
}


/// main
int main(int argc, char * argv[]) {
    // Command line interface
    ARGS_SPECIALS()
    ARGS_DECLARE(role,     NULL)
    ARGS_DECLARE(family,   DEFAULT_FAMILY)
    ARGS_DECLARE(socktype, DEFAULT_SOCKTYPE)
    ARGS_DECLARE(protocol, DEFAULT_PROTOCOL)
    ARGS_DECLARE(node,     DEFAULT_NODE)
    ARGS_DECLARE(service,  DEFAULT_SERVICE)
    ARGS_POSITIONAL(role)
    ARGS_OPTIONS_BEGIN("f:t:p:n:s:")
    ARGS_OPTION_WITH_ARGUMENT('f', family)
    ARGS_OPTION_WITH_ARGUMENT('t', socktype)
    ARGS_OPTION_WITH_ARGUMENT('p', protocol)
    ARGS_OPTION_WITH_ARGUMENT('n', node)
    ARGS_OPTION_WITH_ARGUMENT('s', service)
    ARGS_OPTIONS_END()
    ARGS_CONVERT_FIND(role,   role,     roles)
    ARGS_CONVERT_FIND(option, family,   families)
    ARGS_CONVERT_FIND(option, socktype, socktypes)
    ARGS_CONVERT_FIND(option, protocol, protocols)
    ARGS_CONVERT_NULL(node)
    ARGS_CONVERT_NULL(service)
    ARGS_CATCH()

    // Implementation
    report_info(0, "Using buffer size %d", BUF_SIZE);
    role->run(getsockfd(
        role->action_name,
        role->action,
        role->flags,
        family->value,
        socktype->value,
        protocol->value,
        node,
        service
    ));
}


/// Implementation

//// getsockfd
int getsockfd(
    char const * action_name,
    int (*action)(int sockfd, struct sockaddr const * addr, socklen_t addrlen),
    int flags,
    int family,
    int socktype,
    int protocol,
    char const * node,
    char const * service
) {
    int gai_errno;
    int sockfd;
    {
        struct addrinfo * addrinfos;
        // Get addresses.
        {
            // Populate hints.
            // The extra flags are assumed by the GNU C library if no hints are
            // given (in contradiction to POSIX), and are a good idea.
            struct addrinfo hints;
            memset(&hints, 0, sizeof(hints));
            hints.ai_flags = flags | AI_V4MAPPED | AI_ADDRCONFIG;
            hints.ai_family = family;
            hints.ai_socktype = socktype;
            hints.ai_protocol = protocol;
            // Query.
            if (0 != (gai_errno = getaddrinfo(
                node, service, &hints, &addrinfos
            )))
                report_fatal(gai_errno == EAI_SYSTEM ? errno : 0,
                    "Failed to get addresses for '%s:%s': %s",
                    node, service, gai_strerror(gai_errno)
                );
        }
        // Try action on addresses until one works.
        report_info(0, "Trying to %s...", action_name);
        struct addrinfo * addrinfo;
        for (
            addrinfo = addrinfos;
            addrinfo != NULL;
            addrinfo = addrinfo->ai_next
        ) {
            // Create socket.
            if (-1 == (sockfd = socket(
                addrinfo->ai_family,
                addrinfo->ai_socktype,
                addrinfo->ai_protocol
            ))) {
                report_info(errno, "> Failed to create socket");
                continue;
            }
            // Perform action.
            if (-1 == action(
                sockfd,
                addrinfo->ai_addr,
                addrinfo->ai_addrlen
            )) {
                close(sockfd);
                report_info(errno, "> Failed to %s", action_name);
                continue;
            }
            // Succeeded.
            report_info(0, "Succeeded to %s", action_name);
            break;
        }
        if (NULL == addrinfo)
            report_fatal(0, "Failed to get socket");
        // Clean up.
        freeaddrinfo(addrinfos);
    }
    return sockfd;
}


//// run_server
void run_server(int sockfd) {
    int gai_errno;

    // Client data.
    struct client_data {
        char * name;
    };

    // Client accounting.
    struct client {
        struct client * next;
        struct client * prev;
        struct sockaddr_storage addr;
        struct client_data data;
    };
    struct client * clients = NULL;

    // Allocate buffer.
    int buflen;
    char * buf = malloc(BUF_SIZE);
    if (!buf)
        report_fatal(errno, "Failed to allocate buffer of size %d", BUF_SIZE);

    // Talk to clients.
    while (1) {
        // Peek receive length and address.
        int recvlen;
        struct sockaddr_storage addr;
        socklen_t addrlen = sizeof(addr);
        if (-1 == (recvlen = recvfrom(
            sockfd,
            buf,
            BUF_SIZE - 1,
            MSG_PEEK,
            (struct sockaddr *)&addr,
            &addrlen
        )))
            report_fatal(errno, "Failed to peek receive");
        // Check for too large addresses (guaranteed not to happen).
        if (addrlen > sizeof(addr))
            report_fatal(0,
                "Failed to store address of size %d, can only hold %d",
                addrlen, sizeof(addr)
            );
        // Look up client.
        struct client * client;
        for (
            client = clients;
            client != NULL;
            client = client->next
        )
            if (0 == memcmp(&client->addr, &addr, sizeof(addr)))
                break;
        // Client left.
        if (!recvlen) {
            if (-1 == recv(sockfd, NULL, 0, 0))
                report_fatal(errno, "Failed to receive leave");
            if (client) {
                // Data.
                report_info(0, "Client '%s' left", client->data.name);
                free(client->data.name);
                // Accounting.
                remque(client);
                free(client);
            }
            continue;
        }
        // Client joined.
        if (!client) {
            // Accounting.
            if (NULL == (client = calloc(1, sizeof(*client))))
                report_fatal(errno, "Failed allocate client of size %d",
                    sizeof(*client)
                );
            insque(client, &clients);
            client->addr = addr;
            // Data.
            buflen = 0;
            // Add open delimiter.
            buflen += snprintf(&buf[buflen], BUF_SIZE - buflen - 1, "[");
            // Get client address node name.
            if (0 != (gai_errno = getnameinfo(
                (struct sockaddr *)&addr,
                addrlen,
                &buf[buflen],
                BUF_SIZE - buflen - 1,
                NULL,
                0,
                0
            )))
                report_error(gai_errno == EAI_SYSTEM ? errno : 0,
                    "Failed to get client address node name: %s",
                    gai_strerror(gai_errno)
                );
            buflen += strlen(&buf[buflen]);
            // Add separator.
            buflen += snprintf(&buf[buflen], BUF_SIZE - buflen - 1, ":");
            // Get client address service name.
            if (0 != (gai_errno = getnameinfo(
                (struct sockaddr *)&addr,
                addrlen,
                NULL,
                0,
                &buf[buflen],
                BUF_SIZE - buflen - 1,
                0
            )))
                report_error(gai_errno == EAI_SYSTEM ? errno : 0,
                    "Failed to get client address service name: %s",
                    gai_strerror(gai_errno)
                );
            buflen += strlen(&buf[buflen]);
            // Add close delimiter.
            buflen += snprintf(&buf[buflen], BUF_SIZE - buflen - 1, "] ");
            // Get client user name.
            if (-1 == (recvlen = recv(
                sockfd, &buf[buflen], BUF_SIZE - buflen - 1, 0
            )))
                report_fatal(errno, "Failed to receive client user name");
            buflen += recvlen;
            buflen -= buflen && buf[buflen - 1] == '\0';
            // Store.
            buf[buflen++] = '\0';
            client->data.name = strdup(buf);
            report_info(0, "Client '%s' joined", client->data.name);
            continue;
        }
        // Client sent message.
        report_info(0, "Client '%s' sent %d byte%s",
            client->data.name, recvlen, recvlen == 1 ? "" : "s"
        );
        buflen = 0;
        // Add client user name and separator.
        buflen += snprintf(
            &buf[buflen], BUF_SIZE - buflen - 1, "%s: ", client->data.name
        );
        // Add message.
        if (-1 == (recvlen = recv(
            sockfd, &buf[buflen], BUF_SIZE - buflen - 1, 0
        )))
            report_fatal(errno, "Failed to receive");
        buflen += recvlen;
        buflen -= buflen && buf[buflen - 1] == '\0';
        // Send message to all clients.
        buf[buflen++] = '\0';
        for (
            client = clients;
            client != NULL;
            client = client->next
        ) {
            if (-1 == sendto(
                sockfd,
                buf,
                buflen,
                0,
                (struct sockaddr *)&client->addr,
                sizeof(client->addr)
            ))
                report_fatal(errno, "Failed to send");
        }
    }
}


//// run_client
void run_client(int sockfd) {
    // Allocate socket buffer.
    int sockbuflen;
    char * sockbuf = malloc(BUF_SIZE);
    if (!sockbuf)
        report_fatal(errno, "Failed to allocate socket buffer of size %d",
            BUF_SIZE
        );

    // Allocate input buffer.
    int inputbuflen;
    char * inputbuf = malloc(BUF_SIZE);
    if (!inputbuf)
        report_fatal(errno, "Failed to allocate input buffer of size %d",
            BUF_SIZE
        );

    // Join.
    // Get user name.
    printf("User name: ");
    fflush(stdout);
    if (-1 == (inputbuflen = read(STDIN_FILENO, inputbuf, BUF_SIZE - 1)))
        report_fatal(errno, "Failed to read user name");
    inputbuflen -= inputbuflen && inputbuf[inputbuflen - 1] == '\n';
    // Send join.
    inputbuf[inputbuflen++] = '\0';
    if (-1 == send(sockfd, inputbuf, inputbuflen, 0))
        report_fatal(errno, "Failed to send join");

    // Setup terminal.
    struct termios termios_stdin;
    if (-1 == tcgetattr(STDIN_FILENO, &termios_stdin))
        report_fatal(errno, "Failed to get terminal settings");
    {
        struct termios termios = termios_stdin;
        termios.c_lflag &= ~(ICANON | ECHO);
        if (-1 == tcsetattr(STDIN_FILENO, TCSANOW, &termios))
            report_fatal(errno, "Failed to set terminal settings");
    }

    // Talk to user and server.
    inputbuflen = 0;
    while (1) {
        // Wait for data to be available from input or socket.
        fd_set rfds;
        FD_ZERO(&rfds);
        FD_SET(STDIN_FILENO, &rfds);
        FD_SET(sockfd, &rfds);
        if (-1 == select(sockfd+1, &rfds, NULL, NULL, NULL)) {
            printf("\n");
            report_fatal(errno, "Failed to wait for data");
        }
        // Clear input line.
        printf("\r%*s\r", inputbuflen, "");
        // Socket data available.
        if (FD_ISSET(sockfd, &rfds)) {
            // Receive.
            if (-1 == (sockbuflen = recv(sockfd, sockbuf, BUF_SIZE - 1, 0)))
                report_fatal(errno, "Failed to receive");
            sockbuf[sockbuflen++] = '\0';
            // Print socket line.
            printf("%s\n", sockbuf);
        }
        // Input data available.
        if (FD_ISSET(STDIN_FILENO, &rfds)) {
            // Read.
            char c;
            if (-1 == read(STDIN_FILENO, &c, 1))
                report_fatal(errno, "Failed to read input");
            // Printable with non-full buffer.
            if (isprint(c) && inputbuflen < BUF_SIZE - 1) {
                // Add.
                inputbuf[inputbuflen++] = c;
            // Backspace.
            } else if (c == '\b' || c == 127) {
                // Remove last character from buffer.
                if (inputbuflen)
                    --inputbuflen;
            // Enter.
            } else if (c == '\n' || c == '\r') {
                // Send.
                if (inputbuflen)
                    inputbuf[inputbuflen++] = '\0';
                if (-1 == send(sockfd, inputbuf, inputbuflen, 0))
                    report_fatal(errno, "Failed to send");
                // Quit if input line empty.
                if (!inputbuflen)
                    break;
                // Reset input line.
                inputbuflen = 0;
            }
        }
        // Print input line.
        printf("%.*s", inputbuflen, inputbuf);
        fflush(stdout);
    }

    // Restore terminal.
    if (-1 == tcsetattr(STDIN_FILENO, TCSANOW, &termios_stdin))
        report_fatal(errno, "Failed to restore terminal settings");
}