/*
filename: bayles_hw02.cpp

author: Bo Bayles

assignment: CS 284 Assignment 2

description: This program simulates 3 users using a shared set of bank
  accounts. Each user runs in a thread, and mutual exclusion prevents both
  users from accessing the same resource simultaneously.

note: Compile with g++ -pthread -D_REENTRANT
*/

/*/////////////////////////////////////////////////////////////////////////////
  Compiler Directives
/////////////////////////////////////////////////////////////////////////////*/
#include <iostream>
#include <fstream>
#include <unistd.h>
#include <pthread.h>
using namespace std;


#define CHECKING 0
#define SAVINGS 1
//Account codes

#define DEPOSIT 0
#define WITHDRAWL 1
#define TRANSFER 2
//Operation Codes

#define OPERATION 0
#define ACCOUNT 1
#define AMOUNT 2
//Field Codes

#define MAXSIZE 20

/*/////////////////////////////////////////////////////////////////////////////
  Data type definitions
/////////////////////////////////////////////////////////////////////////////*/

struct bankAccount
{
  int balance[2]; //Index 0 is checking balance; 1 is savings.
  pthread_mutex_t accountLock[2]; //One mutex lock for each account.
  unsigned int user[3][MAXSIZE][3];
  //Command buffer: Structure is [UserNumber (0 to 2)]
  //  [Command number (0 to MAXSIZE-1)] [Operation code (0 to 2),
  //   Account code (0 or 1),
  //   Balance (integer)]
  unsigned int opCount[3]; //One operation counter for each user.
  unsigned int userNo; //Keeps track of the current user.
};

/*/////////////////////////////////////////////////////////////////////////////
  Function declarations
/////////////////////////////////////////////////////////////////////////////*/

void readCommands(istream &inFile, int i, bankAccount &acct);
//Reads the commands from the input files to a buffer.

void deposit(bankAccount *accountSet, const unsigned int dest,
             const unsigned int transAmount);

void withdraw(bankAccount *accountSet, const unsigned int dest,
             const unsigned int transAmount);

void printName(const int userCode);

void * RunThread(void *ptr);

/*/////////////////////////////////////////////////////////////////////////////
  Function definitions
/////////////////////////////////////////////////////////////////////////////*/

int main( int argc, char *argv[] )
{
  bankAccount sharedAccount; //Instantiate a joint set of accounts.
  
  if ( argc != 4 ) //We need 3 filenames to be able to work.
  {
    cout << "Joint account simulator." << endl
         << "Usage: " << argv[0] << " file1 file2 file3" << endl
         << "Where the arguments are joint account simulator script files.\n";

    return 0;
  }
  
  sharedAccount.balance[CHECKING] = 0;
  sharedAccount.balance[SAVINGS] = 0;
  //Initialize the balances to zero.


  for (int i = 0; i < 3; i++)
  {
    for (int j = 0; j < MAXSIZE; j++)
    {
      for (int k = 0; k < 3; k++)
        sharedAccount.user[i][j][k] = 0;
    }
  }
  //Initialize the command buffer.

  sharedAccount.opCount[0] = 0;
  sharedAccount.opCount[1] = 0;
  sharedAccount.opCount[2] = 0;
  //Initialize the operation counters.

  sharedAccount.userNo = 0; //Initialize the user number.

  ifstream firstUser, secondUser, thirdUser; //Declare the input file streams.
  
  firstUser.open(argv[1]);
  secondUser.open(argv[2]);
  thirdUser.open(argv[3]);
  //Open each input file.

  if ( !firstUser.fail() && !secondUser.fail() && !thirdUser.fail() )
  //Make sure the input files opened correctly
  {
    readCommands(firstUser, 0, sharedAccount);
    readCommands(secondUser, 1, sharedAccount);
    readCommands(thirdUser, 2, sharedAccount);
    //Call readCommands to put the commands into the buffers.
  }
  else
  {
    cerr << "Error opening files" << endl;
    return 1;
  }

  pthread_t User1Thread, User2Thread, User3Thread;
  //Declare the threads for each user.

  pthread_mutex_init(&sharedAccount.accountLock[0], NULL);
  pthread_mutex_init(&sharedAccount.accountLock[1], NULL);
  //Initialize the mutexes

  pthread_create(&User1Thread, NULL, RunThread, (void *) &sharedAccount);
  pthread_create(&User2Thread, NULL, RunThread, (void *) &sharedAccount);
  pthread_create(&User3Thread, NULL, RunThread, (void *) &sharedAccount);
  //Create two threads, one for each user

  sched_yield(); //Give up control to one of the threads.

  if ( !pthread_join(User1Thread, NULL) &&
       !pthread_join(User2Thread, NULL) &&
       !pthread_join(User3Thread, NULL) )
  //Make sure both threads terminated properly.
    cout << "\nExiting...\n" << endl;
  else
    cerr << "\nError on thread termination\n" << endl;

  firstUser.close();
  secondUser.close();
  thirdUser.close();
  //Close the input files

  return 0;
}

void readCommands(istream &inFile, int i, bankAccount &acct)
{
 while ( !inFile.eof() )
 //Read until the end of the file
 {
   char temp;
   inFile >> temp;

   if (temp == 'd') //This is a deposit
     acct.user[i][ acct.opCount[i] ][OPERATION] = DEPOSIT;
   else if (temp == 'w') //This is a withdrawl
     acct.user[i][ acct.opCount[i] ][OPERATION] = WITHDRAWL;
   else //This is a transfer
       acct.user[i][ acct.opCount[i] ][OPERATION] = TRANSFER;

   inFile >> acct.user[i][ acct.opCount[i] ][ACCOUNT];
   inFile >> acct.user[i][ acct.opCount[i] ][AMOUNT];

   if ( inFile.eof() ) //Stop reading at the end of the file.
     break;

   acct.opCount[i]++; //Increment the operation count for the current user.
 }

  return;
}

void deposit(bankAccount *accountSet, const unsigned int destAcct,
             const unsigned int transAmount)
{
  pthread_mutex_lock(&accountSet->accountLock[destAcct]); //Lock the account...
    accountSet->balance[destAcct] += transAmount; //...Do the transaction...
  pthread_mutex_unlock(&accountSet->accountLock[destAcct]);
  //...Unlock the account

  return;
}

void withdraw(bankAccount *accountSet, const unsigned int dest,
             const unsigned int transAmount)
{
  pthread_mutex_lock(&accountSet->accountLock[dest]); //Just like a deposit...
    accountSet->balance[dest] -= transAmount;
  pthread_mutex_unlock(&accountSet->accountLock[dest]);

  return;
}

void printName(const int userCode)
{
  if (userCode == 0)
    cout << "\nFred ";
  else if (userCode == 1)
    cout << "\nJennifer ";
  else
    cout << "\nJill ";

  return;
}

void * RunThread(void *ptr)
{
  bankAccount *jointAccount = (bankAccount *) ptr;
  //Cast the pointer to the correct type.

  unsigned int userNumber;
  if ( jointAccount->userNo == 0 )
  {
    userNumber = 0;  //This is Fred.
    jointAccount->userNo++;
  }
  else if ( jointAccount->userNo == 1 )
  {
    userNumber = 1; //This is Jennifer.
    jointAccount->userNo++;
  }
  else
  {
    userNumber = 2; //This is Jill.
  }
  //Keep track of which user this thread simulates.

  for (int i = 0; i < jointAccount->opCount[userNumber]; i++)
  //Execute all the commands in the buffer, one at a time.
  {
    const unsigned int transactionType =
      jointAccount->user[userNumber][i][OPERATION];
    unsigned int destAccount =
      jointAccount->user[userNumber][i][ACCOUNT];
    const int transactionAmount =
      jointAccount->user[userNumber][i][AMOUNT];
    //Declare some variables to decrease line lengths.

    if ( transactionType == DEPOSIT )
    {
      deposit(jointAccount, destAccount, transactionAmount);
      //Note: The mutex is locked and unlocked inside deposit().

      printName(userNumber);
      cout << "deposited $" << transactionAmount
           << " to ";
      if (destAccount == CHECKING)
        cout << "checking." << endl;
      else
        cout << "savings." << endl;
    }
    else if  ( transactionType == WITHDRAWL )
    {
      if ( jointAccount->balance[destAccount] >= transactionAmount )
      //If there's enough money, there's no problem; do the withdrawl.
      {
        withdraw(jointAccount, destAccount, transactionAmount);
        
        printName(userNumber);
        cout << "withdrew $" << transactionAmount
             << " from ";
      }
      else if ( destAccount == CHECKING && jointAccount->balance[SAVINGS] > 0 )
      //If we're withdrawing from checking, and there's money in savings, cover
      //  the overdraw with money from savings.
      {
        int overdraft = transactionAmount - jointAccount->balance[CHECKING];

        withdraw(jointAccount, SAVINGS, overdraft); //Take out the overdraft...
        deposit(jointAccount, CHECKING, overdraft); //Put it in checking...
        withdraw(jointAccount, CHECKING, transactionAmount);
        //Now withdraw. The mutexes are locked and unlocked inside the
        //  functions.

        printName(userNumber);
        cout << "withdrew $" << transactionAmount
             << " from checking by transferring $" << overdraft
             << " from ";
        destAccount = SAVINGS; //This is for the transaction message.
      }
      else
      //Otherwise there is no money in savings; deny the transaction.
      {
        printName(userNumber);
        cout << "can't withdraw $"
             << transactionAmount << " from ";
      }
      if (destAccount == CHECKING)
        cout << "checking." << endl;
      else
        cout << "savings." << endl;
    }
    else //This is a transfer
    {
      unsigned int sourceAccount;
      if (destAccount == CHECKING) //If the destination is checking...
        sourceAccount = SAVINGS; //...then the source is savings...
      else
        sourceAccount = CHECKING; //...and vice-versa.

      if ( jointAccount->balance[sourceAccount] > 0 )
      //If there's enough money in the source account, everything is good; if
      //  there's not enough money we can go negative if we have at least some.
      {
        withdraw(jointAccount, sourceAccount, transactionAmount);
        deposit(jointAccount, destAccount, transactionAmount);
        //Withdraw from the source and deposit in the destination. The mutexes
        //  are locked and unlocked inside the functions.
        
        printName(userNumber);
        cout << "transferred $"
             << transactionAmount << " from ";
      }
      else
      {
        printName(userNumber);
        cout << "can't transfer $"
             << transactionAmount << " from ";
      }
      if (destAccount == CHECKING)
        cout << "savings to checking." << endl;
      else
        cout << "checking to savings." << endl;
    } //End transfer case
    
    if ( jointAccount->balance[CHECKING] < 0 ||
         jointAccount->balance[SAVINGS] < 0 )
      cout << "  Warning: You are overdrawn on one of your accounts." << endl;
      
    cout << "\tAccount Balances - Checking: $"
         << jointAccount->balance[CHECKING]
         << ", Savings: $" << jointAccount->balance[SAVINGS] << endl;

    sched_yield();
    //Give up control to another thread.
  } //End for
  pthread_exit(ptr);
}

