/*
 * Copyright 2003-2014 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.ide.vfs;

import com.intellij.openapi.application.ApplicationManager;
import com.intellij.openapi.application.Result;
import com.intellij.openapi.application.WriteAction;
import com.intellij.openapi.util.io.FileUtil;
import com.intellij.openapi.vfs.JarFileSystem;
import com.intellij.openapi.vfs.LocalFileSystem;
import com.intellij.openapi.vfs.VfsUtil;
import com.intellij.openapi.vfs.VfsUtilCore;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.openapi.vfs.newvfs.ArchiveFileSystem;
import com.intellij.openapi.vfs.newvfs.NewVirtualFile;
import jetbrains.mps.util.EqualUtil;
import jetbrains.mps.vfs.CachingContext;
import jetbrains.mps.vfs.CachingFile;
import jetbrains.mps.vfs.CachingFileSystem;
import jetbrains.mps.vfs.IFile;
import jetbrains.mps.vfs.path.Path;
import jetbrains.mps.vfs.path.UniPath;
import jetbrains.mps.vfs.ex.IFileEx;
import org.apache.log4j.LogManager;
import org.apache.log4j.Logger;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.mps.annotations.Internal;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

/**
 * TODO rewrite using {@link Path}; rewrite {@link #getChildren(),#getDescendant(String)} behavior in the case of jar system
 */
public class IdeaFile implements IFileEx, CachingFile {
  private final static Logger LOG = LogManager.getLogger(IdeaFile.class);

  private final IdeaFileSystem myFileSystem;

  /*
   * remember the name used to create this instance, as it might be different from a name in fs on case-insensitive filesystem
   */
  private String myPath;
  private VirtualFile myVirtualFile = null;

  @Internal
  public IdeaFile(IdeaFileSystem fileSystem, @NotNull String path) {
    myFileSystem = fileSystem;
    myPath = jetbrains.mps.util.FileUtil.normalize(path);
  }

  @Internal
  public IdeaFile(IdeaFileSystem fileSystem, @NotNull VirtualFile virtualFile) {
    myFileSystem = fileSystem;
    myVirtualFile = virtualFile;
    myPath = virtualFile.getPath();
  }

  @NotNull
  @Override
  public String getPath() {
    if (findVirtualFile()) {
      return myVirtualFile.getPath();
    } else {
      return myPath;
    }
  }

  @NotNull
  @Override
  public UniPath toPath() {
    return UniPath.fromString(getPath());
  }

  @Override
  public URL getUrl() throws MalformedURLException {
    if (findVirtualFile()) {
      return VfsUtilCore.convertToURL(myVirtualFile.getUrl());
    } else {
      return new File(myPath).toURI().toURL();
    }
  }

  @NotNull
  @Override
  public CachingFileSystem getFileSystem() {
    return myFileSystem;
  }

  @NotNull
  @Override
  public String getName() {
    if (findVirtualFile()) {
      return myVirtualFile.getName();
    } else {
      return truncFileName(myPath);
    }
  }

  @Override
  public IdeaFile getParent() {
    if (findVirtualFile()) {
      VirtualFile parentVirtualFile = myVirtualFile.getParent();
      if (parentVirtualFile != null) {
        return new IdeaFile(myFileSystem, parentVirtualFile);
      }
      return null;
    } else {
      return new IdeaFile(myFileSystem, truncateDirPath(myPath));
    }
  }

  @Override
  public List<IFile> getChildren() {
    if (findVirtualFile()) {
      VirtualFile[] children = new VirtualFile[0];
      if (myVirtualFile.isValid()) {
        children = myVirtualFile.getChildren();
      }
      ArrayList<IdeaFile> result = new ArrayList<>();
      for (VirtualFile child : children) {
        result.add(new IdeaFile(myFileSystem, child));
      }
      return Collections.unmodifiableList(result);
    } else {
      return Collections.emptyList();
    }
  }

  @Override
  @NotNull
  public IdeaFile getDescendant(@NotNull String suffix) {
    String path = getPath();
    String separator = Path.UNIX_SEPARATOR; // we are system-independent underneath
    return new IdeaFile(myFileSystem, path + (path.endsWith(separator) ? "" : separator) + suffix);
  }

  @Override
  public boolean isDirectory() {
    return findVirtualFile() ? myVirtualFile.isDirectory() : new File(myPath).isDirectory();
  }

  @Override
  public boolean isReadOnly() {
    return exists() && !myVirtualFile.isWritable();
  }

  @Override
  public long lastModified() {
    if (findVirtualFile()) {
      return myVirtualFile.getTimeStamp();
    } else {
      return -1;
    }
  }

  @Override
  public long length() {
    if (findVirtualFile()) {
      return myVirtualFile.getLength();
    } else {
      return -1;
    }
  }

  @Override
  public boolean createNewFile() {
    ApplicationManager.getApplication().assertWriteAccessAllowed();
    if (findVirtualFile()) {
      return !myVirtualFile.isDirectory();
    } else {
      try {
        VirtualFile directory = createDirectories(truncateDirPath(myPath));
        String fileName = truncFileName(myPath);
        directory.findChild(fileName); // This is a workaround for IDEA-67279
        myVirtualFile = directory.createChildData(myFileSystem, fileName);
        return true;
      } catch (IOException e) {
        LOG.error(null, e);
        return false;
      }
    }
  }

  //this was copied from Idea's VfsUtil. The point of copying is changing the requestor not to get back-events during saving models
  private VirtualFile createDirectories(final String directoryPath) throws IOException {
    return new WriteAction<VirtualFile>() {
      @Override
      protected void run(@NotNull Result<VirtualFile> result) throws Throwable {
        VirtualFile res = createDirectoryIfMissing(directoryPath);
        result.setResult(res);
      }
    }.execute().throwException().getResultObject();
  }

  //this was copied from Idea's VfsUtil. The point of copying is changing the requestor not to get back-events during saving models
  private VirtualFile createDirectoryIfMissing(String directoryPath) throws IOException {
    String path = FileUtil.toSystemIndependentName(directoryPath);
    final VirtualFile file = LocalFileSystem.getInstance().refreshAndFindFileByPath(path);
    if (file == null) {
      int pos = path.lastIndexOf('/');
      if (pos < 0) return null;
      VirtualFile parent = createDirectoryIfMissing(path.substring(0, pos));
      if (parent == null) return null;
      final String dirName = path.substring(pos + 1);
      VirtualFile child = parent.findChild(dirName);
      if (child != null && child.isDirectory()) return child;
      return parent.createChildDirectory(myFileSystem, dirName);
    }
    return file;
  }

  @Override
  public boolean mkdirs() {
    ApplicationManager.getApplication().assertWriteAccessAllowed();
    if (findVirtualFile()) {
      return myVirtualFile.isDirectory();
    } else {
      try {
        myVirtualFile = createDirectories(myPath);
        return true;
      } catch (IOException e) {
        return false;
      }
    }
  }

  @Override
  public boolean exists() {
    return findVirtualFile() && myVirtualFile.exists();
  }

  @Override
  public boolean delete() {
    if (findVirtualFile()) {
      try {
        myVirtualFile.delete(myFileSystem);
        myVirtualFile = null;
        return true;
      } catch (IOException e) {
        LOG.warn("Could not delete file: ", e);
        return false;
      }
    } else {
      return true;
    }
  }

  @Override
  public boolean rename(String newName) {
    try {
      myVirtualFile.rename(myFileSystem, newName);
      myPath = myVirtualFile.getPath();
      return true;
    } catch (IOException e) {
      LOG.warn("Could not rename file: ", e);
      return false;
    }
  }

  @Override
  public boolean move(IFile newParent) {
    if (newParent instanceof IdeaFile && ((IdeaFile) newParent).findVirtualFile()) {
      try {
        myVirtualFile.move(myFileSystem, ((IdeaFile) newParent).myVirtualFile);
        return true;
      } catch (IOException e) {
        LOG.warn("Could not rename file: ", e);
        return false;
      }
    } else {
      return false;
    }
  }

  @Override
  public InputStream openInputStream() throws IOException {
    if (findVirtualFile()) {
      return myVirtualFile.getInputStream();
    } else {
      throw new FileNotFoundException("File not found: " + myPath);
    }
  }

  @Override
  public OutputStream openOutputStream() throws IOException {
    ApplicationManager.getApplication().assertWriteAccessAllowed();
    if (findVirtualFile() || createNewFile()) {
      if (myVirtualFile.getFileSystem() instanceof JarFileSystem) {
        throw new UnsupportedOperationException("Cannot write to Jar files");
      } else {
        if (!myVirtualFile.getFileSystem().isCaseSensitive()) {
          // Mac default (HFS), NTFS - are case-insensitive, looking up file "b/A" when there's "b/a" gives
          // existing file. However, Java is strict about case, and won't allow class A to live in file a.java
          // Hence, when we try to write into a file with the name different from one requested initially,
          // try to bring the name up to the desired one.
          final String desiredFileName = truncFileName(myPath);
          if (!myVirtualFile.getName().equals(desiredFileName)) {
            rename(desiredFileName);
          }
        }
        return myVirtualFile.getOutputStream(myFileSystem);
      }
    } else {
      throw new IOException("Could not create file: " + myPath);
    }
  }

  @Nullable
  public VirtualFile getVirtualFile() {
    findVirtualFile();
    return myVirtualFile;
  }

  @Override
  public boolean setTimeStamp(long time) {
    if (findVirtualFile() && myVirtualFile instanceof NewVirtualFile) {
      try {
        ((NewVirtualFile) myVirtualFile).setTimeStamp(time);
        return true;
      } catch (IOException e) {
        LOG.warn("", e);
      }
    }
    return false;
  }

  @Override
  public void refresh(@NotNull CachingContext context) {
    if (findVirtualFile()) {
      myVirtualFile.getChildren(); // This was added to force refresh
      myVirtualFile.refresh(!context.isSynchronous(), context.isRecursive());
    } else {
      findVirtualFile(true);
    }
  }

  @Override
  public boolean isInArchive() {
    if (findVirtualFile()) {
      return myVirtualFile.getFileSystem() instanceof ArchiveFileSystem;
    } else {
      return myPath.contains("!");
    }
  }

  @Override
  public boolean isArchive() {
    return myPath.endsWith(JarFileSystem.PROTOCOL) || myPath.endsWith(JarFileSystem.JAR_SEPARATOR);
  }

  @Override
  public IFile getBundleHome() {
    if (findVirtualFile()) {
      if (myVirtualFile.getFileSystem() instanceof ArchiveFileSystem) {
        VirtualFile fileForJar = ((ArchiveFileSystem) myVirtualFile.getFileSystem()).getLocalByEntry(myVirtualFile);
        if (fileForJar == null) {
          return null;
        }
        return new IdeaFile(myFileSystem, fileForJar);
      } else {
        return getParent();
      }
    } else {
      if (myPath.contains("!")) {
        return new IdeaFile(myFileSystem, myPath.substring(0, myPath.indexOf("!")));
      } else {
        return getParent();
      }
    }
  }

  private boolean findVirtualFile() {
    return findVirtualFile(false);
  }

  /**
   * in the case when the underlying virtual file is not valid we perform a "refresh"
   */
  private boolean findVirtualFile(boolean withRefresh) {
    if (myVirtualFile == null || !myVirtualFile.isValid()) {
      myVirtualFile = findIdeaFile(myPath, withRefresh);
    }
    return myVirtualFile != null;
  }

  // obviously all string-processing methods might be cached
  @Nullable
  private static VirtualFile findIdeaFile(String path, boolean withRefresh) {
    LocalFileSystem localFileSystem = LocalFileSystem.getInstance();
    if (path.contains("!")) {
      int index = path.indexOf("!");
      String jarPath = path.substring(0, index);
      String entryPath = path.substring(index + 1);

      assert entryPath.indexOf('\\') == -1 : "No backslashes are allowed in DOT_JAR entry path: " + path;
      entryPath = entryPath.replace('\\', '/');
      if (entryPath.startsWith("/")) {
        entryPath = entryPath.substring(1);
      }

      VirtualFile jarFile;
      if (withRefresh) {
        jarFile = localFileSystem.refreshAndFindFileByPath(jarPath);
      } else {
        jarFile = localFileSystem.findFileByPath(jarPath);
      }
      JarFileSystem jarFileSystem = JarFileSystem.getInstance();
      if (jarFile != null) {
        VirtualFile jarRootFile = jarFileSystem.getJarRootForLocalFile(jarFile);
        if (jarRootFile != null) {
          return jarRootFile.findFileByRelativePath(entryPath);
        }
      }
      return null;
    } else {
      if (withRefresh) {
        return localFileSystem.refreshAndFindFileByPath(path);
      } else {
        return localFileSystem.findFileByPath(path);
      }
    }
  }

  @NotNull
  private static String truncateDirPath(String path) {
    int index = Math.max(path.lastIndexOf('/'), path.lastIndexOf('\\'));
    if (index == -1) {
      return path;
    } else {
      return path.substring(0, index);
    }
  }

  private static String truncFileName(String path) {
    int index = Math.max(path.lastIndexOf('/'), path.lastIndexOf('\\'));
    if (index == -1) {
      return path;
    } else {
      return path.substring(index + 1);
    }
  }

  private String getSystemIndependentPath() {
    return getPath().replace('\\', '/');
  }

  @Override
  public boolean equals(Object o) {
    if (this == o) return true;
    if (o == null || getClass() != o.getClass()) return false;

    IdeaFile ideaFile = (IdeaFile) o;

    return EqualUtil.equals(getSystemIndependentPath(), ideaFile.getSystemIndependentPath());
  }

  @Override
  public int hashCode() {
    return getSystemIndependentPath().hashCode();
  }

  @Override
  public String toString() {
    if (myVirtualFile != null) {
      return "IdeaFile{" + myVirtualFile + "}";
    } else {
      return "IdeaFile{path: " + myPath + "}";
    }
  }
}
