/* Copyright (C) 2025-2026 Free Software Foundation, Inc.
   Contributed by Thomas Koenig, Nicolas Koenig, Andre Vehreschild

This file is part of the GNU Fortran Shmem Coarray Library (caf_shmem).

Caf_shmem is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 3, or (at your option)
any later version.

Caf_shmem is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

Under Section 7 of GPL version 3, you are granted additional
permissions described in the GCC Runtime Library Exception, version
3.1, as published by the Free Software Foundation.

You should have received a copy of the GNU General Public License and
a copy of the GCC Runtime Library Exception along with this program;
see the files COPYING3 and COPYING.RUNTIME respectively.  If not, see
<http://www.gnu.org/licenses/>.  */

#include "../caf_error.h"
#include "supervisor.h"
#include "teams_mgmt.h"
#include "thread_support.h"

#include <assert.h>
#include <signal.h>
#include <string.h>
#include <unistd.h>
#ifdef HAVE_WAIT_H
#include <wait.h>
#elif HAVE_SYS_WAIT_H
#include <sys/wait.h>
#endif
#if !defined(_SC_PAGE_SIZE) && defined(WIN32)
#include <windows.h>
#endif
#if defined(__hpux__)
#include <sys/mpctl.h>
#endif
#define GFORTRAN_ENV_NUM_IMAGES "GFORTRAN_NUM_IMAGES"
#define GFORTRAN_ENV_SHARED_MEMORY_SIZE "GFORTRAN_SHARED_MEMORY_SIZE"
#define GFORTRAN_ENV_IMAGE_NUM "GFORTRAN_IMAGE_NUM"
#define GFORTRAN_ENV_IMAGE_RESTARTS_LIMITS "GFORTRAN_IMAGE_RESTARTS_LIMIT"

image_local *local = NULL;

image this_image = {-1, NULL};

/* Get image number from environment or sysconf.  */

static int
get_image_num_from_envvar (void)
{
  char *num_images_char;
  int nimages;
  num_images_char = getenv (GFORTRAN_ENV_NUM_IMAGES);

  /* It is expected that the user has set the GFORTRAN_NUM_IMAGES
     environment variable. If not, we fall back to the number of cores.  */
  if (!num_images_char)
#ifdef _SC_NPROCESSORS_ONLN
    return sysconf (_SC_NPROCESSORS_ONLN);
#elif defined(WIN32)
    num_images_char = getenv ("NUMBER_OF_PROCESSORS");
#elif defined (__hpux__)
    return mpctl (MPC_GETNUMSPUS, 0, 0);
#else
#error "Unsupported system: No known way to get number of cores!"
#endif
  nimages = atoi (num_images_char);
  return nimages;
}

/* Get the number of restarts allowed when the shared memory could not be placed
at the same location in each image.  This is mostly important for MacOS, because
this OS acts somewhat arbitrary/indeterministic.  */

static unsigned
get_image_restarts_limit (void)
{
  char *limit_chars;
  unsigned limit = 4000;
  limit_chars = getenv (GFORTRAN_ENV_IMAGE_RESTARTS_LIMITS);
  if (limit_chars)
    limit = atoi (limit_chars);
  return limit;
}

/* Get the amount of memory for the shared memory block.  This is picked from
   an environment variable.  If that is not there, pick a reasonable default.
   Note that on a 64-bit system which allows overcommit, there is no penalty in
   reserving a large space and then not using it.  */

static size_t
get_memory_size_from_envvar (void)
{
  char *e;
  size_t sz = 0;
  e = getenv (GFORTRAN_ENV_SHARED_MEMORY_SIZE);
  if (e)
    {
      char suffix[2];
      int rv;
      rv = sscanf (e, "%zu%1s", &sz, suffix);
      if (rv == 2)
	{
	  switch (suffix[0])
	    {
	    case 'k':
	    case 'K':
	      sz *= ((size_t) 1) << 10;
	      break;
	    case 'm':
	    case 'M':
	      sz *= ((size_t) 1) << 20;
	      break;
	    case 'g':
	    case 'G':
	      sz *= ((size_t) 1) << 30;
	      break;
	    default:
	      sz = 0;
	    }
	}
    }
  if (sz == 0)
    {
      /* Use 256 MB for 32-bit systems and 4 GB for 64-bit systems.  */
      if (sizeof (size_t) == 4)
	sz = ((size_t) 1) << 28;
      else
#ifndef WIN32
	sz = ((size_t) 1) << 34;
#else
	/* Use 1GB on Windows.  */
	sz = ((size_t) 1) << 30;
#endif
    }
  return sz;
}

/* Get a supervisor.  */

static supervisor *
get_supervisor (void)
{
  supervisor *sv;
  sv = SHMPTR_AS (supervisor *,
		  shared_memory_get_master (&local->sm,
					    sizeof (supervisor)
					      + sizeof (image_tracker)
						  * local->total_num_images,
					    __alignof__ (supervisor)),
		  &local->sm);
  sv->failed_images = 0;
  sv->finished_images = 0;
  return sv;
}

/* Defined in shmem.c, but we need it here.  */

extern memid next_memid;

#define SUPERVISOR_MAGIC_NUM 0x12345678

/* Ensure things are initialized.  */

void
ensure_shmem_initialization (void)
{
  size_t shmem_size;
  char *image_num;

  if (local)
    return;

  local = malloc (sizeof (image_local));
  if (!local)
    {
      caf_runtime_error ("can not initialize memory for local cache");
      exit (1);
    }
#if defined(_SC_PAGE_SIZE)
  pagesize = sysconf (_SC_PAGE_SIZE);
#elif defined(WIN32)
  {
    SYSTEM_INFO si;
    GetNativeSystemInfo (&si);
    pagesize = si.dwAllocationGranularity;
  }
#else
#warning                                                                       \
  "Unsupported system: No known way to get memory page size. Assuming 4k!"
  pagesize = 4096;
#endif
  shmem_size = round_to_pagesize (get_memory_size_from_envvar ());
  local->total_num_images = get_image_num_from_envvar ();
  shared_memory_init (&local->sm, shmem_size);
  shared_memory_prepare (&local->sm);

  /* Shared memory needs to be present, before master can be initialized/linked
     to.  */
  image_num = getenv (GFORTRAN_ENV_IMAGE_NUM);
  if (image_num)
    {
      bool created;
      this_image = (image) {atoi (image_num), get_supervisor ()};
      assert (this_image.supervisor->magic_number == SUPERVISOR_MAGIC_NUM);

      alloc_init (&local->ai, &local->sm);

      caf_initial_team = caf_current_team
	= (caf_shmem_team_t) calloc (1, sizeof (struct caf_shmem_team));
      allocator_lock (&local->ai.alloc);
      *caf_initial_team = (struct caf_shmem_team) {
	NULL,
	-1,
	this_image.image_num,
	0,
	NULL,
	{alloc_get_memory_by_id_created (&local->ai,
					 local->total_num_images * sizeof (int)
					   + sizeof (struct shmem_image_info),
					 next_memid++, &created)}};
      if (created)
	{
	  counter_barrier_init (&caf_initial_team->u.image_info->image_count,
				local->total_num_images);
	  collsub_init_supervisor (&caf_initial_team->u.image_info->collsub,
				   alloc_get_allocator (&local->ai),
				   local->total_num_images);
	  caf_initial_team->u.image_info->team_parent_id = 0;
	  caf_initial_team->u.image_info->team_id = -1;
	  caf_initial_team->u.image_info->image_map_size
	    = local->total_num_images;
	  caf_initial_team->u.image_info->num_term_images = 0;
	  caf_initial_team->u.image_info->lastmemid = 0;
	  for (int i = 0; i < local->total_num_images; ++i)
	    caf_initial_team->u.image_info->image_map[i] = i;
	}
      allocator_unlock (&local->ai.alloc);
      sync_init (&local->si, &local->sm);
    }
  else
    {
      this_image = (image) {-1, get_supervisor ()};
      this_image.supervisor->magic_number = SUPERVISOR_MAGIC_NUM;
      thread_support_init_supervisor ();
      counter_barrier_init (&this_image.supervisor->num_active_images,
			    local->total_num_images);
      alloc_init_supervisor (&local->ai, &local->sm);
      sync_init_supervisor (&local->si, &local->ai);
    }
}

#if !defined(environ)
extern char **environ;
#endif

static bool
startWorker (image *im __attribute__ ((unused)),
	     char ***argv __attribute__ ((unused)))
{
#ifdef HAVE_FORK
  caf_shmem_pid new_pid;
  if ((new_pid = fork ()))
    {
      im->supervisor->images[im->image_num]
	= (image_tracker) {new_pid, IMAGE_OK};
      return false;
    }
  else
    {
      if (new_pid == -1)
	caf_runtime_error ("error spawning child\n");
      static char **new_env;
      static char num_image[32];
      size_t n = 2; /* Add one env-var and one for the term NULL.  */

      /* Count the number of entries in the current environment.  */
      for (char **e = environ; *e; ++e, ++n)
	;
      new_env = (char **) malloc (sizeof (char *) * n);
      memcpy (new_env, environ, sizeof (char *) * (n - 2));
      snprintf (num_image, 32, "%s=%d", GFORTRAN_ENV_IMAGE_NUM, im->image_num);
      new_env[n - 2] = num_image;
      new_env[n - 1] = NULL;
      if (execve ((*argv)[0], *argv, new_env) == -1)
	{
	  perror ("execve failed");
	}
      exit (255);
    }
#endif
  return true;
}

#ifdef HAVE_FORK
static void
kill_all_images (supervisor *m)
{
  for (int j = 0; j < local->total_num_images; j++)
    if (m->images[j].status == IMAGE_OK)
      kill (m->images[j].pid, SIGKILL);
}
#endif

/* argc and argv may not be used on certain OSes.  Flag them unused therefore.
 */
int
supervisor_main_loop (int *argc __attribute__ ((unused)),
		      char ***argv __attribute__ ((unused)), int *exit_code)
{
  supervisor *m;
  image im;
#if defined(WIN32) && !defined(HAVE_FORK)
  HANDLE *process_handles = malloc (sizeof (HANDLE) * local->total_num_images),
	 *thread_handles = malloc (sizeof (HANDLE) * local->total_num_images),
	 *waiting_handles = malloc (sizeof (HANDLE) * local->total_num_images);
  int count_waiting = local->total_num_images;
  LPTCH *envs = malloc (sizeof (LPTCH) * local->total_num_images);
  LPTSTR currentDir;
  DWORD cdLen = GetCurrentDirectory (0, NULL);
  currentDir = malloc (cdLen);
  GetCurrentDirectory (cdLen, currentDir);
#else
  int chstatus;
  unsigned restarts = 0, restarts_limit;
  restarts_limit = get_image_restarts_limit ();
#endif

  *exit_code = 0;
  shared_memory_set_env (getpid ());
  im.supervisor = m = this_image.supervisor;

  for (im.image_num = 0; im.image_num < local->total_num_images; im.image_num++)
    {
#ifdef HAVE_FORK
      if (startWorker (&im, argv))
	return 1;
#elif defined(WIN32)
      LPTCH new_env;
      size_t n = 0, es;
      STARTUPINFO si;
      DWORD dwFlags = 0;
      PROCESS_INFORMATION pi;
      LPTCH env = GetEnvironmentStrings ();

      ZeroMemory (&si, sizeof (si));
      si.cb = sizeof (si);
      ZeroMemory (&pi, sizeof (pi));

      /* Count the number of characters in the current environment.  */
      for (LPTSTR e = (LPTSTR) env; *e; es = lstrlen (e) + 1, e += es, n += es)
	;
      new_env = (LPCH) malloc (n + 32 * sizeof (TCHAR));
      memcpy (new_env, env, n);
      snprintf (&((TCHAR *) new_env)[n], 32, "%s=%d%c", GFORTRAN_ENV_IMAGE_NUM,
		im.image_num, (char) 0);
      if (!CreateProcessA (NULL, GetCommandLine (), NULL, NULL, FALSE, dwFlags,
			   new_env, currentDir, &si, &pi))
	{
	  LPVOID lpMsgBuf;
	  DWORD dw = GetLastError ();

	  if (FormatMessage (FORMAT_MESSAGE_ALLOCATE_BUFFER
			       | FORMAT_MESSAGE_FROM_SYSTEM
			       | FORMAT_MESSAGE_IGNORE_INSERTS,
			     NULL, dw,
			     MAKELANGID (LANG_NEUTRAL, SUBLANG_DEFAULT),
			     (LPTSTR) &lpMsgBuf, 0, NULL)
	      == 0)
	    {
	      fprintf (stderr, "formatting the error message failed.\n");
	      ExitProcess (dw);
	    }

	  fprintf (stderr, "error spawning child: %ld, %s\n", dw,
		   (LPCTSTR) lpMsgBuf);

	  LocalFree (lpMsgBuf);
	  exit (1);
	}
      m->images[im.image_num] = (image_tracker) {pi.hProcess, IMAGE_OK};
      process_handles[im.image_num] = waiting_handles[im.image_num]
	= pi.hProcess;
      thread_handles[im.image_num] = pi.hThread;
      envs[im.image_num] = new_env;
#else
#error "no way known to start child processes."
#endif
    }
  for (int i = 0; i < local->total_num_images; i++)
    {
#ifdef HAVE_FORK
      caf_shmem_pid finished_pid = wait (&chstatus);
      int j;

      if (finished_pid == -1)
	{
	  /* Skip wait having an issue.  */
	  perror ("wait failed");
	  --i;
	  continue;
	}
      if (WIFEXITED (chstatus) && !WEXITSTATUS (chstatus))
	{
	  for (j = 0;
	       j < local->total_num_images && m->images[j].pid != finished_pid;
	       j++)
	    ;
	  /* Only set the status, when it has not been set by the (failing)
	     image already.  */
	  if (m->images[j].status == IMAGE_OK)
	    {
	      m->images[j].status = IMAGE_SUCCESS;
	      atomic_fetch_add (&m->finished_images, 1);
	    }
	}
      else if (!WIFEXITED (chstatus) || WEXITSTATUS (chstatus))
	{
	  for (j = 0;
	       j < local->total_num_images && m->images[j].pid != finished_pid;
	       j++)
	    ;
	  if (WEXITSTATUS (chstatus) == 210)
	    {
	      --i;
	      im.image_num = j;
	      ++restarts;
	      if (restarts > restarts_limit)
		{
		  kill_all_images (m);
		  caf_runtime_error (
		    "After restarting images %d times, no common state on "
		    "shared memory could be reached. Giving up...",
		    restarts);
		  exit (1);
		}
	      if (startWorker (&im, argv))
		return 1;
	      continue;
	    }
	  else
	    {
	      fprintf (stderr,
		       "ERROR: Image %d(pid: %d) failed with signal %d, "
		       "exitstatus %d.\n",
		       j + 1, finished_pid, WTERMSIG (chstatus),
		       WEXITSTATUS (chstatus));
	      if (j == local->total_num_images)
		{
		  if (finished_pid == getpid ())
		    {
		       fprintf (stderr,
			"WARNING: Supervisor process got signal %d. Killing "
			"childs and exiting.\n",
			WTERMSIG (chstatus));
		      kill_all_images (m);
		      exit (1);
		    }
		  fprintf (stderr,
			   "WARNING: Got signal %d for unknown process %d. "
			   "Ignoring and trying to continue.\n",
			   WTERMSIG (chstatus), finished_pid);
		  continue;
		}
	      m->images[j].status = IMAGE_FAILED;
	      atomic_fetch_add (&m->failed_images, 1);
	      if (*exit_code < WTERMSIG (chstatus))
		*exit_code = WTERMSIG (chstatus);
	      else if (*exit_code == 0)
		*exit_code = 1;
	    }
	}
      /* Trigger waiting sync images aka sync_table.  */
      for (j = 0; j < local->total_num_images; j++)
	caf_shmem_cond_signal (&SHMPTR_AS (caf_shmem_condvar *,
					   m->sync_shared.sync_images_cond_vars,
					   &local->sm)[j]);
      counter_barrier_add (&m->num_active_images, -1);
#elif defined(WIN32)
      DWORD res = WaitForMultipleObjects (count_waiting, waiting_handles, FALSE,
					  INFINITE);
      HANDLE cand;
      bool progress = false;
      DWORD process_exit_code;
      if (res == WAIT_FAILED)
	caf_runtime_error ("waiting for process termination failed.");
      int index = res - WAIT_OBJECT_0, finished_process;
      bool fail;

      do
	{
	  cand = waiting_handles[index];
	  for (finished_process = 0;
	       finished_process < local->total_num_images
	       && cand != process_handles[finished_process];
	       ++finished_process)
	    ;

	  GetExitCodeProcess (cand, &process_exit_code);
	  fail = process_exit_code != 0;
	  fprintf (stderr, "terminating process %d with fail status %d (%ld)\n",
		   finished_process, fail, process_exit_code);
	  if (finished_process < local->total_num_images)
	    {
	      CloseHandle (process_handles[finished_process]);
	      process_handles[finished_process] = NULL;
	      CloseHandle (thread_handles[finished_process]);
	      FreeEnvironmentStrings (envs[finished_process]);
	      if (fail)
		{
		  m->images[finished_process].status = IMAGE_FAILED;
		  atomic_fetch_add (&m->failed_images, 1);
		  if (*exit_code < process_exit_code)
		    *exit_code = process_exit_code;
		}
	      else
		{
		  m->images[finished_process].status = IMAGE_SUCCESS;
		  atomic_fetch_add (&m->finished_images, 1);
		}
	    }
	  memmove (&waiting_handles[index], &waiting_handles[index + 1],
		   sizeof (HANDLE) * (count_waiting - index - 1));
	  --count_waiting;
	  counter_barrier_add (&m->num_active_images, -1);

	  /* Check if more than one process has terminated already.  */
	  progress = false;
	  for (index = 0; index < count_waiting; ++index)
	    if (WaitForSingleObject (waiting_handles[index], 0)
		== WAIT_OBJECT_0)
	      {
		progress = true;
		++i;
		break;
	      }
	}
      while (progress && count_waiting > 0);
#endif
    }

#if defined(WIN32) && !defined(HAVE_FORK)
  free (process_handles);
  free (thread_handles);
  free (envs);
#endif
  return 0;
}
