package saros.intellij.filesystem;

import com.intellij.openapi.application.ModalityState;
import com.intellij.openapi.util.ThrowableComputable;
import com.intellij.openapi.vfs.VirtualFile;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.file.FileAlreadyExistsException;
import java.util.ArrayList;
import java.util.List;
import org.apache.log4j.Logger;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import saros.filesystem.IFile;
import saros.filesystem.IFolder;
import saros.filesystem.IPath;
import saros.filesystem.IResource;
import saros.intellij.runtime.FilesystemRunner;

/** Intellij implementation of the Saros folder interface. */
public class IntellijFolder extends AbstractIntellijResource implements IFolder {
  private static final Logger log = Logger.getLogger(IntellijFolder.class);

  public IntellijFolder(
      @NotNull IntellijReferencePoint referencePoint, @NotNull IPath relativePath) {

    super(referencePoint, relativePath);
  }

  @Override
  public boolean exists() {
    if (!referencePoint.exists()) {
      return false;
    }

    VirtualFile virtualFile = getVirtualFile();

    return existsInternal(virtualFile);
  }

  /**
   * whether the given virtual file is not <code>null</code>, exists, and is a directory.
   *
   * @param virtualFile the virtual file to check
   * @return whether the given virtual file is not <code>null</code>, exists, and is a directory
   */
  private static boolean existsInternal(@Nullable VirtualFile virtualFile) {
    return virtualFile != null && virtualFile.exists() && virtualFile.isDirectory();
  }

  @Override
  public boolean exists(@NotNull IPath path) {
    return referencePoint.exists(relativePath.append(path));
  }

  @Override
  @NotNull
  public List<IResource> members() throws IOException {
    VirtualFile folder = getVirtualFile();

    if (folder == null || !folder.exists()) {
      throw new FileNotFoundException(
          "Could not obtain child resources for " + this + " as it does not exist or is not valid");
    }

    if (!folder.isDirectory()) {
      throw new IOException(
          "Could not obtain child resources for " + this + " as it is not a directory.");
    }

    List<IResource> result = new ArrayList<>();

    VirtualFile[] children = folder.getChildren();

    for (VirtualFile child : children) {
      IPath childPath = relativePath.append(IntellijPath.fromString(child.getName()));

      result.add(
          child.isDirectory()
              ? new IntellijFolder(referencePoint, childPath)
              : new IntellijFile(referencePoint, childPath));
    }

    return result;
  }

  @Override
  @NotNull
  public IFile getFile(@NotNull String pathString) {
    return getFile(IntellijPath.fromString(pathString));
  }

  @Override
  @NotNull
  public IFile getFile(@NotNull IPath path) {
    if (path.segmentCount() == 0) {
      throw new IllegalArgumentException("Can not create file handle for an empty path");
    }

    IPath referencePointRelativeChildPath = relativePath.append(path);

    return new IntellijFile(referencePoint, referencePointRelativeChildPath);
  }

  @Override
  @NotNull
  public IFolder getFolder(@NotNull String pathString) {
    return getFolder(IntellijPath.fromString(pathString));
  }

  @Override
  @NotNull
  public IFolder getFolder(@NotNull IPath path) {
    if (path.segmentCount() == 0) {
      throw new IllegalArgumentException("Can not create folder handle for an empty path");
    }

    IPath referencePointRelativeChildPath = relativePath.append(path);

    return new IntellijFolder(referencePoint, referencePointRelativeChildPath);
  }

  @Override
  public void delete() throws IOException {
    FilesystemRunner.runWriteAction(
        (ThrowableComputable<Void, IOException>)
            () -> {
              deleteInternal();

              return null;
            },
        ModalityState.defaultModalityState());
  }

  /**
   * Deletes the folder in the filesystem.
   *
   * @throws IOException if the resource is a file or the deletion failed
   * @see VirtualFile#delete(Object)
   */
  private void deleteInternal() throws IOException {
    VirtualFile virtualFile = getVirtualFile();

    if (virtualFile == null || !virtualFile.exists()) {
      log.debug("Ignoring file deletion request for " + this + " as folder does not exist");

      return;
    }

    if (!virtualFile.isDirectory()) {
      throw new IOException("Failed to delete " + this + " as it is not a folder");
    }

    virtualFile.delete(IntellijFolder.this);
  }

  @Override
  public void create() throws IOException {
    FilesystemRunner.runWriteAction(
        (ThrowableComputable<Void, IOException>)
            () -> {
              createInternal();

              return null;
            },
        ModalityState.defaultModalityState());
  }

  /**
   * Creates the folder in the local filesystem.
   *
   * @throws FileAlreadyExistsException if a resource with the same name already exists
   * @throws FileNotFoundException if the parent directory of the folder does not exist
   */
  private void createInternal() throws IOException {
    IResource parent = getParent();

    VirtualFile parentFile = referencePoint.findVirtualFile(parent.getReferencePointRelativePath());

    if (parentFile == null || !parentFile.exists()) {
      throw new FileNotFoundException(
          "Could not create "
              + this
              + " as its parent folder "
              + parent
              + " does not exist or is not valid");
    }

    VirtualFile virtualFile = parentFile.findChild(getName());

    if (virtualFile != null && virtualFile.exists()) {
      throw new FileAlreadyExistsException(
          "Could not create "
              + this
              + " as a resource with the same name already exists: "
              + virtualFile);
    }

    parentFile.createChildDirectory(this, getName());
  }
}