/*
 * Copyright 2026 Naomi Alterman.  All rights reserved.  Permission is
 * hereby granted to students registered for University of Washington
 * CSE 333 for use solely during Spring Quarter 2026 for purposes of
 * the course.  No other use, copying, distribution, or modification
 * is permitted without prior written consent.  Copyrights for
 * third-party components of this work must be honored.  Instructors
 * interested in reusing these course materials should contact the
 * author.
 */

#include <pthread.h>

#include <cstdlib>
#include <ctime>
#include <iostream>
#include <thread>
#include <string>
#include <chrono>

#include "SimpleQueue.h"

using std::cout;
using std::cerr;
using std::endl;
using std::string;

static constexpr int kNumSnacks = 6;
static SimpleQueue queue;               // queue of snacks
static unsigned int seed = time(nullptr);  // initialize random sleep time
static pthread_mutex_t write_lock;      // mutex for cout

// Use cout in a thread-safe manner
void thread_safe_print(const string& str);

// Produces 6 snacks of the given type
void producer(const string& snack_type);

// Start a producer thread by calling producer(*arg_ptr)
// Terminate the thread when the producer function finishes.
void* producer_start(void* arg_ptr);

// Eats 2*kNumSnacks snacks
void consumer();

// Start the consumer thread by calling consumer() and terminate
// the thread when the consumer finishes.
void* consumer_start(void* arg_ptr);

// Start two producer threads and a consumer thread.
// Wait for them to terminate and then exit.
int main(int argc, char** argv) {
  srand(time(nullptr));
  pthread_mutex_init(&write_lock, nullptr);

  // Make the two producers and the single consumer all run concurrently
  // by using pthreads
  string snack_type_1 = "piroshki";
  string snack_type_2 = "nalysnyky";
  pthread_t producer_thd1, producer_thd2, consumer_thd;
  if (pthread_create(&producer_thd1, nullptr,
                     producer_start, &snack_type_1) != 0) {
    cerr << "pthread_create failed for ";
    cerr << snack_type_1 << " producer" << endl;
  }
  if (pthread_create(&producer_thd2, nullptr,
                     producer_start, &snack_type_2) != 0) {
    cerr << "pthread_create failed for ";
    cerr << snack_type_2 << " producer" << endl;
  }
  if (pthread_create(&consumer_thd, nullptr, consumer_start, nullptr) != 0) {
    cerr << "pthread_create failed for consumer" << endl;
  }

  if (pthread_join(producer_thd1, nullptr) != 0) {
    cerr << "pthread_join failed for ";
    cerr << snack_type_1 << " producer" << endl;
  }
  if (pthread_join(producer_thd2, nullptr) != 0) {
    cerr << "pthread_join failed for ";
    cerr << snack_type_2 << " producer" << endl;
  }
  if (pthread_join(consumer_thd, nullptr) != 0) {
    cerr << "pthread_join failed for consumer" << endl;
  }

  pthread_mutex_destroy(&write_lock);
  return EXIT_SUCCESS;
}


void thread_safe_print(const string& str) {
  pthread_mutex_lock(&write_lock);
  // Only one thread can hold the lock at a time, making it safe to
  // use cout. If we didn't lock before using cout, the order of things
  // put into the stream could be mixed up.
  cout << str << endl;
  pthread_mutex_unlock(&write_lock);
}

// You should NOT modify this method
void producer(const string& snack_type) {
  for (int i = 0; i < kNumSnacks; i++) {
    queue.Enqueue(snack_type);
    thread_safe_print(snack_type + " ready!");
    int sleep_time = rand_r(&seed) % 500 + 1;
    std::this_thread::sleep_for(std::chrono::milliseconds(sleep_time));
  }
}

void* producer_start(void* arg_ptr) {
  string* snack_type = reinterpret_cast<string*>(arg_ptr);
  producer(*snack_type);
  return nullptr;  // implicitly terminates thread
}

// You should NOT modify this method
void consumer() {
  for (int i = 0; i < 2 * kNumSnacks; i++) {
    bool successful = false;
    string snack_type;
    while (!successful) {
      while (queue.IsEmpty()) {
        // Sleep for a bit and then check again
        int sleep_time = rand_r(&seed) % 800 + 1;
        std::this_thread::sleep_for(std::chrono::milliseconds(sleep_time));
      }
      successful = queue.Dequeue(&snack_type);
    }
    thread_safe_print(snack_type + " eaten!");
  }
}

void* consumer_start(void* arg_ptr) {
  consumer();
  return nullptr;  // implicitly terminates thread
}