#include "test.h"

#include "../src/handle.h"
#include "../src/print.h"

#include <stdbool.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>

#define MAX_JOBS (1u<<12)

struct tissat_job
{
  pid_t pid;
  unsigned id;
  int expected;
  bool executed;
  bool finished;
  char *command;
  char *application;
  void (*function) (void);
  tissat_job *dependency;
  const char *name;
};

static tissat_job jobs[MAX_JOBS];

unsigned tissat_scheduled;

static unsigned executed;
static unsigned finished;

static tissat_job *
new_job (int expected)
{
  if (tissat_scheduled == MAX_JOBS)
    tissat_fatal ("maximum number %u of scheduled jobs exhausted", MAX_JOBS);
  tissat_job *res = jobs + tissat_scheduled;
  res->id = tissat_scheduled++;
  res->expected = expected;
  return res;
}

void
tissat_schedule_function (void (*function) (void), const char *name)
{
  tissat_job *res = new_job (0);
  res->function = function;
  res->name = name;
}

tissat_job *
tissat_schedule_application (int expected, const char *args)
{
  tissat_job *res = new_job (expected);
  strcpy (res->application = malloc (strlen (args) + 1), args);
  return res;
}

tissat_job *
tissat_schedule_command (int expected,
			 const char *command, tissat_job * dependency)
{
  tissat_job *res = new_job (expected);
  strcpy (res->command = malloc (strlen (command) + 1), command);
  res->dependency = dependency;
  return res;
}

static void
execute_function (tissat_job * job)
{
  tissat_section ("Executing Function '%s'", job->name);
  job->function ();
}

static void
execute_application (tissat_job * job)
{
  tissat_section ("Executing Application 'kissat %s'", job->application);
  tissat_call_application (job->expected, job->application);
}

static void
check_command (tissat_job * job, int status)
{
  assert (job);
  assert (job->command);
  if (WIFEXITED (status))
    {
      int res = WEXITSTATUS (status);
      if (res == job->expected)
	tissat_verbose ("Command '%s' returned '%d' as expected.",
			job->command, res);
      else
	tissat_error ("Command '%s' returns '%d' and not '%d'",
		      job->command, res, job->expected);
    }
  else if (WIFSIGNALED (status))
    tissat_signal (WTERMSIG (status), "executing command '%s", job->command);
  else
    tissat_error ("Unexpected return status of command '%s'");
}

static void
check_function (tissat_job * job, int status)
{
  assert (job);
  assert (job->function);
  if (status)
    tissat_error ("Function job '%s' failed with exit status '%d",
		  job->name, status);
}

static void
check_application (tissat_job * job, int status)
{
  assert (job);
  assert (job->application);
  if (status)
    tissat_error ("Application job 'kissat %s' failed with exit status '%d",
		  job->application, status);
}

static void
execute_command (tissat_job * job)
{
  tissat_section ("Executing Command '%s'", job->command);
  int status = system (job->command);
  if (status < 0)
    tissat_error ("Could not generate child process or retrieve status "
		  "while trying to execute command '%s'", job->command);
  else if (status == 127)
    tissat_error ("Shell could not be executed in the child process "
		  "while trying to execute command '%s'", job->command);
  else
    check_command (job, status);
}

static tissat_job *running_job;

static void
execute_job (tissat_job * job)
{
  running_job = job;
  if (job->function)
    execute_function (job);
  else if (job->application)
    execute_application (job);
  else
    {
      assert (job->command);
      execute_command (job);
    }
}

static void
handle_signal (tissat_job * job, int sig)
{
  if (!job)
    tissat_signal (sig, "but could not find corresponding job");
  else if (job->function)
    tissat_signal (sig, "in function '%s'", job->name);
  else if (job->command)
    tissat_signal (sig, "in command '%s'", job->command);
  else
    {
      assert (job->application);
      tissat_signal (sig, "in application 'kissat %s'", job->application);
    }
}

static void
handle_exit (tissat_job * job, int status)
{
  if (!job)
    tissat_fatal ("exit status '%d' "
		  "but could not find corresponding job", status);
  if (job->function)
    check_function (job, status);
  else if (job->application)
    check_application (job, status);
  else
    check_command (job, status);
}

static void
sequential_signal_handler (int sig)
{
  kissat_reset_signal_handler ();
  tissat_restore_stdout_and_stderr ();
  if (!running_job)
    tissat_signal (sig, "but no job seems to run");
  handle_signal (running_job, sig);
}

static void
set_sequential_signal_handler (void)
{
  kissat_init_signal_handler (sequential_signal_handler);
}

static void
reset_signal_handler (void)
{
  kissat_reset_signal_handler ();
}

static void
sequential_progress (void)
{
  if (!tissat_progress)
    return;
  printf ("sequential: executed %u, finished %u\n", executed, finished);
  fflush (stdout);
}

static void
run_sequential_job (tissat_job * job)
{
  executed++;
  sequential_progress ();
  tissat_divert_stdout_and_stderr_to_dev_null ();
  set_sequential_signal_handler ();
  execute_job (job);
  finished++;
  reset_signal_handler ();
  tissat_restore_stdout_and_stderr ();
  sequential_progress ();
}

static void
run_parallel_job (tissat_job * job)
{
  tissat_divert_stdout_and_stderr_to_dev_null ();
  execute_job (job);
  tissat_restore_stdout_and_stderr ();
}

#define all_jobs(JOB) \
  tissat_job * JOB = jobs, * END_ ## JOB = JOB + tissat_scheduled; \
  JOB != END_ ## JOB; \
  JOB++

static void
run_jobs_sequentially (void)
{
  tissat_message ("Running %u jobs sequentially all in the same process.",
		  tissat_scheduled);
  for (all_jobs (job))
    run_sequential_job (job);
}

static unsigned search_executed;

static tissat_job *
find_executable_job (void)
{
  while (assert (search_executed < tissat_scheduled),
	 jobs[search_executed].executed)
    search_executed++;
  for (unsigned i = search_executed; i < tissat_scheduled; i++)
    {
      tissat_job *job = jobs + i;
      if (job->executed)
	continue;
      tissat_job *dependency = job->dependency;
      if (!dependency)
	return job;
      if (dependency->finished)
	return job;
    }
  return 0;
}

static tissat_job *
find_executed_job (pid_t pid)
{
  for (all_jobs (job))
    if (job->executed && job->pid == pid)
      return job;
  return 0;
}

static void
parallel_progress (unsigned running_jobs)
{
  if (!tissat_progress)
    return;
  printf ("parallel: executed %u, finished %u, running %u\n",
	  executed, finished, running_jobs);
  fflush (stdout);
}

static void
run_jobs_in_parallel (unsigned parallel)
{
  if (parallel == UINT_MAX)
    tissat_message ("Running %u jobs in parallel "
		    "using arbitrary many processes.", tissat_scheduled);
  else
    tissat_message ("Running %u jobs in parallel using up to %d processes.",
		    tissat_scheduled, parallel);
  unsigned running = 0;
  while (finished < tissat_scheduled)
    {
      tissat_job *job;
      if (running < parallel &&
	  executed < tissat_scheduled && (job = find_executable_job ()))
	{
	  job->pid = fork ();
	  if (job->pid < 0)
	    tissat_fatal ("failed to fork job %zu", job->id);
	  else if (!job->pid)
	    {
	      run_parallel_job (job);
	      exit (0);
	    }
	  else
	    {
	      job->executed = true;
	      executed++;
	      running++;
	      parallel_progress (running);
	    }
	}
      else
	{
	  int status;
	  pid_t pid = waitpid (-1, &status, 0);
	  if (pid < 0)
	    tissat_fatal ("waiting on %u unfinished processes failed",
			  executed - finished);
	  tissat_job *other = find_executed_job (pid);
	  if (WIFSIGNALED (status))
	    handle_signal (other, WTERMSIG (status));
	  else if (WIFEXITED (status))
	    handle_exit (other, WEXITSTATUS (status));
	  else
	    tissat_fatal ("unexpected status '%d' of child process '%d'",
			  status, pid);
	  assert (other);
	  other->finished = true;
	  finished++;
	  assert (running);
	  running--;
	  parallel_progress (running);
	}
    }
  assert (!running);
}

void
tissat_run_jobs (int parallel)
{
  if (!parallel)
    run_jobs_sequentially ();
  else
    run_jobs_in_parallel (parallel < 0 ? UINT_MAX : (unsigned) parallel);
}

void
tissat_release_jobs (void)
{
  for (all_jobs (job))
    {
      if (job->command)
	free (job->command);
      if (job->application)
	free (job->application);
      memset (job, 0, sizeof *job);
    }
  tissat_scheduled = 0;
}
