//
// anyRemote
// a wi-fi or bluetooth remote for your PC.
//
// Copyright (C) 2006-2016 Mikhail Fedotov <anyremote@mail.ru>
//
// 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 3 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., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
//

#include <ctype.h>
#include <dirent.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <netinet/in.h>
#include <time.h>
#include <unistd.h>
#ifdef __FreeBSD__
#include <signal.h>
#endif
#ifndef __FreeBSD__
#include <grp.h>
#endif

#include "lib_wrapper.h"
#include "atsend.h"
#include "btio.h"
#include "conf.h"
#include "common.h"
#include "utils.h"
#include "executor.h"
#include "dispatcher.h"
#include "security.h"
#include "list.h"
#include "alarm.h"

#include "pr_frontend.h"
#include "pr_btspp.h"
#include "pr_socket.h"
#ifdef USE_L2CAP
#include "pr_l2cap.h"
#endif
#include "pr_web.h"
#include "pr_stdin.h"
#include "queue.h"
#include "peer.h"

#define TMPDISCONN_TIME 60

extern CONF    conf;
extern char    tmp[MAXMAXLEN];

extern char callerId[MAXLEN];

boolean_t dispIsJoinable = BOOL_YES;


int  gotExitSignal  = 0;

static int         _callTimer     = 0;
static int         _initialized  = 0;

// Then port will be closed from a forked child use 0
void closePort(int final)
{
    if (final) {
        logger(L_DBG,"[DS]: closePort");
    }

    if (getFrontEnd() > 0) {
        if (final)  {
            writeToFrontEnd("Exiting");
        }
    }
    
    closePeers(final);
}

static void handleActiveCall(void)
{
    if (_callTimer >= 20) {  // about a second

        // Will set global callerId [MAXLEN];
    
        boolean_t stillActive = checkActiveCall();
        if (!stillActive) {
            strcpy(callerId, "NO CALLER ID");

            eMessage* em = (eMessage*) malloc(sizeof(eMessage));
            em->peer  = PEER_ANY;
            em->type  = EM_KEY;
            em->value = strdup("Msg:EndCall(,)");

            DEBUG2("[DS]: Send message to executor thread %s", (char*) em->value);
            sendToExecutor(em);
        }
    
        _callTimer = 0;
    } else {
        _callTimer++;
    }
}

char* readFromFile(const char *cmdTag, const char* file, int* size)
{
    FILE *fp;
    struct stat buf;

    long fLen = 0;
    if(stat(file, &buf) == -1) {
        logger(L_ERR,"can't get file size!");
    } else {
        fLen = (int) buf.st_size;
        
        INFO2("[EX]: readFromFile file size id %ld", fLen);

        if (!S_ISREG (buf.st_mode)) {
            logger(L_ERR,"not regular file");
        } else {

            fp=fopen(file,"r");
            if (fp == NULL) {
                logger(L_ERR,"can't open file!");
            } else {

                int prefixSz = strlen(cmdTag);

                uint32_t szh32 = (uint32_t) buf.st_size;
                uint32_t szi32 = htonl(szh32);

                char * fBuffer = (char*) calloc(fLen+prefixSz+7,1);    // 7 =   <size of bin data=4bytes>    ");\0"
                if (fBuffer == NULL) {
                    logger(L_ERR,"no more memory!");

                    *size = -1;
                    
                    fclose(fp);
                    return NULL;
                }

                strcat(fBuffer, cmdTag);
                memcpy((void*)fBuffer+prefixSz, (const void *) &szi32, 4);    // length on binary data

                if (fp) {
                    //size_t dummy = 
		    fread(fBuffer+prefixSz+4, sizeof(char), fLen, fp);
                    fclose(fp);
                }
                strcpy(fBuffer+prefixSz+fLen+4,");");

                *size = fLen+prefixSz+7;
                INFO2("[EX]: Command: CONTENT %s", fBuffer);
                return fBuffer;
            }
        }
    }

    if (strncmp(cmdTag, "Set(cover", 9) == 0) {  // cleanup the cover on client

        char * fBuffer = (char*) malloc(12);
        strcpy(fBuffer, "Set(cover);");

        *size = 11;
        return fBuffer;
    }

    *size = -1;
    return NULL;
}

dMessage* allocDMessage()
{
    dMessage* dm = (dMessage*) malloc(sizeof(dMessage));
    dm->peer     = PEER_ANY;
    dm->type     = 0;
    dm->subtype  = 0;
    dm->size     = 0;
    dm->value    = NULL;
    dm->file     = NULL;
    dm->scaled   = NULL;
    return dm;
}

dMessage* getDFinalizer()
{
    dMessage* dm = allocDMessage();
    dm->type     = DM_SET;
    dm->subtype  = ID_SET_MAX;
    dm->size     = 6;
    dm->value    = (void*) strdup("End();");
    return dm;
}

void freeDMessage(void *ptr)
{
    dMessage* dm = (dMessage*) ptr;
    if (dm) {
        if (dm->value != NULL) {
            free(dm->value);
        }
        if (dm->file != NULL) {
            free(dm->file);
        }
        if (dm->scaled != NULL) {
            free(dm->scaled);
        }
        free(dm);
    }
}

static int dispatchEvent(dMessage* dm)
{
    if (dm->subtype == ID_EVENT_FRONTEND) {
        
        writeToFrontEnd(dm->value);
   
    } else if (dm->subtype == ID_EVENT_INIT) {
        
        logger(L_INF, "[DS]: Got init OK event");
        return EXIT_INITOK;
    
    } else if (dm->subtype == ID_EVENT_DISCONNECT) {
        
        logger(L_INF, "[DS]: Got disconnect event");
        disconnectPeers();
        closePort(0);
        return EXIT_DISCON;
   
    } else if (dm->subtype == ID_EVENT_EXIT) {
        
        logger(L_INF, "[DS]: Got exit event");

        gotExitSignal = 1;

        // in server mode wait client to close connection and then exit
        if (disconnectPeers() == 1) {
            logger(L_INF, "[DS]: Got exit event: exit immediately");
            closePort(1);
            return EXIT_ABORT;
        }
    } 
    return EXIT_OK;
}

static int dispatchSendString(dMessage* dm)
{
    DEBUG2("[DS]: Send(string) %s", (char*) dm->value);
    return writePeers(dm);
}

static int dispatchSendBytes(dMessage* dm)
{
    DEBUG2("[DS]: Send(bytes) %s", (char*) dm->value);
    return writeBytesPeers((char*) dm->value);
}

static int dispatchSet(dMessage* dm)
{
    logger(L_DBG, "[DS]: Set(...)");
    return writePeers(dm);
}

static int dispatchNone(dMessage* dm)
{
    return EXIT_OK;
}

// rely on DispMsgType enum
static struct {
    int id;
    int (*hook)(dMessage* p);
} _dispMsgHooks[] = {
    {DM_SET,      dispatchSet        },
    {DM_GET,      dispatchSet        },
    {DM_SETFILE,  writeFilePeers     },
    {DM_SENDB,    dispatchSendBytes  },
    {DM_SENDS,    dispatchSendString },
    {DM_CKPD,     writeCKPD          },
    //{DM_CMER,     writeCMER        },
    {DM_EVENT,    dispatchEvent      },
    {DM_FRONTEND, dispatchNone       }
};

static int processOutputData()
{
    //logger(L_DBG,"[DS]: processOutputData");

    int ret = EXIT_OK;

    // Verify commands from queue (timeout about 1/2 sec)
    dMessage* dm = (dMessage*) queuePop(Q_DISP);
    if (dm != NULL) {

        //DEBUG2("[DS]: Got event %p %d", dm, dm->type);

        if (connected() == EXIT_NOK && 
            !haveConnectionless() &&
                !(dm->type == DM_EVENT)) { // can process these even if no connection

            //logger(L_DBG, "[DS]: No connection. Skip event");

        } else {
            
            if (dm->type < DM_TYPE_MAX) {
                ret = (_dispMsgHooks[dm->type].hook)(dm);
            }
        }

        freeDMessage(dm);
    }
    
    if (ret == EXIT_ABORT) { 
        DEBUG2("[DS]: processOutputData ret %d", ret); 
    }
    return ret;
}

void parseCommand(int peerId, char* cmd)
{
    //DEBUG2("[DS]: parseCommand >%s<", cmd);

    char *prev, *next;

    if (cmd == NULL) {
        return ;
    }

    //skip lines starting with \n and \r
    if (cmd[0] == '\r') {
        cmd++;
    }
    if (cmd[0] == '\n') {
        cmd++;
    }

    // most common case
    if (!cmd[0]) {
        return;
    }

    // if recieved multiline command - handle line by line and return
    prev = cmd;
    next = strchr(cmd, '\r');

    if (next == NULL) {    // Java client will send +CKEV: 1,1; +CKEV: 1,0
        next = strchr(cmd, ';');
    }

    if (next) {
        logger(L_DBG, "[DS]: parseCommand multiline");

        char copy[1024];
        int len;

        do {
            len = next-prev;

            if (len >= 2) {
                memcpy(copy, prev, len);
                copy[len] = 0;

                // use recursion
                parseCommand(peerId, copy);
            }
            prev = next+1;
            next = strchr(prev, '\r');
            if (next == NULL) { // Java client will send +CKEV: 1,1; +CKEV: 1,0
                next = strchr(prev, ';');
            }

            /* handle in reader
            if (next == NULL && getIViewer()) {
                next = strchr(prev, '\3');        // end-of-text marker in CommandFusion
            }*/

        } while (next) ;

        // now return
        return;
    }

    logger(L_DBG,"[DS]: -------------------- Command read --------------------");
    DEBUG2("[DS]: parseCommand >%s<", cmd);

    if (IS_OK(cmd)) {   // OK - nothing to do
        return;
    }

    if (strncmp(cmd, DEF_AT_CKPD, 8)  == 0 ||  // This is echo of sent message in AT mode; nothing to do
        strncmp(cmd, DEF_CLDEBUG, 12) == 0) {  // This is debug message from java client; nothing to do
        return;
    }
    
    // This is keepalive message, handle it internally
    if (strncmp(cmd, DEF_CLPING, 8) == 0 && useKeepalive()) {
        setKeepalive();
        return;
    }
    
    eMessage* em = (eMessage*) malloc(sizeof(eMessage));
    em->peer  = peerId;
    em->type  = EM_KEY;
    em->value = strdup(cmd);

    DEBUG2("[DS]: Send message from peer %d to executor thread %s", peerId, (char*) em->value);
    sendToExecutor(em);

    return;
}


static void hookInitOnce()
{
    if (_initialized == 0) {

        // setgid
        if(conf.uid && getuid()==0) {
            DEBUG2("[DS]: setuid/setgid %d,%d",conf.uid,conf.gid);
            #ifndef __FreeBSD__
	    setgroups(0, NULL);
	    #endif
	    setgid(conf.gid);
            setuid(conf.uid);
        }

        _initialized++;
    }
}

static int doDisconnect()
{
    logger(L_INF, "[DS]: Got disconnected");
    //printf("Got disconnected\n");

    freeBtAddress();

    if (gotExitSignal) {
        logger(L_INF, "[DS]: Got signal, exiting");
        closePort(1);
        return EXIT_ABORT;

    }
    writeToFrontEnd("Disconnected");
    sendDisconnect();
    sendEventToExecutor(0, ID_EVT_DISCONNECT);

    if (disconnectPeers() == 1) {
        logger(L_INF, "[DS]: Closing the port");
        closePort(0);
    }

    return EXIT_DISCON;
}

void sendFinalizer()
{
    if (needFinalizer() == EXIT_OK) {
        logger(L_DBG,"[DS]: sendFinalizer()");
        eMessage* em = (eMessage*) malloc(sizeof(eMessage));
        em->peer  = PEER_ANY;
        em->type  = EM_AS_IS;
        em->value = strdup("End();");
        sendToExecutor(em);
    }
}

void dispatcherCleanup()
{
    logger(L_DBG,"[DS]: dispatcherCleanup()");
    freePeers();
}

static int processInputData()
{
    //logger(L_DBG,"[DS]: processInputData()");

    int retRead = processPeers();
    //if (retRead != 0) printf("GOT %d\n",retRead);

    if (retRead == EOF) {
        
        logger(L_DBG,"[DS]: processInputData() EOF");
        return doDisconnect();

    } else if (retRead > 0) {  // something was pricessed

        sendFinalizer();

    } // it is OK if retRead == 0 
    
    return EXIT_OK;
}

static int doMessageLoop()
{
    int ret = EXIT_DISCON;

    while (1) {
    
        //logger(L_DBG,"[DS]: doMessageLoop LOOP");
 
        // read from sockets, etc.
        ret = processInputData();

        if (ret == EXIT_ABORT) {
            logger(L_DBG,"[DS]: doMessageLoop abort on read ");
            break;
        } else if (ret == EXIT_DISCON) {
            logger(L_DBG,"[DS]: doMessageLoop disconnect on read");
            break;
        }

        // Is call still active ? (timeout about 1 seconds, check it inside)
        if (hasActiveCall()) {
            handleActiveCall();
        }
    
        ret = processOutputData();
        if (ret == EXIT_ABORT) {
            logger(L_DBG,"[DS]: doMessageLoop abort on check queue");
            break;
        } else if (ret == EXIT_DISCON) {
            logger(L_DBG,"[DS]: doMessageLoop disconnect on check queue");
            break;
        }

        // Main loop timer (1/50 of second)
        usleep(20000);
    } // while (forever)

    return ret;
}

static int doConnectionLoop()
{
    if (setupPeersPre() != 1) {  // Init modem: AT, ATE0, AT+CMER, in server mode waits for connection
        logger(L_DBG,"[DS]: Init connection error");
        return EXIT_OK;
    }

    logger(L_DBG,"[DS]: Init connection OK");
    
    if (connected() == EXIT_OK) {
        logger(L_DBG,"[DS]: doConnectionLoop connectNotify");
        connectNotify(0);
    }

    dispIsJoinable = BOOL_YES;

    logger(L_DBG,"[DS]: Start message loop");

    int ret = doMessageLoop();
    
    DEBUG2("[DS]: Stop message loop %d", ret);
    
    return ret;

    // EXIT_DISCON / EXIT_NOK is OK here
    return EXIT_OK;
}

pointer_t dispatcherRoutine(pointer_t thread)
{
    int ret = EXIT_OK;

    logger(L_DBG,"[DS]: start dispatcher thread");

    // wait init ok event
    while (1) {
        ret = processOutputData();
        if (ret == EXIT_ABORT) {
            dispatcherCleanup();
            return NULL;
        } else if (ret == EXIT_INITOK) {
            break;
        }
        //logger(L_DBG,"[DS]: wait init OK event");
        usleep(50000);
    }
    logger(L_DBG,"[DS]: got init event");

    int dmn = autoConnect();
    int rs  = getRetrySecs();
    strcpy(callerId, "NO CALLER ID");

    if (definePeers() == EXIT_ABORT) {
        logger(L_DBG,"[DS]: Incorrect device specification");
        dispatcherCleanup();
        sendAbort();
        return NULL;
    }

    while (1) {

        logger(L_DBG,"[DS]: ************ outer loop **********");

        if (openPeers() == EXIT_OK) {   // Open device

            logger(L_DBG,"[DS]: Device open OK");

            hookInitOnce();

            dispIsJoinable = BOOL_NO;

            DEBUG2("[DS]: Start connection loop");
            int retLoop = doConnectionLoop();
            DEBUG2("[DS]: Stop connection loop %d", retLoop);
            
            if (retLoop == EXIT_ABORT) {
                DEBUG2("[DS]: Dispatcher abort");
                dispatcherCleanup();
                return NULL;
            }
           
        } else {        // open port
            logger(L_DBG,"[DS]: Device open error");
        }

        //printf("Connection closed or lost\n");
        logger(L_INF, "[DS]: Connection closed or lost");

        // Can't open port or it closed again

        int isServer = isServerMode();

        if (!gotExitSignal &&
                (dmn || isServer == EXIT_OK || ret == EXIT_DISCON)) {

            int timeout;

            if (isServer == EXIT_OK) {

                timeout = 2;    // wait only 2 seconds

            } else if (ret == EXIT_DISCON) {

                timeout = TMPDISCONN_TIME;
                ret = EXIT_OK;

            } else {
                timeout = rs;
            }

            INFO2("[DS]: Wait %d seconds to connect/open server socket ...", timeout);
            //printf("Wait %d seconds to connect/open server socket ...\n", timeout);

            dispIsJoinable = BOOL_NO;
            sleep(timeout);
            dispIsJoinable = BOOL_YES;

        } else {    // Proceed to exit
            printf("Exiting ...\n");
            sendAbort();
            break;
        }
    }

    // Finish all
    logger(L_INF, "[DS]: Stop dispatcher ...");
    closePort(1);
    dispatcherCleanup();

    return NULL;
}

void sendToDispatcher(dMessage *buf)
{
    if (queuePush(Q_DISP, (void*) buf) == RC_OK) {
        DEBUG2("send to dispatcher %d", buf->type);
    }
}
