/***************************************************************************
 *   Copyright (C) 2005 by Brian Lauber   *
 *   bml8@case.edu   *
 *                                                                         *
 *   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 2 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.,                                       *
 *   59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.             *
 ***************************************************************************/


/***************************************************************************
  Generating unique random numbers for multiple processes is a problem.
  If you seed 2 processes too quickly, they will be seeded w/ the same value,
  so they will give the same sequence of "random" numbers.  There are
  various tricks that can fix this problem, but these only offer limited
  success.  Ideally, we want 1 process to generate random values for all
  of the processes.  It just so happens that this is the solution I
  implement here
***************************************************************************/

#ifndef __BL_RANDOM_C__
#define __BL_RANDOM_C__

#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <sys/shm.h>
#include <stdio.h>    // Needed for standard output
#include <stdlib.h>   // Needed for atexit()

#include "typedefs.h"
#include "blError.h"
#include "blRandom.h"
#include "blEnv.h"


// Rather than have the Random Number Process (RNP) create a number
// each time a process requests one, it will generate a queue of
// random numbers for quick access.  We will define the size of the
// queue here (this allows us to easily resize the queue if necessary)
#define QUEUE_SIZE 32


struct SharedVars
{
  int number[QUEUE_SIZE];  // The random numbers will be stored in this queue
  unsigned int next;       // This is the position of the next available number in the queue
  unsigned int attached;   // The number of processes using the Random Number Process (RNP)
  pid_t RandomID;          // The pid of the RNP
};


// This is similar to the producer-consumer problem.  The (RNP) will take empty
// SLOTS and fill them with numbers (ITEMS).  The attached processes remove these
// numbers (ITEMS), thus increasing the total number of SLOTS.
// MUTEX is just a generic mutex used when accessing shared variables
enum eSEMAPHORES {SLOT, ITEM, MUTEX};


void RNP(void);              // This will be the "Random Number Process" that fills the random number queue
void SignalHandler(int sig); // If the program is killed w/ SIGTERM, it will be sure to detach from the RNF
void DetachRandomizer(void); // This will automatically be called at program exit time.
                             // If the current process is the last one attached to the RNP,
                             // then it will destroy the RNP

/******************************************************
  SHARED DATA: These variables are declared globally
       because they are used in all of the functions
       in this file.
******************************************************/
key_t              S_Grp = 0;     // Semaphore group
key_t              M_Grp = 0;     // Shared-memory group

struct SharedVars *shared = NULL; // Shared memory image










void AttachRandomizer(void)
{
  unsigned short     SemLimits[] = {QUEUE_SIZE, 0, 1}; // Initialize semaphores: SLOTS = QUEUE_SIZE, ITEMS = 0, MUTEX = 1
  union semun        UnionThing;

  pid_t              tempRandomID; // RandomID will be stored here initially before being transferred to the shared RandomID.

  // If the shared memory is already initialized, then this process must already be attached to the RNP.
  // Therefore, we can just exit without doing anything else
  if(shared != NULL)
    return;

  // If RAND_SEM is not defined in the environment, then we can assume that we never created the RNP.
  // Thus, we'll create our shared resources and fork the RNP here :-P
  if(!blEnvDefined("RAND_SEM"))
  {
    // Create the shared resources
    S_Grp = Error(semget(IPC_PRIVATE, 3, IPC_CREAT | 0666)); // Create the 3 semaphores -- SLOTS, ITEMS, and MUTEX
    M_Grp = Error(shmget(IPC_PRIVATE, sizeof(struct SharedVars), IPC_CREAT | 0666));

    // Initialize the semaphores
    UnionThing.array = SemLimits;
    Error(semctl(S_Grp, 0, SETALL, UnionThing));

    // Initialize the shared memory.  Note that I do not need to wait on MUTEX when setting these
    // values because there is only 1 process process that knows this memory exists right now
    Error(shared     = shmat(M_Grp, NULL, 0));
    shared->next     = 0;
    shared->attached = 1; 

    // Fork off a process to generate random numbers.  Note that the pid is saved to tempRandomID
    // instead of shared->RandomID.  Since 2 processes return from the fork call, there is a chance
    // that the child process would return after the parent process and overwrite RandomID with 0
    if( (tempRandomID = Error(fork())) == 0)
      RNP(); // Run the random-number generating routine (which never exits)
    Wait(S_Grp, MUTEX);
    shared->RandomID = tempRandomID;
    Signal(S_Grp, MUTEX);

    // Now that the process has been created, it is finally safe to export the environment variables
    blSetEnv("RAND_SEM", S_Grp);
    blSetEnv("RAND_MEM", M_Grp);
  }
  else // In this last case, the environment variables already exist, so we know that the RNP has been created;
  {    // however, because shared is still pointing to NULL, this process must have never attached to the RNP
    S_Grp = blGetEnv("RAND_SEM");       // Locate the semaphore group
    M_Grp = blGetEnv("RAND_MEM");       // Locate the shared memory
    Error(shared = shmat(M_Grp, 0, 0)); // Attach the shared memory to the current process

    // Increase the count of the processes attached to the RNP
    Wait(S_Grp, MUTEX);
    ++(shared->attached);
    Signal(S_Grp, MUTEX);
  }


  // If the RNP was just created or this process just attached to the RNP, then we will set
  // DetachRandomizer to be run at program exit time.  It is possible that this function could
  // be called multiple times for a single process (ie, one process attaches and detaches the RNP
  // several times), but this should not be a problem
  if(atexit(DetachRandomizer) != 0) // Returns non-zero if an error occurs
  {
    fprintf(stderr, "Error: Could not set DetachRandomizer() to be executed at program exit time!\n");
    DetachRandomizer();
    exit(EXIT_FAILURE);
  }


  // It is very likely that the user will want to kill the process w/ control+C.  These next
  // few lines ensure that the program will detach from the RNP properly
  if(signal(SIGINT, SignalHandler) == SIG_ERR)
  {
    fprintf(stderr, "Error: Could not attach signal handler for Random Number Process\n");
    DetachRandomizer();
    exit(EXIT_FAILURE);
  }

}




// When a process that has been attached to the RNP exits, it will call this function to
// clean up shared memory and such
void DetachRandomizer(void)
{
  // The DetachRandomizer function should only be called once for each function that attaches to the RNP.
  // If this next line is reached, something went wrong
  if(shared == NULL)
  {
    fprintf(stderr, "WTF: How did the program get here??\n");
    return;
  }


  Wait(S_Grp, MUTEX);
  --(shared->attached);    // Decrease the number of processes attached to the random # process
  if(shared->attached > 0) // If processes are still attached to the RNP, then just detach the current process
  {
    Signal(S_Grp, MUTEX);
    Error(shmdt(shared));  // Detach the current process from the shared memory
  }
  else                     // Otherwise, we need to remove the RNP so we don't have a dangling process
  {
    Error(kill(shared->RandomID, SIGTERM)); // Tell the random number process to exit
    Error(shmdt(shared));  // Detach the current process from the shared memory
    Error(semctl(S_Grp, 0, IPC_RMID)); // Remove the semaphore group
    Error(shmctl(M_Grp, IPC_RMID, NULL)); // Remove the shared memory group
  }

  shared = NULL;             // Point the shared memory to NULL
  S_Grp = M_Grp = 0;         // Set all of the group keys to 0
}



// Force a "clean" exit from the program
void SignalHandler(int sig)
{  exit(EXIT_FAILURE);  }


// The Random Number Process.  This function will never terminate (unless explicitly killed)
// and continually fill a queue full of random numbers
void RNP(void)
{
  unsigned int current; // The RNP's current position in the queue
  time_t seed;          // This will be used to seed srand

  time(&seed);          // Get the current time
  srand(seed);          // And use it to seed the random number generator
  while(1)
  {
    for(current = 0; current < QUEUE_SIZE; ++current)
    {
      Wait(S_Grp, SLOT); // Wait for a slot to open in the queue
      shared->number[current] = rand(); // Insert a new number into the queue
      Signal(S_Grp, ITEM); // Inform the other processes that there is an additional random number in the queue now
    }
  }
}



// Once a process has attached to the RNP, it can call this function to retrieve a random number from the queue
int  blRand(void)
{
  int randomNumber = 0; // This will be the random number retrieved from the queue
  int retrieve = 0;     // This is the index of the number that WILL be retrieved
                        // (This eliminates the need to wait on 2 semaphores at once)

  // Figure out the index that this process will retrive the number from
  Wait(S_Grp, MUTEX);
  retrieve = (shared->next++);   // Grab the next available index
  if(shared->next >= QUEUE_SIZE) // If next is outside of the queue, reset it to 0
    shared->next = 0;
  Signal(S_Grp, MUTEX);

  Wait(S_Grp, ITEM); // Wait for an ITEM to be inserted into the queue
  randomNumber = shared->number[retrieve]; // Grab the next available random number
  Signal(S_Grp, SLOT);

  return randomNumber;
}


#endif

