/* Thread-local storage (native Windows implementation).
   Copyright (C) 2005-2025 Free Software Foundation, Inc.

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

   This file 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 Lesser General Public License for more details.

   You should have received a copy of the GNU Lesser General Public License
   along with this program.  If not, see <https://www.gnu.org/licenses/>.  */

/* Written by Bruno Haible <bruno@clisp.org>, 2005.  */

#include <config.h>

/* Specification.  */
#include "windows-tls.h"

#include <errno.h>
#include <limits.h>
#include <stdlib.h>

#include "windows-once.h"

void *
glwthread_tls_get (glwthread_tls_key_t key)
{
  return TlsGetValue (key);
}

int
glwthread_tls_set (glwthread_tls_key_t key, void *value)
{
  if (!TlsSetValue (key, value))
    return EINVAL;
  return 0;
}

/* The following variables keep track of TLS keys with non-NULL destructor.  */

static glwthread_once_t dtor_table_init_once = GLWTHREAD_ONCE_INIT;

static CRITICAL_SECTION dtor_table_lock;

struct dtor { glwthread_tls_key_t key; void (*destructor) (void *); };

/* The table of dtors.  */
static struct dtor *dtor_table;
/* Number of active entries in the dtor_table.  */
static unsigned int dtors_count;
/* Valid indices into dtor_table are 0..dtors_used-1.  */
static unsigned int dtors_used;
/* Allocation size of dtor_table.  */
static unsigned int dtors_allocated;
/* Invariant: 0 <= dtors_count <= dtors_used <= dtors_allocated.  */

/* Number of threads that are currently processing destructors.  */
static unsigned int dtor_processing_threads;

static void
dtor_table_initialize (void)
{
  InitializeCriticalSection (&dtor_table_lock);
  /* The other variables are already initialized to NULL or 0, respectively.  */
}

static void
dtor_table_ensure_initialized (void)
{
  glwthread_once (&dtor_table_init_once, dtor_table_initialize);
}

/* Shrinks dtors_used down to dtors_count, by replacing inactive entries
   with active ones.  */
static void
dtor_table_shrink_used (void)
{
  unsigned int i = 0;
  unsigned int j = dtors_used;

  for (;;)
    {
      BOOL i_found = FALSE;
      BOOL j_found = FALSE;
      /* Find the next inactive entry, from the left.  */
      for (; i < dtors_count;)
        {
          if (dtor_table[i].destructor == NULL)
            {
              i_found = TRUE;
              break;
            }
          i++;
        }

      /* Find the next active entry, from the right.  */
      for (; j > dtors_count;)
        {
          j--;
          if (dtor_table[j].destructor != NULL)
            {
              j_found = TRUE;
              break;
            }
        }

      if (i_found != j_found)
        /* dtors_count was apparently wrong.  */
        abort ();

      if (!i_found)
        break;

      /* i_found and j_found are TRUE.  Swap the two entries.  */
      dtor_table[i] = dtor_table[j];

      i++;
    }

  dtors_used = dtors_count;
}

void
glwthread_tls_process_destructors (void)
{
  dtor_table_ensure_initialized ();

  EnterCriticalSection (&dtor_table_lock);
  if (dtor_processing_threads == 0)
    {
      /* Now it's the appropriate time for shrinking dtors_used.  */
      if (dtors_used > dtors_count)
        dtor_table_shrink_used ();
    }
  dtor_processing_threads++;

  for (unsigned int repeat = GLWTHREAD_DESTRUCTOR_ITERATIONS; repeat > 0; repeat--)
    {
      unsigned int destructors_run = 0;

      /* Iterate across dtor_table.  We don't need to make a copy of dtor_table,
         because
           * When another thread calls glwthread_tls_key_create with a non-NULL
             destructor argument, this will possibly reallocate the dtor_table
             array and increase dtors_allocated as well as dtors_used and
             dtors_count, but it will not change dtors_used nor the contents of
             the first dtors_used entries of dtor_table.
           * When another thread calls glwthread_tls_key_delete, this will
             possibly set some 'destructor' member to NULL, thus marking an
             entry as inactive, but it will not otherwise change dtors_used nor
             the contents of the first dtors_used entries of dtor_table.  */
      unsigned int i_limit = dtors_used;

      for (unsigned int i = 0; i < i_limit; i++)
        {
          struct dtor current = dtor_table[i];
          if (current.destructor != NULL)
            {
              /* The current dtor has not been deleted yet.  */
              void *current_value = glwthread_tls_get (current.key);
              if (current_value != NULL)
                {
                  /* The current value is non-NULL.  Run the destructor.  */
                  glwthread_tls_set (current.key, NULL);
                  LeaveCriticalSection (&dtor_table_lock);
                  current.destructor (current_value);
                  EnterCriticalSection (&dtor_table_lock);
                  destructors_run++;
                }
            }
        }

      /* When all TLS values were already NULL, no further iterations are
         needed.  */
      if (destructors_run == 0)
        break;
    }

  dtor_processing_threads--;
  LeaveCriticalSection (&dtor_table_lock);
}

int
glwthread_tls_key_create (glwthread_tls_key_t *keyp, void (*destructor) (void *))
{
  if (destructor != NULL)
    {
      dtor_table_ensure_initialized ();

      EnterCriticalSection (&dtor_table_lock);
      if (dtor_processing_threads == 0)
        {
          /* Now it's the appropriate time for shrinking dtors_used.  */
          if (dtors_used > dtors_count)
            dtor_table_shrink_used ();
        }

      while (dtors_used == dtors_allocated)
        {
          /* Need to grow the dtor_table.  */
          unsigned int new_allocated = 2 * dtors_allocated + 1;
          if (new_allocated < 7)
            new_allocated = 7;
          if (new_allocated <= dtors_allocated) /* overflow? */
            new_allocated = UINT_MAX;

          LeaveCriticalSection (&dtor_table_lock);
          {
            struct dtor *new_table =
              (struct dtor *) malloc (new_allocated * sizeof (struct dtor));
            if (new_table == NULL)
              return ENOMEM;
            EnterCriticalSection (&dtor_table_lock);
            /* Attention! dtors_used, dtors_allocated may have changed!  */
            if (dtors_used < new_allocated)
              {
                if (dtors_allocated < new_allocated)
                  {
                    /* The new_table is useful.  */
                    memcpy (new_table, dtor_table,
                            dtors_used * sizeof (struct dtor));
                    dtor_table = new_table;
                    dtors_allocated = new_allocated;
                  }
                else
                  {
                    /* The new_table is not useful, since another thread
                       meanwhile allocated a drop_table that is at least
                       as large.  */
                    free (new_table);
                  }
                break;
              }
            /* The new_table is not useful, since other threads increased
               dtors_used.  Free it any retry.  */
            free (new_table);
          }
        }
      /* Here dtors_used < dtors_allocated.  */
      {
        /* Allocate a new key.  */
        glwthread_tls_key_t key = TlsAlloc ();
        if (key == (DWORD)-1)
          {
            LeaveCriticalSection (&dtor_table_lock);
            return EAGAIN;
          }
        /* Store the new dtor in the dtor_table, after all used entries.
           Do not overwrite inactive entries with indices < dtors_used, in order
           not to disturb glwthread_tls_process_destructors invocations that may
           be executing in other threads.  */
        dtor_table[dtors_used].key = key;
        dtor_table[dtors_used].destructor = destructor;
        dtors_used++;
        dtors_count++;
        LeaveCriticalSection (&dtor_table_lock);
        *keyp = key;
      }
    }
  else
    {
      /* Allocate a new key.  */
      glwthread_tls_key_t key = TlsAlloc ();
      if (key == (DWORD)-1)
        return EAGAIN;
      *keyp = key;
    }
  return 0;
}

int
glwthread_tls_key_delete (glwthread_tls_key_t key)
{
  /* Should the destructor be called for all threads that are currently running?
     Probably not, because
       - ISO C does not specify when the destructor is to be invoked at all.
       - In POSIX, the destructor functions specified with pthread_key_create()
         are invoked at thread exit.
       - It would be hard to implement, because there are no primitives for
         accessing thread-specific values from a different thread.  */
  dtor_table_ensure_initialized ();

  EnterCriticalSection (&dtor_table_lock);
  if (dtor_processing_threads == 0)
    {
      /* Now it's the appropriate time for shrinking dtors_used.  */
      if (dtors_used > dtors_count)
        dtor_table_shrink_used ();
      /* Here dtors_used == dtors_count.  */

      /* Find the key in dtor_table.  */
      {
        unsigned int i_limit = dtors_used;

        for (unsigned int i = 0; i < i_limit; i++)
          if (dtor_table[i].key == key)
            {
              if (i < dtors_used - 1)
                /* Swap the entries i and dtors_used - 1.  */
                dtor_table[i] = dtor_table[dtors_used - 1];
              dtors_count = dtors_used = dtors_used - 1;
              break;
            }
      }
    }
  else
    {
      /* Be careful not to disturb the glwthread_tls_process_destructors
         invocations that are executing in other threads.  */
      unsigned int i_limit = dtors_used;

      for (unsigned int i = 0; i < i_limit; i++)
        if (dtor_table[i].destructor != NULL /* skip inactive entries */
            && dtor_table[i].key == key)
          {
            /* Mark this entry as inactive.  */
            dtor_table[i].destructor = NULL;
            dtors_count = dtors_count - 1;
            break;
          }
    }
  LeaveCriticalSection (&dtor_table_lock);
  /* Now we have ensured that glwthread_tls_process_destructors will no longer
     use this key.  */

  if (!TlsFree (key))
    return EINVAL;
  return 0;
}
