/*
 * 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.psi.stubs;

import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.application.ModalityState;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.editor.Document;
import com.intellij.openapi.fileEditor.FileDocumentManager;
import com.intellij.openapi.project.DumbService;
import com.intellij.openapi.project.NoAccessDuringPsiEvents;
import com.intellij.openapi.project.Project;
import com.intellij.openapi.project.ProjectManager;
import com.intellij.openapi.util.RecursionManager;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.psi.PsiDocumentManager;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.psi.impl.PsiManagerEx;
import com.intellij.psi.impl.source.PsiFileImpl;
import com.intellij.psi.search.GlobalSearchScope;
import com.intellij.util.indexing.*;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.io.IOException;
import java.util.List;

/**
 * @author yole
 */
public class StubTreeLoaderImpl extends StubTreeLoader {
  private static final Logger LOG = Logger.getInstance("#com.intellij.psi.stubs.StubTreeLoaderImpl");
  private static volatile boolean ourStubReloadingProhibited = false;

  @Override
  @Nullable
  public ObjectStubTree readOrBuild(Project project, final VirtualFile vFile, @Nullable PsiFile psiFile) {
    final ObjectStubTree fromIndices = readFromVFile(project, vFile);
    if (fromIndices != null) {
      return fromIndices;
    }

    try {
      byte[] content = vFile.contentsToByteArray();
      vFile.setPreloadedContentHint(content);
      final FileContent fc;
      try {
        fc = new FileContentImpl(vFile, content);
        fc.putUserData(IndexingDataKeys.PROJECT, project);
        if (psiFile != null && !vFile.getFileType().isBinary()) {
          fc.putUserData(IndexingDataKeys.FILE_TEXT_CONTENT_KEY, psiFile.getViewProvider().getContents());
          // but don't reuse psiFile itself to avoid loading its contents. If we load AST, the stub will be thrown out anyway.
        }

        Stub element = RecursionManager.doPreventingRecursion(vFile, false, () -> StubTreeBuilder.buildStubTree(fc));
        if (element instanceof PsiFileStub) {
          StubTree tree = new StubTree((PsiFileStub)element);
          tree.setDebugInfo("created from file content");
          return tree;
        }
      }
      finally {
        vFile.setPreloadedContentHint(null);
      }
    }
    catch (IOException e) {
      LOG.info(e); // content can be not cached yet, and the file can be deleted on disk already, without refresh
    }

    return null;
  }

  @Override
  @Nullable
  public ObjectStubTree readFromVFile(Project project, final VirtualFile vFile) {
    if (DumbService.getInstance(project).isDumb() || NoAccessDuringPsiEvents.isInsideEventProcessing()) {
      return null;
    }

    final int id = Math.abs(FileBasedIndex.getFileId(vFile));
    if (id <= 0) {
      return null;
    }

    boolean wasIndexedAlready = ((FileBasedIndexImpl)FileBasedIndex.getInstance()).isFileUpToDate(vFile);

    Document document = FileDocumentManager.getInstance().getCachedDocument(vFile);
    boolean saved = document == null || !FileDocumentManager.getInstance().isDocumentUnsaved(document);

    final List<SerializedStubTree> datas = FileBasedIndex.getInstance().getValues(StubUpdatingIndex.INDEX_ID, id, GlobalSearchScope
        .fileScope(project, vFile));
    final int size = datas.size();

    if (size == 1) {
      SerializedStubTree stubTree = datas.get(0);
      
      if (!stubTree.contentLengthMatches(vFile.getLength(), getCurrentTextContentLength(project, vFile, document))) {
        return processError(vFile,
                            "Outdated stub in index: " + vFile + " " + getIndexingStampInfo(vFile) +
                            ", doc=" + document +
                            ", docSaved=" + saved +
                            ", wasIndexedAlready=" + wasIndexedAlready +
                            ", queried at " + vFile.getTimeStamp(),
                            null);
      }

      Stub stub;
      try {
        stub = stubTree.getStub(false);
      }
      catch (SerializerNotFoundException e) {
        return processError(vFile, "No stub serializer: " + vFile.getPresentableUrl() + ": " + e.getMessage(), e);
      }
      ObjectStubTree<?> tree = stub instanceof PsiFileStub ? new StubTree((PsiFileStub)stub) : new ObjectStubTree((ObjectStubBase)stub, true);
      tree.setDebugInfo("created from index");
      checkDeserializationCreatesNoPsi(tree);
      return tree;
    }
    else if (size != 0) {
      return processError(vFile, "Twin stubs: " + vFile.getPresentableUrl() + " has " + size + " stub versions. Should only have one. id=" + id,
                          null);
    }

    return null;
  }

  private static void checkDeserializationCreatesNoPsi(ObjectStubTree<?> tree) {
    if (ourStubReloadingProhibited) return;

    for (Stub each : tree.getPlainListFromAllRoots()) {
      if (each instanceof StubBase) {
        PsiElement cachedPsi = ((StubBase)each).getCachedPsi();
        if (cachedPsi != null) {
          ourStubReloadingProhibited = true;
          throw new AssertionError("Stub deserialization shouldn't create PSI: " + cachedPsi + "; " + each);
        }
      }
    }
  }

  @Override
  public boolean isStubReloadingProhibited() {
    return ourStubReloadingProhibited;
  }

  private static int getCurrentTextContentLength(Project project, VirtualFile vFile, Document document) {
    if (vFile.getFileType().isBinary()) {
      return -1;
    }
    PsiFile psiFile = PsiManagerEx.getInstanceEx(project).getFileManager().getCachedPsiFile(vFile);
    if (psiFile instanceof PsiFileImpl && ((PsiFileImpl)psiFile).isContentsLoaded()) {
      return psiFile.getTextLength();
    }
    
    if (document != null) {
      return PsiDocumentManager.getInstance(project).getLastCommittedText(document).length();
    }
    return -1;
  }

  private static ObjectStubTree processError(final VirtualFile vFile, String message, @Nullable Exception e) {
    LOG.error(message, e);

    ApplicationManager.getApplication().invokeLater(() -> {
      final Document doc = FileDocumentManager.getInstance().getCachedDocument(vFile);
      if (doc != null) {
        FileDocumentManager.getInstance().saveDocument(doc);
      }
    }, ModalityState.NON_MODAL);

    FileBasedIndex.getInstance().requestReindex(vFile);
    return null;
  }

  @Override
  public void rebuildStubTree(VirtualFile virtualFile) {
    FileBasedIndex.getInstance().requestReindex(virtualFile);
  }

  @Override
  public boolean canHaveStub(VirtualFile file) {
    return StubUpdatingIndex.canHaveStub(file);
  }

  @Override
  protected boolean hasPsiInManyProjects(@NotNull final VirtualFile virtualFile) {
    int count = 0;
    for (Project project : ProjectManager.getInstance().getOpenProjects()) {
      if (PsiManagerEx.getInstanceEx(project).getFileManager().findCachedViewProvider(virtualFile) != null) {
        count++;
      }
    }
    return count > 1;
  }

  @Override
  protected IndexingStampInfo getIndexingStampInfo(@NotNull VirtualFile file) {
    return StubUpdatingIndex.getIndexingStampInfo(file);
  }
}
