//
// 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 <errno.h>
#include <fcntl.h>
#include <math.h>
#include <netdb.h>
#include <netinet/in.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/ioctl.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/time.h>
#include <sys/types.h>
#include <sys/un.h>
#include <termios.h>
#include <unistd.h>

#include "pr_socket.h"
#include "common.h"
#include "utils.h"
#include "conf.h"
#include "lib_wrapper.h"
#include "sys_util.h"
#include "security.h"
#include "dispatcher.h"

extern char tmp[MAXMAXLEN];
extern boolean_t stillRun;

typedef struct _SocketConnection_ {
    int           fileDescriptor;
    int           serverFileDescriptor;
    char*         uSocket;
} _SocketConnection;

//
// Support server mode sockets
//

int socketFD(ConnectInfo* conn)
{
    _SocketConnection* cn = (_SocketConnection*) conn->connectionData;
    if (!cn) {
        return -1;
    }
    return (conn->state == PEER_WAIT_ACCEPT || 
            conn->state == PEER_WAIT_LISTEN ? cn->serverFileDescriptor : cn->fileDescriptor);
}

static int socketOpenInternal(ConnectInfo* conn)
{
    struct sockaddr_in tcp_addr;
    struct sockaddr_un un_addr;
    struct sockaddr*   socketaddr = NULL;

    int addFamily = 0;
    int proto     = 0;
    int sz;

    if (conn->connectionData && ((_SocketConnection*) conn->connectionData)->serverFileDescriptor > 0) {
        logger(L_ERR, "[DS]: socketOpenInternal: TCP/local socket was already opened");
        return 1;
    }
    
    if (conn->connectionData) {
        free(((_SocketConnection*) conn->connectionData)->uSocket);
        free(conn->connectionData);
    }
    
    conn->connectionData = (_SocketConnection*) malloc(sizeof(_SocketConnection));
    _SocketConnection* cn = (_SocketConnection*) conn->connectionData;
    
    cn->serverFileDescriptor = -1;
    cn->fileDescriptor       = -1;
    cn->uSocket              = NULL;
    
    const char *path = (conn->portStr ? conn->portStr->str : NULL);

    if (conn->mode == SERVER_TCP) {
        addFamily = AF_INET;
        proto     = IPPROTO_TCP;
    } else if (conn->mode == SERVER_UX) {
        addFamily = AF_UNIX;
        proto     = 0;
    } else {
        logger(L_ERR, "socketOpenInternal: incorrect input");
        return -1;
    }

    if ((cn->serverFileDescriptor = socket(addFamily, SOCK_STREAM|SOCK_CLOEXEC, proto)) < 0) {
        
        logger(L_ERR, "[DS]: socketOpenInternal: opening TCP socket");
        errnoDebug("opening TCP socket ",errno);  // testing debug
        
        printf("ERROR: socketOpenInternal: opening TCP socket\n");
        cn->serverFileDescriptor = -1;
        return -1;
    }

    int optval = 1;
    setsockopt(cn->serverFileDescriptor, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval));

    if (conn->mode == SERVER_TCP) {

        memset((void *) &tcp_addr, 0, sizeof(tcp_addr));
        tcp_addr.sin_family = AF_INET;
        tcp_addr.sin_addr.s_addr = INADDR_ANY;
        tcp_addr.sin_port = htons(conn->port);

        socketaddr=(struct sockaddr *)&tcp_addr;
        sz = sizeof(tcp_addr);

    } else if (conn->mode == SERVER_UX && path != NULL) {

        memset(&un_addr, 0, sizeof(un_addr));
        un_addr.sun_family = AF_UNIX;
        strncpy(un_addr.sun_path, path, sizeof un_addr.sun_path - 1);
        printf("ERROR: SOCKET %s\n", path);
        socketaddr=(struct sockaddr *)&un_addr;
        sz = sizeof(un_addr);

    } else {
        logger(L_ERR, "socketOpenInternal: incorrect input 2");
        return -1;
    }

    if (bind(cn->serverFileDescriptor, (struct sockaddr *) socketaddr, sz) < 0) {
        logger(L_ERR, "[DS]: socketOpenInternal: on bind()");
        printf("ERROR: on bind socket to the address %s (%d)\n", strerror(errno), errno);
        return -1;
    }

    if (conn->mode == SERVER_UX) {
        cn->uSocket = strdup(path);
    }
    return 1;
}

int socketOpen(ConnectInfo* conn)
{
    if (conn->mode == SERVER_TCP) {
        DEBUG2("[DS]: socketOpen: TCP mode. Use port %d", conn->port);
    } else if (conn->mode == SERVER_UX) {
        DEBUG2("[DS]: socketOpen: Local socket mode. Use file %s", conn->portStr->str);
    } else {
        DEBUG2("[DS]: socketOpen: improper mode. %d", conn->mode);
        return EXIT_NOK;
    }

    if (socketOpenInternal(conn) < 0) {
        return EXIT_NOK;
    } 
    
    conn->state = PEER_WAIT_LISTEN;
    return EXIT_OK;
}

void socketClose(ConnectInfo* conn, int final)
{
    _SocketConnection* cn = (_SocketConnection*) conn->connectionData;
    if (!cn) return;
    
    if (final) {
        logger(L_INF, "[DS]: closeSocket");
    }
    
    if (cn->fileDescriptor >= 0) {
        if (final) {
            logger(L_INF, "[DS]: closeSocket close socket");
        }
        close(cn->fileDescriptor);
        cn->fileDescriptor = -1;
    }
    if (cn->serverFileDescriptor >= 0) {
        if (final) {
            logger(L_INF, "[DS]: closeSocket close server socket");
        }
        close(cn->serverFileDescriptor);
        cn->serverFileDescriptor = -1;
    }
    
    if (final) {
        if (conn->mode == SERVER_UX) {
            unlink(cn->uSocket);
            free(cn->uSocket);
        }
    }
    free(cn);
    conn->connectionData = NULL;
    conn->state = PEER_DISCONNECTED;
}

void socketReset(ConnectInfo* conn)
{
    _SocketConnection* cn = (_SocketConnection*) conn->connectionData;
    if (cn) {
        if (cn->fileDescriptor >= 0) {
            close(cn->fileDescriptor);
            cn->fileDescriptor = -1;
        }
        conn->state = PEER_WAIT_ACCEPT;
    } else {
        conn->state = PEER_DISCONNECTED;  // should not happens
    }
}

//
// Setup listen
//
int socketListen(ConnectInfo* conn)
{
    logger(L_INF, "[DS]: socketListen");
    
    _SocketConnection* cn = (_SocketConnection*) conn->connectionData;
    if (!cn) {
        return -1;
    }
    int ret = listen(cn->serverFileDescriptor,0);
    if (ret >= 0) {
        conn->state = PEER_WAIT_ACCEPT;
    }
    return (ret < 0 ? -1 : 1);
}
     
//
// Wait for incoming connection
//
int socketAccept(ConnectInfo* conn)
{
    struct sockaddr* socketaddr = NULL;
    struct sockaddr_in ip_addr;
    struct sockaddr_un un_addr;
    
    int cnt, sz;
    int type = conn->mode;
    
    INFO2("[DS]: socketAccept %d", conn->id);
    cnt = 0;

    if (type == SERVER_TCP) {
        socketaddr=(struct sockaddr *)&ip_addr;
        sz = sizeof(ip_addr);
    } else if (type == SERVER_UX) {
        socketaddr=(struct sockaddr *)&un_addr;
        sz = sizeof(un_addr);
    }
    
    if (!socketaddr) {
        return -1;
    }
    
    _SocketConnection* cn = (_SocketConnection*) conn->connectionData;
    if (!cn) return -1;

    while (stillRun) {

        INFO2("[DS]: socketAccept: accept on %d", conn->id);
 
        cn->fileDescriptor = accept(cn->serverFileDescriptor, (struct sockaddr *) socketaddr, (socklen_t *)&sz);

        if (cn->fileDescriptor < 0 && errno == EAGAIN) {

            if (cnt >= 60) {    // Print to log every minute
                logger(L_INF, "socketAccept: waiting for connection");
                //printf(".");
                cnt = 0;
            }
            fflush(stdout);

            sleep(1);
            cnt++;

            continue;
        }

        if (cn->fileDescriptor < 0) {
            logger(L_ERR, "[DS]: on accept");
            printf("ERROR: on accept %d\n", errno);
            return -1;
            /*} else {
                // Set non-blocking mode
                if (-1 == (flags = fcntl(portfd, F_GETFL, 0))) {
                    flags = 0;
                }
                fcntl(portfd, F_SETFL, flags | O_NONBLOCK);
            */
        }
    
        char buf[INET6_ADDRSTRLEN];
        buf[0] = '\0';

        if (type == SERVER_TCP) {

            peerName(cn->fileDescriptor,buf,INET6_ADDRSTRLEN);

            if (!isAllowed(buf)) {
                INFO2("[DS]: socketAccept: host %s is not in the list of accepted host, close connection", buf);
                write(cn->fileDescriptor,CMD_STR_DISCONNECT,strlen(CMD_STR_DISCONNECT));

                close(cn->fileDescriptor);
                cn->fileDescriptor = -1;
                conn->state = PEER_DISCONNECTED;

                return -1;
            }
        }

        if (getUsePassword() && !getIViewer()) {
            logger(L_DBG,"[DS]: socketAccept: Do password verification");

            int ret = EXIT_OK;

            int i = 0;
            for ( ; i<3; i++) {

                ret = verifyPassword(cn->fileDescriptor);

                if (ret == EXIT_OK) {    // got it
                   break;
                }
            }

            if (ret != EXIT_OK) {

                if (ret == EXIT_NOK) {  // if it is EXIT_STOP connection is already lost
                    write(cn->fileDescriptor,CMD_STR_DISCONNECT,strlen(CMD_STR_DISCONNECT));
                }

                close(cn->fileDescriptor);
                cn->fileDescriptor = -1;
                conn->state = PEER_DISCONNECTED;

                return -1;
            }

            logger(L_DBG,"[DS]: socketAccept: Password verification OK");
        }
    
        INFO2("[DS]: socketAccept: accepted from %s", buf);
        conn->state = PEER_CONNECTED;
        
        // force to detect H/W and cover size. need to do that before (Connect) or syncPeer() handling
        getClientSize(conn->id, cn->fileDescriptor);  

        break;
    }
    
    logger(L_INF, "[DS]: socketAccept exit");
    return 1;
}

int socketWrite(ConnectInfo* connInfo, const dMessage* msg)
{
    logger(L_DBG, "[DS]: socketWrite");
    if (!msg) {
        return EXIT_OK;
    }
    const char* command = msg->value;
    int count           = msg->size;
    
    if (!command || count <= 0) {
        return EXIT_OK;
    }
    
    if (strcmp("End();",command) == 0) {  // used only for WEB/CMXML
        return EXIT_OK;
    }
    
    _SocketConnection* cn = (_SocketConnection*) connInfo->connectionData;
    if (!cn) {
        return EXIT_NOK;
    }

    // send command
    if (cn->fileDescriptor >= 0) {
    
        if (msg->type == DM_SET) {
            if (isDataOld(connInfo, msg->subtype, command, count)) {
                INFO2("[DS]: Skip to send the same data to TCP peer");
                return EXIT_OK;
            }
        }

        memset(tmp, 0, MAXMAXLEN);
        strcat(tmp, "[DS]: socketWrite ");

        int logSz = (count > 256 ? 255 : count);

        // it is possible to get binary data here
        memcpy(tmp, command, logSz); // Do not dump long commands
        tmp[logSz] = '\0';
        logger(L_DBG, tmp);

        sprintf(tmp, "[DS]: socketWrite %d bytes", count);
        logger(L_INF, tmp);

        int n = write(cn->fileDescriptor,command,count);
        if (n < 0) {
            logger(L_ERR, "[DS]: error writing to socket");
            return EXIT_NOK;
        }
        return EXIT_OK;
    } else {
        logger(L_ERR, "[DS]: error writing to socket: already closed");
    }
    return EXIT_NOK;

}
