#include "main.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#include "fcfs.h"
#include "lottery.h"
#include "priority.h"
#include "round_robin.h"
#include "sjf.h"
#include "srtf.h"

// See main.h
struct process;
struct scheduler;

scheduler *schedulers[] = {
    &sjf, &fcfs, &round_robin, &priority, &priority_aging, &lottery, &srtf};
int num_schedulers = sizeof(schedulers) / sizeof(*schedulers);

int read_comma_sep(process list[], int num, size_t offset, int is_long) {
  char *buffer = malloc(num * 10);
  fgets(buffer, num * 10, stdin);
  int processed = 0;
  char *value_str = strtok(buffer, ",");
  while (value_str != NULL) {
    if (is_long)
      sscanf(value_str, "%ld", (long *)((void *)&list[processed] + offset));
    else
      sscanf(value_str, "%d", (int *)((void *)&list[processed] + offset));
    processed++;
    value_str = strtok(NULL, ",");
  }
  free(buffer);
  if (processed != num) {
    return 0;
  }
  return 1;
}

void run_scheduler(scheduler *scheduler, process processes[], int num_processes,
                   int verbose, int machine_readable) {
  if (!machine_readable) {
    if (scheduler->description != NULL)
      printf("\nScheduler %s (%s):\n", scheduler->name, scheduler->description);
    else
      printf("\nScheduler %s:\n", scheduler->name);
  }

  scheduler->run(processes, num_processes);

  cpu_time_t total_time = 0;
  cpu_time_t used_time = 0;
  cpu_time_t total_waiting = 0;
  cpu_time_t total_turnaround = 0;

  if (!machine_readable && verbose)
    printf("Schedule times: ");
  for (int i = 0; i < num_processes; i++) {
    used_time += processes[i].burst;

    if (!machine_readable && verbose) {
      for (int j = 0; j < processes[i].num_schedules; j++) {
        printf("%ld-%ld%s", processes[i].starts[j],
               processes[i].starts[j] + processes[i].durations[j],
               i == num_processes - 1 && j == processes[i].num_schedules - 1
                   ? "\n"
               : j == processes[i].num_schedules - 1 ? ", "
                                                     : " ");
      }
    }
  }

  if (!machine_readable && verbose)
    printf("Waiting times: ");
  for (int i = 0; i < num_processes; i++) {
    cpu_time_t first_start = processes[i].starts[0];
    for (int j = 1; j < processes[i].num_schedules; j++) {
      if (processes[i].starts[j] < first_start)
        first_start = processes[i].starts[j];
    }
    processes[i].waiting = first_start - processes[i].arrival;
    total_waiting += processes[i].waiting;

    if (!machine_readable && verbose)
      printf("%ld%s", processes[i].waiting,
             i == num_processes - 1 ? "\n" : ", ");
  }

  if (!machine_readable && verbose)
    printf("Turnaround times: ");
  for (int i = 0; i < num_processes; i++) {
    cpu_time_t process_end = processes[i].starts[0] + processes[i].durations[0];
    for (int j = 0; j < processes[i].num_schedules; j++) {
      int end = processes[i].starts[j] + processes[i].durations[j];
      if (process_end < end)
        process_end = end;
      if (total_time < end)
        total_time = end;
    }
    processes[i].turnaround = process_end - processes[i].arrival;
    total_turnaround += processes[i].turnaround;

    if (!machine_readable && verbose)
      printf("%ld%s", processes[i].turnaround,
             i == num_processes - 1 ? "\n" : ", ");
  }

  double avg_waiting = (double)total_waiting / (double)num_processes;
  double avg_turnaround = (double)total_turnaround / (double)num_processes;
  double cpu_util = (double)used_time / (double)total_time;
  double throughput = (double)num_processes / (double)total_time;

  if (machine_readable) {
    printf("%lf,%lf,%lf,%lf\n", avg_waiting, avg_turnaround, cpu_util,
           throughput);
  } else {
    printf("Avg. Waiting Time: %.3lf\n", avg_waiting);
    printf("Avg. Turnaround Time: %.3lf\n", avg_turnaround);
    printf("CPU Utilization: %.2lf%%\n", cpu_util * 100);
    printf("Throughput: %.6lf proc/t\n", throughput);
  }

  // Free memory
  for (int i = 0; i < num_processes; i++) {
    free(processes[i].starts);
    free(processes[i].durations);
  }
}

int main(int argc, char *argv[]) {
  // Parse argument (-v verbose, for debug)
  char opt;
  int verbose = 0;
  int machine_readable = 0;
  while ((opt = getopt(argc, argv, "vm")) != -1) {
    switch (opt) {
    case 'v':
      verbose = 1;
      break;
    case 'm':
      machine_readable = 1;
      break;
    default:
      printf("Usage: %s [-v] [-m]\n", argv[0]);
      return 1;
    }
  }

  if (!machine_readable) {
    printf("CPU Scheduling Simulator\n");
    printf("========================\n\n");
  }

  // Input scheduler to run

  char scheduler_name[32];

  if (!machine_readable) {
    printf("Scheduler to run (");
    for (int i = 0; i < num_schedulers; i++) {
      printf("%s, ", schedulers[i]->name);
    }
    printf("compare): ");
  }
  fgets(scheduler_name, sizeof(scheduler_name), stdin);
  if (scheduler_name[strlen(scheduler_name) - 1] != '\n') {
    printf("Too many characters\n");
    return 1;
  }
  scheduler_name[strlen(scheduler_name) - 1] = '\0';

  scheduler *scheduler = NULL;
  if (strcmp(scheduler_name, "compare") != 0) {
    for (int i = 0; i < num_schedulers; i++) {
      if (strcmp(scheduler_name, schedulers[i]->name) == 0) {
        scheduler = schedulers[i];
        break;
      }
    }
    if (scheduler == NULL) {
      printf("Invalid scheduler name\n");
      return 1;
    }
  }

  // Number of processes

  int num_processes = 0;
  if (!machine_readable)
    printf("Number of processes: ");
  char num_processes_str[10];
  fgets(num_processes_str, sizeof(num_processes_str), stdin);
  sscanf(num_processes_str, "%d", &num_processes);
  if (num_processes < 1) {
    printf("Invalid number of processes\n");
    return 1;
  }

  process processes[num_processes];
  for (int i = 0; i < num_processes; i++) {
    processes[i] = (process){.pid = i};
  }

  // Arrival times

  if (!machine_readable)
    printf("Process arrival times (ordered, comma-separated): ");
  if (!read_comma_sep(processes, num_processes,
                      (void *)&(processes->arrival) - (void *)processes, 1)) {
    printf("Invalid arrival times\n");
    return 1;
  }
  // Check ordering
  for (int i = 1; i < num_processes; i++) {
    if (processes[i].arrival < processes[i - 1].arrival) {
      printf("Arrival times not ordered\n");
      return 1;
    }
  }

  // Burst times

  if (!machine_readable)
    printf("Process burst times (comma-separated): ");
  if (!read_comma_sep(processes, num_processes,
                      (void *)&(processes->burst) - (void *)processes, 1)) {
    printf("Invalid burst times\n");
    return 1;
  }

  // Priorities

  if (scheduler == NULL || scheduler->uses_priority) {
    if (!machine_readable)
      printf("Process priorities (comma-separated): ");
    if (!read_comma_sep(processes, num_processes,
                        (void *)&(processes->priority) - (void *)processes,
                        0)) {
      printf("Invalid priorities\n");
      return 1;
    }
  }

  // Run scheduler(s)

  if (scheduler != NULL) {
    run_scheduler(scheduler, processes, num_processes, verbose,
                  machine_readable);
  } else {
    for (int i = 0; i < num_schedulers; i++) {
      // Reset process fields
      for (int i = 0; i < num_processes; i++) {
        processes[i].num_schedules = 0;
        processes[i].executed = 0;
      }

      run_scheduler(schedulers[i], processes, num_processes, verbose,
                    machine_readable);
    }
  }
}
