package com.google.inject.internal;

import com.google.common.base.Preconditions;
import com.google.common.base.Supplier;
import com.google.common.collect.ImmutableListMultimap;
import com.google.common.collect.LinkedHashMultimap;
import com.google.common.collect.ListMultimap;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Multimap;
import com.google.common.collect.Multimaps;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * Simplified version of {@link Lock} that is special due to how it handles deadlocks detection.
 *
 * <p>Is an inherent part of {@link SingletonScope}, moved into a upper level class due to its size
 * and complexity.
 *
 * @param <ID> Lock identification provided by the client, is returned unmodified to the client when
 *     lock cycle is detected to identify it. Only toString() needs to be implemented. Lock
 *     references this object internally, for the purposes of Garbage Collection you should not use
 *     heavy IDs. Lock is referenced by a lock factory as long as it's owned by a thread.
 * @see SingletonScope
 * @see com.google.inject.internal.CycleDetectingLock.CycleDetectingLockFactory
 * @author timofeyb (Timothy Basanov)
 */
interface CycleDetectingLock<ID> {

  /**
   * Takes a lock in a blocking fashion in case no potential deadlocks are detected. If the lock was
   * successfully owned, returns an empty map indicating no detected potential deadlocks.
   *
   * <p>Otherwise, a map indicating threads involved in a potential deadlock are returned. Map is
   * ordered by dependency cycle and lists locks for each thread that are part of the loop in order,
   * the last lock in the list is the one that the thread is currently waiting for. Returned map is
   * created atomically.
   *
   * <p>In case no cycle is detected performance is O(threads creating singletons), in case cycle is
   * detected performance is O(singleton locks).
   */
  ListMultimap<Thread, ID> lockOrDetectPotentialLocksCycle();

  /** Unlocks previously locked lock. */
  void unlock();

  /**
   * Wraps locks so they would never cause a deadlock. On each {@link
   * CycleDetectingLock#lockOrDetectPotentialLocksCycle} we check for dependency cycles within locks
   * created by the same factory. Either we detect a cycle and return it or take it atomically.
   *
   * <p>Important to note that we do not prevent deadlocks in the client code. As an example: Thread
   * A takes lock L and creates singleton class CA depending on the singleton class CB. Meanwhile
   * thread B is creating class CB and is waiting on the lock L. Issue happens due to client code
   * creating interdependent classes and using locks, where no guarantees on the creation order from
   * Guice are provided.
   *
   * <p>Instances of these locks are not intended to be exposed outside of {@link SingletonScope}.
   */
  class CycleDetectingLockFactory<ID> {

    /**
     * Specifies lock that thread is currently waiting on to own it. Used only for purposes of locks
     * cycle detection.
     *
     * <ul>
     * <li>Key: thread
     * <li> Value: lock that is being waited on
     * </ul>
     *
     * <p>Element is added inside {@link #lockOrDetectPotentialLocksCycle()} before {@link
     * Lock#lock} is called. Element is removed inside {@link #lockOrDetectPotentialLocksCycle()}
     * after {@link Lock#lock} and synchronously with adding it to {@link #locksOwnedByThread}.
     *
     * <p>Same lock can be added for several threads in case all of them are trying to take it.
     *
     * <p>Guarded by {@code CycleDetectingLockFactory.class}.
     */
    private static Map<Thread, ReentrantCycleDetectingLock<?>> lockThreadIsWaitingOn =
        Maps.newHashMap();

    /**
     * Lists locks that thread owns. Used only to populate locks in a potential cycle when it is
     * detected.
     *
     * <ul>
     * <li>Key: thread
     * <li>Value: stack of locks that were owned.
     * </ul>
     *
     * <p>Element is added inside {@link #lockOrDetectPotentialLocksCycle()} after {@link Lock#lock}
     * is called. Element is removed inside {@link #unlock()} synchronously with {@link
     * Lock#unlock()} call.
     *
     * <p>Same lock can only be present several times for the same thread as locks are reentrant.
     * Lock can not be owned by several different threads as the same time.
     *
     * <p>Guarded by {@code CycleDetectingLockFactory.class}.
     */
    private static final Multimap<Thread, ReentrantCycleDetectingLock<?>> locksOwnedByThread =
        LinkedHashMultimap.create();

    /**
     * Creates new lock within this factory context. We can guarantee that locks created by the same
     * factory would not deadlock.
     *
     * @param userLockId lock id that would be used to report lock cycles if detected
     */
    CycleDetectingLock<ID> create(ID userLockId) {
      return new ReentrantCycleDetectingLock<ID>(this, userLockId, new ReentrantLock());
    }

    /** The implementation for {@link CycleDetectingLock}. */
    static class ReentrantCycleDetectingLock<ID> implements CycleDetectingLock<ID> {

      /** Underlying lock used for actual waiting when no potential deadlocks are detected. */
      private final Lock lockImplementation;
      /** User id for this lock. */
      private final ID userLockId;
      /** Factory that was used to create this lock. */
      private final CycleDetectingLockFactory<ID> lockFactory;
      /**
       * Thread that owns this lock. Nullable. Guarded by {@code CycleDetectingLockFactory.this}.
       */
      private Thread lockOwnerThread = null;

      /**
       * Number of times that thread owned this lock. Guarded by {@code
       * CycleDetectingLockFactory.this}.
       */
      private int lockReentranceCount = 0;

      ReentrantCycleDetectingLock(
          CycleDetectingLockFactory<ID> lockFactory, ID userLockId, Lock lockImplementation) {
        this.lockFactory = lockFactory;
        this.userLockId = Preconditions.checkNotNull(userLockId, "userLockId");
        this.lockImplementation =
            Preconditions.checkNotNull(lockImplementation, "lockImplementation");
      }

      @Override
      public ListMultimap<Thread, ID> lockOrDetectPotentialLocksCycle() {
        final Thread currentThread = Thread.currentThread();
        synchronized (CycleDetectingLockFactory.class) {
          checkState();
          // Add this lock to the waiting map to ensure it is included in any reported lock cycle.
          lockThreadIsWaitingOn.put(currentThread, this);
          ListMultimap<Thread, ID> locksInCycle = detectPotentialLocksCycle();
          if (!locksInCycle.isEmpty()) {
            // We aren't actually going to wait for this lock, so remove it from the map.
            lockThreadIsWaitingOn.remove(currentThread);
            // potential deadlock is found, we don't try to take this lock
            return locksInCycle;
          }

        }

        // this may be blocking, but we don't expect it to cause a deadlock
        lockImplementation.lock();

        synchronized (CycleDetectingLockFactory.class) {
          // current thread is no longer waiting on this lock
          lockThreadIsWaitingOn.remove(currentThread);
          checkState();

          // mark it as owned by us
          lockOwnerThread = currentThread;
          lockReentranceCount++;
          // add this lock to the list of locks owned by a current thread
          locksOwnedByThread.put(currentThread, this);
        }
        // no deadlock is found, locking successful
        return ImmutableListMultimap.of();
      }

      @Override
      public void unlock() {
        final Thread currentThread = Thread.currentThread();
        synchronized (CycleDetectingLockFactory.class) {
          checkState();
          Preconditions.checkState(
              lockOwnerThread != null, "Thread is trying to unlock a lock that is not locked");
          Preconditions.checkState(
              lockOwnerThread == currentThread,
              "Thread is trying to unlock a lock owned by another thread");

          // releasing underlying lock
          lockImplementation.unlock();

          // be sure to release the lock synchronously with updating internal state
          lockReentranceCount--;
          if (lockReentranceCount == 0) {
            // we no longer own this lock
            lockOwnerThread = null;
            Preconditions.checkState(
                locksOwnedByThread.remove(currentThread, this),
                "Internal error: Can not find this lock in locks owned by a current thread");
            if (locksOwnedByThread.get(currentThread).isEmpty()) {
              // clearing memory
              locksOwnedByThread.removeAll(currentThread);
            }
          }
        }
      }

      /** Check consistency of an internal state. */
      void checkState() throws IllegalStateException {
        final Thread currentThread = Thread.currentThread();
        Preconditions.checkState(
            !lockThreadIsWaitingOn.containsKey(currentThread),
            "Internal error: Thread should not be in a waiting thread on a lock now");
        if (lockOwnerThread != null) {
          // check state of a locked lock
          Preconditions.checkState(
              lockReentranceCount >= 0,
              "Internal error: Lock ownership and reentrance count internal states do not match");
          Preconditions.checkState(
              locksOwnedByThread.get(lockOwnerThread).contains(this),
              "Internal error: Set of locks owned by a current thread and lock "
                  + "ownership status do not match");
        } else {
          // check state of a non locked lock
          Preconditions.checkState(
              lockReentranceCount == 0,
              "Internal error: Reentrance count of a non locked lock is expect to be zero");
          Preconditions.checkState(
              !locksOwnedByThread.values().contains(this),
              "Internal error: Non locked lock should not be owned by any thread");
        }
      }

      /**
       * Algorithm to detect a potential lock cycle.
       *
       * <p>For lock's thread owner check which lock is it trying to take. Repeat recursively. When
       * current thread is found a potential cycle is detected.
       *
       * @see CycleDetectingLock#lockOrDetectPotentialLocksCycle()
       */
      private ListMultimap<Thread, ID> detectPotentialLocksCycle() {
        final Thread currentThread = Thread.currentThread();
        if (lockOwnerThread == null || lockOwnerThread == currentThread) {
          // if nobody owns this lock, lock cycle is impossible
          // if a current thread owns this lock, we let Guice to handle it
          return ImmutableListMultimap.of();
        }

        ListMultimap<Thread, ID> potentialLocksCycle =
            Multimaps.newListMultimap(
                new LinkedHashMap<Thread, Collection<ID>>(),
                new Supplier<List<ID>>() {
                  @Override
                  public List<ID> get() {
                    return Lists.newArrayList();
                  }
                });
        // lock that is a part of a potential locks cycle, starts with current lock
        ReentrantCycleDetectingLock<?> lockOwnerWaitingOn = this;
        // try to find a dependency path between lock's owner thread and a current thread
        while (lockOwnerWaitingOn != null && lockOwnerWaitingOn.lockOwnerThread != null) {
          Thread threadOwnerThreadWaits = lockOwnerWaitingOn.lockOwnerThread;
          // in case locks cycle exists lock we're waiting for is part of it
          lockOwnerWaitingOn =
              addAllLockIdsAfter(threadOwnerThreadWaits, lockOwnerWaitingOn, potentialLocksCycle);
          if (threadOwnerThreadWaits == currentThread) {
            // owner thread depends on current thread, cycle detected
            return potentialLocksCycle;
          }
        }
        // no dependency path from an owner thread to a current thread
        return ImmutableListMultimap.of();
      }

      /**
       * Adds all locks held by the given thread that are after the given lock and then returns the
       * lock the thread is currently waiting on, if any
       */
      private ReentrantCycleDetectingLock<?> addAllLockIdsAfter(
          Thread thread,
          ReentrantCycleDetectingLock<?> lock,
          ListMultimap<Thread, ID> potentialLocksCycle) {
        boolean found = false;
        Collection<ReentrantCycleDetectingLock<?>> ownedLocks = locksOwnedByThread.get(thread);
        Preconditions.checkNotNull(
            ownedLocks, "Internal error: No locks were found taken by a thread");
        for (ReentrantCycleDetectingLock<?> ownedLock : ownedLocks) {
          if (ownedLock == lock) {
            found = true;
          }
          if (found && ownedLock.lockFactory == this.lockFactory) {
            // All locks are stored in a shared map therefore there is no way to
            // enforce type safety. We know that our cast is valid as we check for a lock's
            // factory. If the lock was generated by the
            // same factory it has to have same type as the current lock.
            @SuppressWarnings("unchecked")
            ID userLockId = (ID) ownedLock.userLockId;
            potentialLocksCycle.put(thread, userLockId);
          }
        }
        Preconditions.checkState(
            found,
            "Internal error: We can not find locks that created a cycle that we detected");
        ReentrantCycleDetectingLock<?> unownedLock = lockThreadIsWaitingOn.get(thread);
        // If this thread is waiting for a lock add it to the cycle and return it
        if (unownedLock != null && unownedLock.lockFactory == this.lockFactory) {
          @SuppressWarnings("unchecked")
          ID typed = (ID) unownedLock.userLockId;
          potentialLocksCycle.put(thread, typed);
        }
        return unownedLock;
      }

      @Override
      public String toString() {
        // copy is made to prevent a data race
        // no synchronization is used, potentially stale data, should be good enough
        Thread thread = this.lockOwnerThread;
        if (thread != null) {
          return String.format("%s[%s][locked by %s]", super.toString(), userLockId, thread);
        } else {
          return String.format("%s[%s][unlocked]", super.toString(), userLockId);
        }
      }
    }
  }
}
