#include <errno.h>
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <unistd.h>

#define NUM_ACCOUNTS 8
#define NUM_TELLERS 128
#define TRANSACTIONS_PER_TELLER 1000

// Monetary value in cents
typedef long currency;

void sprintf_currency(char *buffer, currency c) {
  if (c < 0) {
    c = -c;
    sprintf(buffer, "-");
    buffer += sizeof(char);
  }
  sprintf(buffer, "$%ld.%.2ld", c / 100, c % 100);
}

void printf_currency(currency c) {
  if (c < 0) {
    c = -c;
    printf("-");
  }
  printf("$%ld.%.2ld", c / 100, c % 100);
}

// Accounts (shared resource)
typedef struct account {
  int account_id;
  currency balance;
  int transaction_count;
  pthread_mutex_t lock; // Account access mutex
} account;

void deposit(account *acc, currency amount) {
  pthread_mutex_lock(&acc->lock);
  // CRITICAL SECTION BEGIN
  acc->balance += amount;
  acc->transaction_count++;
  // CRITICAL SECTION END
  pthread_mutex_unlock(&acc->lock);
}

// Global account array
account accounts[NUM_ACCOUNTS];

// Teller threads

pthread_t tellers[NUM_TELLERS];

typedef struct teller_args {
  int teller_id;
  currency total;
} teller_args;

void *teller_thread(void *arg) {
  teller_args *args = (teller_args *)arg;
  int id = args->teller_id;
  currency total = 0;

  unsigned int seed = time(NULL) + pthread_self();
  for (int i = 0; i < TRANSACTIONS_PER_TELLER; i++) {
    // Deposit or withdraw random currency amount from $0 to $1000
    currency random_amount = ((long)rand_r(&seed)) % 100000;

    char *operation;
    currency random_amount_i = random_amount;
    if (rand_r(&seed) % 2) {
      random_amount_i = -random_amount;
      operation = "Withdrawing";
    } else {
      operation = "Depositing";
    }

    int random_acc = rand_r(&seed) % NUM_ACCOUNTS;
    deposit(&accounts[random_acc], random_amount_i);

    // Display result of transaction
    char buffer[8];
    sprintf_currency(buffer, random_amount);
    printf("Thread %u: %s %s into account %u\n", id, operation, buffer,
           random_acc);

    total += random_amount_i;
  }

  args->total = total;
  return NULL;
}

int main() {
  // Initialize all accounts with 0 balance
  for (int i = 0; i < NUM_ACCOUNTS; i++) {
    accounts[i] = (account){.account_id = i, .balance = 0};
    pthread_mutex_init(&accounts[i].lock, NULL);
  }

  // Initialize tellers
  teller_args args[NUM_TELLERS];
  for (int i = 0; i < NUM_TELLERS; i++) {
    args[i] = (teller_args){.teller_id = i};
    pthread_create(&tellers[i], NULL, teller_thread, &args[i]);
  }

  // Wait for all threads to finish
  currency expected_total = 0;
  for (int i = 0; i < NUM_TELLERS; i++) {
    pthread_join(tellers[i], NULL);
    expected_total += args[i].total;
  }

  currency actual_total = 0;
  for (int i = 0; i < NUM_ACCOUNTS; i++) {
    actual_total += accounts[i].balance;
  }

  printf("Total balance: ");
  printf_currency(actual_total);
  printf("\nExpected total: ");
  printf_currency(expected_total);
  printf("\n");

  return 0;
}
