/*
 * Copyright 2000-2016 JetBrains s.r.o.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.intellij.openapi.application;

import com.google.common.base.MoreObjects;
import com.intellij.openapi.Disposable;
import com.intellij.openapi.application.ex.ApplicationEx;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.progress.ProcessCanceledException;
import com.intellij.openapi.util.Condition;
import com.intellij.openapi.util.Disposer;
import com.intellij.openapi.util.registry.Registry;
import com.intellij.util.concurrency.Semaphore;
import com.intellij.util.containers.ContainerUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.util.Map;
import java.util.Queue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.atomic.AtomicLong;

/**
 * @author peter
 */
public class TransactionGuardImpl extends TransactionGuard {
  private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.application.TransactionGuardImpl");
  private final Queue<Transaction> myQueue = new LinkedBlockingQueue<>();
  private final Map<ModalityState, TransactionIdImpl> myModality2Transaction = ContainerUtil.createConcurrentWeakMap();

  /**
   * Remembers the value of {@link #myWritingAllowed} at the start of each modality. If writing wasn't allowed at that moment
   * (e.g. inside SwingUtilities.invokeLater), it won't be allowed for all dialogs inside such modality, even from user activity.
   */
  private final Map<ModalityState, Boolean> myWriteSafeModalities = ContainerUtil.createConcurrentWeakMap();
  private TransactionIdImpl myCurrentTransaction;
  private boolean myWritingAllowed;
  private boolean myErrorReported;
  private static boolean ourTestingTransactions;

  public TransactionGuardImpl() {
    myWriteSafeModalities.put(ModalityState.NON_MODAL, true);
  }

  @NotNull
  private Queue<Transaction> getQueue(@Nullable TransactionIdImpl transaction) {
    while (transaction != null && transaction.myFinished) {
      transaction = transaction.myParent;
    }
    return transaction == null ? myQueue : transaction.myQueue;
  }

  private void pollQueueLater() {
    invokeLater(() -> {
      Queue<Transaction> queue = getQueue(myCurrentTransaction);
      Transaction next = queue.peek();
      if (next != null && canRunTransactionNow(next, false)) {
        queue.remove();
        runSyncTransaction(next);
      }
    });
  }

  private void runSyncTransaction(@NotNull Transaction transaction) {
    ApplicationManager.getApplication().assertIsDispatchThread();
    if (Disposer.isDisposed(transaction.parentDisposable)) return;

    boolean wasWritingAllowed = myWritingAllowed;
    myWritingAllowed = true;
    myCurrentTransaction = new TransactionIdImpl(myCurrentTransaction);

    try {
      transaction.runnable.run();
    }
    finally {
      Queue<Transaction> queue = getQueue(myCurrentTransaction.myParent);
      queue.addAll(myCurrentTransaction.myQueue);
      if (!queue.isEmpty()) {
        pollQueueLater();
      }

      myWritingAllowed = wasWritingAllowed;
      myCurrentTransaction.myFinished = true;
      myCurrentTransaction = myCurrentTransaction.myParent;
    }
  }

  @Override
  public void submitTransaction(@NotNull Disposable parentDisposable, @Nullable TransactionId expectedContext, @NotNull Runnable _transaction) {
    final TransactionIdImpl expectedId = (TransactionIdImpl)expectedContext;
    final Transaction transaction = new Transaction(_transaction, expectedId, parentDisposable);
    final Application app = ApplicationManager.getApplication();
    final boolean isDispatchThread = app.isDispatchThread();
    Runnable runnable = () -> {
      if (canRunTransactionNow(transaction, isDispatchThread)) {
        runSyncTransaction(transaction);
      }
      else {
        getQueue(expectedId).offer(transaction);
        pollQueueLater();
      }
    };

    if (isDispatchThread) {
      runnable.run();
    } else {
      invokeLater(runnable);
    }
  }

  private boolean canRunTransactionNow(Transaction transaction, boolean sync) {
    if (sync && !myWritingAllowed) {
      return false;
    }

    TransactionIdImpl currentId = myCurrentTransaction;
    if (currentId == null) {
      return true;
    }

    return transaction.expectedContext != null && currentId.myStartCounter <= transaction.expectedContext.myStartCounter;
  }

  @Override
  public void submitTransactionAndWait(@NotNull final Runnable runnable) throws ProcessCanceledException {
    Application app = ApplicationManager.getApplication();
    if (app.isDispatchThread()) {
      Transaction transaction = new Transaction(runnable, getContextTransaction(), app);
      if (!canRunTransactionNow(transaction, true)) {
        String message = "Cannot run synchronous submitTransactionAndWait from invokeLater. " +
                         "Please use asynchronous submit*Transaction. " +
                         "See TransactionGuard FAQ for details.\nTransaction: " + runnable;
        if (!isWriteSafeModality(ModalityState.current())) {
          message += "\nUnsafe modality: " + ModalityState.current();
        }
        LOG.error(message);
      }
      runSyncTransaction(transaction);
      return;
    }

    if (app.isReadAccessAllowed()) {
      throw new IllegalStateException("submitTransactionAndWait should not be invoked from a read action");
    }
    final Semaphore semaphore = new Semaphore();
    semaphore.down();
    final Throwable[] exception = {null};
    submitTransaction(Disposer.newDisposable("never disposed"), getContextTransaction(), () -> {
      try {
        runnable.run();
      }
      catch (Throwable e) {
        exception[0] = e;
      }
      finally {
        semaphore.up();
      }
    });
    semaphore.waitFor();
    if (exception[0] != null) {
      throw new RuntimeException(exception[0]);
    }
  }

  /**
   * An absolutely guru method!<p/>
   *
   * Executes the given code and marks it as a user activity, to allow write actions to be run without requiring transactions.
   * This is only to be called from UI infrastructure, during InputEvent processing and wrap the point where the control
   * goes to custom input event handlers for the first time.<p/>
   *
   * If you wish to invoke some actionPerformed,
   * please consider using {@code ActionManager.tryToExecute()} instead, or ensure in some other way that the action is enabled
   * and can be invoked in the current modality state.
   */
  public void performUserActivity(Runnable activity) {
    ApplicationManager.getApplication().assertIsDispatchThread();
    AccessToken token = startActivity(true);
    try {
      activity.run();
    }
    finally {
      token.finish();
    }
  }

  /**
   * An absolutely guru method, only intended to be used from Swing event processing. Please consult Peter if you think you need to invoke this.
   */
  @NotNull
  public AccessToken startActivity(boolean userActivity) {
    myErrorReported = false;
    boolean allowWriting = userActivity && isWriteSafeModality(ModalityState.current());
    if (myWritingAllowed == allowWriting) {
      return AccessToken.EMPTY_ACCESS_TOKEN;
    }

    ApplicationManager.getApplication().assertIsDispatchThread();
    final boolean prev = myWritingAllowed;
    myWritingAllowed = allowWriting;
    return new AccessToken() {
      @Override
      public void finish() {
        myWritingAllowed = prev;
      }
    };
  }

  public boolean isWriteSafeModality(ModalityState state) {
    return Boolean.TRUE.equals(myWriteSafeModalities.get(state));
  }

  public void assertWriteActionAllowed() {
    ApplicationManager.getApplication().assertIsDispatchThread();
    if (areAssertionsEnabled() && !myWritingAllowed && !myErrorReported) {
      String message = "Write access is allowed from write-safe contexts only. " +
                       "Please ensure you're using invokeLater/invokeAndWait with a correct modality state (not \"any\"). " +
                       "See TransactionGuard documentation for details." +
                       "\n  current modality=" + ModalityState.current() +
                       "\n  known modalities=" + myWriteSafeModalities;
      // please assign exceptions here to Peter
      LOG.error(message);
      myErrorReported = true;
    }
  }

  private static boolean areAssertionsEnabled() {
    Application app = ApplicationManager.getApplication();
    if (app.isUnitTestMode() && !ourTestingTransactions) {
      return false;
    }
    if (app instanceof ApplicationEx && !((ApplicationEx)app).isLoaded()) {
      return false;
    }
    return Registry.is("ide.require.transaction.for.model.changes", false);
  }

  @Override
  public void submitTransactionLater(@NotNull final Disposable parentDisposable, @NotNull final Runnable transaction) {
    final TransactionIdImpl id = getContextTransaction();
    final ModalityState startModality = ModalityState.defaultModalityState();
    invokeLater(() -> {
      boolean allowWriting = ModalityState.current() == startModality;
      AccessToken token = startActivity(allowWriting);
      try {
        submitTransaction(parentDisposable, id, transaction);
      }
      finally {
        token.finish();
      }
    });
  }

  private static void invokeLater(Runnable runnable) {
    ApplicationManager.getApplication().invokeLater(runnable, ModalityState.any(), Condition.FALSE);
  }

  @Override
  public TransactionIdImpl getContextTransaction() {
    if (!ApplicationManager.getApplication().isDispatchThread()) {
      return myModality2Transaction.get(ModalityState.defaultModalityState());
    }

    return myWritingAllowed ? myCurrentTransaction : null;
  }

  public void enteredModality(@NotNull ModalityState modality) {
    TransactionIdImpl contextTransaction = getContextTransaction();
    if (contextTransaction != null) {
      myModality2Transaction.put(modality, contextTransaction);
    }
    myWriteSafeModalities.put(modality, myWritingAllowed);
  }

  @Nullable
  public TransactionIdImpl getModalityTransaction(@NotNull ModalityState modalityState) {
    return myModality2Transaction.get(modalityState);
  }

  @NotNull
  public Runnable wrapLaterInvocation(@NotNull final Runnable runnable, @NotNull ModalityState modalityState) {
    if (isWriteSafeModality(modalityState)) {
      return new Runnable() {
        @Override
        public void run() {
          ApplicationManager.getApplication().assertIsDispatchThread();
          final boolean prev = myWritingAllowed;
          myWritingAllowed = true;
          try {
            runnable.run();
          } finally {
            myWritingAllowed = prev;
          }
        }

        @Override
        public String toString() {
          return runnable.toString();
        }
      };
    }

    return runnable;
  }

  @Override
  public String toString() {
    return MoreObjects.toStringHelper(this)
      .add("currentTransaction", myCurrentTransaction)
      .add("writingAllowed", myWritingAllowed)
      .toString();
  }

  public static void setTestingTransactions(boolean testingTransactions) {
    ourTestingTransactions = testingTransactions;
  }

  private static class Transaction {
    @NotNull  final Runnable runnable;
    @Nullable final TransactionIdImpl expectedContext;
    @NotNull  final Disposable parentDisposable;

    Transaction(@NotNull Runnable runnable, @Nullable TransactionIdImpl expectedContext, @NotNull Disposable parentDisposable) {
      this.runnable = runnable;
      this.expectedContext = expectedContext;
      this.parentDisposable = parentDisposable;
    }
  }

  private static class TransactionIdImpl implements TransactionId {
    private static final AtomicLong ourTransactionCounter = new AtomicLong();
    final long myStartCounter = ourTransactionCounter.getAndIncrement();
    final Queue<Transaction> myQueue = new LinkedBlockingQueue<>();
    boolean myFinished;
    final TransactionIdImpl myParent;

    public TransactionIdImpl(@Nullable TransactionIdImpl parent) {
      myParent = parent;
    }

    @Override
    public String toString() {
      return "Transaction " + myStartCounter + (myFinished ? "(finished)" : "");
    }
  }
}