/**
 * This file is part of the Meeds project (https://meeds.io/).
 *
 * Copyright (C) 2020 - 2025 Meeds Association contact@meeds.io
 *
 * This program 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 3 of the License, or (at your option) any later version.
 * This program 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, write to the Free Software Foundation,
 * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 */
package org.exoplatform.container;

import org.exoplatform.services.log.ExoLogger;
import org.exoplatform.services.log.Log;

import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
import java.util.concurrent.RunnableFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * This class is used to be aware of all the {@link Lock} currently used to prevent
 * deadlocks
 * 
 */
public class LockManager
{

   /**
    * The logger
    */
   private static final Log LOG = ExoLogger.getLogger("exo.kernel.container.mt.LockManager");

   /**
    * The singleton
    */
   private static final LockManager INSTANCE = new LockManager();

   /**
    * Current lockable resources
    */
   private final ConcurrentMap<Thread, Lockable> locks = new ConcurrentHashMap<Thread, Lockable>();

   /**
    * The total amount of uncompleted tasks
    */
   private final AtomicInteger totalUncompletedTasks = new AtomicInteger();

   private LockManager()
   {
   }

   /**
    * The unique instance of the {@link LockManager}
    */
   public static LockManager getInstance()
   {
      return INSTANCE;
   }

   /**
    * Gives a new {@link Lock} instance
    */
   public Lock createLock()
   {
      return new InternalReentrantLock();
   }

   /**
    * Creates a new {@link RunnableFuture} instance
    */
   public <T> RunnableFuture<T> createRunnableFuture(Runnable runnable, T value)
   {
      return new InternalFutureTask<T>(runnable, value);
   }

   /**
    * Creates a new {@link RunnableFuture} instance
    */
   public <T> RunnableFuture<T> createRunnableFuture(Callable<T> callable)
   {
      return new InternalFutureTask<T>(callable);
   }

   /**
    * Gives the total amount of uncompleted tasks
    */
   int getTotalUncompletedTasks()
   {
      return totalUncompletedTasks.get();
   }

   /**
    * Increments and get the total amount of uncompleted tasks
    */
   int incrementAndGetTotalUncompletedTasks()
   {
      return totalUncompletedTasks.incrementAndGet();
   }

   /**
    * Indicates whether or not there are some remaining lockable resources
    */
   boolean isEmpty()
   {
      return locks.isEmpty();
   }

   /**
    * Registers a lockable resource for the current thread
    */
   private void register(Lockable l)
   {
      locks.put(Thread.currentThread(), l);
   }

   /**
    * Unregisters a lockable resource for the current thread
    */
   private void unregister(Lockable l)
   {
      locks.remove(Thread.currentThread(), l);
   }

   /**
    * Checks if there is a deadlock, if so an {@link InterruptedException}
    * will be thrown.
    * <p>
    * When two threads enter lockInterruptibly() concurrently, each calls
    * register() and then checkDeadLock() almost simultaneously.  There is a
    * window where thread A has already registered but thread B has not yet
    * registered when A runs its check – so A sees an empty entry for B's
    * thread and returns "no deadlock", then both threads block forever.
    * <p>
    * To close this window we retry the walk a few times with a brief yield
    * between attempts.  If the deadlock graph materialises within the retry
    * budget we detect and break it; if it never materialises the lock owner
    * really does not hold anything and we let the underlying primitive block
    * normally.
    */
   private void checkDeadLock(Lockable l) throws InterruptedException
   {
      if (!l.isLocked())
      {
         LOG.trace("The lock is not locked so we cannot have a deadlock");
         return;
      }
      final Thread owner = l.getOwner();
      if (owner == null || owner == Thread.currentThread())
      {
         LOG.trace("The lock is not locked or the lock owner is the current "
            + "thread so we cannot have a deadlock");
         return;
      }
      // Retry loop: give concurrent threads a chance to complete their own
      // register() call before we conclude there is no deadlock.
      final int MAX_RETRIES = 10;
      for (int attempt = 0; attempt < MAX_RETRIES; attempt++)
      {
         boolean conclusive = checkDeadLockOnce(l, owner);
         if (conclusive)
            return; // confirmed no deadlock
         // Inconclusive: other thread hasn't registered yet. Yield and retry.
         Thread.yield();
      }
      LOG.trace("No deadlock detected after retries – treating as no deadlock");
   }

   /**
    * Single deadlock-graph walk. Returns {@code true} when the walk concludes
    * "no deadlock" with certainty (e.g. the lock became free, or owner chain
    * does not loop back); returns {@code false} when the result is inconclusive
    * because a concurrent thread has not yet completed its register() call
    * (i.e. {@code locks.get(currentOwner)} returned null while the owner is
    * actively running); throws {@link InterruptedException} when a deadlock is
    * confirmed.
    */
   private boolean checkDeadLockOnce(Lockable l, Thread owner) throws InterruptedException
   {
      Thread currentOwner = owner;
      while (true)
      {
         Lockable lock = locks.get(currentOwner);
         if (lock == null)
         {
            // The owner thread is not waiting on anything right now.
            // This could mean it truly holds no other lock (no deadlock),
            // OR it has not finished its register() call yet (inconclusive).
            // We return false (inconclusive) so the caller retries.
            LOG.trace("Owner has no registered lockable resource yet – result inconclusive, will retry");
            return false;
         }
         // We first check the locks
         Thread lockToAcquireOwner = lock.getOwner();
         if (lockToAcquireOwner == null)
         {
            LOG.trace("The lockable resource has no owner anymore so we cannot have a deadlock");
            return true;
         }
         else if (lockToAcquireOwner == Thread.currentThread())
         {
            // A potential deadlock has been detected
            if (owner == l.getOwner() && l.isLocked())
            {
               LOG.debug("A deadlock has been detected, both threads will be interrupted");
               // The owner did not change so we have a deadlock, so
               // we will interrupt both threads
               owner.interrupt();
               throw new InterruptedException();
            }
            else
            {
               LOG.trace("The owner has changed or the resource is no more locked so we cannot have a deadlock");
               return true;
            }
         }
         currentOwner = lockToAcquireOwner;
      }
   }

   /**
    * Internal sub-class of {@link ReentrantLock} needed to be able to register
    * and unregister all the locks automatically
    */
   private class InternalReentrantLock extends ReentrantLock implements Lockable
   {

      /**
       * The serial version UID
       */
      private static final long serialVersionUID = 1696442015918441687L;

      /**
       * {@inheritDoc}
       */
      public Thread getOwner()
      {
         return super.getOwner();
      }

      /**
       * {@inheritDoc}
       */
      @Override
      public void lock()
      {
         register(this);
         super.lock();
         unregister(this);
      }

      /**
       * {@inheritDoc}
       */
      @Override
      public void lockInterruptibly() throws InterruptedException
      {
         register(this);
         try
         {
            checkDeadLock(this);
            super.lockInterruptibly();
         }
         finally
         {
            unregister(this);
         }
      }

      /**
       * {@inheritDoc}
       */
      @Override
      public boolean tryLock()
      {
         register(this);
         boolean result = super.tryLock();
         unregister(this);
         return result;
      }

      /**
       * {@inheritDoc}
       */
      @Override
      public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException
      {
         register(this);
         try
         {
            checkDeadLock(this);
            return super.tryLock(timeout, unit);
         }
         finally
         {
            unregister(this);
         }
      }
   }

   /**
    * Internal sub-class of {@link FutureTask} needed to be able to register
    * and unregister all the tasks automatically
    */
   private class InternalFutureTask<V> extends FutureTask<V> implements Lockable
   {
      /**
       * The current owner of exclusive mode synchronization.
       */
      private final AtomicReference<Thread> exclusiveOwnerThread = new AtomicReference<Thread>();

      /**
       * {@inheritDoc}
       */
      public InternalFutureTask(Callable<V> callable)
      {
         super(callable);
      }

      /**
       * {@inheritDoc}
       */
      public InternalFutureTask(Runnable runnable, V result)
      {
         super(runnable, result);
      }

      /**
       * Checks if there is a deadlock, if so it will interrupt the thread waiting for the lock
       */
      private void checkDeadLock()
      {
         try
         {
            LockManager.this.checkDeadLock(this);
         }
         catch (InterruptedException e)
         {
            LOG.debug("An InterruptedException has been caught, but a task must not be interrupted");
         }
      }


      /**
       * {@inheritDoc}
       */
      @Override
      public V get() throws InterruptedException, ExecutionException
      {
         register(this);
         checkDeadLock();
         try
         {
            return super.get();
         }
         finally
         {
            unregister(this);
         }
      }

      /**
       * {@inheritDoc}
       */
      @Override
      public V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException
      {
         register(this);
         checkDeadLock();
         try
         {
            return super.get(timeout, unit);
         }
         finally
         {
            unregister(this);
         }
      }

      /**
       * {@inheritDoc}
       */
      @Override
      public void run()
      {
         exclusiveOwnerThread.compareAndSet(null, Thread.currentThread());
         try
         {
            super.run();
         }
         finally
         {
            totalUncompletedTasks.decrementAndGet();
            exclusiveOwnerThread.compareAndSet(Thread.currentThread(), null);
         }
      }

      /**
       * Gives the Owner of the task
       */
      public Thread getOwner()
      {
         return exclusiveOwnerThread.get();
      }

      /**
       * Indicates whether the task is locked or not, in practice it will be considered as locked if it is not done
       */
      public boolean isLocked()
      {
         return !isDone();
      }
   }

   /**
    * Defines a lockable resource
    */
   private static interface Lockable
   {
      /**
       * Gives the owner in case the resource is locked
       */
      Thread getOwner();

      /**
       * Indicates whether the resource is locked or not
       */
      boolean isLocked();
   }
}
