/*
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" << endl
                 << "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

    pthread_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;

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