/**
 * File: data-processor.cc
 * ---------------------------------------------
 * This program is an example of parallel processing and
 * a program that has multiple mutexes around independent state.
 */

#include <thread>
#include <mutex>
#include <iostream>
#include <map>
#include <vector>
#include "ostreamlock.h"
#include "random-generator.h"
#include "thread-utils.h"
using namespace std;

/**
 * Defines a collection of data/timing constants used for 
 * the data simulation.
 */

static const unsigned int kLowDataValue = 0;
static const unsigned int kHighDataValue = 120;
static const unsigned int kLowProcessTime = 100;
static const unsigned int kHighProcessTime = 2000;

/**
 * Defines the single RandomGenerator class and helper functions used to 
 * generate random timing amounts/data.
 */

static mutex rgenLock;
static RandomGenerator rgen;

// Assumed to be the only thing operating on this vector
static void fillVectorWithRandomData(vector<int>& data, int size) {
  for (int i = 0; i < size; i++) {
    data.push_back(rgen.getNextInt(kLowDataValue, kHighDataValue));
  }
}

static unsigned int getProcessTime() {
  rgenLock.lock();
  unsigned int value = rgen.getNextInt(kLowProcessTime, kHighProcessTime);
  rgenLock.unlock();
  return value;
}

// Converts datapoint from Fahrenheit to Celcius
static int processDatapoint(int datapoint) {
  return round(((float)datapoint - 32) * (5.0/9.0));
}
  
void printData(vector<int>& processedData, map<int, int>& maxDataMap) {
  cout << "Processed data: [";
  for (int i = 0; i < processedData.size(); i++) {
    cout << processedData[i] << " ";
  }
  cout << "]" << endl << endl;

  cout << "Max values seen per thread:" << endl;
  for (const auto& [key, value] : maxDataMap) {
    cout << "\tThread " << key << " max = " << value << endl;
  }
}

/**
 * Defines the collection of constants
 * needed for the data simulation.
 */

static const size_t kNumProcessors = 5;
static const size_t kNumDatapointsPerProcessor = 5;

static void processData(size_t id, vector<int>& data, int startIndex, 
  int stopIndex, mutex& vectorLock, map<int, int>& maxDataMap, mutex& mapLock) {

  // Track the max processed datapoint we see
  int maxSeen;
  bool first = true;

  for (int i = startIndex; i < stopIndex; i++) {
    // Get the next datapoint we process
    vectorLock.lock();
    int datapoint = data[i];
    vectorLock.unlock();

    // "Process" it and record if new max
    sleep_for(getProcessTime());
    datapoint = processDatapoint(datapoint);
    if (first || datapoint > maxSeen) {
      maxSeen = datapoint;
      if (first) first = false;
    }

    // Add it back to the dataset
    vectorLock.lock();
    data[i] = datapoint;
    vectorLock.unlock();
  } 

  // At the end, report our max
  mapLock.lock();
  maxDataMap[id] = maxSeen;
  mapLock.unlock();
}

int main(int argc, const char *argv[]) {
  /* Contains the unprocessed data; each thread will replace an element
   * with the processed version.
   */
  vector<int> data;
  mutex vectorLock;

  fillVectorWithRandomData(data, kNumDatapointsPerProcessor * kNumProcessors);

  /* Initially empty; each thread will add a mapping of its ID->largest data
   * value it processed.
   */
  map<int, int> maxDataMap;
  mutex mapLock;

  thread processors[kNumProcessors];
  for (size_t i = 0; i < kNumProcessors; i++) {
    processors[i] = thread(processData, i, ref(data), i * kNumDatapointsPerProcessor, 
      (i + 1) * kNumDatapointsPerProcessor,
      ref(vectorLock), ref(maxDataMap), ref(mapLock));
  }
  for (thread& p: processors) p.join();

  printData(data, maxDataMap);

  return 0;
}
