/***************************************************************************
 *   Copyright (C) 2005 by Stefan Walkner                                  *
 *   swalkner@gmail.com                                                    *
 *                                                                         *
 *   This program is free software; you can redistribute it and/or modify  *
 *   it under the terms of the GNU General Public License as published by  *
 *   the Free Software Foundation; either version 2 of the License, or     *
 *   (at your option) any later version.                                   *
 *                                                                         *
 *   This program is distributed in the hope that it will be useful,       *
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
 *   GNU General Public License for more details.                          *
 *                                                                         *
 *   You should have received a copy of the GNU General Public License     *
 *   along with this program; if not, write to the                         *
 *   Free Software Foundation, Inc.,                                       *
 *   59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.             *
 ***************************************************************************/

#include <stdio.h>
#include <stdlib.h>
#include <signal.h>
#include <sys/select.h>
#include <unistd.h>
#include <getopt.h>
#include <string.h>
#include <arpa/inet.h>

#include "StdTypes.h"

#include "Connection/AbstractConnection.h"
#include "MessageLogger/MessageLogger.h"

#include "SmtpCore/ClientSession.h"
#include "SmtpCore/WorkHandler.h"

/* global variables: (only used within this file) */
/* connection of the server */
ConnectionId serverConnectionId;
/* session (context) list for connected clients */
ClientSessionList sessionList;

/* CLI options we offer */
static struct option const long_options[] =
{
    { "domain", required_argument, 0, 'd' },
    { "port",   required_argument, 0, 'p' },
};

/* little function to display the options to call this program */
void printUsage(const char *programName)
{
    printf("usage: %s -d|--domain <domain> -p|--port <port>\n", programName);
}

/* smoothly shuts down the server service [closing all connections etc] */
void emitShutdown(int signal)
{
    ClientSession *session, *currSession;
    logAlways(__FILE__, __LINE__, "Received shutdown signal -> closing socket(s)");

    /* close connected client connections */
    session = sessionList.first;
    while ( session != NULL )
    {
        closeConnection(session->connectionId);

        currSession = session->next;
        destroyClientSession(session);

        session  = currSession;
    }

    /* close server connection */
    closeConnection(serverConnectionId);

    /* finally (smoothly) exit with a nice failure code */
    exit(EXIT_FAILURE);
}

int startupSmtpService(char *domain, int serverPort)
{
    struct sockaddr_in serverAddr, clientAddr;

    int connectionsActive,   /* number of connections that got ready during select() */
        maxConnectionId;    /* maxConnectionId number - used for select */

    /*
     * during the main loop below it indicates whether a connection
     * changed or not...
     */
    Boolean workDone;

    /* file descriptor sets we are listening for changes */
    fd_set readConnections,
           activeReadConnections,
           writeConnections,
           activeWriteConnections,
           exceptionConnections;

    ConnectionId newConnectionId;
    ServerInfo serverInfo; /* info about our server - all sessions will point to */
    SmtpWorkResult result; /* result which will filled by SmtpCore */

    /* pretty redundant declarations but IMHO better to NOT use newSession for looping */
    ClientSession *newSession,     /* used for new sessions */
                  *tmpSession,     /* temp var for storing new sessions */
                  *currSession, /* for looping through the list */
                  *prevSession;    /* for looping through -> pointer to previous element */

    /* register signal handler - to be able to close the connection */
    signal(SIGINT,  emitShutdown);
    signal(SIGTERM, emitShutdown);

    /* create the server connection */
    serverConnectionId = createPassiveConnection(&serverAddr, serverPort, CONNECTION_TCP|CONNECTION_NON_BLOCKING);
    if ( serverConnectionId < 0 )
    {
        logError(__FILE__, __LINE__, "Could not establish Server Connection on Port [%d]", serverPort);
        return EXIT_FAILURE;
    }

    /* clear our select connection sets - just to make sure... */
    FD_ZERO(&readConnections);
    FD_ZERO(&activeReadConnections);
    FD_ZERO(&writeConnections);
    FD_ZERO(&activeWriteConnections);
    FD_ZERO(&exceptionConnections);

    /* add serverConnection to the read connection set */
    FD_SET(serverConnectionId, &readConnections);

    /*
    * currently the max file descriptor id is
    * the serverConnectionId which is the only connection ATM
    */
    maxConnectionId = serverConnectionId;

    /* init some vars */
    sessionList.first = NULL;
    serverInfo.domain = domain;

    /* enter the server main loop */
    logDebug(__FILE__, __LINE__, "Entering Server main loop");
    while ( 1 )
    {
        /*
         * NOTE: select will change the description set to the actually changed descriptors
         * so, we will ALWAYS change "readConnections" and copy the full set over here
         */
        activeReadConnections  = readConnections;
        activeWriteConnections = writeConnections;
        /*
         * ALL connections are listed in readConnections -> so we will observer ALL
         * connections for exceptions...
         */
        exceptionConnections = readConnections;

        /*
        * wait for a connection to change its status
        * we use NULL as timeout here because the server should run forever!
        * the only way to shutdown the server is to send SIGTERM
        * we also don't provide an exception set...
        */
        connectionsActive = select(maxConnectionId + 1, &activeReadConnections, &activeWriteConnections, &exceptionConnections, NULL);

        /* some descriptor changed - so take some action ^^ */
        if ( connectionsActive < 0 )
        {
            /* some error occoured ;-( - time to shutdown! */
            logError(__FILE__, __LINE__, "Select() emitted wrong alarm");
            emitShutdown(SIGTERM);
            /* never reached - shutdown exits */
        }

        /* did we receive something on the server connection? > could only be a new connection */
        if ( FD_ISSET(serverConnectionId, &activeReadConnections) )
        {
            /* server connection changed -> incoming connection! */
            memset(&clientAddr, 0x0, sizeof(struct sockaddr));
            newConnectionId = acceptConnection(serverConnectionId, &clientAddr, CONNECTION_NON_BLOCKING);

            /* if accept did not succeed -1 is returned */
            if ( newConnectionId > 0 )
            {
                logDebug(__FILE__, __LINE__, "new Client Connection connected");

                /* try to create a new client session; if connection forbidden -> NULL is returned */
                newSession = createClientSession(newConnectionId, &serverInfo, &clientAddr);
                if ( newSession == NULL )
                {
                    logAlways(__FILE__, __LINE__, "Client [%s] not allowed to connect...", inet_ntoa(clientAddr.sin_addr));
                    destroyClientSession(newSession);
                    closeConnection(newConnectionId);
                    continue;
                }

                /* append the session to the list */
                tmpSession = sessionList.first;
                if ( tmpSession == NULL )
                {
                    sessionList.first = newSession;
                }
                else
                {
                    /* go to end of the list */
                    while ( tmpSession->next != NULL )
                    {
                        tmpSession = tmpSession->next;
                    }

                    /* store new session there */
                    tmpSession->next = newSession;
                }

                /*
                 * also add the connection to the readConnections/writeConnections
                 * which we watch for changes
                 */
                FD_SET(newSession->connectionId, &readConnections);
                FD_SET(newSession->connectionId, &writeConnections);

                /*
                * if the new connection is greater than the max connection (used for select)
                * then update the maxConnectionId value!
                */
                if ( newSession->connectionId > maxConnectionId )
                {
                    maxConnectionId = newSession->connectionId;
                }
            }
        }

        /* additionally: poll all other connections */
        /* logDebug(__FILE__, __LINE__, "...Polling..."); */

        /* start from first element of the list to search through it */
        currSession = sessionList.first;
        prevSession = NULL;

        /* reset the observed connectionIds because of removals etc */
        maxConnectionId = serverConnectionId;
        while ( currSession != NULL )
        {
            workDone = FALSE;

            /* update max connection id appropriately */
            if ( currSession->connectionId > maxConnectionId )
            {
                maxConnectionId = currSession->connectionId;
            }

            clearSmtpResult(&result);

            /*
             * check write connections BEFORE the read connections
             * otherwise we might get some data into the read buffer
             * allthough there are still some -> according to WorkHandler.h
             */
            if ( FD_ISSET(currSession->connectionId, &activeWriteConnections) )
            {
                dispatchSmtpOutgoingWork(currSession, &result);
                workDone = TRUE;
            }

            /* check read connections - only if everything was written! */
            if ( FD_ISSET(currSession->connectionId, &activeReadConnections) )
            {
                dispatchSmtpIncomingWork(currSession, &result);
                workDone = TRUE;

                /*
                 * a new session was created - add it to the session list
                 * and the to observing readConnections list
                 */
                if ( result.status == SMTP_NEWSESSION )
                {
                    /* add the new session to the list */
                    newSession = getCreatedSession(currSession);

                    newSession->next  = currSession->next;
                    currSession->next = newSession;

                    /* VERY IMPORTANT: also update the connection id!! */
                    if ( newSession->connectionId > maxConnectionId )
                    {
                        maxConnectionId = newSession->connectionId;
                    }

                    /*
                     * add new session only to read connections
                     * we are expecting some reply with 220
                     */
                    FD_SET(newSession->connectionId, &readConnections);
                }

            }

            /* did we really do something read/write? */
            if ( workDone == TRUE )
            {
                /*
                 * obviously there is something to be written
                 * append to the write connection observer list
                 */
                if ( result.todo == WRITE_TODO )
                {
                    FD_SET(currSession->connectionId, &writeConnections);
                }
                else
                {
                    /* delete from list -> nothing to be written */
                    FD_CLR(currSession->connectionId, &writeConnections);
                }
            }

            /* if we are supposed to remove a client or a quit command was received */
            if (
                 FD_ISSET(currSession->connectionId, &exceptionConnections) ||
                 ((currSession->quitGiven==TRUE) && (currSession->cmdBufferUsedBy!=BUFFER_USAGE_WRITE)) ||
                 ((result.status==SMTP_CONNABORT) && (result.todo==NOOP_TODO))
               )
            {
                logDebug(__FILE__, __LINE__, "Client [%d] connection closed", currSession->connectionId);

                /* remove the handle from the observer list */
                FD_CLR(currSession->connectionId, &readConnections);
                FD_CLR(currSession->connectionId, &writeConnections);

                /* close the connection */
                closeConnection(currSession->connectionId);

                /* remove the client from the session list */
                if ( prevSession == NULL )
                {
                    /* we are the first element */
                    sessionList.first = sessionList.first->next;
                    destroyClientSession(currSession);
                    currSession = sessionList.first;
                }
                else
                {
                    /* element other than first one */
                    prevSession->next = currSession->next;
                    destroyClientSession(currSession);
                    currSession = prevSession->next;
                }
            }
            else
            {
                /* store pointer to prevSession for the next loop */
                prevSession = currSession;

                /*
                 * when removing a session from the list skipping
                 * is automatically done otherwise skip manually
                 */
                currSession = currSession->next;
            }
        }
    }

    logError(__FILE__, __LINE__, "Server main loop stopped unexpected ;-(");

    return EXIT_FAILURE;
}

/*
 * the main program which invokes the smtp server
 * create a server socket which accepts incoming connections
 * the server socket will be bound to a CLI specified port
 * our server will use a CLI specified domain
 */
int main(int argc, char **argv)
{
    char c;
    char *domain;
    int port;

    domain = NULL;
    while ( (c = getopt_long(argc, argv, "d:p:", long_options, 0)) != -1 )
    {
        switch ( c )
        {
            case 'd':
                domain = optarg;
            break;

            case 'p':
                port = atoi(optarg);
            break;
        }
    }

    if ( (domain==NULL) || (port<0) || (port>65535) )
    {
        printUsage(argv[0]);
        exit(EXIT_FAILURE);
    }

    /* we successfully got our args -> startup the service */
    return startupSmtpService(domain, port);
}

