package ru.yandex.qe.util.io;

import java.io.File;
import java.io.IOException;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.attribute.PosixFilePermission;
import java.nio.file.attribute.PosixFilePermissions;
import java.util.EnumSet;
import java.util.Set;

import javax.annotation.Nonnull;

import com.google.common.annotations.VisibleForTesting;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public final class FileModes {
  private static final Logger LOG = LoggerFactory.getLogger(FileModes.class);

  public static final boolean IS_POSIX =
          FileSystems.getDefault().supportedFileAttributeViews().contains("posix");

  public static final int PERM_O_X = 0001;
  public static final int PERM_O_W = 0002;
  public static final int PERM_O_R = 0004;
  public static final int PERM_G_X = 0010;
  public static final int PERM_G_W = 0020;
  public static final int PERM_G_R = 0040;
  public static final int PERM_U_X = 0100;
  public static final int PERM_U_W = 0200;
  public static final int PERM_U_R = 0400;

  @VisibleForTesting
  /*package*/ static final int PERM_X = PERM_U_X | PERM_G_X | PERM_O_X;
  private static final int[] ALL_PERM_MASKS = {
      PERM_O_X, PERM_O_W, PERM_O_R,
      PERM_G_X, PERM_G_W, PERM_G_R,
      PERM_U_X, PERM_U_W, PERM_U_R,
  };

  private static final int SAFE_FILE_MODE =
      PERM_U_R | PERM_U_W |
      PERM_G_R |
      PERM_O_R;
  private static final int SAFE_DIR_MODE =
      PERM_U_X | PERM_U_R | PERM_U_W |
      PERM_G_X | PERM_G_R |
      PERM_O_X | PERM_O_R;

  private FileModes() {}

  /**
   * Tries to set file permissions according to a numeric file mode, e.g. {@code 644}.
   *
   * @param file file to set permissions on
   * @param mode numeric file mode made up of one or flags, e.g. <code>{@link #PERM_U_W} | {@link #PERM_U_R}</code>
   *
   * @see #setFileMode(File, int)
   * @see #posixPermissionsFromMode(int)
   */
  public static void trySetFileMode(@Nonnull File file, int mode) {
    Set<PosixFilePermission> permissions = posixPermissionsFromMode(mode);
    try {
      setFileMode(file, mode);
    } catch (IOException e) {
      LOG.warn("Cannot set file permissions " + PosixFilePermissions.toString(permissions) + " on file " + file, e);
    }
  }

  /**
   * Sets file permissions according to a numeric file mode, e.g. {@code 644}.
   *
   * @param file file to set permissions on
   * @param mode numeric file mode made up of one or flags, e.g. <code>{@link #PERM_U_W} | {@link #PERM_U_R}</code>
   *
   * @see #trySetFileMode(File, int)
   * @see #posixPermissionsFromMode(int)
   *
   * @throws IOException could not alter file permissions
   */
  public static void setFileMode(@Nonnull File file, int mode) throws IOException {
    if (IS_POSIX) {
      Set<PosixFilePermission> permissions = posixPermissionsFromMode(mode);
      Files.setPosixFilePermissions(file.toPath(), permissions);
    }
  }

  /**
   * Converts numeric file mode to a set of equivalent {@link PosixFilePermission}s.
   *
   * @param mode numberic file mode, e.g. {@code 400}
   * @return POSIX file permissions corresponding to the mode
   */
  @Nonnull
  public static Set<PosixFilePermission> posixPermissionsFromMode(int mode) {
    final Set<PosixFilePermission> set = EnumSet.noneOf(PosixFilePermission.class);
    if ((mode & PERM_O_X) == PERM_O_X) set.add(PosixFilePermission.OTHERS_EXECUTE);
    if ((mode & PERM_O_W) == PERM_O_W) set.add(PosixFilePermission.OTHERS_WRITE);
    if ((mode & PERM_O_R) == PERM_O_R) set.add(PosixFilePermission.OTHERS_READ);
    if ((mode & PERM_G_X) == PERM_G_X) set.add(PosixFilePermission.GROUP_EXECUTE);
    if ((mode & PERM_G_W) == PERM_G_W) set.add(PosixFilePermission.GROUP_WRITE);
    if ((mode & PERM_G_R) == PERM_G_R) set.add(PosixFilePermission.GROUP_READ);
    if ((mode & PERM_U_X) == PERM_U_X) set.add(PosixFilePermission.OWNER_EXECUTE);
    if ((mode & PERM_U_W) == PERM_U_W) set.add(PosixFilePermission.OWNER_WRITE);
    if ((mode & PERM_U_R) == PERM_U_R) set.add(PosixFilePermission.OWNER_READ);
    return set;
  }

  /**
   * Returns a "safe" file mode, which ensures that file can at least be read and written to
   * (and directory can be listed.)
   * <p>
   * Rationale: .zip archives written in a non-Unix environment and read by Apache commons-compress
   * appear to have files with modes like {@code 0000} or {@code 0100000}.
   * {@code safeMode()} will convert such modes into minimally appropriate values, e.g.
   * {@code 644} for files and {@code 755} for directories.
   *
   * @param mode original file mode
   * @param directory {@code true} if we need a safe file mode for directory, {@code false} otherwise
   *
   * @return safe file mode value
   *
   * @see #SAFE_FILE_MODE
   * @see #SAFE_DIR_MODE
   */
  public static int safeMode(long mode, boolean directory) {
    // e.g. 000 or 0100000
    if (!matchesAny(mode, ALL_PERM_MASKS)) {
      return directory ? SAFE_DIR_MODE : SAFE_FILE_MODE;
    } else {
      return (int) (directory ? mode | PERM_X : mode);
    }
  }

  private static boolean matchesAny(long mode, @Nonnull int[] masks) {
    for (int mask: masks) {
      if ((mode & mask) != 0) {
        return true;
      }
    }
    return false;
  }
}

