
/*
 * Evan Krell
 * COSC 3346 - Operating Systems
 * 
 * Professor: Dr. Won
 */

#include <stdio.h>
#include <stdlib.h>
#include <signal.h> 
#include <unistd.h>
#include <string.h>
#include <cctype>
#include <sys/types.h> 
#include <sys/wait.h> 
using namespace std;

//External Declaration of gettocks from guish.l
extern "C"
{
   extern char **gettoks();
}

/*
 * Section B: Function Prototypes
 * See section E for function descriptions
 *
 */

void handleSignal( int signal );
int checkShellCommand( char * sCommand );
bool performShellCommand( int iCommandFlag, char ** saCommandHistory, int iHistoryCurrentLength, bool & bContinue, bool & bRedo );
void commandHistoryAppend( char ** saCommandHistory, int iHistoryMaxLength, int & iHistoryCurrentLength, char * sHistoryElementTemp );
void commandHistoryShift( char ** saCommandHistory, int iHistoryMaxLength, int iHistoryCurrentLength );
void printHelpInfo();

/*
 * Section C: Global Variables
 *
 */

//Version Number
char iVersion[4] = "0.1";
 
//Counters for keeping track of times that the user
//has issued signals: SIGINT, SIGQUIT, and SIGTSTP
int iSigCount_SIGINT;
int iSigCount_SIGQUIT;
int iSigCount_SIGTSTP;

//Enum to indicate the the user selected a "built-in" shell commands
enum ShellCommands
{
   NONE = 0,
   HIST = 1,
   R = 2,
   EXIT = 3,
   ERR = 4
};

//PID for handling forks
pid_t pid;

/*
 * Section D: Main 
 *
 */

int main( int argc, char * argv[] ) 
{

   //If the program was executed with the '-h' flag,
   //display the help info
   if( argc > 1)
   {
      if( strncmp( argv[1], "-h", strlen( argv[1] ) ) == 0 )
      {
         printHelpInfo();
      }
   }

   struct sigaction sa;
    
   /* 
    * Section D.A: Variable, Handler Initialization
    *
    */

   //Initialize the signal counters to 0
   iSigCount_SIGINT = 0;
   iSigCount_SIGQUIT = 0;
   iSigCount_SIGTSTP = 0;

   //General use temporary storage
   int iBufferSize = 2000;
   char sBuffer[iBufferSize];
   for( int i = 0; i < iBufferSize; i++ )
   {
      sBuffer[i] = '0';
   }

   //Strings for information used to populate the shell prompt
   int iUserNameSize = 200;
   char sUserName[iUserNameSize]; //The user's name on the system
   for( int i = 0; i < iUserNameSize; i++ )
   {
      sUserName[i] = '0';
   }
   int iHostNameSize = 200;
   char sHostName[iHostNameSize]; //The computer's host name
   for( int i = 0; i < iHostNameSize; i++ )
   {
      sHostName[i] = '0';
   }
   //String array (2D C-string) to contain user input,
   //with each space-separated 'word' of the input as an element
   char ** saTokens = NULL;

   //2D character array for the user's command history
   int iHistoryMaxLength = 10;
   int iHistoryCurrentLength = 0;
   char ** saCommandHistory;
   saCommandHistory = new char * [iHistoryMaxLength];


   //Temporary storage for concatinate a user's input into on char before storage
   int iHistoryElementSize = 2000;
   char sHistoryElementTemp[iHistoryElementSize];
   for( int i = 0; i < iHistoryElementSize; i++ )
   {
      sHistoryElementTemp[i] = '0';
   }

   //Flag value indicating the current user-input "built-in" shell command
   //Default of 0 indicates that the input was not a built-in command 
   int iShellCommand = NONE;


   //Inititialize the signal handling struct
   sa.sa_handler = &handleSignal;
    
   //If not set, interruptable signals will fail when interrupted with this sig
   sa.sa_flags = SA_RESTART;
    
//!   //Blocks signals while others are being handled.. from what I could tell
   sigfillset(&sa.sa_mask);
    
   //The actual catching of interrupt signals
   if( sigaction( SIGINT, &sa, NULL ) == -1 ) 
   {
      perror( "ERR: SIGINT failed to be handled" ); 
   }

   if( sigaction( SIGQUIT, &sa, NULL ) == -1 )
   {
      perror( "ERR: SIGQUIT failed to be handled" );
   }

   if( sigaction( SIGTSTP, &sa, NULL ) == -1 )
   {
      perror( "ERR: SIGTSTP failed to be handled" );
   }
    
   //Flag to continue execution of main program loop
   //Should switch to false when the user supplies the "exit" command
   bool bContinue = true;

   //Populate sUserName and sHostName

   /*    Temporarily commented out for submission so as to follow assignment
   gethostname( sBuffer, iBufferSize );
   strncpy( sHostName, sBuffer, iHostNameSize );

   strncpy( sBuffer, getenv( "USER" ), iBufferSize );
   strncpy( sUserName, sBuffer, iUserNameSize );
   */

   while( bContinue )
   {
      ////Reset the shell command flag to 0 (NONE)
      iShellCommand = NONE;

      //Boolean to check that the "built-in" shell command was completed
      bool bCommandComplete = false;
      bool bCommandR = false;

        //Clear sHistoryElementTemp for storing the input
      sHistoryElementTemp[ 0 ] = '\0';


      //Print the shell prompt
      printf( "<Krell>" );
      /*    Temporarily commented out for submission so as to follow assignment
      printf( "[%s@%s ]", sUserName, sHostName );
      */

      
      saTokens = gettoks();      

      
      for( int i = 0; saTokens[i] != NULL; i++ )
      {
         strncat( sHistoryElementTemp, saTokens[i], strlen( saTokens[i] ) );
         strncat( sHistoryElementTemp, " ", 1);
      }


      sHistoryElementTemp[ strlen( sHistoryElementTemp ) ] = '\0';

      //Check if the entered command was a "built-in" shell command
      iShellCommand = checkShellCommand( sHistoryElementTemp );


      if( iShellCommand != R && iShellCommand != EXIT && saTokens[0] != NULL )
      {

         commandHistoryAppend( saCommandHistory, iHistoryMaxLength, iHistoryCurrentLength, sHistoryElementTemp );

      }


      //If iShellCommand != 0, a "built-in" shell command was entered, 
      //and shall be handled within performShellCommand
      if(iShellCommand != NONE)
      {
         bCommandComplete = performShellCommand( iShellCommand, saCommandHistory, iHistoryCurrentLength, bContinue, bCommandR ); 
      
         //If the command 'r n' was used.
         //It was intended that all shell command processing would be handled within performShellCommand(), but
         //several more parameters would need to be passed.
         //Perhaps the params should be in a struct?
         if( bCommandR && ( ( atoi( saTokens[1] ) > 0 ) && ( atoi( saTokens[1] ) <= iHistoryCurrentLength )  ) ) 
         {

            
            //String array (2D C-string) to contain the command fetched from history,
            //where element 0 is the command, and elements 1 - n are the command's arguments
            char ** saRedoTokens = NULL;
            
            //Temporary string to covert to 2-D via strtok 
            //Select the command from the history based on user selection
            //Since the format is "r n", saTokens[ 1 ] will have the requested number
            //Despite the formating for the user, the history is 0-based, so subtract 1 to access correct command
            int iRedoCommand = atoi( saTokens[ 1 ] ) - 1;
            int iRedoLength = strlen( saCommandHistory[ iRedoCommand ] );

            char * saRedoCommand = new char[ iRedoLength + 1 ]; //plus 1 top be safe for the null terminator
            for( int i = 0; i < iRedoLength; i++ )
            {
               saRedoCommand[ i ] = '0';
            }

            strncpy( saRedoCommand, saCommandHistory[ iRedoCommand ], iRedoLength );
            
            saRedoCommand[ iRedoLength ] = '\0';

   
            //temp storage of each elem
            char * pch;

            int n_spaces = 0, i;

            int iNumRedoTokens = 0;

            //For the first run-through, determine number of tokens
            pch = strtok ( saRedoCommand ," ");

            while (pch != NULL)
            {
               //allocate space for 2D C-string, saRedoTokens
               iNumRedoTokens++;

               pch = strtok (NULL, " ");

            }

            //Allocate space using the determined number of tokens
            saRedoTokens = new char * [ iNumRedoTokens ];

            int iNumRedoCounter = 0;
 

            //strtoks destroys the string that it uses, so copy once again into saRedoCommnd
            for( int i = 0; i < iRedoLength; i++ )
            {
               saRedoCommand[ i ] = '0';
            }

            strncpy( saRedoCommand, saCommandHistory[ iRedoCommand ], iRedoLength );
            
            saRedoCommand[ iRedoLength ] = '\0';


            pch = new char;

            //This time, read through and make the copies into saRedoTokens
            pch = strtok ( saRedoCommand ," ");

            while (pch != NULL)
            {

               //allocate space for 2D C-string, saRedoTokens

               int iTokenSize = strlen( pch );

               saRedoTokens[ iNumRedoCounter ] = new char[ iTokenSize + 1 ];
               for( int i = 0; i < iTokenSize; i++ )
               {
                  saRedoTokens[ iNumRedoCounter ][ i ] = 'O';
               }

               strncpy( saRedoTokens[ iNumRedoCounter ], pch, iTokenSize );

               saRedoTokens[ iNumRedoCounter ][ iTokenSize ] = '\0';

               iNumRedoCounter++;

               pch = strtok (NULL, " ");

            }

            delete [] saRedoCommand;


            //fork a child process to handle execution of the command
            pid = fork();

            if( pid < 0 ) //If an error occurred, exit
            {
               perror( "Process failed to fork" );
               exit( -1 );
            }
            else if( pid == 0 ) //The child process is executing
            {
               execvp( saRedoTokens[ 0 ], saRedoTokens);

               exit( 0 );
            }
            else //Parent process
            {
               wait( NULL );

               for(int i = 0; i < iNumRedoTokens; i++)
               {
                  delete [] saRedoTokens[i];
               }
               delete [] saRedoTokens;

            }

         }

      }
      else if(saTokens[0] != NULL) //perform the user's command via fork() & exec()
      {

         //fork a child process to handle execution of the command
         pid = fork();

         if( pid < 0 ) //If an error occurred, exit
         {
            perror( "Process failed to fork" );
            exit( -1 );
         }
         else if( pid == 0 ) //The child process is executing
         {
            execvp( saTokens[ 0 ], saTokens );
            exit( 0 );
         }
         else //Parent process
         {
            wait( NULL );
         }

         
      }

   
/*    //DEBUG: display number of caught signals
      printf("\nSIGINT count: %i", iSigCount_SIGINT);
      printf("\nSIGQUIT count: %i", iSigCount_SIGQUIT);
      printf("\nSIGTSTP count: %i", iSigCount_SIGTSTP);
*/

   }
    printf("\n");

    //Clean up memory
    for (int i = 0; i < iHistoryCurrentLength; i++)
    {
       delete [] saCommandHistory[i];
    }

    delete [] saCommandHistory;

    return 0;

}

/*
 * Function: checkShellCommand
 * ---------------------------
 * Examine the user's requested command to see if it is
 * a built-in shell command
 *
 * return: the ID of the shell command as defined in enum ShellCommands
 */

int checkShellCommand( char * sCommand )
{

   char sFirstThreeChars[3];
   strncpy(sFirstThreeChars, sCommand, 3);

   if(strncmp(sCommand, "hist", strlen("hist")) == 0)
   {
      return HIST;
   }
   else if( (strncmp(sFirstThreeChars, "r ", strlen("r ")) == 0)  && isdigit(sFirstThreeChars[2]))
   {
      return R;
   }
   else if(strncmp(sCommand, "exit", strlen("exit")) == 0)
   {
      return EXIT;
   }
   else
   {
      return NONE;
   }
}

/*
 * Function: performShellCommand
 * -----------------------------
 * Determine the action to take in response to a shell command
 *
 * HIST - Diplay history
 * R - Redo the nth command in history
 * EXIT - Set bContinue to false to terminate program
 *
 * iCommand: numeric command ID
 * saCommandHistory: 2D string ptr to history of commands entered by the user
 * iHistoryMaxLength: Maximum number of commands to remember in history
 * bContinue: Flag to determine if the program should continue
 * bRedo: If true, redo the nth command
 *
 * return: true if one of the commands were selected and executed
 *    false, otherwise
 *
 * side-effects: bContinue is set to false if EXIT, bRedo is set to true if R
 */

bool performShellCommand( int iCommand, char ** saCommandHistory, int iHistoryCurrentLength, bool & bContinue, bool & bRedo )
{
   if(iCommand == HIST)
   {
      for(int i = 0; i < iHistoryCurrentLength; i++)
      {
         printf("\n%i] %s", i + 1, saCommandHistory[i]);
      }
      printf("\n");
      return true;
   }
   else if(iCommand == R)
   {
      bRedo = true;
      return true;
   }
   else if(iCommand == EXIT)
   {
      bContinue = false;
      return true;
   }
   else
   {
      return false;
   }
}

/*
 * Function: handleSignal
 * ----------------------
 * This is just for testing out signal handling.
 * If SIGINT, SIGQUIT, or SIGTSTP are caught, their counters are incremented
 *
 * signal: integer ID of signal
 *
 * return: void
 *
 * Side-effects: modified iSigCount_SIGTSTP, iSigCount_SIGQUIT, iSigCount_SIGINT 
 *    global variables
 */
 
void handleSignal(int signal) {

   const char *signal_name;
   sigset_t pending;
    
   //Determine which signal is being handled
   //And increment the corrosponding counter
   switch (signal) 
   {
      case SIGINT:
         iSigCount_SIGINT++;
         return;
      case SIGQUIT:
         iSigCount_SIGQUIT++;
         return;
      case SIGSTOP:
         return;
      case SIGTSTP:
         iSigCount_SIGTSTP++;
         return;
      default:

      fprintf(stderr, "catch error, %d, is unhandled\n", signal);

      return;

   }
}

/*
 * Function: commandHistoryAppend
 * ------------------------------
 *
 *  Whenever a user executes a command, that command should be stored in history,
 *  up to the a certain maximum history length
 *  This function handles the allocation and copying associated with the history list.
 *
 *  saCommandHistory: 2D string ptr to history of commands entered by the user
 *  iHistoryMaxLength: Maximum number of commands to remember in history
 *  iHistoryCurrentLength: Current number of commands remembered in history
 *  sHistoryElementTemp: The command to be appended to the history list
 *
 *  returns: void, but saCommandHistory is passed by reference.
 *    iHistoryCurrentLength is also passed by reference, and is incremented if current < maximum
 */

void commandHistoryAppend( char ** saCommandHistory, int iHistoryMaxLength, int & iHistoryCurrentLength, char * sHistoryElementTemp )
{
   int iLength = iHistoryCurrentLength;

   if( iHistoryCurrentLength >= iHistoryMaxLength )
   {
      commandHistoryShift( saCommandHistory, iHistoryMaxLength, iHistoryCurrentLength );
      iLength--;
   }
   else
   {
      iHistoryCurrentLength++;
   }
      
      saCommandHistory[ iLength ] = new char[ strlen( sHistoryElementTemp ) + 1 ];
      for( int i = 0; i < strlen( sHistoryElementTemp ) + 1; i++ )
      {
         saCommandHistory[ iLength ][ i ] = '0';
      }
      saCommandHistory[ iLength ][ 0 ] = '\0';
      strncpy( saCommandHistory[ iLength ], sHistoryElementTemp, strlen( sHistoryElementTemp ) );
      saCommandHistory[ iLength ][ strlen( sHistoryElementTemp ) ] = '\0';
}


/*
 * Function: commandHistoryShift
 * -----------------------------
 * The number of commands to remember is limited by a maximum value.
 *    If that maximum is exceeded, the earliest history is "forgotten" to make
 *    room for the new command
 *
 * saCommandHistory: 2D string ptr to history of commands entered by the user
 * iHistoryMaxLength: Maximum number of commands to remember in history
 * iHistoryCurrentLength: Current number of commands remembered in history
 *
 * returns: void, but saCommandHistory is passed by reference
 */

void commandHistoryShift( char ** saCommandHistory, int iHistoryMaxLength, int iHistoryCurrentLength)
{

//! //Use as mem template, as valgrind reported no error
   for(int i = 1; i < iHistoryMaxLength; i++)
   {
      delete [] saCommandHistory[ i - 1 ];
      int iCommLength = strlen( saCommandHistory[i] ) + 1;
      saCommandHistory[ i - 1 ] = new char[ iCommLength ];
      for( int j = 0; j < iCommLength; j++ )
      {
         saCommandHistory[ i - 1 ][j] = '0';
      }
      strncpy(saCommandHistory[ i - 1 ], saCommandHistory[i], iCommLength );
      saCommandHistory[ i - 1 ][ iCommLength - 1 ] = '\0';
   }

}

/*
 * Function: printHelpInfo
 * -----------------------
 * Prints out version and usage information
 *
 *  returns: void
 */

void printHelpInfo()
{
   printf("\nKrellShell version %s", iVersion);
   printf("\nSimple shell program demonstrating the use of fork() and exec()");
   printf("\nUsage:");
   printf("\nEnter a command with its arguments to execute, such as \"ls\" or \"mplayer /dev/sr0\"");
   printf("\nEnter 'hist' to print the 10 most recent commands");
   printf("\nEnter 'r n' where n is a number from the history to re-execute that command");
   printf("\nTerminal options:");
   printf("\n\t -h \t \"help\": display this usage information");
   printf("\n\n");

   exit(0);
}

