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

import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.openapi.vfs.VirtualFileSystem;
import com.intellij.util.LocalTimeCounter;
import jetbrains.mps.extapi.module.TransientSModule;
import jetbrains.mps.smodel.ModelAccessHelper;
import org.apache.log4j.LogManager;
import org.apache.log4j.Logger;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.mps.openapi.model.SNode;
import org.jetbrains.mps.openapi.model.SNodeReference;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;

public final class MPSNodeVirtualFile extends VirtualFile {
  private static final byte[] CONTENTS = new byte[0];
  private static final Logger LOG = LogManager.getLogger(MPSNodeVirtualFile.class);
  static final String NODE_PREFIX = "node://";

  private SNodeReference myNode;
  private final RepositoryVirtualFiles myRepoFiles;
  private String myPath;
  private String myName;
  private String myPresentationName;
  private long myModificationStamp = LocalTimeCounter.currentTime();
  private long myTimeStamp = -1;

  MPSNodeVirtualFile(@NotNull SNodeReference nodePointer, @NotNull RepositoryVirtualFiles vfs) {
    myNode = nodePointer;
    myRepoFiles = vfs;
    updateFields();
  }

  // FIXME: check, perhaps is invoked with proper model access already.
  // for exposed files, this shall happen in exclusive read (so that different threads from readAction do not get different
  // result e.g. for getName().
  /*package*/ void updateFields() {
    myRepoFiles.getRepository().getModelAccess().runReadAction(() -> {
      SNode node = myNode.resolve(myRepoFiles.getRepository());
      if (node == null) {
        LOG.warn("Cannot find node for passed SNodeReference: " + myNode);
        myName = myPresentationName = "";
        myPath = "";
      } else {
        myName = myPresentationName = String.valueOf(node.getPresentation());
        if (node.getModel() != null && node.getModel().getModule() instanceof TransientSModule) {
          // it's common to open same node from different generation steps (transient models)
          // and to tell nodes from different steps we append model's identification
          final String s = node.getModel().getName().getStereotype();
          if (!s.isEmpty()) {
            myPresentationName = myName + '@' + s;
          }
        }
        myPath = NODE_PREFIX + myRepoFiles.getPathFacility().serializeNode(node);
        myTimeStamp = node.getModel().getSource().getTimestamp();
      }
    });
  }

  @Nullable
  public SNode getNode() {
    return myNode.resolve(myRepoFiles.getRepository());
  }

  @NotNull
  public SNodeReference getSNodePointer() {
    return myNode;
  }

  @NotNull
  @Override
  public String getPath() {
    return myPath;
  }

  @Override
  @NotNull
  public VirtualFileSystem getFileSystem() {
    return myRepoFiles.getFileSystem();
  }

  /**
   * Pre-evaluated name of the file. This method doesn't require model access.
   */
  @Override
  @NotNull
  @NonNls
  public String getName() {
    return myName;
  }

  /**
   * Pre-evaluated user-presentable name of the file, may include extra information to distinguish nodes with the same {@linkplain #getName() name}.
   * This method doesn't require model access.
   */
  @Override
  public String getPresentableName() {
    return myPresentationName;
  }

  @Override
  public boolean isDirectory() {
    return false;
  }

  @Override
  public long getLength() {
    return 0;
  }

  @Override
  public InputStream getInputStream() throws IOException {
    throw new UnsupportedOperationException();
  }

  @Override
  @NotNull
  public OutputStream getOutputStream(Object requestor, long newModificationStamp, long newTimeStamp) throws IOException {
    throw new UnsupportedOperationException();
  }

  @Override
  @NotNull
  public byte[] contentsToByteArray() throws IOException {
    return CONTENTS;
  }

  @Override
  @Nullable
  public VirtualFile getParent() {
    // Returning the parent of this node's model virtual file
    // i.e. a real directory wherein the model file lives
    // Needed for idea scope to work (see PsiSearchScopeUtil.isInScope)
    // but why it's not MPSModelVirtualFile that serves as parent for node VF?
    if (myNode == null || myNode.getModelReference() == null) return null;
    return new ModelAccessHelper(myRepoFiles.getRepository()).runReadAction(() -> {
      if (myNode == null) {
        // wow! this double check is needed even with the fact, that read action is run in the same thread
        // i.e. getParent() and this runnable are in the same thread
        // But! idea waits for the current write action to complete before proceeding to the read action
        // (see ApplicationalImpl.startRead())
        // And it happens so that invalidate() which sets myNode to null reproducibly happens exactly
        // in the write action we're waiting for, hence NPE
        return null;
      }
      org.jetbrains.mps.openapi.model.SModelReference modelRef = myNode.getModelReference();
      if (modelRef.resolve(myRepoFiles.getRepository()) == null) {
        return null;
      }
      MPSModelVirtualFile modelVFile = myRepoFiles.getFileFor(modelRef);
      if (modelVFile != null) {
        return modelVFile.getParent();
      }
      return null;
    });
  }

  @Override
  public VirtualFile[] getChildren() {
    return null;
  }

  @Override
  public void refresh(boolean asynchronous, boolean recursive, Runnable postRunnable) {
    if (postRunnable != null) {
      postRunnable.run();
    }
  }

  @Override
  public boolean isWritable() {
    return true;
  }

  @Override
  public boolean isValid() {
    return myNode != null;
  }

  /*package*/ void invalidate() {
    if (myNode == null) {
      // With proper fix of https://youtrack.jetbrains.com/issue/MPS-24244 (shared VFS notifier instance), shall not happen,
      // nevertheless, doesn't hurt to be alert.
      LOG.error("Attempt to invalidate already disposed file", new Throwable());
      return;
    }
    myRepoFiles.forgetVirtualFile(myNode);
    myNode = null;
  }

  // XXX what's the contract of the method???
  public boolean hasValidMPSNode() {
    return isValid() && myRepoFiles.hasVirtualFileFor(myNode);
  }

  @Override
  public long getTimeStamp() {
    return myTimeStamp;
  }

  public void setTimeStamp(long newTimeStamp) {
    myTimeStamp = newTimeStamp;
  }

  @Override
  public long getModificationStamp() {
    return myModificationStamp;
  }

  public void setModificationStamp(long newValue) {
    myModificationStamp = newValue;
  }
}
