// TODO: Break out `run_*` into `*.{c,h}`?
// TODO: Harmonize all reporting strings and comments.
// TODO: Harmonize all conditional and loop statements.
// TODO: Add `listen` and `accept` 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?
// TODO: Create graph of parameter flow.
// TODO: Use Kaj and Börje in readme example.
// TODO: Document idioms:
// - Trim: `buflen -= buflen && buf[buflen - 1] == char;`
// - Format: `buflen += snprintf(&buf[buflen], BUFLEN_MAX - buflen, format, args...);`
// TODO: Document "size" for raw bytes and "len" for "logical" data (e.g.
// excluding string terminating null).
// TODO: Give each client an `id`.
// TODO: Also make the server send a timestamp base on join?
// TODO: `#include <stdbool.h>` and change any relevant `int`s to `bool`s.
// TODO: Change to `run_*(int * sockfd, int sockfdlen, bool nonblock)`.
// TODO: Do different things on `initsockfd` depending on `family`.
// TODO: We can combine IDs and actions: an ID of 0 implies a JOIN action,
// otherwise a MESSAGE action.

/// 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 "arg.h"
#include "report.h"


/// Constants

//// Program

#define PROGNAME "sockchat"

//// 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 " 1.0\n"
#define USAGE \
    "Usage:\n" \
    "  " PROGNAME " (server|client) [options]\n" \
    "  " PROGNAME " -h|--help\n" \
    "  " PROGNAME " --version\n"
#define DESCRIPTION \
    "Chat with unlimited number of peers through a variety of sockets.\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 BUFLEN_MAX _POSIX_MAX_CANON


/// Forward declarations

//// Implementation
int initsockfd(
    char const * init_name,
    int (*init)(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
#define ROLE(ROLE, INIT, FLAGS) { #ROLE, #INIT, INIT, FLAGS, run_##ROLE }
struct role {
    char const * name;
    char const * init_name;
    int (*init)(int sockfd, struct sockaddr const * addr, socklen_t addrlen);
    int flags;
    void (*run)(int sockfd);
};
static struct role roles[] = {
    ROLE(server, bind, AI_PASSIVE),
    ROLE(client, connect, 0),
};

#define CONST_FAMILY(FAMILY) { #FAMILY, AF_##FAMILY }
static struct arg_const families[] = {
    CONST_FAMILY(UNSPEC),
    CONST_FAMILY(INET),
    CONST_FAMILY(INET6),
    CONST_FAMILY(UNIX),
};

#define CONST_SOCKTYPE(SOCKTYPE) { #SOCKTYPE, SOCK_##SOCKTYPE }
static struct arg_const socktypes[] = {
    // { "0", 0 },
    CONST_SOCKTYPE(DGRAM),
    // CONST_SOCKTYPE(SEQPACKET),
    // CONST_SOCKTYPE(STREAM),
    // CONST_SOCKTYPE(RAW),
};

#define CONST_PROTOCOL(PROTOCOL) { #PROTOCOL, IPPROTO_##PROTOCOL }
static struct arg_const protocols[] = {
    { "0", 0 },
    // CONST_PROTOCOL(IP),
    // CONST_PROTOCOL(IPV6),
    // CONST_PROTOCOL(ICMP),
    // CONST_PROTOCOL(TCP),
    // CONST_PROTOCOL(UDP),
    // CONST_PROTOCOL(RAW),
};


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


/// Main

int main(int argc, char * argv[]) {
    // Command line interface
    ARG_SPECIALS()
    ARG_DECLARE(role,     NULL)
    ARG_DECLARE(family,   DEFAULT_FAMILY)
    ARG_DECLARE(socktype, DEFAULT_SOCKTYPE)
    ARG_DECLARE(protocol, DEFAULT_PROTOCOL)
    ARG_DECLARE(node,     DEFAULT_NODE)
    ARG_DECLARE(service,  DEFAULT_SERVICE)
    ARG_POSITIONAL(role)
    ARG_GETOPT_BEGIN("f:t:p:n:s:")
    ARG_GETOPT_WITH_ARGUMENT('f', family)
    ARG_GETOPT_WITH_ARGUMENT('t', socktype)
    ARG_GETOPT_WITH_ARGUMENT('p', protocol)
    ARG_GETOPT_WITH_ARGUMENT('n', node)
    ARG_GETOPT_WITH_ARGUMENT('s', service)
    ARG_GETOPT_END()
    ARG_CONVERT_FIND(role, role, roles)
    ARG_CONVERT_FIND_CONST(family,   families)
    ARG_CONVERT_FIND_CONST(socktype, socktypes)
    ARG_CONVERT_FIND_CONST(protocol, protocols)
    ARG_CONVERT_NULL(node)
    ARG_CONVERT_NULL(service)
    ARG_CATCH()

    // Implementation
    report_info(0, "Using buffer length %d", BUFLEN_MAX);
    role->run(initsockfd(
        role->init_name,
        role->init,
        role->flags,
        family,
        socktype,
        protocol,
        node,
        service
    ));
}


/// Implementation

//// Common

// `NI_MAXHOST` and `NI_MAXSERV` are non-standard.
static char gai_host[1025];
static char gai_serv[32];
static int gai_errno;

#define ACTION_NONE    0
#define ACTION_LEAVE   1
#define ACTION_JOIN    2
#define ACTION_MESSAGE 3
struct action {
    char const * name;
    int value;
};
#define ACTION(ACTION) { #ACTION, ACTION_##ACTION }
static struct action actions[] = {
    ACTION(NONE),
    ACTION(LEAVE),
    ACTION(JOIN),
    ACTION(MESSAGE),
};

// TODO: Call and print results of `getsockname` and `getpeername`?
int initsockfd(
    char const * init_name,
    int (*init)(int sockfd, struct sockaddr const * addr, socklen_t addrlen),
    int flags,
    int family,
    int socktype,
    int protocol,
    char const * node,
    char const * service
) {
    int sockfd;
    // Initialize socket.
    {
        struct addrinfo * addrinfos;
        // Get addresses.
        {
            // The assumption of these flags are non-standard.
            flags |= AI_V4MAPPED | AI_ADDRCONFIG;
            struct addrinfo hints = {
                flags, family, socktype, protocol, 0, 0, 0, 0
            };
            if (0 != (gai_errno = getaddrinfo(
                node, service, &hints, &addrinfos
            )))
                report_fatal(
                    gai_errno == EAI_SYSTEM ? errno : 0,
                    "Failed to get %s addresses for '%s:%s': %s",
                    init_name,
                    node    ? node    : "",
                    service ? service : "",
                    gai_strerror(gai_errno)
                );
        }
        // Try init on addresses until one works.
        struct addrinfo * addrinfo;
        for (
            addrinfo = addrinfos;
            addrinfo != NULL;
            addrinfo = addrinfo->ai_next
        ) {
            // Get address name.
            if (0 != (gai_errno = getnameinfo(
                addrinfo->ai_addr,
                addrinfo->ai_addrlen,
                gai_host,
                sizeof(gai_host),
                gai_serv,
                sizeof(gai_serv),
                NI_NUMERICHOST | NI_NUMERICSERV
            ))) {
                report_error(
                    gai_errno == EAI_SYSTEM ? errno : 0,
                    "Failed to get %s address name: %s",
                    init_name,
                    gai_strerror(gai_errno)
                );
                continue;
            }
            report_info(0, "Trying to %s to '%s:%s'...",
                init_name, gai_host, gai_serv
            );
            // Create socket.
            if (-1 == (sockfd = socket(
                addrinfo->ai_family,
                addrinfo->ai_socktype,
                addrinfo->ai_protocol
            ))) {
                report_error(errno, "Failed to create %s socket", init_name);
                continue;
            }
            // Perform init.
            if (-1 == init(
                sockfd,
                addrinfo->ai_addr,
                addrinfo->ai_addrlen
            )) {
                report_error(errno, "Failed to %s to '%s:%s'",
                    init_name, gai_host, gai_serv
                );
                close(sockfd);
                continue;
            }
            // Succeeded.
            report_info(0, "Succeeded to %s to '%s:%s'",
                init_name, gai_host, gai_serv
            );
            break;
        }
        // Fail.
        if (NULL == addrinfo)
            report_fatal(0, "Failed to %s", init_name);
        // Clean up.
        freeaddrinfo(addrinfos);
    }
    return sockfd;
}


//// Server

// TODO: Also store `addlen` in `client` (and use in `memcmp`)?
void run_server(int sockfd) {
    // Client data.
    struct client_data {
        char * user_name;
    };

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

    // Allocate socket buffer.
    ssize_t sockbuflen;
    char * sockbuf = malloc(BUFLEN_MAX + 1);
    if (!sockbuf)
        report_fatal(errno, "Failed to allocate %s of size %d",
            "input buffer", BUFLEN_MAX + 1
        );

    // Talk to clients.
    while (1) {
        // Get client and action.
        // `NI_MAXHOST` and `NI_MAXSERV` are non-standard.
        struct sockaddr_storage addr;
        struct client * client;
        struct action action;
        {
            // Peek receive.
            ssize_t recvlen;
            socklen_t addrlen = sizeof(addr);
            if (-1 == (recvlen = recvfrom(
                sockfd,
                // TODO
                sockbuf,
                BUFLEN_MAX,
                // NULL,
                // 0,
                MSG_PEEK, // TODO: Also specify `MSG_TRUNC` so that we don't have to give the buffer?
                (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, max size is %d",
                    addrlen, sizeof(addr)
                );
            // Get client address name.
            if (0 != (gai_errno = getnameinfo(
                (struct sockaddr *)&addr,
                addrlen,
                gai_host,
                sizeof(gai_host),
                gai_serv,
                sizeof(gai_serv),
                NI_NUMERICHOST | NI_NUMERICSERV
            )))
                report_fatal(
                    gai_errno == EAI_SYSTEM ? errno : 0,
                    "Failed to get client address name: %s",
                    gai_strerror(gai_errno)
                );
            // Look up client.
            for (
                client = clients;
                client != NULL;
                client = client->next
            )
                if (0 == memcmp(&client->addr, &addr, sizeof(addr)))
                    break;
            // Look up action.
            action = actions[!!recvlen<<1 | !!client<<0];
            // Report.
            report_info(0, "Received %d byte%s from '%s:%s' %s",
                recvlen,
                recvlen == 1 ? "" : "s",
                gai_host,
                gai_serv,
                action.name
            );
        }
        // No action.
        if (action.value == ACTION_NONE) {
            // Receive none.
            if (-1 == recv(sockfd, NULL, 0, 0))
                report_fatal(errno, "Failed to receive %s", action.name);
            continue;
        }
        // Client left.
        else if (action.value == ACTION_LEAVE) {
            // Receive leave.
            if (-1 == recv(sockfd, NULL, 0, 0))
                report_fatal(errno, "Failed to receive %s", action.name);
            // Client data.
            struct client_data * client_data = client->data;
            free(client_data->user_name);
            free(client_data);
            // Client accounting.
            remque(client);
            free(client);
            // Done.
            continue;
        }
        // Client joined.
        else if (action.value == ACTION_JOIN) {
            sockbuflen = 0;
            // Receive.
            {
                ssize_t recvlen;
                if (-1 == (recvlen = recv(
                    sockfd, sockbuf, BUFLEN_MAX - sockbuflen, 0
                )))
                    report_fatal(errno, "Failed to receive %s", action.name);
                sockbuflen += recvlen;
            }
            // Validate.
            if (!sockbuflen || sockbuf[sockbuflen - 1] != '\0') {
                report_error(0, "Malformed %s", action.name);
            }
            // Client data.
            struct client_data * client_data;
            if (NULL == (client_data = malloc(sizeof(*client_data))))
                report_fatal(errno, "Failed to allocate %s of size %d",
                    "client data", sizeof(*client_data)
                );
            if (NULL == (client_data->user_name = strdup(sockbuf)))
                report_fatal(errno, "Failed to allocate %s",
                    "client user name"
                );
            // Client accounting.
            if (NULL == (client = calloc(1, sizeof(*client))))
                report_fatal(errno, "Failed allocate %s of size %d",
                    "client", sizeof(*client)
                );
            insque(client, &clients);
            client->addr = addr;
            client->data = client_data;
            // Done.
            continue;
        }
        // Client sent message.
        else if (action.value == ACTION_MESSAGE) {
            sockbuflen = 0;
            // Client data.
            struct client_data * client_data = client->data;
            // TODO: `snprintf` returns how many bytes *would* have been
            // written! Use `strlen`?
            sockbuflen += snprintf(
                &sockbuf[sockbuflen], BUFLEN_MAX - sockbuflen, "[%s:%s] %s: ",
                gai_host, gai_serv, client_data->user_name
            );
            // Receive message.
            {
                ssize_t recvlen;
                if (-1 == (recvlen = recv(
                    sockfd, &sockbuf[sockbuflen], BUFLEN_MAX - sockbuflen, 0
                )))
                    report_fatal(errno, "Failed to receive %s", action.name);
                sockbuflen += recvlen;
            }
            sockbuflen -= sockbuflen && sockbuf[sockbuflen - 1] == '\0';
            // Finalize buffer.
            sockbuf[sockbuflen++] = '\0';
            // Send message.
            for (
                client = clients;
                client != NULL;
                client = client->next
            ) {
                if (-1 == sendto(
                    sockfd,
                    sockbuf,
                    sockbuflen,
                    0,
                    (struct sockaddr *)&client->addr,
                    sizeof(client->addr)
                ))
                    report_fatal(errno, "Failed to send %s", action.name);
            }
        } else {
            report_fatal(0, "Unexpected action %d", action.value);
        }
    }
}


//// Client

static struct termios termios_stdin;
static void termios_restore() {
    if (-1 == tcsetattr(STDIN_FILENO, TCSANOW, &termios_stdin))
        report_fatal(errno, "Failed to restore terminal settings");
}
static void termios_setup() {
    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");
    }
    atexit(termios_restore);
}

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

    // Allocate input buffer.
    ssize_t inputbuflen;
    char * inputbuf = malloc(BUFLEN_MAX + 1);
    if (!inputbuf)
        report_fatal(errno, "Failed to allocate %s of size %d",
            "socket buffer", BUFLEN_MAX + 1
        );

    // Join.
    {
        struct action action = actions[ACTION_JOIN];
        // Get user name.
        printf("User name: ");
        fflush(stdout);
        if (-1 == (inputbuflen = read(STDIN_FILENO, inputbuf, BUFLEN_MAX)))
            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 %s", action.name);
        // if (-1 == recv(sockfd, sockbuf, BUFLEN_MAX, MSG_PEEK))
        //     report_fatal(errno, "Failed to receive %s", action.name);
    }

    // Setup terminal.
    termios_setup();

    // 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", (int)inputbuflen, "");
        // Socket data available.
        if (FD_ISSET(sockfd, &rfds)) {
            // Receive.
            if (-1 == (sockbuflen = recv(sockfd, sockbuf, BUFLEN_MAX, 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");
            // Printable with non-full buffer.
            if (isprint(c) && inputbuflen < BUFLEN_MAX) {
                // 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.
                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.
        // TODO: Have we not "finalized" the buffer here?
        printf("%.*s", (int)inputbuflen, inputbuf);
        fflush(stdout);
    }
}
