/* -*- Mode: C++; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

#include "primpl.h"

_PRCPU* _pr_primordialCPU = NULL;

PRInt32 _pr_md_idle_cpus; /* number of idle cpus */
/*
 * The idle threads in MxN models increment/decrement _pr_md_idle_cpus.
 * If _PR_HAVE_ATOMIC_OPS is not defined, they can't use the atomic
 * increment/decrement routines (which are based on PR_Lock/PR_Unlock),
 * because PR_Lock asserts that the calling thread is not an idle thread.
 * So we use a _MDLock to protect _pr_md_idle_cpus.
 */
#if !defined(_PR_LOCAL_THREADS_ONLY) && !defined(_PR_GLOBAL_THREADS_ONLY)
#  ifndef _PR_HAVE_ATOMIC_OPS
static _MDLock _pr_md_idle_cpus_lock;
#  endif
#endif
PRUintn _pr_numCPU;
PRInt32 _pr_cpus_exit;
PRUint32 _pr_cpu_affinity_mask = 0;

#if !defined(_PR_GLOBAL_THREADS_ONLY)

static PRUintn _pr_cpuID;

static void PR_CALLBACK _PR_CPU_Idle(void*);

static _PRCPU* _PR_CreateCPU(void);
static PRStatus _PR_StartCPU(_PRCPU* cpu, PRThread* thread);

#  if !defined(_PR_LOCAL_THREADS_ONLY)
static void _PR_RunCPU(void* arg);
#  endif

void _PR_InitCPUs() {
  PRThread* me = _PR_MD_CURRENT_THREAD();

  if (_native_threads_only) {
    return;
  }

  _pr_cpuID = 0;
  _MD_NEW_LOCK(&_pr_cpuLock);
#  if !defined(_PR_LOCAL_THREADS_ONLY) && !defined(_PR_GLOBAL_THREADS_ONLY)
#    ifndef _PR_HAVE_ATOMIC_OPS
  _MD_NEW_LOCK(&_pr_md_idle_cpus_lock);
#    endif
#  endif

#  ifdef _PR_LOCAL_THREADS_ONLY

#    ifdef HAVE_CUSTOM_USER_THREADS
  _PR_MD_CREATE_PRIMORDIAL_USER_THREAD(me);
#    endif

  /* Now start the first CPU. */
  _pr_primordialCPU = _PR_CreateCPU();
  _pr_numCPU = 1;
  _PR_StartCPU(_pr_primordialCPU, me);

  _PR_MD_SET_CURRENT_CPU(_pr_primordialCPU);

  /* Initialize cpu for current thread (could be different from me) */
  _PR_MD_CURRENT_THREAD()->cpu = _pr_primordialCPU;

  _PR_MD_SET_LAST_THREAD(me);

#  else /* Combined MxN model */

  _pr_primordialCPU = _PR_CreateCPU();
  _pr_numCPU = 1;
  _PR_CreateThread(PR_SYSTEM_THREAD, _PR_RunCPU, _pr_primordialCPU,
                   PR_PRIORITY_NORMAL, PR_GLOBAL_THREAD, PR_UNJOINABLE_THREAD,
                   0, _PR_IDLE_THREAD);

#  endif /* _PR_LOCAL_THREADS_ONLY */

  _PR_MD_INIT_CPUS();
}

#  ifdef WINNT
/*
 * Right now this function merely stops the CPUs and does
 * not do any other cleanup.
 *
 * It is only implemented for WINNT because bug 161998 only
 * affects the WINNT version of NSPR, but it would be nice
 * to implement this function for other platforms too.
 */
void _PR_CleanupCPUs(void) {
  PRUintn i;
  PRCList* qp;
  _PRCPU* cpu;

  _pr_cpus_exit = 1;
  for (i = 0; i < _pr_numCPU; i++) {
    _PR_MD_WAKEUP_WAITER(NULL);
  }
  for (qp = _PR_CPUQ().next; qp != &_PR_CPUQ(); qp = qp->next) {
    cpu = _PR_CPU_PTR(qp);
    _PR_MD_JOIN_THREAD(&cpu->thread->md);
  }
}
#  endif

static _PRCPUQueue* _PR_CreateCPUQueue(void) {
  PRInt32 index;
  _PRCPUQueue* cpuQueue;
  cpuQueue = PR_NEWZAP(_PRCPUQueue);

  _MD_NEW_LOCK(&cpuQueue->runQLock);
  _MD_NEW_LOCK(&cpuQueue->sleepQLock);
  _MD_NEW_LOCK(&cpuQueue->miscQLock);

  for (index = 0; index < PR_ARRAY_SIZE(cpuQueue->runQ); index++) {
    PR_INIT_CLIST(&(cpuQueue->runQ[index]));
  }
  PR_INIT_CLIST(&(cpuQueue->sleepQ));
  PR_INIT_CLIST(&(cpuQueue->pauseQ));
  PR_INIT_CLIST(&(cpuQueue->suspendQ));
  PR_INIT_CLIST(&(cpuQueue->waitingToJoinQ));

  cpuQueue->numCPUs = 1;

  return cpuQueue;
}

/*
 * Create a new CPU.
 *
 * This function initializes enough of the _PRCPU structure so
 * that it can be accessed safely by a global thread or another
 * CPU.  This function does not create the native thread that
 * will run the CPU nor does it initialize the parts of _PRCPU
 * that must be initialized by that native thread.
 *
 * The reason we cannot simply have the native thread create
 * and fully initialize a new CPU is that we need to be able to
 * create a usable _pr_primordialCPU in _PR_InitCPUs without
 * assuming that the primordial CPU thread we created can run
 * during NSPR initialization.  For example, on Windows while
 * new threads can be created by DllMain, they won't be able
 * to run during DLL initialization.  If NSPR is initialized
 * by DllMain, the primordial CPU thread won't run until DLL
 * initialization is finished.
 */
static _PRCPU* _PR_CreateCPU(void) {
  _PRCPU* cpu;

  cpu = PR_NEWZAP(_PRCPU);
  if (cpu) {
    cpu->queue = _PR_CreateCPUQueue();
    if (!cpu->queue) {
      PR_DELETE(cpu);
      return NULL;
    }
  }
  return cpu;
}

/*
 * Start a new CPU.
 *
 * 'cpu' is a _PRCPU structure created by _PR_CreateCPU().
 * 'thread' is the native thread that will run the CPU.
 *
 * If this function fails, 'cpu' is destroyed.
 */
static PRStatus _PR_StartCPU(_PRCPU* cpu, PRThread* thread) {
  /*
  ** Start a new cpu. The assumption this code makes is that the
  ** underlying operating system creates a stack to go with the new
  ** native thread. That stack will be used by the cpu when pausing.
  */

  PR_ASSERT(!_native_threads_only);

  cpu->last_clock = PR_IntervalNow();

  /* Before we create any threads on this CPU we have to
   * set the current CPU
   */
  _PR_MD_SET_CURRENT_CPU(cpu);
  _PR_MD_INIT_RUNNING_CPU(cpu);
  thread->cpu = cpu;

  cpu->idle_thread = _PR_CreateThread(
      PR_SYSTEM_THREAD, _PR_CPU_Idle, (void*)cpu, PR_PRIORITY_NORMAL,
      PR_LOCAL_THREAD, PR_UNJOINABLE_THREAD, 0, _PR_IDLE_THREAD);

  if (!cpu->idle_thread) {
    /* didn't clean up CPU queue XXXMB */
    PR_DELETE(cpu);
    return PR_FAILURE;
  }
  PR_ASSERT(cpu->idle_thread->cpu == cpu);

  cpu->idle_thread->no_sched = 0;

  cpu->thread = thread;

  if (_pr_cpu_affinity_mask) {
    PR_SetThreadAffinityMask(thread, _pr_cpu_affinity_mask);
  }

  /* Created and started a new CPU */
  _PR_CPU_LIST_LOCK();
  cpu->id = _pr_cpuID++;
  PR_APPEND_LINK(&cpu->links, &_PR_CPUQ());
  _PR_CPU_LIST_UNLOCK();

  return PR_SUCCESS;
}

#  if !defined(_PR_GLOBAL_THREADS_ONLY) && !defined(_PR_LOCAL_THREADS_ONLY)
/*
** This code is used during a cpu's initial creation.
*/
static void _PR_RunCPU(void* arg) {
  _PRCPU* cpu = (_PRCPU*)arg;
  PRThread* me = _PR_MD_CURRENT_THREAD();

  PR_ASSERT(NULL != me);

  /*
   * _PR_StartCPU calls _PR_CreateThread to create the
   * idle thread.  Because _PR_CreateThread calls PR_Lock,
   * the current thread has to remain a global thread
   * during the _PR_StartCPU call so that it can wait for
   * the lock if the lock is held by another thread.  If
   * we clear the _PR_GLOBAL_SCOPE flag in
   * _PR_MD_CREATE_PRIMORDIAL_THREAD, the current thread
   * will be treated as a local thread and have trouble
   * waiting for the lock because the CPU is not fully
   * constructed yet.
   *
   * After the CPU is started, it is safe to mark the
   * current thread as a local thread.
   */

#    ifdef HAVE_CUSTOM_USER_THREADS
  _PR_MD_CREATE_PRIMORDIAL_USER_THREAD(me);
#    endif

  me->no_sched = 1;
  _PR_StartCPU(cpu, me);

#    ifdef HAVE_CUSTOM_USER_THREADS
  me->flags &= (~_PR_GLOBAL_SCOPE);
#    endif

  _PR_MD_SET_CURRENT_CPU(cpu);
  _PR_MD_SET_CURRENT_THREAD(cpu->thread);
  me->cpu = cpu;

  while (1) {
    PRInt32 is;
    if (!_PR_IS_NATIVE_THREAD(me)) {
      _PR_INTSOFF(is);
    }
    _PR_MD_START_INTERRUPTS();
    _PR_MD_SWITCH_CONTEXT(me);
  }
}
#  endif

static void PR_CALLBACK _PR_CPU_Idle(void* _cpu) {
  _PRCPU* cpu = (_PRCPU*)_cpu;
  PRThread* me = _PR_MD_CURRENT_THREAD();

  PR_ASSERT(NULL != me);

  me->cpu = cpu;
  cpu->idle_thread = me;
  if (_MD_LAST_THREAD()) {
    _MD_LAST_THREAD()->no_sched = 0;
  }
  if (!_PR_IS_NATIVE_THREAD(me)) {
    _PR_MD_SET_INTSOFF(0);
  }
  while (1) {
    PRInt32 is;
    PRIntervalTime timeout;
    if (!_PR_IS_NATIVE_THREAD(me)) {
      _PR_INTSOFF(is);
    }

    _PR_RUNQ_LOCK(cpu);
#  if !defined(_PR_LOCAL_THREADS_ONLY) && !defined(_PR_GLOBAL_THREADS_ONLY)
#    ifdef _PR_HAVE_ATOMIC_OPS
    _PR_MD_ATOMIC_INCREMENT(&_pr_md_idle_cpus);
#    else
    _PR_MD_LOCK(&_pr_md_idle_cpus_lock);
    _pr_md_idle_cpus++;
    _PR_MD_UNLOCK(&_pr_md_idle_cpus_lock);
#    endif /* _PR_HAVE_ATOMIC_OPS */
#  endif
    /* If someone on runq; do a nonblocking PAUSECPU */
    if (_PR_RUNQREADYMASK(me->cpu) != 0) {
      _PR_RUNQ_UNLOCK(cpu);
      timeout = PR_INTERVAL_NO_WAIT;
    } else {
      _PR_RUNQ_UNLOCK(cpu);

      _PR_SLEEPQ_LOCK(cpu);
      if (PR_CLIST_IS_EMPTY(&_PR_SLEEPQ(me->cpu))) {
        timeout = PR_INTERVAL_NO_TIMEOUT;
      } else {
        PRThread* wakeThread;
        wakeThread = _PR_THREAD_PTR(_PR_SLEEPQ(me->cpu).next);
        timeout = wakeThread->sleep;
      }
      _PR_SLEEPQ_UNLOCK(cpu);
    }

    /* Wait for an IO to complete */
    (void)_PR_MD_PAUSE_CPU(timeout);

#  ifdef WINNT
    if (_pr_cpus_exit) {
      /* _PR_CleanupCPUs tells us to exit */
      _PR_MD_END_THREAD();
    }
#  endif

#  if !defined(_PR_LOCAL_THREADS_ONLY) && !defined(_PR_GLOBAL_THREADS_ONLY)
#    ifdef _PR_HAVE_ATOMIC_OPS
    _PR_MD_ATOMIC_DECREMENT(&_pr_md_idle_cpus);
#    else
    _PR_MD_LOCK(&_pr_md_idle_cpus_lock);
    _pr_md_idle_cpus--;
    _PR_MD_UNLOCK(&_pr_md_idle_cpus_lock);
#    endif /* _PR_HAVE_ATOMIC_OPS */
#  endif

    _PR_ClockInterrupt();

    /* Now schedule any thread that is on the runq
     * INTS must be OFF when calling PR_Schedule()
     */
    me->state = _PR_RUNNABLE;
    _PR_MD_SWITCH_CONTEXT(me);
    if (!_PR_IS_NATIVE_THREAD(me)) {
      _PR_FAST_INTSON(is);
    }
  }
}
#endif /* _PR_GLOBAL_THREADS_ONLY */

PR_IMPLEMENT(void) PR_SetConcurrency(PRUintn numCPUs) {
#if defined(_PR_GLOBAL_THREADS_ONLY) || defined(_PR_LOCAL_THREADS_ONLY)

  /* do nothing */

#else /* combined, MxN thread model */

  PRUintn newCPU;
  _PRCPU* cpu;
  PRThread* thr;

  if (!_pr_initialized) {
    _PR_ImplicitInitialization();
  }

  if (_native_threads_only) {
    return;
  }

  _PR_CPU_LIST_LOCK();
  if (_pr_numCPU < numCPUs) {
    newCPU = numCPUs - _pr_numCPU;
    _pr_numCPU = numCPUs;
  } else {
    newCPU = 0;
  }
  _PR_CPU_LIST_UNLOCK();

  for (; newCPU; newCPU--) {
    cpu = _PR_CreateCPU();
    thr = _PR_CreateThread(PR_SYSTEM_THREAD, _PR_RunCPU, cpu,
                           PR_PRIORITY_NORMAL, PR_GLOBAL_THREAD,
                           PR_UNJOINABLE_THREAD, 0, _PR_IDLE_THREAD);
  }
#endif
}

PR_IMPLEMENT(_PRCPU*) _PR_GetPrimordialCPU(void) {
  if (_pr_primordialCPU) {
    return _pr_primordialCPU;
  } else {
    return _PR_MD_CURRENT_CPU();
  }
}
