/****************************************************************************
 * libs/libc/pthread/pthread_spinlock.c
 *
 *   Copyright (C) 2019 Gregory Nutt. All rights reserved.
 *   Author: Gregory Nutt <gnutt@nuttx.org>
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in
 *    the documentation and/or other materials provided with the
 *    distribution.
 * 3. Neither the name NuttX nor the names of its contributors may be
 *    used to endorse or promote products derived from this software
 *    without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
 * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
 * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
 * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS
 * OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
 * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
 * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 *
 ****************************************************************************/

/****************************************************************************
 * Included Files
 ****************************************************************************/

#include <nuttx/config.h>

#include <sys/types.h>
#include <sys/boardctl.h>

#include <pthread.h>
#include <sched.h>

/* The architecture specific spinlock.h header file must provide the
 * following:
 *
 *   SP_LOCKED    - A definition of the locked state value (usually 1)
 *   SP_UNLOCKED  - A definition of the unlocked state value (usually 0)
 *   spinlock_t   - The type of a spinlock memory object (usually uint8_t).
 */

#include <arch/spinlock.h>

#ifdef CONFIG_PTHREAD_SPINLOCKS

/****************************************************************************
 * Pre-processor Definitions
 ****************************************************************************/

#define IMPOSSIBLE_THREAD ((pthread_t)-1)

/****************************************************************************
 * Public Functions
 ****************************************************************************/

/****************************************************************************
 * Name: pthread_spin_init
 *
 * Description:
 *   The pthread_spin_init() function will allocate any resources required
 *   to use the spin lock referenced by lock and initialize the lock to an
 *   unlocked state.
 *
 *   The results are undefined if pthread_spin_init() is called specifying
 *   an already initialized spin lock. The results are undefined if a spin
 *   lock is used without first being initialized.
 *
 *   If the pthread_spin_init() function fails, the lock is not initialized
 *   and the contents of lock are undefined.
 *
 *   Only the object referenced by lock may be used for performing
 *   synchronization.  The result of referring to copies of that object in
 *   calls to pthread_spin_destroy(), pthread_spin_lock(),
 *   pthread_spin_trylock(), or pthread_spin_unlock() is undefined.
 *
 * Input Parameters:
 *   lock - A reference to the spinlock object to be initialized.
 *   pshared - Unused
 *
 * Returned Value:
 *   Zero (OK) is returned if a valid lock value is provided.
 *
 * POSIX Compatibility:
 *   Not supported: "If the Thread Process-Shared Synchronization option is
 *   supported and the value of pshared is PTHREAD_PROCESS_SHARED, the
 *   implementation shall permit the spin lock to be operated upon by any
 *   thread that has access to the memory where the spin lock is allocated,
 *   even if it is allocated in memory that is shared by multiple
 *   processes."
 *
 ****************************************************************************/

int pthread_spin_init(FAR pthread_spinlock_t *lock, int pshared)
{
  int ret = EINVAL;

  DEBUGASSERT(lock != NULL);
  if (lock != NULL)
    {
      lock->sp_lock   = SP_UNLOCKED;
      lock->sp_holder = IMPOSSIBLE_THREAD;
      ret             = OK;
    }

  return ret;
}

/****************************************************************************
 * Name: pthread_spin_destroy
 *
 * Description:
 *   The pthread_spin_destroy() function will destroy the spin lock
 *   referenced by lock and release any resources used by the lock.  The
 *   effect of subsequent use of the lock is undefined until the lock is
 *   reinitialized by another call to pthread_spin_init(). The results are
 *   undefined if pthread_spin_destroy() is called when a thread holds the
 *   lock, or if this function is called with an uninitialized thread spin
 *   lock.
 *
 * Input Parameters:
 *   lock    - A reference to the spinlock object to be initialized.
 *   pshared - Unused
 *
 * Returned Value:
 *   Zero (OK) is always returned.
 *
 * POSIX Compatibility:
 *   Not supported: "If the Thread Process-Shared Synchronization option is
 *   supported and the value of pshared is PTHREAD_PROCESS_SHARED, the
 *   implementation shall permit the spin lock to be operated upon by any
 *   thread that has access to the memory where the spin lock is allocated,
 *   even if it is allocated in memory that is shared by multiple
 *   processes."
 *
 ****************************************************************************/

int pthread_spin_destroy(pthread_spinlock_t *lock)
{
  return OK;
}

/****************************************************************************
 * Name: pthread_spin_lock
 *
 * Description:
 *   The pthread_spin_lock() function will lock the spin lock referenced by
 *   lock. The calling thread will acquire the lock if it is not held by
 *   another thread. Otherwise, the thread will spin (that is, will not
 *   return from the pthread_spin_lock() call) until the lock becomes
 *   available.
 *
 *   If it is determined the value specified by the lock argument to
 *   pthread_spin_lock() refers to a spin lock object for which the calling
 *   thread already holds the lock, it is recommended that the function will
 *   fail and report an EDEADLK error.
 *
 * Input Parameters:
 *   lock - A reference to the spinlock object to lock.
 *
 * Returned Value:
 *   Zero is returned if the lock was successfully acquired.  Otherwise one
 *   of the following errno values are retured:
 *
 *   EINVAL    - 'lock' does not refer to a valid spinlock object
 *   EDEADLOCK - The caller already holds the spinlock
 *
 ****************************************************************************/

int pthread_spin_lock(pthread_spinlock_t *lock)
{
  pthread_t me = pthread_self();
  int ret;

  DEBUGASSERT(lock != NULL);
  if (lock == NULL)
    {
      return EINVAL;
    }
  else if (lock->sp_holder == me)
    {
      return EDEADLOCK;
    }

  /* Loop until we successfully take the spinlock (i.e., until the previous
   * state of the spinlock was SP_UNLOCKED).  NOTE that the test/set operaion
   * is performed via boardctl() to avoid a variety of issues.  An option
   * might be to move the implementation of up_testset() to libs/libc/machine.
   */

  do
    {
      ret = boardctl(BOARDIOC_TESTSET, (uintptr_t)&lock->sp_lock);
    }
  while (ret == 1);

  /* Check for success (previous state was SP_UNLOCKED) */

  if (ret == 0)
    {
      lock->sp_holder = me;
    }
  else
    {
      /* An error of some kind is the only other possibility */

      DEBUGASSERT(ret < 0);
      ret = -ret;
    }

  return ret;
}

/****************************************************************************
 * Name: pthread_spin_trylock
 *
 * Description:
 *   The pthread_spin_trylock() function will lock the spin lock referenced
 *   by lock. The calling thread will acquire the lock if it is not held by
 *   another thread. Otherwise, The pthread_spin_trylock() will return a
 *   failure.
 *
 * Input Parameters:
 *   lock - A reference to the spinlock object to lock.
 *
 * Returned Value:
 *   Zero is returned if the lock was successfully acquired.  Otherwise one
 *   of the following errno values are retured:
 *
 *   EINVAL - 'lock' does not refer to a valid spinlock object
 *   EBUSY  - The spinlock is held by another thread
 *
 ****************************************************************************/

int pthread_spin_trylock(pthread_spinlock_t *lock)
{
  pthread_t me = pthread_self();
  int ret;

  DEBUGASSERT(lock != NULL);
  if (lock == NULL)
    {
      ret = EINVAL;
    }
  else if (lock->sp_holder == me)
    {
      ret = OK;
    }
  else
    {
      /* Perform the test/set operation via boardctl() */

      ret = boardctl(BOARDIOC_TESTSET, (uintptr_t)&lock->sp_lock);
      switch (ret)
        {
          case 0:  /* Previously unlocked.  We hold the spinlock */
            lock->sp_holder = me;
            break;

          case 1:  /* Previously locked.  We did not get the spinlock  */
            ret = EBUSY;
            break;

          default:
            DEBUGASSERT(ret < 0);
            ret = -ret;
            break;
        }
    }

  return ret;
}

/****************************************************************************
 * Name: pthread_spin_unlock
 *
 * Description:
 *   The pthread_spin_unlock() function will release the spin lock
 *   referenced by lock which was locked via the pthread_spin_lock() or
 *   pthread_spin_trylock() functions.
 *
 *   The results are undefined if the lock is not held by the calling thread.
 *
 *   If there are threads spinning on the lock when pthread_spin_unlock() is
 *   called, the lock becomes available and an unspecified spinning thread
 *   will acquire the lock.
 *
 *   The results are undefined if this function is called with an
 *   uninitialized thread spin lock.
 *
 * Input Parameters:
 *   lock - A reference to the spinlock object to unlock.
 *
 * Returned Value:
 *   Zero is returned if the lock was successfully acquired.  Otherwise one
 *   of the following errno values are retured:
 *
 *   EINVAL    - 'lock' does not refer to a valid spinlock object
 *   EDEADLOCK - The caller already holds the spinlock
 *
 ****************************************************************************/

int pthread_spin_unlock(pthread_spinlock_t *lock)
{
  pthread_t me = pthread_self();

  DEBUGASSERT(lock != NULL &&
              lock->sp_lock == SP_LOCKED &&
              lock->sp_holder == me);

  if (lock == NULL)
    {
      return EINVAL;
    }
  else if (lock->sp_lock != SP_LOCKED || lock->sp_holder != me)
    {
      return EPERM;
    }

  /* Release the lock */

  lock->sp_holder = IMPOSSIBLE_THREAD;
  lock->sp_lock   = SP_UNLOCKED;
  return OK;
}

#endif  /* CONFIG_PTHREAD_SPINLOCKS */