//-----------------------------------------------------------------------------
// Copyright 2018 Thiago Alves
// This file is part of the OpenPLC Software Stack.
//
// OpenPLC 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.
//
// OpenPLC 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 OpenPLC.  If not, see <http://www.gnu.org/licenses/>.
//------
//
// This is the file for the interactive server. It has procedures to create
// a socket, bind it, start network communication, and process commands. The 
// interactive server only responds to localhost and it is used to communicate
// with the Python webserver GUI only.
//
// Thiago Alves, Jun 2018
//-----------------------------------------------------------------------------

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <netdb.h>
#include <string.h>
#include <pthread.h>
#include <arpa/inet.h>
#include <fcntl.h>
#include <time.h>

#include "ladder.h"

//Global Variables
bool run_modbus = 0;
int modbus_port = 502;
bool run_dnp3 = 0;
int dnp3_port = 20000;
unsigned char server_command[1024];
int command_index = 0;
bool processing_command = 0;
time_t start_time;
time_t end_time;

//Global Threads
pthread_t modbus_thread;
pthread_t dnp3_thread;

//-----------------------------------------------------------------------------
// Start the Modbus Thread
//-----------------------------------------------------------------------------
void *modbusThread(void *arg)
{
    startServer(modbus_port);
}

//-----------------------------------------------------------------------------
// Start the DNP3 Thread
//-----------------------------------------------------------------------------
void *dnp3Thread(void *arg)
{
    dnp3StartServer(dnp3_port);
}

//-----------------------------------------------------------------------------
// Read the argument from a command function
//-----------------------------------------------------------------------------
int readCommandArgument(unsigned char *command)
{
    int i = 0;
    int j = 0;
    unsigned char argument[1024];
    
    while (command[i] != '(' && command[i] != '\0') i++;
    if (command[i] == '(') i++;
    while (command[i] != ')' && command[i] != '\0')
    {
        argument[j] = command[i];
        i++;
        j++;
        argument[j] = '\0';
    }
    
    return atoi(argument);
}

//-----------------------------------------------------------------------------
// Create the socket and bind it. Returns the file descriptor for the socket
// created.
//-----------------------------------------------------------------------------
int createSocket_interactive(int port)
{
    unsigned char log_msg[1000];
    int socket_fd;
    struct sockaddr_in server_addr;

    //Create TCP Socket
    socket_fd = socket(AF_INET,SOCK_STREAM,0);
    if (socket_fd<0)
    {
        sprintf(log_msg, "Interactive Server: error creating stream socket => %s\n", strerror(errno));
        log(log_msg);
        exit(1);
    }
    
    //Set SO_REUSEADDR
    int enable = 1;
    if (setsockopt(socket_fd, SOL_SOCKET, SO_REUSEADDR, &enable, sizeof(int)) < 0)
        perror("setsockopt(SO_REUSEADDR) failed");
        
    SetSocketBlockingEnabled(socket_fd, false);
    
    //Initialize Server Struct
    bzero((char *) &server_addr, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = inet_addr("127.0.0.1");
    server_addr.sin_port = htons(port);

    //Bind socket
    if (bind(socket_fd,(struct sockaddr *)&server_addr,sizeof(server_addr)) < 0)
    {
        sprintf(log_msg, "Interactive Server: error binding socket => %s\n", strerror(errno));
        log(log_msg);
        exit(1);
    }
    // we accept max 5 pending connections
    listen(socket_fd,5);
    sprintf(log_msg, "Interactive Server: Listening on port %d\n", port);
    log(log_msg);

    return socket_fd;
}

//-----------------------------------------------------------------------------
// Blocking call. Wait here for the client to connect. Returns the file
// descriptor to communicate with the client.
//-----------------------------------------------------------------------------
int waitForClient_interactive(int socket_fd)
{
    int client_fd;
    struct sockaddr_in client_addr;
    socklen_t client_len;

    printf("Interactive Server: waiting for new client...\n");

    client_len = sizeof(client_addr);
    while (run_openplc)
    {
        client_fd = accept(socket_fd, (struct sockaddr *)&client_addr, &client_len); //non-blocking call
        if (client_fd > 0)
        {
            SetSocketBlockingEnabled(client_fd, true);
            break;
        }
        sleepms(100);
    }

    return client_fd;
}

//-----------------------------------------------------------------------------
// Blocking call. Holds here until something is received from the client.
// Once the message is received, it is stored on the buffer and the function
// returns the number of bytes received.
//-----------------------------------------------------------------------------
int listenToClient_interactive(int client_fd, unsigned char *buffer)
{
    bzero(buffer, 1024);
    int n = read(client_fd, buffer, 1024);
    return n;
}

//-----------------------------------------------------------------------------
// Process client's commands for the interactive server
//-----------------------------------------------------------------------------
void processCommand(unsigned char *buffer, int client_fd)
{
    unsigned char log_msg[1000];
    int count_char = 0;
    
    if (processing_command)
    {
        count_char = sprintf(buffer, "Processing command...\n");
        write(client_fd, buffer, count_char);
        return;
    }
    
    if (strncmp(buffer, "quit()", 6) == 0)
    {
        processing_command = true;
        sprintf(log_msg, "Issued quit() command\n");
        log(log_msg);
        if (run_modbus)
        {
            run_modbus = 0;
            pthread_join(modbus_thread, NULL);
            sprintf(log_msg, "Modbus server was stopped\n");
            log(log_msg);
        }
        if (run_dnp3)
        {
            run_dnp3 = 0;
            pthread_join(dnp3_thread, NULL);
            sprintf(log_msg, "DNP3 server was stopped\n");
            log(log_msg);
        }
        run_openplc = 0;
        processing_command = false;
    }
    else if (strncmp(buffer, "start_modbus(", 13) == 0)
    {
        processing_command = true;
        sprintf(log_msg, "Issued start_modbus() command to start on port: %d\n", readCommandArgument(buffer));
        log(log_msg);
        modbus_port = readCommandArgument(buffer);
        if (run_modbus)
        {
            sprintf(log_msg, "Modbus server already active. Restarting on port: %d\n", modbus_port);
            log(log_msg);
            //Stop Modbus server
            run_modbus = 0;
            pthread_join(modbus_thread, NULL);
            sprintf(log_msg, "Modbus server was stopped\n");
            log(log_msg);
        }
        //Start Modbus server
        run_modbus = 1;
        pthread_create(&modbus_thread, NULL, modbusThread, NULL);
        processing_command = false;
    }
    else if (strncmp(buffer, "stop_modbus()", 13) == 0)
    {
        processing_command = true;
        sprintf(log_msg, "Issued stop_modbus() command\n");
        log(log_msg);
        if (run_modbus)
        {
            run_modbus = 0;
            pthread_join(modbus_thread, NULL);
            sprintf(log_msg, "Modbus server was stopped\n");
            log(log_msg);
        }
        processing_command = false;
    }
    else if (strncmp(buffer, "start_dnp3(", 11) == 0)
    {
        processing_command = true;
        sprintf(log_msg, "Issued start_dnp3() command to start on port: %d\n", readCommandArgument(buffer));
        log(log_msg);
        dnp3_port = readCommandArgument(buffer);
        if (run_dnp3)
        {
            sprintf(log_msg, "DNP3 server already active. Restarting on port: %d\n", dnp3_port);
            log(log_msg);
            //Stop DNP3 server
            run_dnp3 = 0;
            pthread_join(dnp3_thread, NULL);
            sprintf(log_msg, "DNP3 server was stopped\n");
            log(log_msg);
        }
        //Start DNP3 server
        run_dnp3 = 1;
        pthread_create(&dnp3_thread, NULL, dnp3Thread, NULL);
        processing_command = false;
    }
    else if (strncmp(buffer, "stop_dnp3()", 11) == 0)
    {
        processing_command = true;
        sprintf(log_msg, "Issued stop_dnp3() command\n");
        log(log_msg);
        if (run_dnp3)
        {
            run_dnp3 = 0;
            pthread_join(dnp3_thread, NULL);
            sprintf(log_msg, "DNP3 server was stopped\n");
            log(log_msg);
        }
        processing_command = false;
    }
    else if (strncmp(buffer, "runtime_logs()", 14) == 0)
    {
        processing_command = true;
        printf("Issued runtime_logs() command\n");
        write(client_fd, log_buffer, log_index);
        processing_command = false;
        return;
    }
    else if (strncmp(buffer, "exec_time()", 11) == 0)
    {
        processing_command = true;
        time(&end_time);
        count_char = sprintf(buffer, "%llu\n", (unsigned long long)difftime(end_time, start_time));
        write(client_fd, buffer, count_char);
        processing_command = false;
        return;
    }
    else
    {
        processing_command = true;
        count_char = sprintf(buffer, "Error: unrecognized command\n");
        write(client_fd, buffer, count_char);
        processing_command = false;
        return;
    }
    
    count_char = sprintf(buffer, "OK\n");
    write(client_fd, buffer, count_char);
}

//-----------------------------------------------------------------------------
// Process client's request
//-----------------------------------------------------------------------------
void processMessage_interactive(unsigned char *buffer, int bufferSize, int client_fd)
{
    for (int i = 0; i < bufferSize; i++)
    {
        if (buffer[i] == '\r' || buffer[i] == '\n' || command_index >= 1024)
        {
            processCommand(server_command, client_fd);
            command_index = 0;
            break;
        }
        server_command[command_index] = buffer[i];
        command_index++;
        server_command[command_index] = '\0';
    }
}

//-----------------------------------------------------------------------------
// Thread to handle requests for each connected client
//-----------------------------------------------------------------------------
void *handleConnections_interactive(void *arguments)
{
    int client_fd = *(int *)arguments;
    unsigned char buffer[1024];
    int messageSize;

    printf("Interactive Server: Thread created for client ID: %d\n", client_fd);

    while(run_openplc)
    {
        //unsigned char buffer[1024];
        //int messageSize;

        messageSize = listenToClient_interactive(client_fd, buffer);
        if (messageSize <= 0 || messageSize > 1024)
        {
            // something has  gone wrong or the client has closed connection
            if (messageSize == 0)
            {
                printf("Interactive Server: client ID: %d has closed the connection\n", client_fd);
            }
            else
            {
                printf("Interactive Server: Something is wrong with the  client ID: %d message Size : %i\n", client_fd, messageSize);
            }
            break;
        }

        processMessage_interactive(buffer, messageSize, client_fd);
    }
    //printf("Debug: Closing client socket and calling pthread_exit in interactive_server.cpp\n");
    closeSocket(client_fd);
    printf("Terminating interactive server connections\r\n");
    pthread_exit(NULL);
}

//-----------------------------------------------------------------------------
// Function to start the server. It receives the port number as argument and
// creates an infinite loop to listen and parse the messages sent by the
// clients
//-----------------------------------------------------------------------------
void startInteractiveServer(int port)
{
    unsigned char log_msg[1000];
    int socket_fd, client_fd;
    socket_fd = createSocket_interactive(port);

    while(run_openplc)
    {
        client_fd = waitForClient_interactive(socket_fd); //block until a client connects
        if (client_fd < 0)
        {
            sprintf(log_msg, "Interactive Server: Error accepting client!\n");
            log(log_msg);
        }

        else
        {
            int arguments[1];
            pthread_t thread;
            int ret = -1;

            printf("Interactive Server: Client accepted! Creating thread for the new client ID: %d...\n", client_fd);
            arguments[0] = client_fd;
            ret = pthread_create(&thread, NULL, handleConnections_interactive, arguments);
            if (ret==0) 
            {
                pthread_detach(thread);
            }
        }
    }
    printf("Closing socket...");
    closeSocket(socket_fd);
    closeSocket(client_fd);
    sprintf(log_msg, "Terminating interactive server thread\r\n");
    log(log_msg);
}
