/*
 * Copyright (c) 2019 Clementine Computing LLC.
 *
 * This file is part of PopuFare.
 * 
 * PopuFare is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 * 
 * PopuFare 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 Affero General Public License for more details.
 * 
 * You should have received a copy of the GNU Affero General Public License
 * along with PopuFare.  If not, see <https://www.gnu.org/licenses/>.
 *
 */

#include <sys/termios.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/ioctl.h>

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <signal.h>
#include <string.h>
#include <poll.h>
#include <syslog.h>
#include <sysexits.h>
#include <stdarg.h>

#include <dirent.h>
#include <fnmatch.h>

#include "common_defs.h"

#ifndef DEFAULT_BAUD
    #define DEFAULT_BAUD 	(B115200)
#endif


//------------------------------------------- This horrible blob of code goes and hunts for package install records and gathers
//------------------------------------------- them into a sane data structure for display to the user in status/diagnostic screens.
#define PKG_SEARCH_DIR	CONFIG_FILE_PATH
#define PATTERN_SUFFIX  ".tgz.version"
#define CHECKSUM_SUFFIX ".tgz.checksum"
#define PATTTERN        ("*" PATTERN_SUFFIX)



static int package_filter(const struct dirent *file)
{
    int match = fnmatch(PATTTERN, file->d_name, 0);
    return !match;
}


int find_packages(package_signature *pkgs, int n)
{
    int i = 0;
    int scandir_ret;
    struct dirent **matchlist;
    
    int j;
    int match_len;
    int suffix_len = strlen(PATTERN_SUFFIX);
    FILE *f;

    struct stat st;

    char pathbuffer[1024];

    //Look for any file that matches the pattern PATTERN (see package_filter above) and return into matchlist a pointer
    //to and array of pointers to dirent structures for each directory entry that made the cut, sorted alphabetically
    scandir_ret = scandir(PKG_SEARCH_DIR, &matchlist, package_filter, alphasort);

    //If that seemed to work....
    if( (scandir_ret >= 0) && (matchlist != NULL) )
    {
        //Walk the resultant list
        for(i=0; i < scandir_ret; i++)
        {
            //if we've filled our caller's buffer, break out now
            if(i >= n)
                break;
        
            //Go figure out how long the module name part is (it'll be the length of the filename minus the length of the suffix)
            match_len = strlen(matchlist[i]->d_name) - suffix_len;
            
            //If that would cause an overflow, truncate it...
            if(match_len > (PKG_STRING_SIZE - 1))
            {
                match_len = PKG_STRING_SIZE - 1;
            }
        
            //Copy our package name
            strncpy(pkgs[i].pkgname, matchlist[i]->d_name, match_len);
            pkgs[i].pkgname[match_len] = '\0';
        
        
            //(if fscanf fails, we want an empty string rather than random crap from memory...)
            pkgs[i].pkgver[0] = '\0';

            //generate the file name of the package version string...            
            snprintf(pathbuffer, sizeof(pathbuffer), "%s%s%s", PKG_SEARCH_DIR, pkgs[i].pkgname, PATTERN_SUFFIX);
            
            //Go and read the contents of that into the pkgver part of our structure...
            f = fopen(pathbuffer, "rb");
            if(f)
            {
                fscanf(f, PKG_STRING_FORMAT, pkgs[i].pkgver);
                fclose(f);
            }
        

            pkgs[i].checksum[0] = '\0';
            
            //generate the file name of the file containing the package checksum
            snprintf(pathbuffer, sizeof(pathbuffer), "%s%s%s", PKG_SEARCH_DIR, pkgs[i].pkgname, CHECKSUM_SUFFIX);

            //Go and read the contents of that into the checkdum part of our structure...
            f = fopen(pathbuffer, "rb");            
            if(f)
            {
                fscanf(f, PKG_STRING_FORMAT, pkgs[i].checksum);
                fclose(f);
            }
        
        
            pkgs[i].installed = 0;
            //Go and grab the mtime of the package install file
            if(!stat(pathbuffer, &st))
            {
                pkgs[i].installed = st.st_mtime;
            }
        
            //Free this dirent
            free(matchlist[i]);
        }
    
        //Clean up any leftovers
        for(j = i; j < scandir_ret; j++)
        {
            free(matchlist[i]);
        }
    
        //free the array of pointers
        free(matchlist);
    }
    
    //return the number actually filled
    return i;
}
//--------------------------------------------------------------------------------------------------

//This function checks the dropfile to see if the tunnel is up...
int tunnel_is_up()
{
    struct stat st;
    int retval;
    
    retval = stat(TUNNEL_DROPFILE, &st);
    
    if(retval)
    {
        return 0;
    }
    else
    {
        return 1;
    }
}

//This function checks the dropfile to see if the GPRS modem is up...
int gprs_is_up()
{
    struct stat st;
    int retval;
    
    retval = stat(GPRS_DROPFILE, &st);
    
    if(retval)
    {
        return 0;
    }
    else
    {
        return 1;
    }
}

//This function gets the equipment number from the appropriate config file
//If it cannot be gotten, it returns -1
int get_equip_num()
{
    FILE *f;
    int num = -1;
    
    f = fopen(EQUIPNUM_FILE, "rb");
    
    if(f)
    {
        fscanf(f,"%d",&num);
        fclose(f);
    }
    else
    {
        num = -1;
    }

    return num;    
}

//This function sets the euipment number in the config file.
//if this operation fails, -1 is returned.
int set_equip_num(int num)
{
    FILE *f;

    if( (num < 1) || (num > MAX_EQUIPNUM) )
    {
        return -1;
    }
    
    f = fopen(EQUIPNUM_TEMPFILE, "wb");
    
    if(f)
    {
        fprintf(f,"%d\n", num);
        fclose(f);
        rename(EQUIPNUM_TEMPFILE, EQUIPNUM_FILE);
        sync();
        return 0;
    }
    
    return -1;
}

int get_server_desc(char *desc, int len)
{
    char svrname[LINE_BUFFER_SIZE];
    FILE *f;
    
    if(!desc)
        return -1;
    
    if(len <= 0)
        return -1;
    
    f = fopen(SERVER_DESC_FILE, "rb");
    
    if(f)
    {
        fgets(svrname, LINE_BUFFER_SIZE, f);
        svrname[LINE_BUFFER_SIZE - 1] = '\0';
        strip_crlf(svrname);
        
        strncpy(desc, svrname, len);
        desc[len - 1] = '\0';
        return 0;
    }    
        
    return -1;
}

int get_field(char *dest, char *src, int dest_len, int *eol_flag)
{
    int i,j;
    int done = 0;
    int eol = 0;
    
    i = j = 0;

    while( ! done )
    {
        switch(src[i])
        {
            case '\0':		//If we've hit the end of the input line
            case '\n':
            case '\r':
              
              if(j < dest_len)  //cap off our destination string if there is room
              {
                  dest[j++] = '\0';
              }
              
              eol = 1;		//set our internal End Of Line flag
              done = 1;		//set the done flag so we break out of the while
              break;
        
            case '\t':		//If we have hit a record boundary	
              if(j < dest_len)	//cap off our input line
              {
                  dest[j++] = '\0';
              }
              
              i++;		//Advance past this input character
              done = 1;		//flag us as done
              break;
              
            default:		//otherwise (for normal characters) just copy them if there is space
              if(j < dest_len)
              {
                  dest[j++] = src[i];
              }
              i++; 		//and advance past them             
              break;
        }
    }

    if( j == dest_len )		//If we have filled our line to capacity
    {				//Truncate it by 1 character to fit the terminating NUL
        dest[dest_len - 1] = '\0';	
    }

    if( eol_flag != NULL )	//If the user has passed us a place to store our EOL flag,
    {
        *eol_flag = eol;	//store it there
    }
    
    return i;			//Return the index of the next character to be consumed.
}


unsigned long long stringhash(char *string)           //magic hash function hashes english strings well enough for our needs
{
    unsigned long long hash = 5381;
    int c;

    while ((c = *string++))
      hash = ((hash << 5) + hash) + c; /* hash * 33 + c */

    return hash;
}

int strip_crlf(char *buffer)
{
  char *src,*dst;
  int count=0;
  
  //Set both our source and destination pointers to the buffer in question
  src=dst=buffer;

  //While we still have unprocessed buffer bytes
  while(*src)
  {
    //If we encounter a CR or LF character
    if((*src == '\r') || (*src == '\n'))
    {
      //If the NEXT character is not another EOL character or a terminating nul
      if( (src[1] != '\0') && (src[1] != '\r') && (src[1] != '\n') )
      {
          src++;
          *dst++ = ' ';	//replace the CR or LF with a space.
          count++;
          continue;
      }
      else	//On the other hand, if what comes next is EOL or end of string,
      {
          src++;	//just trim this character
          continue;
      }
    }
    
    //If this character isn't one of the special ones, just copy it.
    *dst++ = *src++;
    count++;
  }  
  
  //Copy the terminating NUL
  *dst++ = *src++;
  return count;
}

int open_rs232_device(char *devname, int custom_baud, int linemode)
{
    struct termios tty={0};
    int retval;
    int fd;
    
    //Open the specified character device READ/WRITE and not as a Controlling TTY
    fd = open(devname, O_RDWR | O_NOCTTY);
    
    if(fd < 0)
    {
        fprintf(stderr, "Warning:  Cannot open TTY %s\n", devname);
        return -1;
    }
    
    //Try and fetch the TTY properties of the device
    retval = ioctl(fd, TCGETS, &tty);
    
    if(retval)
    {
        fprintf(stderr, "Warning:  Cannot get TTY attributes on %s.  Not a TTY?\n", devname);
        close(fd);
        return -1;
    }

    
    if(custom_baud > 0)
    {
        tty.c_cflag = custom_baud | CS8 | CREAD | CLOCAL;
    }
    else
    {
        tty.c_cflag = DEFAULT_BAUD | CS8 | CREAD | CLOCAL;
    }


    if(linemode)
    {
        tty.c_iflag = IGNBRK;
        tty.c_oflag = 0;
        tty.c_lflag = ICANON;
        tty.c_line = 0;
        tty.c_cc[VMIN] = 1;			//minimum one character read
        
        tty.c_cc[VTIME] = ACCUM_SECONDS * 10;	//wait ACCUM_SECONDS without any further chars 
                                                //before giving up on the arrival of a newline.
                                                //(VTIME is specified in deciseconds (who knows why...))
                                                
        tty.c_cc[VEOL] = '\n';			//allow a newline to release the buffer
    }
    else
    {
        tty.c_iflag = IGNBRK;
        tty.c_oflag = 0;
        tty.c_lflag = 0;
        tty.c_line = 0;
        tty.c_cc[VMIN] = 1;
        tty.c_cc[VTIME] = 5;
    }
    
    //try and plunk our desired settings down on the device    
    retval = ioctl(fd, TCSETS, &tty);
    
    if(retval)
    {
        fprintf(stderr, "Warning:  Cannot set TTY attributes on %s. Unsupported mode?\n", devname);
        close(fd);
        return -1;
    }

    //flush the serial port buffers to clean up any detritus that has accumulated before we got here
    tcflush(fd, TCIOFLUSH);

    return fd;
}


static int read_with_timeout(int fd, void *buffer, int n, int timeout)
{
    int retval;
    struct pollfd fds[1];
    
    fds[0].fd = fd;
    fds[0].events = POLLIN;
    
    retval = poll(fds, 1, timeout);
    
    if(retval > 0)
    {
        return read(fd, buffer, n);
    }
    else
    {
        return retval;
    }
}

static int init_device(int fd, device_test_vector *vec, char *diag)
{
    char buffer[1024] = {0};

    int i;
    int retval;
    int len;
    char *trav;
    
    int timeout = DEVICE_TEST_TIMEOUT;

    if(vec->init_timeout > 0)
    {
        timeout = vec->init_timeout;
    }

    if(vec->init_string != NULL)	//If we were given an init string
    {
        len = strlen(vec->init_string);	//calculate how long it is
        
        retval = write(fd, vec->init_string, len);	//send it on its way
        
        if(retval != len)	//if that didn't work
        {
            if(diag)	//complain and fail
            {
                snprintf(diag, DIAG_BUFFER_SIZE, "Cannot write init string to device.");
            }
            
            return -1;
        }
    }    
  
    i = 0;
    
    while(i < vec->n_reply_lines)
    {
        retval = read_with_timeout(fd, buffer, sizeof(buffer), timeout);	//Ask the kernel for a line from the TTY
            
        if(retval > 0)	//If we DID read something from the device, make sure it is correct
        {
            buffer[retval] = '\0';
            strip_crlf(buffer);
               
            trav = buffer;
                 
//            printf("%d: %s\n", i, trav);
            
            //Skip any garbage before the start character
            while(*trav && (*trav != '/'))
                trav++;
            
            if(*trav == '\0')
            {
                continue;	//ignore blank lines
            }
            
            if( (trav[0] == '/') && (trav[1] == '?') && (trav[2] == ':') )
            {
                continue;	//ignore device ID lines
            }
                
            //The order of these tests in importand.  Short circuit keeps us from 
            //dereferencing expected_reply_lines if the first test fails    
            if( (vec->reply_strings != NULL) && (vec->reply_strings[i] != NULL) )	//If we have a pattern to test against
            {
                
                if(strcmp(trav, vec->reply_strings[i]))
                {
                    if(diag)
                    {
                        snprintf(diag, DIAG_BUFFER_SIZE, "Read init reply line %d from device, expected \"%s\" but got \"%s\"", i, vec->reply_strings[i], trav);
                    }
                    
                    return -1;
                }
            }
                
        }
        else	//If we DIDN'T read anything from the device...
        {
            if(diag)	//Complain
            {
                snprintf(diag, DIAG_BUFFER_SIZE, "Reading init reply from device timed out waiting for line %d", i);
            }
            return -1;	//And fail
        }
        
        i++;
    }

    return 0;    
}

int test_and_init_device(int fd, device_test_vector *vec, char *diag)
{
    char buffer[DEV_INIT_BUFFER_SIZE] = {0};
    char module_id[DEV_INIT_BUFFER_SIZE] = {0};

    int tries = DEVICE_TEST_TRIES;
    int timeout = DEVICE_TEST_TIMEOUT;

    int retval;
    char *trav;
    
    if(vec == NULL)
    {
        return -1;
    }
    
    if(vec->dev_id == NULL)
    {
        return -1;
    }
    
    if(vec->init_tries > 0)
    {
        tries = vec->init_tries;
    }
    
    if(vec->init_timeout > 0)
    {
        timeout = vec->init_timeout;
    }
    
    do	//We want to iterate through DEVICE_TEST_TRIES tries at getting a valid line from the device
    {
        write(fd,"\r", 1);				//Send a CR to stimulate the device to spit out its help message
        retval = read_with_timeout(fd, buffer, sizeof(buffer), timeout);	//Ask the kernel for a line from the TTY
        
        //If we actually got a line of data
        if(retval > 0)
        {
            buffer[retval] = '\0';
            strip_crlf(buffer);        
        
            //Start examining our buffer
            trav = buffer;	
            
            //Skip any garbage before the start character
            while(*trav && (*trav != '/'))
                trav++;

            //See if it is our Device ID / help line...
            if( (trav[0] == '/') && (trav[1] == '?') && (trav[2] == ':') )
            {
                trav += 3;	//Skip the header and go to the body

                retval = sscanf(trav, " ?=%s", module_id);	//Look for our module ID string
                
                if(retval < 1)
                {
                    retval = sscanf(trav, "?=%s", module_id);      //Look for our module ID string without leading space
                }
                
                if(retval == 1)		//If we have found id
                {
                    if(!strcmp(module_id, vec->dev_id))	//See if it is the correct one
                    {
                        if(diag)	//If so, pass on our diagnostic message if we have a place to
                        {
                            snprintf(diag, DIAG_BUFFER_SIZE, "Device connected OK");
                        }

                        return init_device(fd, vec, diag);   //Perform initialization and return the status of that operation
                    }
                    else	//Otherwise, if it is NOT the one we are expecting
                    {
                        if(diag)	//Complain if we have a place to
                        {
                            snprintf(diag, DIAG_BUFFER_SIZE, "Device present:  Expecting: \"%s\" Got: \"%s\"", vec->dev_id, module_id);
                        }
                        
                        return -2;	//Return a distinct failure code
                    }
                }
            }
            else	//If we DIDN'T get the line we were looking for, pretend it was blank so we'll try again...
            {
                retval = 0;	//This pretends the received line was black.
            }    
            
        }	//if we got 0 bytes or EINTR from the alarm firing
        
    } while( (retval <= 0) && ( --tries > 0) );	//While we still have tries and a previous try didn't work
    
    if(diag)
    {
        snprintf(diag, DIAG_BUFFER_SIZE, "Could not get reply from device");
    }
    return -1;
}

//========================================================================================================
//------------------------------- WATCHDOG TIMER and other SIGNAL HANDLERS -------------------------------
//========================================================================================================

volatile int hup_request_status = 0;

void request_hup(char *fmt, ...)
{
    va_list ap;
    
    hup_request_status = 1;

    va_start(ap, fmt);
        vprintf(fmt, ap);
    va_end(ap);
}

//Our signal handlers and standard message handlers will OR these bits into exit_request_status

volatile int exit_request_status = 0;
static int exit_signal_counter = 0;

void request_polite_exit(int reason, char *fmt, ...)
{
    va_list ap;
    
    exit_request_status |= reason;
    
    if(reason)
    {
        exit_signal_counter++;
    }
    
    if(exit_request_status & EXIT_REQUEST_CRASH)
    {
        va_start(ap, fmt);
            vsyslog(LOG_ERR, fmt, ap);
        va_end(ap);
        
        exit(EX_SOFTWARE);
    }

    if(exit_signal_counter >= MAX_POLITE_EXIT_REQUESTS)
    {
        va_start(ap, fmt);
            vsyslog(LOG_NOTICE, fmt, ap);
        va_end(ap);
        
        exit(SIGTERM);    
    }
}

static void watchdog_handler(int signum, siginfo_t *info, void *data)
{
    request_polite_exit(EXIT_REQUEST_CRASH, "Watchdog timer has expired!");
}

static void term_int_handler(int signum, siginfo_t *info, void *data)
{
    request_polite_exit(EXIT_REQUEST_INT_TERM, "Received signal %d", signum);    
}

static void hard_crash_handler(int signum, siginfo_t *info, void *data)
{
    switch(signum)
    {
        case SIGSEGV:
            request_polite_exit(EXIT_REQUEST_CRASH, "Segmentation fault at virtual address %p", info->si_addr); 
            break;
            
        case SIGILL:
            request_polite_exit(EXIT_REQUEST_CRASH, "Illegal instruction at virtual address %p", info->si_addr);
            break;
            
        case SIGFPE:
            request_polite_exit(EXIT_REQUEST_CRASH, "Floating point exception at virtual address %p", info->si_addr);
            break;
            
        case SIGBUS:
            request_polite_exit(EXIT_REQUEST_CRASH, "SIGBUS (hardware error!) at address %p", info->si_addr);
            break;
            
        default:
            request_polite_exit(EXIT_REQUEST_CRASH, "Caught Signal %d", signum);          
            break;
    }
}

void configure_signal_handlers(char *procname)
{
    struct sigaction sa = {{0}};

    openlog(procname, LOG_CONS | LOG_PERROR, LOG_USER);
    
    #ifdef USE_WATCHDOG_ALARM
        sa.sa_sigaction = watchdog_handler;	
        sa.sa_flags =  SA_SIGINFO;		
        sigfillset(&sa.sa_mask);			

        sigaction(SIGALRM, &sa, NULL);
        
        RESET_WATCHDOG();
    #endif

    //Install our "boy did we ever fuck up this time" signal handler
    //to trap segmentation faults, illegal instructions, divides by zero,
    //and things like RAM chips popping off a running board (SIGBUS).
    sa.sa_sigaction = hard_crash_handler;
    sa.sa_flags =  SA_SIGINFO;
    sigaction(SIGSEGV, &sa, NULL);
    sigaction(SIGILL, &sa, NULL);
    sigaction(SIGFPE, &sa, NULL);
    sigaction(SIGBUS, &sa, NULL);
    
    //Install our polite exit handler...
    sa.sa_sigaction = term_int_handler;	
    sa.sa_flags =  SA_SIGINFO | SA_RESTART;	//Allow interrupted I/O calls to finish to facilitate clean exit
    sigaction(SIGTERM, &sa,  NULL);
    sigaction(SIGINT, &sa,  NULL);
}

//------------------


