/*
 * Copyright 2003-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 jetbrains.mps.typesystem.checking;

import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.application.ModalityState;
import com.intellij.openapi.project.IndexNotReadyException;
import jetbrains.mps.checkers.ErrorReportUtil;
import jetbrains.mps.errors.IErrorReporter;
import jetbrains.mps.errors.MessageStatus;
import jetbrains.mps.errors.QuickFixProvider;
import jetbrains.mps.errors.QuickFix_Runtime;
import jetbrains.mps.nodeEditor.EditorComponent;
import jetbrains.mps.nodeEditor.EditorMessage;
import jetbrains.mps.nodeEditor.HighlighterMessage;
import jetbrains.mps.nodeEditor.checking.BaseEditorChecker;
import jetbrains.mps.nodeEditor.checking.UpdateResult;
import jetbrains.mps.openapi.editor.EditorContext;
import jetbrains.mps.openapi.editor.cells.EditorCell;
import jetbrains.mps.openapi.editor.message.EditorMessageOwner;
import jetbrains.mps.smodel.event.SModelEvent;
import jetbrains.mps.typesystem.inference.TypeCheckingContext;
import jetbrains.mps.typesystem.inference.TypeContextManager;
import jetbrains.mps.util.Cancellable;
import jetbrains.mps.util.NameUtil;
import jetbrains.mps.util.Pair;
import jetbrains.mps.util.WeakSet;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.mps.openapi.model.SNode;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

/**
 * User: fyodor
 * Date: 4/30/13
 */
public abstract class AbstractTypesystemEditorChecker extends BaseEditorChecker implements EditorMessageOwner {
  public static boolean IMMEDIATE_QFIX_DISABLED = false;
  private WeakSet<QuickFix_Runtime> myOnceExecutedQuickFixes = new WeakSet<QuickFix_Runtime>();
  private boolean myHasEvents = false;

  @NotNull
  protected abstract UpdateResult doCreateMessages(TypeCheckingContext context, boolean wasCheckedOnce, EditorContext editorContext,
      SNode rootNode, Cancellable cancellable, boolean applyQuickFixes);

  @Override
  public void processEvents(List<SModelEvent> events) {
    myHasEvents |= !events.isEmpty();
  }

  @Override
  public boolean needsUpdate(EditorComponent editorComponent) {
    return myHasEvents;
  }

  @Override
  public void doneUpdating() {
    myHasEvents = false;
  }

  @NotNull
  @Override
  public UpdateResult update(final EditorComponent editorComponent, final boolean incremental, final boolean applyQuickFixes,
      final Cancellable cancellable) {
    try {
      return TypeContextManager.getInstance().runTypeCheckingComputation(editorComponent.getTypecheckingContextOwner(), editorComponent.getEditedNode(),
          context -> doCreateMessages(context, incremental, editorComponent.getEditorContext(), editorComponent.getEditedNode(), cancellable, applyQuickFixes));
    } catch (IndexNotReadyException e) {
      if (editorComponent.getNodeForTypechecking() != null) {
        TypeContextManager.getInstance().acquireTypecheckingContext(editorComponent.getNodeForTypechecking(), editorComponent);
        TypeContextManager.getInstance().releaseTypecheckingContext(editorComponent);
      }
      throw e;
    }
  }

  protected Collection<EditorMessage> collectMessagesForNodesWithErrors(TypeCheckingContext context, final EditorContext editorContext,
      boolean typesystemErrors, boolean applyQuickFixes) {
    Set<EditorMessage> messages = new HashSet<EditorMessage>();
    for (Pair<SNode, List<IErrorReporter>> errorNode : context.getNodesWithErrors(typesystemErrors)) {
      if (!ErrorReportUtil.shouldReportError(errorNode.o1)) {
        // although we might need to check IErrorReporter.getSNode(), I assume pair's first element always match that of IErrorReporter
        continue;
      }
      List<IErrorReporter> errors = new ArrayList<IErrorReporter>(errorNode.o2);
      Collections.sort(errors, new Comparator<IErrorReporter>() {
        @Override
        public int compare(IErrorReporter o1, IErrorReporter o2) {
          return o2.getMessageStatus().compareTo(o1.getMessageStatus());
        }
      });
      boolean instantIntentionApplied = false;
      for (IErrorReporter errorReporter : errors) {

        MessageStatus status = errorReporter.getMessageStatus();
        String errorString = errorReporter.reportError();
        HighlighterMessage message = HighlightUtil.createHighlighterMessage(
            errorNode.o1,
            NameUtil.capitalize(status.getPresentation()) + ": " + errorString,
            errorReporter,
            AbstractTypesystemEditorChecker.this
        );

        List<QuickFixProvider> intentionProviders = message.getIntentionProviders();
        final SNode quickFixNode = errorNode.o1;
        if (applyQuickFixes && !instantIntentionApplied) {
          if (intentionProviders.size() == 1 &&
              intentionProviders.get(0) != null &&
              intentionProviders.get(0).isExecutedImmediately() &&
              !AbstractTypesystemEditorChecker.IMMEDIATE_QFIX_DISABLED) {
            QuickFixProvider intentionProvider = intentionProviders.get(0);
            instantIntentionApplied = applyInstantIntention(editorContext, quickFixNode, intentionProvider);
            if (instantIntentionApplied) {
              // skip the message
              continue;
            }
          }
        }

        messages.add(message);
      }
    }
    return messages;
  }

  private boolean applyInstantIntention(final EditorContext editorContext, final SNode quickFixNode,
      QuickFixProvider intentionProvider) {
    final QuickFix_Runtime intention = intentionProvider.getQuickFix();
    if (intention != null) {
      if (!myOnceExecutedQuickFixes.contains(intention)) {
        myOnceExecutedQuickFixes.add(intention);
        // XXX why Application.invokeLater, not ThreadUtils or ModelAccess (likely, shall use SNodeReference for quickFixNode, not SNode, and resolve inside)
        ApplicationManager.getApplication().invokeLater(new Runnable() {
          @Override
          public void run() {
            EditorCell selectedCell = editorContext.getSelectedCell();
            if (selectedCell == null) {
              return;
            }
            int caretX = selectedCell.getCaretX();
            int caretY = selectedCell.getBaseline();

            editorContext.getRepository().getModelAccess().executeUndoTransparentCommand(new Runnable() {
              @Override
              public void run() {
                intention.execute(quickFixNode);
              }
            });

            editorContext.flushEvents();
            if (editorContext.getSelectionManager().getSelection() == null) {
              EditorCell rootCell = editorContext.getEditorComponent().getRootCell();
              EditorCell leaf = rootCell.findLeaf(caretX, caretY);
              if (leaf != null) {
                editorContext.getEditorComponent().changeSelection(leaf);
                leaf.setCaretX(caretX);
              }
            }
          }
        }, ModalityState.NON_MODAL);
      }
      return true;
    }
    return false;
  }
}
