/*
 * 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.idea.core.psi.impl;

import com.intellij.openapi.actionSystem.LangDataKeys;
import com.intellij.openapi.application.TransactionGuard;
import com.intellij.openapi.components.AbstractProjectComponent;
import com.intellij.openapi.fileEditor.FileEditor;
import com.intellij.openapi.fileEditor.FileEditorDataProvider;
import com.intellij.openapi.fileEditor.FileEditorDataProviderManager;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.psi.PsiManager;
import com.intellij.psi.PsiTreeChangeEvent;
import com.intellij.psi.impl.PsiManagerEx;
import com.intellij.psi.impl.PsiManagerImpl;
import com.intellij.psi.impl.PsiModificationTrackerImpl;
import com.intellij.psi.impl.PsiTreeChangeEventImpl;
import com.intellij.psi.impl.file.impl.FileManager;
import jetbrains.mps.ide.project.ProjectHelper;
import jetbrains.mps.idea.core.psi.MPS2PsiMapperUtil;
import jetbrains.mps.idea.core.psi.MPSPsiNodeFactory;
import jetbrains.mps.idea.core.psi.impl.events.SModelEventProcessor;
import jetbrains.mps.idea.core.psi.impl.events.SModelEventProcessor.ModelProvider;
import jetbrains.mps.idea.core.psi.impl.events.SModelEventProcessor.ReloadableModel;
import jetbrains.mps.nodefs.MPSNodeVirtualFile;
import jetbrains.mps.smodel.GlobalSModelEventsManager;
import jetbrains.mps.smodel.ModelAccessHelper;
import jetbrains.mps.smodel.event.SModelCommandListener;
import jetbrains.mps.smodel.event.SModelEvent;
import jetbrains.mps.util.Computable;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.mps.openapi.language.SAbstractConcept;
import org.jetbrains.mps.openapi.language.SConcept;
import org.jetbrains.mps.openapi.model.EditableSModel;
import org.jetbrains.mps.openapi.model.SModel;
import org.jetbrains.mps.openapi.model.SModelReference;
import org.jetbrains.mps.openapi.model.SNode;
import org.jetbrains.mps.openapi.model.SNodeId;
import org.jetbrains.mps.openapi.model.SNodeReference;
import org.jetbrains.mps.openapi.module.SModule;
import org.jetbrains.mps.openapi.module.SModuleListenerBase;
import org.jetbrains.mps.openapi.module.SRepository;

import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

/**
 * evgeny, 1/25/13
 */
public class MPSPsiProvider extends AbstractProjectComponent {

  // TODO softReference..
  private final ConcurrentMap<SModelReference, MPSPsiModel> models = new ConcurrentHashMap<SModelReference, MPSPsiModel>();
  private final PsiModificationTrackerImpl myModificationTracker;

  public static MPSPsiProvider getInstance(@NotNull Project project) {
    return project.getComponent(MPSPsiProvider.class);
  }

  private SModelEventProcessor myEventProcessor;

  /**
   * We're notifying about changes in PSI via
   * {@link com.intellij.psi.impl.PsiModificationTrackerImpl#incCounter()}).
   * It asserts {@link com.intellij.openapi.application.TransactionGuardImpl#assertWriteActionAllowed()
   * The problem is the command which created the events we're reacting to might have been either
   * inside a transaction or not. Currently, most MPS actions don't invoke a transaction. Moreover,
   * calls to runWriteInEDT() and the like, which happen to exist in MPS actions, run the given code
   * via LaterInvocator and the code is executed not in a transaction.
   * <p>
   * Thus, we should expect both scenarios.
   * <p>
   * In the future, maybe we should drop the case when we're creating our own fake transaction here.
   * It can be done either by removing runWriteInEDT in actions (because every action is wrapped
   * in {@link com.intellij.openapi.application.TransactionGuardImpl#performUserActivity(Runnable)})
   * or by invoking a transaction explicitly in the action.
   */
  private SModelCommandListener myListener = new SModelCommandListener() {
    public void eventsHappenedInCommand(List<SModelEvent> events) {
      Runnable processEvents = () -> myEventProcessor.process(events);
      if (TransactionGuard.getInstance().getContextTransaction() != null) {
        // the command that caused the events was in a transaction
        processEvents.run();
      } else {
        // hackish, might be dropped in the future
        TransactionGuard.submitTransaction(myProject, () -> {
          SRepository repository = ProjectHelper.getProjectRepository(myProject);
          if (repository != null) {
            repository.getModelAccess().runWriteAction(processEvents);
          }
        });
      }

      // TODO PsiModificationTrackerImpl.incCounter/incOutOfCodeBlockModificationCounter (see JavaCodeBlockModificationListener)
      // TODO notify ANY_PSI_CHANGE_TOPIC
    }
  };

  protected MPSPsiProvider(Project project) {
    super(project);
    myEventProcessor = createEventProcessor();
    PsiManager psiManager = PsiManagerEx.getInstance(project);
    this.myModificationTracker = (PsiModificationTrackerImpl) psiManager.getModificationTracker();
  }

  public void initComponent() {
    GlobalSModelEventsManager.getInstance().addGlobalCommandListener(myListener);
    FileEditorDataProviderManager.getInstance(myProject).registerDataProvider(new PsiFileEditorDataProvider(), null);
  }

  public void disposeComponent() {
    GlobalSModelEventsManager.getInstance().removeGlobalCommandListener(myListener);
  }

  public PsiElement getPsi(SNodeReference nodeRef) {
    if (nodeRef == null) return null;

    final SNode node = nodeRef.resolve(ProjectHelper.getProjectRepository(myProject));
    if (node == null) return null;
    return getPsi(node);
  }

  public PsiElement getPsi(SNode node) {
    if (node == null) return null;

    // give chance to other to tell us what the PSI element is
    PsiElement source = MPS2PsiMapperUtil.getPsiElement(node, myProject);
    if (source != null) {
      return source;
    }

    final SModel containingModel = node.getModel();
    if (containingModel == null) return null;

    MPSPsiModel psiModel = getPsi(containingModel);
    if (psiModel == null) return null;

    return psiModel.lookupNode(node.getNodeId());
  }

  public MPSPsiModel getPsi(SModel model) {
    // TODO check GlobalSearchScope.projectScope(myProject).contains(modelFile)

    final SModelReference modelRef = model.getReference();
    MPSPsiModel cached = models.get(modelRef);
    if (cached != null) return cached;

    return getMPSPsiModel(model, modelRef);
  }

  public MPSPsiModel getPsi(SModelReference modelRef) {
    MPSPsiModel cached = models.get(modelRef);
    if (cached != null) return cached;

    SModel model = modelRef.resolve(ProjectHelper.getProjectRepository(myProject));

    // TODO check if the model is valid

    return getMPSPsiModel(model, modelRef);
  }

  public MPSPsiNode create(SNodeId id, SConcept concept, String containingRole) {
    for (MPSPsiNodeFactory factory : MPSPsiNodeFactory.EP_NAME.getExtensions()) {
      final MPSPsiNode psiNode = factory.create(id, concept, containingRole, PsiManager.getInstance(myProject));
      if (psiNode != null) {
        return psiNode;
      }
    }
    return new MPSPsiNode(id, concept.getQualifiedName(), containingRole, PsiManager.getInstance(myProject));
  }

  public MPSPsiRef createReferenceNode(String role, SAbstractConcept linkTargetConcept, SModelReference targetModel, SNodeId targetId) {
    if (linkTargetConcept != null) {
      for (MPSPsiNodeFactory factory : MPSPsiNodeFactory.EP_NAME.getExtensions()) {
        final MPSPsiRef psiRefNode = factory.createReferenceNode(role, linkTargetConcept, targetModel, targetId, PsiManager.getInstance(myProject));
        if (psiRefNode != null) {
          return psiRefNode;
        }
      }
    }
    return new MPSPsiRef(role, targetModel, targetId, PsiManager.getInstance(myProject));
  }

  public MPSPsiRef createReferenceNode(String role, SAbstractConcept linkTargetConcept, String referenceText) {
    if (linkTargetConcept != null) {
      for (MPSPsiNodeFactory factory : MPSPsiNodeFactory.EP_NAME.getExtensions()) {
        final MPSPsiRef psiRefNode = factory.createReferenceNode(role, linkTargetConcept, referenceText, PsiManager.getInstance(myProject));
        if (psiRefNode != null) {
          return psiRefNode;
        }
      }
    }
    return new MPSPsiRef(role, referenceText, PsiManager.getInstance(myProject));
  }

  private MPSPsiModel getMPSPsiModel(final SModel model, final SModelReference modelRef) {
    if (MPS2PsiMapperUtil.hasCorrespondingPsi(model)) return null;

    // synchronizing by model:
    // we guard MPSPsiModel.reload() exactly by model,
    // on the other hand, the key in models is modelRef, but different models in one repo seem to always have
    // different modelRefs
    synchronized (model) {
      MPSPsiModel result = models.get(modelRef);
      if (result == null) {
        result = new MPSPsiModel(modelRef, PsiManager.getInstance(myProject));
        // since some time ago we have to guard MPSPsiModel in this way:
        // MPSPsiModel.reload() should not happen for different instances, which are connected
        // to one SModel, because root nodes are re-used in case of file-per-root persistence.
        // I.e. those root nodes cannot be added as children to a model when they are already children of another
        result.reload(model);
        models.put(modelRef, result);
        model.getModule().addModuleListener(new SModuleListenerBase() {
          @Override
          public void beforeModelRenamed(SModule module, SModel model, SModelReference newRef) {
            models.remove(model.getReference());
          }

          @Override
          public void beforeModelRemoved(SModule module, SModel removedModel) {
            if (removedModel != model) return;
            models.remove(modelRef);
          }
        });
        model.addModelListener(myProject.getComponent(PsiModelReloadListener.class));
      }
      return result;
    }
  }

  private SModelEventProcessor createEventProcessor() {
    return new SModelEventProcessor(new ModelProvider() {
      @Override
      public ReloadableModel lookupModel(SModelReference modelReference) {
        // must be alright concurrency-wise, because ConcurrentHashMap creates a memory barrier
        final MPSPsiModel psiModel = models.get(modelReference);
        if (psiModel == null) return null;

        // MPPsiModel.reload() relies on roots' virtual files being up-to-date, so we save the model in case
        // root name might have changed
        return new ReloadableModel() {
          @Override
          public void reload(SNodeId sNodeId) {
            MPSPsiNode oldPsiNode = psiModel.lookupNode(sNodeId);
            if (oldPsiNode != null && psiModel.isRoot(oldPsiNode)) {
              // sNodeId corresponds to root node
              save(psiModel);
            }
            MPSPsiNode psiNode = psiModel.reload(sNodeId);
            notifyPsiChanged(psiModel, psiNode);
          }

          @Override
          public void reloadAll() {
            save(psiModel);
            psiModel.reloadAll();
            notifyPsiChanged(psiModel, null);
          }

          private void save(MPSPsiModel psiModel) {
            SModel smodel = psiModel.getSModelReference().resolve(ProjectHelper.getProjectRepository(psiModel.getProject()));
            if (smodel instanceof EditableSModel) {
              ((EditableSModel) smodel).save();
            }
          }
        };
      }
    });
  }

  void notifyPsiChanged(MPSPsiModel model, MPSPsiNodeBase node) {

    if (!model.isValid()) return;

    PsiManager manager = model.getManager();
    if (manager == null || !(manager instanceof PsiManagerImpl)) return;

    myModificationTracker.incCounter();

    // TODO: this is a dumb straightforward solution, better use beforeChage*. Or not?
    manager.dropResolveCaches();

    PsiTreeChangeEventImpl event = new PsiTreeChangeEventImpl(manager);
    event.setParent(node != null ? node : model);
    event.setGenericChange(false);

    ((PsiManagerImpl) manager).childrenChanged(event);
  }

  void notifyModelRenamed(MPSPsiModel model, String oldName, String newName) {
    PsiManager manager = model.getManager();
    if (manager == null || !(manager instanceof PsiManagerImpl)) return;

    myModificationTracker.incCounter();

    // TODO: this is a dumb straightforward solution, better use beforeChage*. Or not?
    manager.dropResolveCaches();

    PsiTreeChangeEventImpl event = new PsiTreeChangeEventImpl(manager);
    event.setElement(model);
    event.setPropertyName(PsiTreeChangeEvent.PROP_FILE_NAME);
    event.setOldValue(oldName);
    event.setNewValue(newName);
    ((PsiManagerImpl) manager).propertyChanged(event);
  }

  private class PsiFileEditorDataProvider implements FileEditorDataProvider {

    @Nullable
    @Override
    public Object getData(String dataId, FileEditor e, VirtualFile file) {
      if (!file.isValid()) return null;

//      if (LangDataKeys.PSI_FILE.is(dataId)) {
//        return getPsiFile(file);
//      }

      if (LangDataKeys.PSI_ELEMENT.is(dataId)) {
        return getPsiPsiElement(file);
      }

      return null;
    }

    private PsiElement getPsiPsiElement(VirtualFile snodeVFile) {
      if (snodeVFile instanceof MPSNodeVirtualFile) {
        final MPSNodeVirtualFile mpsFile = (MPSNodeVirtualFile) snodeVFile;

        final SNodeReference sNodePointer = mpsFile.getSNodePointer();

        MPSPsiModel psiModel = models.get(sNodePointer.getModelReference());
        if (psiModel == null) return null;

        PsiElement psiElement = new ModelAccessHelper(ProjectHelper.getModelAccess(myProject)).runReadAction(new Computable<PsiElement>() {
          @Override
          public PsiElement compute() {
            return getPsi(sNodePointer);
          }
        });
        if (psiElement != null) return psiElement;

        for (MPSPsiRootNode rootNode : psiModel.getChildren(MPSPsiRootNode.class)) {
          if (rootNode.getSNodeReference().equals(mpsFile.getSNodePointer())) {
            return rootNode;
          }
        }

        // TODO not cached node
      }
      return null;
    }

    private PsiFile getPsiFile(VirtualFile snodeVFile) {
      if (snodeVFile instanceof MPSNodeVirtualFile) {
        final MPSNodeVirtualFile mpsFile = (MPSNodeVirtualFile) snodeVFile;
        SNodeReference sNodePointer = mpsFile.getSNodePointer();
        MPSPsiModel mpsPsiModel = models.get(sNodePointer.getModelReference());
        if (mpsPsiModel == null) return null;
        VirtualFile sourceVFile = mpsPsiModel.getSourceVirtualFile();

        FileManager fileManager = ((PsiManagerEx) PsiManagerEx.getInstance(myProject)).getFileManager();
        return fileManager.findFile(sourceVFile);


        // TODO not cached node
      }
      return null;
    }
  }
}
