// ========================================================================================
// You will complete the Simulator class.
// You may edit this file as needed.
// ========================================================================================

#include <iostream>
#include <iomanip>
#include <cassert>
#include <algorithm>

#include "timestamp.h"
#include "job.h"
#include "simulator.h"

// ========================================================================================
// ========================================================================================
// CONSTRUCTOR
Simulator::Simulator(const TimeStamp &start, int p, double timestep, bool niceness,
                     const std::string &vf, const std::string &lf) :
  current(start), num_processors(p), prioritize_by_niceness(niceness), vis_str(vf), log_str(lf) {
  
  if (timestep < 0) {
    // use the finish event queue
    timestep_milliseconds = -1;
  } else {
    timestep_milliseconds = timestep*1000;
  }
  
  // initialize the member variables
  processors = std::vector<Job>(num_processors);
  job_count = 0; 
  total_waiting_time = 0;
  max_waiting_time = 0;
  non_idle_time = 0;
  nice_total_waiting_times = std::vector<double>(41,0);
  nice_waiting_counts = std::vector<int>(41,0);
  
  // verify that the output filestreams were opened successfully
  // (if the filenames are not empty string)
  if (vf != "") {
    assert (vis_str.good());
    // print a header row for the ASCII art visualization table
    vis_str << "timestamp    ";
    for (int i = 0; i < num_processors; i++) {
      vis_str << "| processor " << std::left << std::setw(11) << i;
    }
    vis_str << "| queue" << std::endl;
  }
  if (lf != "") {
    assert (log_str.good());
  }
}


// ========================================================================================
// ========================================================================================
int Simulator::timestepMilliseconds(const TimeStamp &next_upload) const {

  // SPECIAL CASE: If next_upload is 0 hours, 0 min, 0 sec, 0 ms, 
  // then we will not have any additional job uploads.  
  bool additional_jobs = next_upload > TimeStamp(0,0,0,0);

  // If we expect additional jobs and the next upload time is in the past,
  // then do not advance the timestep.  We need to add the next job to the 
  // todo list/queue.
  if (additional_jobs && next_upload <= current) return 0;
  
  // Otherwise, calculate the amount of time to advance the timestep.
  if (timestep_milliseconds > 0) {
  
    // a fixed timestep
    return timestep_milliseconds;
  
  } else {

    
    //
    // ASSIGNMENT: You'll need to update this function to vary the
    // timestep based on the next job finish time and the time until
    // the next upload.
    //

    
    // placeholder code
    return timestep_milliseconds;
  }
}
  

// ========================================================================================
// FUNCTIONS RELATED TO THE JOBS QUEUE
// ========================================================================================


//
// ASSIGNMENT: You'll need to update these functions to use your the
// priority queue based on niceness values.
//


int Simulator::numJobsTodo() const {
  return todo_list.size();
}

void Simulator::addJob(Job j) {
  todo_list.push(j);
}

Job Simulator::peekNextJob() {
  assert (numJobsTodo() > 0);
  return todo_list.front();
}

void Simulator::removeNextJob() {
  assert (numJobsTodo() > 0);
  todo_list.pop();
}


// ========================================================================================
// FUNCTIONS RELATED TO THE SIMULATION
// ========================================================================================

void Simulator::cleanupJobs() {
  // clear out finished jobs

  //
  // NOTE: This function could be optimized by using the finish time priority queue.
  //

  for (int i = 0; i < num_processors; i++) {
    if (processors[i].getName() != "" && processors[i].getFinishTime() <= current) {
      printJobToLog(processors[i], i);
      processors[i] = Job();
    }
  }
}


void Simulator::startJobs() {

  
  //
  // ASSIGNMENT: You'll need to update this function to use your event
  // finish time priority queue.
  //

  
  while(numJobsTodo() > 0) {
    // first, find an available processor
    int available = -1;
    for (int i = 0; i < num_processors; i++) {
      if (processors[i].getName() == "") { available = i; break; }
    }
    // return if all processors are busy
    if (available == -1) { return; }
    
    // then, assign the next job to that processor
    Job job = peekNextJob();
    std::string name = job.getName();
    processors[available] = job;
    processors[available].startGrading(current);
    removeNextJob();
  }
}


bool Simulator::simulationComplete() const {
  if (numJobsTodo() > 0) {
    // if we still have jobs waiting to start, then the simulation is
    // not complete
    return false;
  } else {
    // if any processor is still working on on a job, then the
    // simulation is not complete
	
    //
    // NOTE: This function could be optimized by using the finish time priority queue.
    //

    for (int i = 0; i < num_processors; i++) {
      if (processors[i].getName() != "") return false;
    }
  }
  return true;
}


// ========================================================================================
// FUNCTIONS FOR PRINTING
// ========================================================================================

void Simulator::printVisualizationRow() {
  static TimeStamp last = current;
  last = current;
  if (vis_str.good()) {
    vis_str << current;
    for (int i = 0; i < num_processors; i++) {
      vis_str << " | " << std::setw(20) << std::left
              << processors[i].getName();
    }
    vis_str << " | " << std::setw(5) << std::right << numJobsTodo() << std::endl; 
  }
}


void Simulator::printJobToLog(const Job &job, int which_processor) {
  // first update global statistics
  job_count++;
  total_waiting_time+= job.waiting_time();
  int nice = job.getNiceness();
  assert (nice >= -20 && nice <= 20);
  nice_total_waiting_times[nice+20] += job.waiting_time();
  nice_waiting_counts[nice+20]++;
  max_waiting_time = std::max(max_waiting_time,job.waiting_time());
  non_idle_time += job.grading_time();
  // then print to the log
  if (log_str.good()) {
    log_str << std::left << std::setw(20) << job.getName() << "  "
            << "upload: " << job.getUploadTime() << "  "
            << "start: " << job.getStartTime() << "  "
            << "finish: " << job.getFinishTime() << "  "
            << "nice: " << std::setw(3) << std::right << job.getNiceness() << "  "
            << "processor: " << std::setw(4) << std::left << which_processor << "  "
            << "wait: " << std::right << std::setw(8) << std::setprecision(3) << std::fixed << job.waiting_time() << " sec  "
            << "grade: " << std::setw(8) << std::setprecision(3) << std::fixed  << job.grading_time() << " sec" << std::endl;
  }
}


void Simulator::printSummaryStatistics(double simulation_running_time) {
  assert (TimeStamp(23,0,0) < current);
  double elapsed_seconds = MillisecondsToSeconds(elapsedMilliseconds(TimeStamp(23,0,0),current));
  double average_waiting = total_waiting_time / double(job_count);
  double processor_time = num_processors * elapsed_seconds;
  double idle_percent  = 100 * (processor_time - non_idle_time) / double(processor_time);

  if (timestep_milliseconds > 0) {
    std::cout << "timestep: " << std::setw(26) << std::fixed << std::setprecision(3) << timestep_milliseconds / 1000.0 << " sec" << std::endl;
  } else {
    std::cout << "timestep:             finish event queue" << std::endl;
  }

  if (prioritize_by_niceness) {
    std::cout << "job priority:        niceness, then FIFO" << std::endl;
  } else {
    std::cout << "job priority:                       FIFO" << std::endl;
  }
  
  std::cout << "time to empty queue:     "
            << std::fixed << std::setprecision(3) << std::setw(11) <<  elapsed_seconds << " sec" << std::endl;
  std::cout << "maximum waiting time:    "
            << std::fixed << std::setprecision(3) << std::setw(11) << max_waiting_time << " sec" << std::endl;
  std::cout << "average waiting time:    "
            << std::fixed << std::setprecision(3) << std::setw(11) << average_waiting << " sec";
  std::cout << std::setw(7) << job_count << " jobs" << std::endl;

  for (int i = -20; i <= 20; i++) {
    if (nice_waiting_counts[i+20] == 0) continue;
    double nice_average_waiting = nice_total_waiting_times[i+20] / double(nice_waiting_counts[i+20]);
    std::cout << "nice=" << std::setw(2) << std::left << i
              << " waiting time:    " << std::right << std::fixed << std::setprecision(3) << std::setw(11) << nice_average_waiting << " sec";
    std::cout << std::setw(7) << nice_waiting_counts[i+20] << " jobs" << std::endl;
  }
  
  std::cout << "idle percentage:         "
            << std::right << std::setw(11) << std::fixed << std::setprecision(3) << idle_percent << "   %" << std::endl;
  std::cout << "simulation running time: "
	    << std::setw(11) << std::fixed << std::setprecision(3) << simulation_running_time << " sec" << std::endl;
}

// ========================================================================================






