/*
 *  WINGs server.c: example how to create a network server using WMConnection
 *
 *  Copyright (c) 2001-2002 Dan Pascu
 *
 */


#include <stdio.h>
#include <unistd.h>
#include <string.h>

#include <WINGs/WINGs.h>


#define _(P)         P
#define MAXCMD_SIZE  512


char *SEConnectionShouldBeRemovedNotification = "SEConnectionShouldBeRemovedNotification";




static void didReceiveInput(ConnectionDelegate *self, WMConnection *cPtr);

static void connectionDidDie(ConnectionDelegate *self, WMConnection *cPtr);

static void connectionDidTimeout(ConnectionDelegate *self, WMConnection *cPtr);


extern char *SEConnectionShouldBeRemovedNotification;

static WMUserDefaults *timeDB = NULL;
static char *ServerAddress = NULL;
static char *ServerPort = NULL;
static WMArray *allowedHostList = NULL;
static WMArray *clientConnections = NULL;
static WMConnection *serverPtr = NULL;



static ConnectionDelegate socketDelegate = {
    NULL,                 /* client data */
    NULL,                 /* didCatchException */
    connectionDidDie,     /* didDie */
    NULL,                 /* didInitialize */
    didReceiveInput,      /* didReceiveInput */
    connectionDidTimeout  /* didTimeout */
};



void
wAbort(Bool foo)
{
    exit(1);
}


static void
printHelp(char *progname)
{
    printf(_("usage: %s [options]\n\n"), progname);
    puts(_(" --help				print this message"));
    puts(_(" --listen [address:]port	only listen on the specified address/port"));
    puts(_(" --allow host1[,host2...]	only allow connections from listed hosts\n"));
    puts(_(" By default server listens on all interfaces and port 34567, unless"
           " something\nelse is specified with the --listen option. If address is"
           " omitted or the keyword\n'Any' is used, it will listen on all interfaces else"
           " only on the specified one.\n\nFor example --listen localhost: will"
           " listen on the default port 34567, but only\non connections comming"
           " in through the loopback interface.\n\n Also by default the server"
           " listens to incoming connections from any host,\nunless a list of"
           " hosts is given with the --allow option, in which case it will\nreject"
           " connections not comming from those hosts.\nThe list of hosts is comma"
           " separated and should NOT contain ANY spaces."));
}


static void
enqueueConnectionForRemoval(WMConnection *cPtr)
{
    WMNotification *notif;

    /*don't release notif here. it will be released by queue after processing */
    notif = WMCreateNotification(SEConnectionShouldBeRemovedNotification,
                                 cPtr, NULL);
    WMEnqueueNotification(WMGetDefaultNotificationQueue(), notif, WMPostASAP);
}


static int
sendMessage(WMConnection *cPtr, char *message)
{
    WMData *aData;
    int res;

    if (WMGetConnectionState(cPtr)!=WCConnected)
        return -1;

    aData = WMCreateDataWithBytes(message, strlen(message));
    res = WMSendConnectionData(cPtr, aData);
    WMReleaseData(aData);

    return res;
}


static Bool
enqueueMessage(WMConnection *cPtr, char *message)
{
    WMData *aData;
    Bool res;

    if (WMGetConnectionState(cPtr)!=WCConnected)
        return False;

    aData = WMCreateDataWithBytes(message, strlen(message));
    res = WMEnqueueConnectionData(cPtr, aData);
    WMReleaseData(aData);

    return res;
}


static unsigned char*
findDelimiter(unsigned char *data, unsigned const char *endPtr)
{
    wassertrv(data < endPtr, NULL);

    while (data<endPtr && *data!='\n' && *data!='\r' && *data!=';' && *data!='\0')
        data++;

    if (data < endPtr)
        return data;

    return NULL;
}


static WMArray*
getAvailableMessages(WMConnection *cPtr)
{
    char *ptr, *crtPos, *buffer;
    const char *bytes, *endPtr;
    WMData *aData, *receivedData, *holdData;
    WMRange range;
    WMArray *messages;
    int length;

    receivedData = WMGetConnectionAvailableData(cPtr);
    if (!receivedData)
        return NULL;
    if ((length=WMGetDataLength(receivedData))==0) {
        WMReleaseData(receivedData);
        return NULL;
    }

    holdData = (WMData*)WMGetConnectionClientData(cPtr);
    if (holdData) {
        WMAppendData(holdData, receivedData);
        WMReleaseData(receivedData);
        WMSetConnectionClientData(cPtr, NULL);
        aData = holdData;
    } else {
        aData = receivedData;
    }

    length = WMGetDataLength(aData);
    bytes = (char*)WMDataBytes(aData);
    endPtr = bytes + length;

    messages = WMCreateArrayWithDestructor(1, wfree);
    crtPos = (char*)bytes;
    while (crtPos<endPtr && (ptr = findDelimiter(crtPos, endPtr))!=NULL) {
        range.position = (crtPos - bytes);
        range.count = (ptr - crtPos);
        if (range.count > MAXCMD_SIZE) {
            /* Hmmm... The message is too long. Possibly that someone is
             * flooding us, or there is a dumb client which do not know
             * who is talking to. */
            sendMessage(cPtr, "Command too long\n\r");
            WMFreeArray(messages);
            WMReleaseData(aData);
            WMCloseConnection(cPtr);
            enqueueConnectionForRemoval(cPtr);
            return NULL;
        }
        buffer = wmalloc(range.count+1);
        WMGetDataBytesWithRange(aData, buffer, range);
        buffer[range.count] = '\0';
        WMAddToArray(messages, buffer);
        crtPos = ptr;
        while (crtPos<endPtr && (*crtPos=='\n' || *crtPos=='\r' ||
                                 *crtPos=='\t' || *crtPos=='\0' ||
                                 *crtPos==';' || *crtPos==' ')) {
            crtPos++;
        }
    }

    if (crtPos<endPtr) {
        range.position = (crtPos - bytes);
        range.count = (endPtr - crtPos);
        if (range.count > MAXCMD_SIZE) {
            /* Flooooooding!!!! */
            sendMessage(cPtr, "Message too long\n\r");
            WMFreeArray(messages);
            WMReleaseData(aData);
            WMCloseConnection(cPtr);
            enqueueConnectionForRemoval(cPtr);
            return NULL;
        }
        holdData = WMGetSubdataWithRange(aData, range);
        WMSetConnectionClientData(cPtr, holdData);
    }
    WMReleaseData(aData);

    if (WMGetArrayItemCount(messages)==0) {
        WMFreeArray(messages);
        messages = NULL;
    }
    return messages;
}



static void
complainAboutBadArgs(WMConnection *cPtr, char *cmdName, char *badArgs)
{
    char *buf = wmalloc(strlen(cmdName) + strlen(badArgs) + 100);

    sprintf(buf, _("Invalid parameters '%s' for command %s. Use HELP for"
                   " a list of commands.\n"), badArgs, cmdName);
    sendMessage(cPtr, buf);
    wfree(buf);
}


static void
sendUpdateMessage(WMConnection *cPtr, char *id, int time)
{
    char *buf = wmalloc(strlen(id) + 100);

    sprintf(buf, "%s has %i minutes left\n", id, time);
    sendMessage(cPtr, buf);
    wfree(buf);
}


static void
showId(WMConnection *cPtr)
{
    sendMessage(cPtr, "Server example based on WMConnection\n");
}


static void
showHelp(WMConnection *cPtr)
{
    char *buf = wmalloc(strlen(WMGetApplicationName()) + 16);

    sprintf(buf, _("%s commands:\n\n"), WMGetApplicationName());

    enqueueMessage(cPtr, _("\n"));
    enqueueMessage(cPtr, buf);
    enqueueMessage(cPtr, _("GET <id>\t- return time left (in minutes) "
                           "for user with id <id>\n"));
    enqueueMessage(cPtr, _("SET <id> <time>\t- set time limit to <time> "
                           "minutes for user with id <id>\n"));
    enqueueMessage(cPtr, _("ADD <id> <time>\t- add <time> minutes "
                           "for user with id <id>\n"));
    enqueueMessage(cPtr, _("SUB <id> <time>\t- subtract <time> minutes "
                           "for user with id <id>\n"));
    enqueueMessage(cPtr, _("REMOVE <id>\t- remove time limitations for "
                           "user with id <id>\n"));
    enqueueMessage(cPtr, _("LIST\t\t- list all users and their "
                           "corresponding time limit\n"));
    enqueueMessage(cPtr, _("ID\t\t- returns the Time Manager "
                           "identification string\n"));
    enqueueMessage(cPtr, _("EXIT\t\t- exits session\n"));
    enqueueMessage(cPtr, _("QUIT\t\t- exits session\n"));
    enqueueMessage(cPtr, _("HELP\t\t- show this message\n\n"));
    /* Just flush the queue we made before */
    WMFlushConnection(cPtr);
    wfree(buf);
}


static void
listUsers(WMConnection *cPtr)
{
    WMPropList *userList;
    char *id;
    int i, time;

    userList = WMGetUDKeys(timeDB);

    for (i=0; i<WMGetPropListItemCount(userList); i++) {
        id = WMGetFromPLString(WMGetFromPLArray(userList, i));
        time = WMGetUDIntegerForKey(timeDB, id);
        sendUpdateMessage(cPtr, id, time);
    }

    WMReleasePropList(userList);
}


static void
setTimeForUser(WMConnection *cPtr, char *cmdArgs)
{
    char *id;
    int i, time;

    id = wmalloc(strlen(cmdArgs));
    if (sscanf(cmdArgs, "%s %d", id, &time)!=2) {
        complainAboutBadArgs(cPtr, "SET", cmdArgs);
        wfree(id);
        return;
    }
    if (time<0)
        time = 0;

    WMSetUDIntegerForKey(timeDB, time, id);

    for (i=0; i<WMGetArrayItemCount(clientConnections); i++) {
        cPtr = WMGetFromArray(clientConnections, i);
        sendUpdateMessage(cPtr, id, time);
    }
    wfree(id);
}


static void
addTimeToUser(WMConnection *cPtr, char *cmdArgs)
{
    char *id;
    int i, time, newTime;

    id = wmalloc(strlen(cmdArgs));
    if (sscanf(cmdArgs, "%s %d", id, &time)!=2) {
        complainAboutBadArgs(cPtr, "ADD", cmdArgs);
        wfree(id);
        return;
    }

    newTime = WMGetUDIntegerForKey(timeDB, id) + time;
    if (newTime<0)
        newTime = 0;

    WMSetUDIntegerForKey(timeDB, newTime, id);

    for (i=0; i<WMGetArrayItemCount(clientConnections); i++) {
        cPtr = WMGetFromArray(clientConnections, i);
        sendUpdateMessage(cPtr, id, newTime);
    }
    wfree(id);
}


static void
subTimeFromUser(WMConnection *cPtr, char *cmdArgs)
{
    char *id;
    int i, time, newTime;

    id = wmalloc(strlen(cmdArgs));
    if (sscanf(cmdArgs, "%s %d", id, &time)!=2) {
        complainAboutBadArgs(cPtr, "SUB", cmdArgs);
        wfree(id);
        return;
    }

    newTime = WMGetUDIntegerForKey(timeDB, id) - time;
    if (newTime<0)
        newTime = 0;

    WMSetUDIntegerForKey(timeDB, newTime, id);

    for (i=0; i<WMGetArrayItemCount(clientConnections); i++) {
        cPtr = WMGetFromArray(clientConnections, i);
        sendUpdateMessage(cPtr, id, newTime);
    }
    wfree(id);
}


static void
removeTimeForUser(WMConnection *cPtr, char *cmdArgs)
{
    char *ptr;
    int i;

    if (cmdArgs[0]=='\0') {
        sendMessage(cPtr, _("Missing parameter for command REMOVE."
                            " Use HELP for a list of commands.\n"));
        return;
    }

    ptr = cmdArgs;
    while (*ptr && *ptr!=' ' && *ptr!='\t')
        ptr++;
    *ptr = '\0';

    WMRemoveUDObjectForKey(timeDB, cmdArgs);

    for (i=0; i<WMGetArrayItemCount(clientConnections); i++) {
        cPtr = WMGetFromArray(clientConnections, i);
        sendUpdateMessage(cPtr, cmdArgs, -1);
    }
}


static void
getTimeForUser(WMConnection *cPtr, char *cmdArgs)
{
    char *ptr;
    int time;

    if (cmdArgs[0]=='\0') {
        sendMessage(cPtr, _("Missing parameter for command GET."
                            " Use HELP for a list of commands.\n"));
        return;
    }

    ptr = cmdArgs;
    while (*ptr && *ptr!=' ' && *ptr!='\t')
        ptr++;
    *ptr = '\0';

    if (WMGetUDObjectForKey(timeDB, cmdArgs)!=NULL)
        time = WMGetUDIntegerForKey(timeDB, cmdArgs);
    else
        time = -1;

    sendUpdateMessage(cPtr, cmdArgs, time);
}


static void
handleConnection(WMConnection *cPtr)
{
    char *command, *ptr, *cmdArgs, *buffer;
    WMArray *commands;
    int i;

    commands = getAvailableMessages(cPtr);
    if (!commands)
        return;

    for (i=0; i<WMGetArrayItemCount(commands); i++) {
        command = WMGetFromArray(commands, i);
        while (*command && (*command==' ' || *command=='\t'))
            command++;
        ptr = command;
        while(*ptr && *ptr!=' ' && *ptr!='\t')
            ptr++;
        if (*ptr) {
            *ptr = '\0';
            ptr++;
        }
        while (*ptr && (*ptr==' ' || *ptr=='\t'))
            ptr++;

        cmdArgs = ptr;

        fprintf(stderr, "Command: '%s', args: '%s'\n", command, cmdArgs);

        if (strcasecmp(command, "quit")==0 || strcasecmp(command, "exit")==0) {
            sendMessage(cPtr, "Bye\n");
            WMCloseConnection(cPtr);
            enqueueConnectionForRemoval(cPtr);
            WMFreeArray(commands);
            return;
        } else if (strcasecmp(command, "id")==0) {
            showId(cPtr);
        } else if (strcasecmp(command, "help")==0) {
            showHelp(cPtr);
        } else if (strcasecmp(command, "list")==0) {
            listUsers(cPtr);
        } else if (strcasecmp(command, "set")==0) {
            setTimeForUser(cPtr, cmdArgs);
        } else if (strcasecmp(command, "add")==0) {
            addTimeToUser(cPtr, cmdArgs);
        } else if (strcasecmp(command, "sub")==0) {
            subTimeFromUser(cPtr, cmdArgs);
        } else if (strcasecmp(command, "remove")==0) {
            removeTimeForUser(cPtr, cmdArgs);
        } else if (strcasecmp(command, "get")==0) {
            getTimeForUser(cPtr, cmdArgs);
        } else {
            buffer = wmalloc(strlen(command) + 100);
            sprintf(buffer, _("Unknown command '%s'. Try HELP for"
                              " a list of commands.\n"), command);
            sendMessage(cPtr, buffer);
            wfree(buffer);
        }
    }

    WMFreeArray(commands);
}


static Bool
isAllowedToConnect(WMConnection *cPtr)
{
    WMHost *hPtr;
    int i;

    if (allowedHostList == NULL)
        return True; /* No list. Allow all by default */

    hPtr = WMGetHostWithAddress(WMGetConnectionAddress(cPtr));
    for (i=0; i<WMGetArrayItemCount(allowedHostList); i++) {
        if (WMIsHostEqualToHost(hPtr, WMGetFromArray(allowedHostList, i))) {
            WMReleaseHost(hPtr);
            return True;
        }
    }

    WMReleaseHost(hPtr);

    return False;
}


static void
didReceiveInput(ConnectionDelegate *self, WMConnection *cPtr)
{
    if (cPtr == serverPtr) {
        WMConnection *newPtr = WMAcceptConnection(cPtr);

        if (newPtr) {
            if (isAllowedToConnect(newPtr)) {
                WMSetConnectionDelegate(newPtr, &socketDelegate);
                WMSetConnectionSendTimeout(newPtr, 120);
                WMAddToArray(clientConnections, newPtr);
            } else {
                sendMessage(newPtr, "Sorry, you are not allowed to connect.\n");
                WMDestroyConnection(newPtr);
            }
        }
    } else {
        /* Data arriving on an already-connected socket */
        handleConnection(cPtr);
    }
}


static void
connectionDidTimeout(ConnectionDelegate *self, WMConnection *cPtr)
{
    WMHost *hPtr;

    if (cPtr == serverPtr) {
        wfatal(_("The server listening socket did timeout. Exiting."));
        exit(1);
    }

    hPtr = WMGetHostWithAddress(WMGetConnectionAddress(cPtr));
    wwarning(_("Connection with %s did timeout. Closing connection."),
             WMGetHostName(hPtr));
    WMReleaseHost(hPtr);

    enqueueConnectionForRemoval(cPtr);
}


static void
connectionDidDie(ConnectionDelegate *self, WMConnection *cPtr)
{
    if (cPtr == serverPtr) {
        /* trouble. server listening port itself died!!! */
        wfatal(_("The server listening socket died. Exiting."));
        exit(1);
    }

    enqueueConnectionForRemoval(cPtr);
}


static void
removeConnection(void *observer, WMNotification *notification)
{
    WMConnection *cPtr = (WMConnection*)WMGetNotificationObject(notification);
    WMData *data;

    WMRemoveFromArray(clientConnections, cPtr);
    if ((data = (WMData*)WMGetConnectionClientData(cPtr))!=NULL)
        WMReleaseData(data);
    WMDestroyConnection(cPtr);
}

static void
updatedDomain(void *observer, WMNotification *notification)
{
    wmessage("defaults domain file changed on disk. synchronizing.");
}


#if 0
static Bool
isDifferent(char *str1, char *str2)
{
    if ((!str1 && !str2) || (str1 && str2 && strcmp(str1, str2)==0))
        return False;

    return True;
}
#endif


int
main(int argc, char **argv)
{
    int i;

    wsetabort(wAbort);

    WMInitializeApplication("server", &argc, argv);

    if (argc>1) {
        for (i=1; i<argc; i++) {
            if (strcmp(argv[i], "--help")==0) {
                printHelp(argv[0]);
                exit(0);
            } else if (strcmp(argv[i], "--listen")==0) {
                char *p;

                if ((p = strchr(argv[++i], ':')) != NULL) {
                    *p = 0;
                    ServerAddress = wstrdup(argv[i]);
                    ServerPort = wstrdup(p+1);
                    *p = ':';
                    if (ServerAddress[0] == 0) {
                        wfree(ServerAddress);
                        ServerAddress = NULL;
                    }
                    if (ServerPort[0] == 0) {
                        wfree(ServerPort);
                        ServerPort = "34567";
                    }
                } else if (argv[i][0]!=0) {
                    ServerPort = argv[i];
                }
            } else if (strcmp(argv[i], "--allow")==0) {
                char *p, *ptr;
                int done;
                WMHost *hPtr;

                ptr = argv[++i];
                done = 0;
                while (!done) {
                    if ((p = strchr(ptr, ',')) != NULL) {
                        *p = 0;
                    }
                    if (*ptr != 0) {
                        hPtr = WMGetHostWithName(ptr);
                        if (hPtr) {
                            if (!allowedHostList)
                                allowedHostList = WMCreateArray(4);
                            WMAddToArray(allowedHostList, hPtr);
                        } else {
                            wwarning(_("Unknown host '%s'. Ignored."), ptr);
                        }
                    }

                    if (p!=NULL) {
                        *p = ',';
                        ptr = p+1;
                    } else {
                        done = 1;
                    }
                }
            } else {
                printf(_("%s: invalid argument '%s'\n"), argv[0], argv[i]);
                printf(_("Try '%s --help' for more information\n"), argv[0]);
                exit(1);
            }
        }
    }

    timeDB = WMGetDefaultsFromPath("./UserTime.plist");
    WMAddNotificationObserver(updatedDomain, NULL,
                              WMUserDefaultsDidChangeNotification, NULL);

    clientConnections = WMCreateArray(4);

    /* A NULL ServerAddress means to listen on any address the host has.
     * Else if ServerAddress points to a specific address (like "localhost",
     * "host.domain.com" or "192.168.1.1"), then it will only listen on that
     * interface and ignore incoming connections on the others. */
    if (ServerAddress && strcasecmp(ServerAddress, "Any")==0)
        ServerAddress = NULL;
    if (ServerPort==NULL)
        ServerPort = "34567";

    printf("Server will listen on '%s:%s'\n", ServerAddress?ServerAddress:"Any",
           ServerPort);
    printf("This server will allow connections from:");
    if (allowedHostList) {
        int i;
        char *hName;

        for (i=0; i<WMGetArrayItemCount(allowedHostList); i++) {
            hName = WMGetHostName(WMGetFromArray(allowedHostList, i));
            printf("%s'%s'", i==0?" ":", ", hName);
        }
        printf(".\n");
    } else {
        printf(" any host.\n");
    }

    serverPtr = WMCreateConnectionAsServerAtAddress(ServerAddress, ServerPort,
                                                    NULL);

    if (!serverPtr) {
        wfatal("could not create server on `%s:%s`. Exiting.",
               ServerAddress ? ServerAddress : "localhost", ServerPort);
        exit(1);
    }

    WMSetConnectionDelegate(serverPtr, &socketDelegate);

    WMAddNotificationObserver(removeConnection, NULL,
                              SEConnectionShouldBeRemovedNotification, NULL);

    while (1) {
        /* The ASAP notification queue is called at the end of WHandleEvents()
         * There's where died connections we get while running through
         * WHandleEvents() get removed. */
        WHandleEvents();
    }

    return 0;
}


