/*
 * 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.nodeEditor.highlighter;

import com.intellij.openapi.project.IndexNotReadyException;
import jetbrains.mps.make.IMakeService;
import jetbrains.mps.nodeEditor.EditorComponent;
import jetbrains.mps.nodeEditor.EditorMessage;
import jetbrains.mps.nodeEditor.NodeHighlightManager;
import jetbrains.mps.nodeEditor.PriorityComparator;
import jetbrains.mps.nodeEditor.checking.EditorChecker;
import jetbrains.mps.nodeEditor.checking.UpdateResult;
import jetbrains.mps.nodeEditor.checking.UpdateResult.Completed;
import jetbrains.mps.smodel.ModelAccess;
import jetbrains.mps.smodel.ModelAccessHelper;
import jetbrains.mps.smodel.SNodePointer;
import jetbrains.mps.typesystem.inference.TypeContextManager;
import jetbrains.mps.util.Computable;
import jetbrains.mps.util.Pair;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.mps.openapi.model.SNode;
import org.jetbrains.mps.openapi.model.SNodeUtil;
import org.jetbrains.mps.openapi.module.SRepository;

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

public class HighlighterUpdateSession {
  private final IHighlighter myHighlighter;
  private final Collection<EditorCheckerWrapper> myCheckers;
  private final List<EditorComponent> myAllEditorComponents;
  @Nullable
  private final EditorComponent myInspector;

  public HighlighterUpdateSession(IHighlighter highlighter, Collection<EditorCheckerWrapper> checkers,
      List<EditorComponent> allEditorComponents, @Nullable EditorComponent inspector) {
    myHighlighter = highlighter;
    myCheckers = checkers;
    myAllEditorComponents = allEditorComponents;
    myInspector = inspector;
  }

  @NotNull
  private static <T> T runLoPrioRead(final Computable<T> computable) {
    assert !ModelAccess.instance().canRead() : "Lo-prio read with acquired read can be a reason of a deadlock";
    T result;
    do {
      while (IMakeService.INSTANCE.isSessionActive()) {
        try {
          Thread.sleep(600);
        } catch (InterruptedException ignored) {
        }
      }
      result = ModelAccess.instance().runReadAction(new Computable<T>() {
        @Override
        public T compute() {
          if (IMakeService.INSTANCE.isSessionActive() || ModelAccess.instance().hasScheduledWrites()) return null;
          return computable.compute();
        }
      });
    } while (result == null);

    return result;
  }

  private void doUpdate() {
    if (myCheckers.isEmpty()) {
      return;
    }

    List<Pair<EditorComponent, Boolean>> input = new ArrayList<Pair<EditorComponent, Boolean>>();
    HashSet<SNodePointer> visited = new HashSet<SNodePointer>();
    for (EditorComponent ecomp : myAllEditorComponents) {
      SNodePointer pointer = new SNodePointer(ecomp.getNodeForTypechecking());
      input.add(new Pair<EditorComponent, Boolean>(ecomp, !visited.contains(pointer)));
      visited.add(pointer);
    }

    final boolean[] isUpdated = {false};
    for (Pair<EditorComponent, Boolean> pair : input) {
      final EditorComponent editorComponent = pair.o1;
      final Boolean applyQuickFixes = pair.o2;

      if (myHighlighter.isPausedOrStopping()) {
        return;
      }
      TypeContextManager.getInstance().runTypecheckingAction(editorComponent.getTypecheckingContextOwner(), new Runnable() {
        @Override
        public void run() {
          if (updateEditorComponent(editorComponent, false, applyQuickFixes)) {
            isUpdated[0] = true;
          }
        }
      });
    }

    if (myHighlighter.isPausedOrStopping()) {
      return;
    }

    if (myInspector != null) {
      TypeContextManager.getInstance().runTypecheckingAction(myInspector.getTypecheckingContextOwner(), new Runnable() {
        @Override
        public void run() {
          updateEditorComponent(myInspector, isUpdated[0], false);
        }
      });
    }
  }

  private boolean updateEditorComponent(final EditorComponent component, final boolean mainEditorMessagesChanged, final boolean applyQuickFixes) {
    HighlighterEditorTracker editorTracker = myHighlighter.getEditorTracker();
    final SRepository repository = component.getEditorContext().getRepository();
    boolean needsUpdate = new ModelAccessHelper(repository).runReadAction(new Computable<Boolean>() {
      @Override
      public Boolean compute() {
        final SNode editedNode = component.getEditedNode();
        return editedNode != null && SNodeUtil.isAccessible(editedNode, repository);
      }
    });
    if (!needsUpdate) return false;

    final Set<EditorCheckerWrapper> checkersToRecheck = new LinkedHashSet<>();
    boolean rootWasCheckedOnce = editorTracker.wasCheckedOnce(component);
    if (!rootWasCheckedOnce) {
      checkersToRecheck.addAll(myCheckers);
    } else {
      repository.getModelAccess().runReadAction(new Runnable() {
        @Override
        public void run() {
          if (myHighlighter.isPausedOrStopping()) return;

          for (EditorCheckerWrapper checker : myCheckers) {
            if (checker.needsUpdate(component)) {
              checkersToRecheck.add(checker);
            }
          }
        }
      });
    }

    if (checkersToRecheck.isEmpty() || myHighlighter.isPausedOrStopping()) return false;

    List<EditorCheckerWrapper> checkersToRecheckList = new ArrayList<>(checkersToRecheck);
    checkersToRecheckList.sort(new PriorityComparator());

    boolean recreateInspectorMessages = mainEditorMessagesChanged || !editorTracker.wereInspectorMessagesCreated();
    editorTracker.markCheckedOnce(component);

    return updateEditor(component, rootWasCheckedOnce, checkersToRecheckList, recreateInspectorMessages, applyQuickFixes);
  }

  private boolean updateEditor(final EditorComponent editor, final boolean wasCheckedOnce,
      List<EditorCheckerWrapper> checkersToRecheck, boolean recreateInspectorMessages, final boolean applyQuickFixes) {
    if (editor == null || editor.getRootCell() == null) return false;

    final NodeHighlightManager highlightManager = editor.getHighlightManager();
    boolean anyMessageChanged = false;
    for (final EditorCheckerWrapper checker : checkersToRecheck) {
      UpdateResult checkResult = runLoPrioRead(new Computable<UpdateResult>() {
        @Override
        public UpdateResult compute() {
          if (myHighlighter.isPausedOrStopping()) return UpdateResult.CANCELLED;

          SNode node = editor.getEditedNode(); // XXX perhaps, shall use getEditedNodePointer and resolve it, rather than check isAccessible?
          if (node == null) {
            return UpdateResult.CANCELLED;
          }
          if (!SNodeUtil.isAccessible(node, editor.getEditorContext().getRepository())) {
            // asking runLoPrioRead() implementation to re-execute this task later:
            // editor was not updated in accordance with last modelReload event yet.
            return null;
          }

          return checker.withChecker(checker -> {
            try {
              return checker.update(editor, wasCheckedOnce, applyQuickFixes,
                  new HighlighterUpdateSessionCancellable(myHighlighter, checker.toString(), editor));
            } catch (IndexNotReadyException ex) {
              highlightManager.clearForOwner(checker.getEditorMessageOwner(), true);
              throw ex;
            }
          }, UpdateResult.CANCELLED);
        }
      });
      if (myHighlighter.isStopping()) return false;

      if (checkResult instanceof Completed) {
        Completed completed = (Completed) checkResult;
        if (completed.myMessagesChanged || myHighlighter.getEditorTracker().isInspector(editor) && recreateInspectorMessages) {
          anyMessageChanged = true;
          highlightManager.clearForOwner(checker.getEditorMessageOwner(), false);
          for (EditorMessage message : completed.myMessages) {
            highlightManager.mark(message);
          }
        }
      }
    }
    if (myHighlighter.isStopping()) return false;

    if (anyMessageChanged) {
      highlightManager.repaintAndRebuildEditorMessages();
      editor.updateStatusBarMessage();
    }

    return anyMessageChanged;
  }

  private void doneUpdating() {
    for (EditorCheckerWrapper checker : myCheckers) {
      checker.doneUpdating();
    }
  }

  public void update() {
    doUpdate();
    doneUpdating();
  }
}
