@NotNull
  private static <K, V> ConcurrentMap<K, V> createWeakMap() {
    return new ConcurrentWeakKeySoftValueHashMap<K, V>(
        100,
        0.75f,
        Runtime.getRuntime().availableProcessors(),
        ContainerUtil.<K>canonicalStrategy()) {
      @NotNull
      @Override
      protected ValueReference<K, V> createValueReference(
          @NotNull final V value, @NotNull ReferenceQueue<V> queue) {
        ValueReference<K, V> result;
        if (value == NULL_RESULT || value instanceof Object[] && ((Object[]) value).length == 0) {
          // no use in creating SoftReference to null
          result = createStrongReference(value);
        } else {
          result = super.createValueReference(value, queue);
        }
        return result;
      }

      @Override
      public V get(@NotNull Object key) {
        V v = super.get(key);
        return v == NULL_RESULT ? null : v;
      }
    };
  }
@SuppressWarnings({
  "UtilityClassWithoutPrivateConstructor",
  "MethodOverridesStaticMethodOfSuperclass"
})
public class FileUtil extends FileUtilRt {

  @NonNls public static final String ASYNC_DELETE_EXTENSION = ".__del__";

  public static final int REGEX_PATTERN_FLAGS =
      SystemInfo.isFileSystemCaseSensitive ? 0 : Pattern.CASE_INSENSITIVE;

  public static final TObjectHashingStrategy<String> PATH_HASHING_STRATEGY =
      FilePathHashingStrategy.create();

  public static final TObjectHashingStrategy<File> FILE_HASHING_STRATEGY =
      SystemInfo.isFileSystemCaseSensitive
          ? ContainerUtil.<File>canonicalStrategy()
          : new TObjectHashingStrategy<File>() {
            @Override
            public int computeHashCode(File object) {
              return fileHashCode(object);
            }

            @Override
            public boolean equals(File o1, File o2) {
              return filesEqual(o1, o2);
            }
          };

  private static final Logger LOG = Logger.getInstance("#com.intellij.openapi.util.io.FileUtil");

  @NotNull
  public static String join(@NotNull final String... parts) {
    return StringUtil.join(parts, File.separator);
  }

  @Nullable
  public static String getRelativePath(File base, File file) {
    return FileUtilRt.getRelativePath(base, file);
  }

  @Nullable
  public static String getRelativePath(
      @NotNull String basePath, @NotNull String filePath, final char separator) {
    return FileUtilRt.getRelativePath(basePath, filePath, separator);
  }

  @Nullable
  public static String getRelativePath(
      @NotNull String basePath,
      @NotNull String filePath,
      final char separator,
      final boolean caseSensitive) {
    return FileUtilRt.getRelativePath(basePath, filePath, separator, caseSensitive);
  }

  public static boolean isAbsolute(@NotNull String path) {
    return new File(path).isAbsolute();
  }

  /**
   * Check if the {@code ancestor} is an ancestor of {@code file}.
   *
   * @param ancestor supposed ancestor.
   * @param file supposed descendant.
   * @param strict if {@code false} then this method returns {@code true} if {@code ancestor} equals
   *     to {@code file}.
   * @return {@code true} if {@code ancestor} is parent of {@code file}; {@code false} otherwise.
   */
  public static boolean isAncestor(@NotNull File ancestor, @NotNull File file, boolean strict) {
    return isAncestor(ancestor.getPath(), file.getPath(), strict);
  }

  public static boolean isAncestor(@NotNull String ancestor, @NotNull String file, boolean strict) {
    return !ThreeState.NO.equals(isAncestorThreeState(ancestor, file, strict));
  }

  @NotNull
  public static ThreeState isAncestorThreeState(
      @NotNull String ancestor, @NotNull String file, boolean strict) {
    String ancestorPath = toCanonicalPath(ancestor);
    String filePath = toCanonicalPath(file);

    if (ancestorPath == null || filePath == null) {
      return ThreeState.NO;
    }

    return startsWith(filePath, ancestorPath, strict, SystemInfo.isFileSystemCaseSensitive, true);
  }

  public static boolean startsWith(@NotNull String path, @NotNull String start) {
    return !ThreeState.NO.equals(
        startsWith(path, start, false, SystemInfo.isFileSystemCaseSensitive, false));
  }

  public static boolean startsWith(
      @NotNull String path, @NotNull String start, boolean caseSensitive) {
    return !ThreeState.NO.equals(startsWith(path, start, false, caseSensitive, false));
  }

  /** @return ThreeState.YES if same path or immediate parent */
  @NotNull
  private static ThreeState startsWith(
      @NotNull String path,
      @NotNull String start,
      boolean strict,
      boolean caseSensitive,
      boolean checkImmediateParent) {
    final int length1 = path.length();
    final int length2 = start.length();
    if (length2 == 0) return length1 == 0 ? ThreeState.YES : ThreeState.UNSURE;
    if (length2 > length1) return ThreeState.NO;
    if (!path.regionMatches(!caseSensitive, 0, start, 0, length2)) return ThreeState.NO;
    if (length1 == length2) {
      return strict ? ThreeState.NO : ThreeState.YES;
    }
    char last2 = start.charAt(length2 - 1);
    int slashOrSeparatorIdx = length2;
    if (last2 == '/' || last2 == File.separatorChar) {
      slashOrSeparatorIdx = length2 - 1;
    }
    char next1 = path.charAt(slashOrSeparatorIdx);
    if (next1 == '/' || next1 == File.separatorChar) {
      if (!checkImmediateParent) return ThreeState.YES;

      if (slashOrSeparatorIdx == length1 - 1) return ThreeState.YES;
      int idxNext = path.indexOf(next1, slashOrSeparatorIdx + 1);
      idxNext =
          idxNext == -1
              ? path.indexOf(next1 == '/' ? '\\' : '/', slashOrSeparatorIdx + 1)
              : idxNext;
      return idxNext == -1 ? ThreeState.YES : ThreeState.UNSURE;
    } else {
      return ThreeState.NO;
    }
  }

  /** @param removeProcessor parent, child */
  public static <T> Collection<T> removeAncestors(
      final Collection<T> files,
      final Convertor<T, String> convertor,
      final PairProcessor<T, T> removeProcessor) {
    if (files.isEmpty()) return files;
    final TreeMap<String, T> paths = new TreeMap<String, T>();
    for (T file : files) {
      final String path = convertor.convert(file);
      assert path != null;
      final String canonicalPath = toCanonicalPath(path);
      paths.put(canonicalPath, file);
    }
    final List<Map.Entry<String, T>> ordered =
        new ArrayList<Map.Entry<String, T>>(paths.entrySet());
    final List<T> result = new ArrayList<T>(ordered.size());
    result.add(ordered.get(0).getValue());
    for (int i = 1; i < ordered.size(); i++) {
      final Map.Entry<String, T> entry = ordered.get(i);
      final String child = entry.getKey();
      boolean parentNotFound = true;
      for (int j = i - 1; j >= 0; j--) {
        // possible parents
        final String parent = ordered.get(j).getKey();
        if (parent == null) continue;
        if (startsWith(child, parent)
            && removeProcessor.process(ordered.get(j).getValue(), entry.getValue())) {
          parentNotFound = false;
          break;
        }
      }
      if (parentNotFound) {
        result.add(entry.getValue());
      }
    }
    return result;
  }

  @Nullable
  public static File getParentFile(@NotNull File file) {
    return FileUtilRt.getParentFile(file);
  }

  @NotNull
  public static byte[] loadFileBytes(@NotNull File file) throws IOException {
    byte[] bytes;
    final InputStream stream = new FileInputStream(file);
    try {
      final long len = file.length();
      if (len < 0) {
        throw new IOException("File length reported negative, probably doesn't exist");
      }

      if (isTooLarge(len)) {
        throw new FileTooBigException(
            "Attempt to load '" + file + "' in memory buffer, file length is " + len + " bytes.");
      }

      bytes = loadBytes(stream, (int) len);
    } finally {
      stream.close();
    }
    return bytes;
  }

  public static boolean processFirstBytes(
      @NotNull InputStream stream, int length, @NotNull Processor<ByteSequence> processor)
      throws IOException {
    final byte[] bytes = BUFFER.get();
    assert bytes.length >= length
        : "Cannot process more than " + bytes.length + " in one call, requested:" + length;

    int n = stream.read(bytes, 0, length);
    if (n <= 0) return false;

    return processor.process(new ByteSequence(bytes, 0, n));
  }

  @NotNull
  public static byte[] loadFirst(@NotNull InputStream stream, int maxLength) throws IOException {
    ByteArrayOutputStream buffer = new ByteArrayOutputStream();
    final byte[] bytes = BUFFER.get();
    while (maxLength > 0) {
      int n = stream.read(bytes, 0, Math.min(maxLength, bytes.length));
      if (n <= 0) break;
      buffer.write(bytes, 0, n);
      maxLength -= n;
    }
    buffer.close();
    return buffer.toByteArray();
  }

  @NotNull
  public static String loadTextAndClose(@NotNull InputStream stream) throws IOException {
    //noinspection IOResourceOpenedButNotSafelyClosed
    return loadTextAndClose(new InputStreamReader(stream));
  }

  @NotNull
  public static String loadTextAndClose(@NotNull Reader reader) throws IOException {
    try {
      return StringFactory.createShared(adaptiveLoadText(reader));
    } finally {
      reader.close();
    }
  }

  @NotNull
  public static char[] adaptiveLoadText(@NotNull Reader reader) throws IOException {
    char[] chars = new char[4096];
    List<char[]> buffers = null;
    int count = 0;
    int total = 0;
    while (true) {
      int n = reader.read(chars, count, chars.length - count);
      if (n <= 0) break;
      count += n;
      if (total > 1024 * 1024 * 10) throw new FileTooBigException("File too big " + reader);
      total += n;
      if (count == chars.length) {
        if (buffers == null) {
          buffers = new ArrayList<char[]>();
        }
        buffers.add(chars);
        int newLength = Math.min(1024 * 1024, chars.length * 2);
        chars = new char[newLength];
        count = 0;
      }
    }
    char[] result = new char[total];
    if (buffers != null) {
      for (char[] buffer : buffers) {
        System.arraycopy(buffer, 0, result, result.length - total, buffer.length);
        total -= buffer.length;
      }
    }
    System.arraycopy(chars, 0, result, result.length - total, total);
    return result;
  }

  @NotNull
  public static byte[] adaptiveLoadBytes(@NotNull InputStream stream) throws IOException {
    byte[] bytes = new byte[4096];
    List<byte[]> buffers = null;
    int count = 0;
    int total = 0;
    while (true) {
      int n = stream.read(bytes, count, bytes.length - count);
      if (n <= 0) break;
      count += n;
      if (total > 1024 * 1024 * 10) throw new FileTooBigException("File too big " + stream);
      total += n;
      if (count == bytes.length) {
        if (buffers == null) {
          buffers = new ArrayList<byte[]>();
        }
        buffers.add(bytes);
        int newLength = Math.min(1024 * 1024, bytes.length * 2);
        bytes = new byte[newLength];
        count = 0;
      }
    }
    byte[] result = new byte[total];
    if (buffers != null) {
      for (byte[] buffer : buffers) {
        System.arraycopy(buffer, 0, result, result.length - total, buffer.length);
        total -= buffer.length;
      }
    }
    System.arraycopy(bytes, 0, result, result.length - total, total);
    return result;
  }

  @NotNull
  public static Future<Void> asyncDelete(@NotNull File file) {
    return asyncDelete(Collections.singleton(file));
  }

  @NotNull
  public static Future<Void> asyncDelete(@NotNull Collection<File> files) {
    List<File> tempFiles = new ArrayList<File>();
    for (File file : files) {
      final File tempFile = renameToTempFileOrDelete(file);
      if (tempFile != null) {
        tempFiles.add(tempFile);
      }
    }
    if (!tempFiles.isEmpty()) {
      return startDeletionThread(tempFiles.toArray(new File[tempFiles.size()]));
    }
    return new FixedFuture<Void>(null);
  }

  private static Future<Void> startDeletionThread(@NotNull final File... tempFiles) {
    final RunnableFuture<Void> deleteFilesTask =
        new FutureTask<Void>(
            new Runnable() {
              @Override
              public void run() {
                final Thread currentThread = Thread.currentThread();
                final int priority = currentThread.getPriority();
                currentThread.setPriority(Thread.MIN_PRIORITY);
                try {
                  for (File tempFile : tempFiles) {
                    delete(tempFile);
                  }
                } finally {
                  currentThread.setPriority(priority);
                }
              }
            },
            null);

    try {
      // attempt to execute on pooled thread
      final Class<?> aClass = Class.forName("com.intellij.openapi.application.ApplicationManager");
      final Method getApplicationMethod = aClass.getMethod("getApplication");
      final Object application = getApplicationMethod.invoke(null);
      final Method executeOnPooledThreadMethod =
          application.getClass().getMethod("executeOnPooledThread", Runnable.class);
      executeOnPooledThreadMethod.invoke(application, deleteFilesTask);
    } catch (Exception ignored) {
      new Thread(deleteFilesTask, "File deletion thread").start();
    }
    return deleteFilesTask;
  }

  @Nullable
  private static File renameToTempFileOrDelete(@NotNull File file) {
    final File tempDir = new File(getTempDirectory());
    boolean isSameDrive = true;
    if (SystemInfo.isWindows) {
      String tempDirDrive = tempDir.getAbsolutePath().substring(0, 2);
      String fileDrive = file.getAbsolutePath().substring(0, 2);
      isSameDrive = tempDirDrive.equalsIgnoreCase(fileDrive);
    }

    if (isSameDrive) {
      // the optimization is reasonable only if destination dir is located on the same drive
      final String originalFileName = file.getName();
      File tempFile = getTempFile(originalFileName, tempDir);
      if (file.renameTo(tempFile)) {
        return tempFile;
      }
    }

    delete(file);

    return null;
  }

  private static File getTempFile(@NotNull String originalFileName, @NotNull File parent) {
    int randomSuffix = (int) (System.currentTimeMillis() % 1000);
    for (int i = randomSuffix; ; i++) {
      @NonNls String name = "___" + originalFileName + i + ASYNC_DELETE_EXTENSION;
      File tempFile = new File(parent, name);
      if (!tempFile.exists()) return tempFile;
    }
  }

  public static boolean delete(@NotNull File file) {
    FileAttributes attributes = FileSystemUtil.getAttributes(file);
    if (attributes == null) return true;

    if (attributes.isDirectory() && !attributes.isSymLink()) {
      File[] files = file.listFiles();
      if (files != null) {
        for (File child : files) {
          if (!delete(child)) return false;
        }
      }
    }

    return deleteFile(file);
  }

  public static boolean createParentDirs(@NotNull File file) {
    return FileUtilRt.createParentDirs(file);
  }

  public static boolean createDirectory(@NotNull File path) {
    return FileUtilRt.createDirectory(path);
  }

  public static boolean createIfDoesntExist(@NotNull File file) {
    return FileUtilRt.createIfNotExists(file);
  }

  public static boolean ensureCanCreateFile(@NotNull File file) {
    return FileUtilRt.ensureCanCreateFile(file);
  }

  public static void copy(@NotNull File fromFile, @NotNull File toFile) throws IOException {
    performCopy(fromFile, toFile, true);
  }

  public static void copyContent(@NotNull File fromFile, @NotNull File toFile) throws IOException {
    performCopy(fromFile, toFile, false);
  }

  private static void performCopy(
      @NotNull File fromFile, @NotNull File toFile, final boolean syncTimestamp)
      throws IOException {
    final FileOutputStream fos;
    try {
      fos = openOutputStream(toFile);
    } catch (IOException e) {
      if (SystemInfo.isWindows
          && e.getMessage() != null
          && e.getMessage().contains("denied")
          && WinUACTemporaryFix.nativeCopy(fromFile, toFile, syncTimestamp)) {
        return;
      }
      throw e;
    }

    try {
      final FileInputStream fis = new FileInputStream(fromFile);
      try {
        copy(fis, fos);
      } finally {
        fis.close();
      }
    } finally {
      fos.close();
    }

    if (syncTimestamp) {
      final long timeStamp = fromFile.lastModified();
      if (timeStamp < 0) {
        LOG.warn("Invalid timestamp " + timeStamp + " of '" + fromFile + "'");
      } else if (!toFile.setLastModified(timeStamp)) {
        LOG.warn("Unable to set timestamp " + timeStamp + " to '" + toFile + "'");
      }
    }

    if (SystemInfo.isUnix && fromFile.canExecute()) {
      FileSystemUtil.clonePermissions(fromFile.getPath(), toFile.getPath());
    }
  }

  private static FileOutputStream openOutputStream(@NotNull final File file) throws IOException {
    try {
      return new FileOutputStream(file);
    } catch (FileNotFoundException e) {
      final File parentFile = file.getParentFile();
      if (parentFile == null) {
        throw new IOException("Parent file is null for " + file.getPath(), e);
      }
      createParentDirs(file);
      return new FileOutputStream(file);
    }
  }

  public static void copy(@NotNull InputStream inputStream, @NotNull OutputStream outputStream)
      throws IOException {
    FileUtilRt.copy(inputStream, outputStream);
  }

  public static void copy(
      @NotNull InputStream inputStream, int maxSize, @NotNull OutputStream outputStream)
      throws IOException {
    final byte[] buffer = BUFFER.get();
    int toRead = maxSize;
    while (toRead > 0) {
      int read = inputStream.read(buffer, 0, Math.min(buffer.length, toRead));
      if (read < 0) break;
      toRead -= read;
      outputStream.write(buffer, 0, read);
    }
  }

  public static void copyDir(@NotNull File fromDir, @NotNull File toDir) throws IOException {
    copyDir(fromDir, toDir, true);
  }

  /**
   * Copies content of {@code fromDir} to {@code toDir}. It's equivalent to "cp -r fromDir/* toDir"
   * unix command.
   *
   * @param fromDir source directory
   * @param toDir destination directory
   * @throws IOException in case of any IO troubles
   */
  public static void copyDirContent(@NotNull File fromDir, @NotNull File toDir) throws IOException {
    File[] children = ObjectUtils.notNull(fromDir.listFiles(), ArrayUtil.EMPTY_FILE_ARRAY);
    for (File child : children) {
      File target = new File(toDir, child.getName());
      if (child.isFile()) {
        copy(child, target);
      } else {
        copyDir(child, target, true);
      }
    }
  }

  public static void copyDir(@NotNull File fromDir, @NotNull File toDir, boolean copySystemFiles)
      throws IOException {
    copyDir(
        fromDir,
        toDir,
        copySystemFiles
            ? null
            : new FileFilter() {
              @Override
              public boolean accept(File file) {
                return !StringUtil.startsWithChar(file.getName(), '.');
              }
            });
  }

  public static void copyDir(
      @NotNull File fromDir, @NotNull File toDir, @Nullable final FileFilter filter)
      throws IOException {
    ensureExists(toDir);
    if (isAncestor(fromDir, toDir, true)) {
      LOG.error(fromDir.getAbsolutePath() + " is ancestor of " + toDir + ". Can't copy to itself.");
      return;
    }
    File[] files = fromDir.listFiles();
    if (files == null)
      throw new IOException(
          CommonBundle.message("exception.directory.is.invalid", fromDir.getPath()));
    if (!fromDir.canRead())
      throw new IOException(
          CommonBundle.message("exception.directory.is.not.readable", fromDir.getPath()));
    for (File file : files) {
      if (filter != null && !filter.accept(file)) {
        continue;
      }
      if (file.isDirectory()) {
        copyDir(file, new File(toDir, file.getName()), filter);
      } else {
        copy(file, new File(toDir, file.getName()));
      }
    }
  }

  public static void ensureExists(@NotNull File dir) throws IOException {
    if (!dir.exists() && !dir.mkdirs()) {
      throw new IOException(
          CommonBundle.message("exception.directory.can.not.create", dir.getPath()));
    }
  }

  @NotNull
  public static String getNameWithoutExtension(@NotNull File file) {
    return getNameWithoutExtension(file.getName());
  }

  @NotNull
  public static String getNameWithoutExtension(@NotNull String name) {
    return FileUtilRt.getNameWithoutExtension(name);
  }

  public static String createSequentFileName(
      @NotNull File aParentFolder,
      @NotNull @NonNls String aFilePrefix,
      @NotNull String aExtension) {
    return findSequentNonexistentFile(aParentFolder, aFilePrefix, aExtension).getName();
  }

  public static File findSequentNonexistentFile(
      @NotNull File parentFolder, @NotNull String filePrefix, @NotNull String extension) {
    int postfix = 0;
    String ext = extension.isEmpty() ? "" : '.' + extension;
    File candidate = new File(parentFolder, filePrefix + ext);
    while (candidate.exists()) {
      postfix++;
      candidate = new File(parentFolder, filePrefix + Integer.toString(postfix) + ext);
    }
    return candidate;
  }

  @NotNull
  public static String toSystemDependentName(@NonNls @NotNull String aFileName) {
    return FileUtilRt.toSystemDependentName(aFileName);
  }

  @NotNull
  public static String toSystemIndependentName(@NonNls @NotNull String aFileName) {
    return FileUtilRt.toSystemIndependentName(aFileName);
  }

  @NotNull
  public static String nameToCompare(@NonNls @NotNull String name) {
    return (SystemInfo.isFileSystemCaseSensitive ? name : name.toLowerCase()).replace('\\', '/');
  }

  /**
   * Converts given path to canonical representation by eliminating '.'s, traversing '..'s, and
   * omitting duplicate separators. Please note that this method is symlink-unfriendly (i.e. result
   * of "/path/to/link/../next" most probably will differ from what {@link
   * java.io.File#getCanonicalPath()} will return) - so use with care.
   */
  @Contract("null -> null")
  public static String toCanonicalPath(@Nullable String path) {
    return toCanonicalPath(path, File.separatorChar, true);
  }

  @Contract("null, _ -> null")
  public static String toCanonicalPath(@Nullable String path, char separatorChar) {
    return toCanonicalPath(path, separatorChar, true);
  }

  @Contract("null -> null")
  public static String toCanonicalUriPath(@Nullable String path) {
    return toCanonicalPath(path, '/', false);
  }

  @Contract("null, _, _ -> null")
  private static String toCanonicalPath(
      @Nullable String path, char separatorChar, boolean removeLastSlash) {
    if (path == null || path.isEmpty()) {
      return path;
    } else if (".".equals(path)) {
      return "";
    }

    path = path.replace(separatorChar, '/');
    if (path.indexOf('/') == -1) {
      return path;
    }

    StringBuilder result = new StringBuilder(path.length());
    int start = processRoot(path, result), dots = 0;
    boolean separator = true;

    for (int i = start; i < path.length(); ++i) {
      char c = path.charAt(i);
      if (c == '/') {
        if (!separator) {
          processDots(result, dots, start);
          dots = 0;
        }
        separator = true;
      } else if (c == '.') {
        if (separator || dots > 0) {
          ++dots;
        } else {
          result.append('.');
        }
        separator = false;
      } else {
        if (dots > 0) {
          StringUtil.repeatSymbol(result, '.', dots);
          dots = 0;
        }
        result.append(c);
        separator = false;
      }
    }

    if (dots > 0) {
      processDots(result, dots, start);
    }

    int lastChar = result.length() - 1;
    if (removeLastSlash && lastChar >= 0 && result.charAt(lastChar) == '/' && lastChar > start) {
      result.deleteCharAt(lastChar);
    }

    return result.toString();
  }

  private static int processRoot(String path, StringBuilder result) {
    if (SystemInfo.isWindows
        && path.length() > 1
        && path.charAt(0) == '/'
        && path.charAt(1) == '/') {
      result.append("//");

      int hostStart = 2;
      while (hostStart < path.length() && path.charAt(hostStart) == '/') hostStart++;
      if (hostStart == path.length()) return hostStart;
      int hostEnd = path.indexOf('/', hostStart);
      if (hostEnd < 0) hostEnd = path.length();
      result.append(path, hostStart, hostEnd);
      result.append('/');

      int shareStart = hostEnd;
      while (shareStart < path.length() && path.charAt(shareStart) == '/') shareStart++;
      if (shareStart == path.length()) return shareStart;
      int shareEnd = path.indexOf('/', shareStart);
      if (shareEnd < 0) shareEnd = path.length();
      result.append(path, shareStart, shareEnd);
      result.append('/');

      return shareEnd;
    } else if (path.length() > 0 && path.charAt(0) == '/') {
      result.append('/');
      return 1;
    } else if (path.length() > 2 && path.charAt(1) == ':' && path.charAt(2) == '/') {
      result.append(path, 0, 3);
      return 3;
    } else {
      return 0;
    }
  }

  private static void processDots(StringBuilder result, int dots, int start) {
    if (dots == 2) {
      int pos = -1;
      if (!StringUtil.endsWith(result, "/../") && !StringUtil.equals(result, "../")) {
        pos = StringUtil.lastIndexOf(result, '/', start, result.length() - 1);
        if (pos >= 0) {
          ++pos; // separator found, trim to next char
        } else if (start > 0) {
          pos = start; // path is absolute, trim to root ('/..' -> '/')
        } else if (result.length() > 0) {
          pos = 0; // path is relative, trim to default ('a/..' -> '')
        }
      }
      if (pos >= 0) {
        result.delete(pos, result.length());
      } else {
        result.append("../"); // impossible to traverse, keep as-is
      }
    } else if (dots != 1) {
      StringUtil.repeatSymbol(result, '.', dots);
      result.append('/');
    }
  }

  /**
   * converts back slashes to forward slashes removes double slashes inside the path, e.g. "x/y//z"
   * => "x/y/z"
   */
  @NotNull
  public static String normalize(@NotNull String path) {
    final StringBuilder result = new StringBuilder(path.length());

    int start = 0;
    boolean separator = false;
    if (SystemInfo.isWindows && (path.startsWith("//") || path.startsWith("\\\\"))) {
      start = 2;
      result.append("//");
      separator = true;
    }

    for (int i = start; i < path.length(); ++i) {
      final char c = path.charAt(i);
      if (c == '/' || c == '\\') {
        if (!separator) result.append('/');
        separator = true;
      } else {
        result.append(c);
        separator = false;
      }
    }

    return result.toString();
  }

  @NotNull
  public static String unquote(@NotNull String urlString) {
    urlString = urlString.replace('/', File.separatorChar);
    return URLUtil.unescapePercentSequences(urlString);
  }

  public static boolean isFilePathAcceptable(@NotNull File root, @Nullable FileFilter fileFilter) {
    File file = root;
    do {
      if (fileFilter != null && !fileFilter.accept(file)) return false;
      file = file.getParentFile();
    } while (file != null);
    return true;
  }

  public static void rename(@NotNull File source, @NotNull File target) throws IOException {
    if (source.renameTo(target)) return;
    if (!source.exists()) return;

    copy(source, target);
    delete(source);
  }

  public static boolean filesEqual(@Nullable File file1, @Nullable File file2) {
    // on MacOS java.io.File.equals() is incorrectly case-sensitive
    return pathsEqual(
        file1 == null ? null : file1.getPath(), file2 == null ? null : file2.getPath());
  }

  public static boolean pathsEqual(@Nullable String path1, @Nullable String path2) {
    if (path1 == path2) return true;
    if (path1 == null || path2 == null) return false;

    path1 = toCanonicalPath(path1);
    path2 = toCanonicalPath(path2);
    return PATH_HASHING_STRATEGY.equals(path1, path2);
  }

  /** optimized version of pathsEqual - it only compares pure names, without file separators */
  public static boolean namesEqual(@Nullable String name1, @Nullable String name2) {
    if (name1 == name2) return true;
    if (name1 == null || name2 == null) return false;

    return PATH_HASHING_STRATEGY.equals(name1, name2);
  }

  public static int compareFiles(@Nullable File file1, @Nullable File file2) {
    return comparePaths(
        file1 == null ? null : file1.getPath(), file2 == null ? null : file2.getPath());
  }

  public static int comparePaths(@Nullable String path1, @Nullable String path2) {
    path1 = path1 == null ? null : toSystemIndependentName(path1);
    path2 = path2 == null ? null : toSystemIndependentName(path2);
    return StringUtil.compare(path1, path2, !SystemInfo.isFileSystemCaseSensitive);
  }

  public static int fileHashCode(@Nullable File file) {
    return pathHashCode(file == null ? null : file.getPath());
  }

  public static int pathHashCode(@Nullable String path) {
    return StringUtil.isEmpty(path)
        ? 0
        : PATH_HASHING_STRATEGY.computeHashCode(toCanonicalPath(path));
  }

  /**
   * @deprecated this method returns extension converted to lower case, this may not be correct for
   *     case-sensitive FS. Use {@link FileUtilRt#getExtension(String)} instead to get the unchanged
   *     extension. If you need to check whether a file has a specified extension use {@link
   *     FileUtilRt#extensionEquals(String, String)}
   */
  @NotNull
  public static String getExtension(@NotNull String fileName) {
    return FileUtilRt.getExtension(fileName).toLowerCase();
  }

  @NotNull
  public static String resolveShortWindowsName(@NotNull String path) throws IOException {
    return SystemInfo.isWindows && StringUtil.containsChar(path, '~')
        ? new File(path).getCanonicalPath()
        : path;
  }

  public static void collectMatchedFiles(
      @NotNull File root, @NotNull Pattern pattern, @NotNull List<File> outFiles) {
    collectMatchedFiles(root, root, pattern, outFiles);
  }

  private static void collectMatchedFiles(
      @NotNull File absoluteRoot,
      @NotNull File root,
      @NotNull Pattern pattern,
      @NotNull List<File> files) {
    final File[] dirs = root.listFiles();
    if (dirs == null) return;
    for (File dir : dirs) {
      if (dir.isFile()) {
        final String relativePath = getRelativePath(absoluteRoot, dir);
        if (relativePath != null) {
          final String path = toSystemIndependentName(relativePath);
          if (pattern.matcher(path).matches()) {
            files.add(dir);
          }
        }
      } else {
        collectMatchedFiles(absoluteRoot, dir, pattern, files);
      }
    }
  }

  @RegExp
  @NotNull
  public static String convertAntToRegexp(@NotNull String antPattern) {
    return convertAntToRegexp(antPattern, true);
  }

  /**
   * @param antPattern ant-style path pattern
   * @return java regexp pattern. Note that no matter whether forward or backward slashes were used
   *     in the antPattern the returned regexp pattern will use forward slashes ('/') as file
   *     separators. Paths containing windows-style backslashes must be converted before matching
   *     against the resulting regexp
   * @see com.intellij.openapi.util.io.FileUtil#toSystemIndependentName
   */
  @RegExp
  @NotNull
  public static String convertAntToRegexp(@NotNull String antPattern, boolean ignoreStartingSlash) {
    final StringBuilder builder = new StringBuilder();
    int asteriskCount = 0;
    boolean recursive = true;
    final int start =
        ignoreStartingSlash
                && (StringUtil.startsWithChar(antPattern, '/')
                    || StringUtil.startsWithChar(antPattern, '\\'))
            ? 1
            : 0;
    for (int idx = start; idx < antPattern.length(); idx++) {
      final char ch = antPattern.charAt(idx);

      if (ch == '*') {
        asteriskCount++;
        continue;
      }

      final boolean foundRecursivePattern =
          recursive && asteriskCount == 2 && (ch == '/' || ch == '\\');
      final boolean asterisksFound = asteriskCount > 0;

      asteriskCount = 0;
      recursive = ch == '/' || ch == '\\';

      if (foundRecursivePattern) {
        builder.append("(?:[^/]+/)*?");
        continue;
      }

      if (asterisksFound) {
        builder.append("[^/]*?");
      }

      if (ch == '(' || ch == ')' || ch == '[' || ch == ']' || ch == '^' || ch == '$' || ch == '.'
          || ch == '{' || ch == '}' || ch == '+' || ch == '|') {
        // quote regexp-specific symbols
        builder.append('\\').append(ch);
        continue;
      }
      if (ch == '?') {
        builder.append("[^/]{1}");
        continue;
      }
      if (ch == '\\') {
        builder.append('/');
        continue;
      }
      builder.append(ch);
    }

    // handle ant shorthand: mypackage/test/ is interpreted as if it were mypackage/test/**
    final boolean isTrailingSlash =
        builder.length() > 0 && builder.charAt(builder.length() - 1) == '/';
    if (asteriskCount == 0 && isTrailingSlash || recursive && asteriskCount == 2) {
      if (isTrailingSlash) {
        builder.setLength(builder.length() - 1);
      }
      if (builder.length() == 0) {
        builder.append(".*");
      } else {
        builder.append("(?:$|/.+)");
      }
    } else if (asteriskCount > 0) {
      builder.append("[^/]*?");
    }
    return builder.toString();
  }

  public static boolean moveDirWithContent(@NotNull File fromDir, @NotNull File toDir) {
    if (!toDir.exists()) return fromDir.renameTo(toDir);

    File[] files = fromDir.listFiles();
    if (files == null) return false;

    boolean success = true;

    for (File fromFile : files) {
      File toFile = new File(toDir, fromFile.getName());
      success = success && fromFile.renameTo(toFile);
    }
    //noinspection ResultOfMethodCallIgnored
    fromDir.delete();

    return success;
  }

  /**
   * Has duplicate: {@link
   * com.intellij.coverage.listeners.CoverageListener#sanitize(java.lang.String, java.lang.String)}
   * as FileUtil is not available in client's vm
   */
  @NotNull
  public static String sanitizeFileName(@NotNull String name) {
    StringBuilder result = new StringBuilder();

    for (int i = 0; i < name.length(); i++) {
      final char ch = name.charAt(i);

      if (ch > 0 && ch < 255) {
        if (Character.isLetterOrDigit(ch)) {
          result.append(ch);
        } else {
          result.append("_");
        }
      }
    }

    return result.toString();
  }

  public static boolean canExecute(@NotNull File file) {
    return file.canExecute();
  }

  public static void setReadOnlyAttribute(@NotNull String path, boolean readOnlyFlag) {
    final boolean writableFlag = !readOnlyFlag;
    final File file = new File(path);
    if (!file.setWritable(writableFlag) && file.canWrite() != writableFlag) {
      LOG.warn("Can't set writable attribute of '" + path + "' to " + readOnlyFlag);
    }
  }

  public static void appendToFile(@NotNull File file, @NotNull String text) throws IOException {
    writeToFile(file, text.getBytes("UTF-8"), true);
  }

  public static void writeToFile(@NotNull File file, @NotNull byte[] text) throws IOException {
    writeToFile(file, text, false);
  }

  public static void writeToFile(@NotNull File file, @NotNull String text) throws IOException {
    writeToFile(file, text.getBytes("UTF-8"), false);
  }

  public static void writeToFile(@NotNull File file, @NotNull byte[] text, int off, int len)
      throws IOException {
    writeToFile(file, text, off, len, false);
  }

  public static void writeToFile(@NotNull File file, @NotNull byte[] text, boolean append)
      throws IOException {
    writeToFile(file, text, 0, text.length, append);
  }

  private static void writeToFile(
      @NotNull File file, @NotNull byte[] text, int off, int len, boolean append)
      throws IOException {
    createParentDirs(file);

    OutputStream stream = new FileOutputStream(file, append);
    try {
      stream.write(text, off, len);
    } finally {
      stream.close();
    }
  }

  public static boolean processFilesRecursively(
      @NotNull File root, @NotNull Processor<File> processor) {
    return processFilesRecursively(root, processor, null);
  }

  public static boolean processFilesRecursively(
      @NotNull File root,
      @NotNull Processor<File> processor,
      @Nullable final Processor<File> directoryFilter) {
    final LinkedList<File> queue = new LinkedList<File>();
    queue.add(root);
    while (!queue.isEmpty()) {
      final File file = queue.removeFirst();
      if (!processor.process(file)) return false;
      if (directoryFilter != null && (!file.isDirectory() || !directoryFilter.process(file)))
        continue;

      final File[] children = file.listFiles();
      if (children != null) {
        ContainerUtil.addAll(queue, children);
      }
    }
    return true;
  }

  @Nullable
  public static File findFirstThatExist(@NotNull String... paths) {
    for (String path : paths) {
      if (!StringUtil.isEmptyOrSpaces(path)) {
        File file = new File(toSystemDependentName(path));
        if (file.exists()) return file;
      }
    }

    return null;
  }

  @NotNull
  public static List<File> findFilesByMask(@NotNull Pattern pattern, @NotNull File dir) {
    final ArrayList<File> found = new ArrayList<File>();
    final File[] files = dir.listFiles();
    if (files != null) {
      for (File file : files) {
        if (file.isDirectory()) {
          found.addAll(findFilesByMask(pattern, file));
        } else if (pattern.matcher(file.getName()).matches()) {
          found.add(file);
        }
      }
    }
    return found;
  }

  @NotNull
  public static List<File> findFilesOrDirsByMask(@NotNull Pattern pattern, @NotNull File dir) {
    final ArrayList<File> found = new ArrayList<File>();
    final File[] files = dir.listFiles();
    if (files != null) {
      for (File file : files) {
        if (pattern.matcher(file.getName()).matches()) {
          found.add(file);
        }
        if (file.isDirectory()) {
          found.addAll(findFilesOrDirsByMask(pattern, file));
        }
      }
    }
    return found;
  }

  /**
   * Returns empty string for empty path. First checks whether provided path is a path of a file
   * with sought-for name. Unless found, checks if provided file was a directory. In this case
   * checks existence of child files with given names in order "as provided". Finally checks
   * filename among brother-files of provided. Returns null if nothing found.
   *
   * @return path of the first of found files or empty string or null.
   */
  @Nullable
  public static String findFileInProvidedPath(String providedPath, String... fileNames) {
    if (StringUtil.isEmpty(providedPath)) {
      return "";
    }

    File providedFile = new File(providedPath);
    if (providedFile.exists()) {
      String name = providedFile.getName();
      for (String fileName : fileNames) {
        if (name.equals(fileName)) {
          return toSystemDependentName(providedFile.getPath());
        }
      }
    }

    if (providedFile.isDirectory()) { // user chose folder with file
      for (String fileName : fileNames) {
        File file = new File(providedFile, fileName);
        if (fileName.equals(file.getName()) && file.exists()) {
          return toSystemDependentName(file.getPath());
        }
      }
    }

    providedFile = providedFile.getParentFile(); // users chose wrong file in same directory
    if (providedFile != null && providedFile.exists()) {
      for (String fileName : fileNames) {
        File file = new File(providedFile, fileName);
        if (fileName.equals(file.getName()) && file.exists()) {
          return toSystemDependentName(file.getPath());
        }
      }
    }

    return null;
  }

  public static boolean isAbsolutePlatformIndependent(@NotNull String path) {
    return isUnixAbsolutePath(path) || isWindowsAbsolutePath(path);
  }

  public static boolean isUnixAbsolutePath(@NotNull String path) {
    return path.startsWith("/");
  }

  public static boolean isWindowsAbsolutePath(@NotNull String pathString) {
    if (pathString.length() >= 2
        && Character.isLetter(pathString.charAt(0))
        && pathString.charAt(1) == ':') {
      return true;
    }
    return false;
  }

  @Contract("null -> null")
  public static String getLocationRelativeToUserHome(@Nullable final String path) {
    if (path == null) return null;

    if (SystemInfo.isUnix) {
      final File projectDir = new File(path);
      final File userHomeDir = new File(SystemProperties.getUserHome());
      if (isAncestor(userHomeDir, projectDir, true)) {
        return "~/" + getRelativePath(userHomeDir, projectDir);
      }
    }

    return path;
  }

  @NotNull
  public static String expandUserHome(@NotNull String path) {
    if (path.startsWith("~/") || path.startsWith("~\\")) {
      path = SystemProperties.getUserHome() + path.substring(1);
    }
    return path;
  }

  @NotNull
  public static File[] notNullize(@Nullable File[] files) {
    return notNullize(files, ArrayUtil.EMPTY_FILE_ARRAY);
  }

  @NotNull
  public static File[] notNullize(@Nullable File[] files, @NotNull File[] defaultFiles) {
    return files == null ? defaultFiles : files;
  }

  public static boolean isHashBangLine(CharSequence firstCharsIfText, String marker) {
    if (firstCharsIfText == null) {
      return false;
    }
    final int lineBreak = StringUtil.indexOf(firstCharsIfText, '\n');
    if (lineBreak < 0) {
      return false;
    }
    String firstLine = firstCharsIfText.subSequence(0, lineBreak).toString();
    if (!firstLine.startsWith("#!")) {
      return false;
    }
    return firstLine.contains(marker);
  }

  @NotNull
  public static File createTempDirectory(
      @NotNull @NonNls String prefix, @Nullable @NonNls String suffix) throws IOException {
    return FileUtilRt.createTempDirectory(prefix, suffix);
  }

  @NotNull
  public static File createTempDirectory(
      @NotNull @NonNls String prefix, @Nullable @NonNls String suffix, boolean deleteOnExit)
      throws IOException {
    return FileUtilRt.createTempDirectory(prefix, suffix, deleteOnExit);
  }

  @NotNull
  public static File createTempDirectory(
      @NotNull File dir, @NotNull @NonNls String prefix, @Nullable @NonNls String suffix)
      throws IOException {
    return FileUtilRt.createTempDirectory(dir, prefix, suffix);
  }

  @NotNull
  public static File createTempDirectory(
      @NotNull File dir,
      @NotNull @NonNls String prefix,
      @Nullable @NonNls String suffix,
      boolean deleteOnExit)
      throws IOException {
    return FileUtilRt.createTempDirectory(dir, prefix, suffix, deleteOnExit);
  }

  @NotNull
  public static File createTempFile(@NotNull @NonNls String prefix, @Nullable @NonNls String suffix)
      throws IOException {
    return FileUtilRt.createTempFile(prefix, suffix);
  }

  @NotNull
  public static File createTempFile(
      @NotNull @NonNls String prefix, @Nullable @NonNls String suffix, boolean deleteOnExit)
      throws IOException {
    return FileUtilRt.createTempFile(prefix, suffix, deleteOnExit);
  }

  @NotNull
  public static File createTempFile(
      @NonNls File dir, @NotNull @NonNls String prefix, @Nullable @NonNls String suffix)
      throws IOException {
    return FileUtilRt.createTempFile(dir, prefix, suffix);
  }

  @NotNull
  public static File createTempFile(
      @NonNls File dir,
      @NotNull @NonNls String prefix,
      @Nullable @NonNls String suffix,
      boolean create)
      throws IOException {
    return FileUtilRt.createTempFile(dir, prefix, suffix, create);
  }

  @NotNull
  public static File createTempFile(
      @NonNls File dir,
      @NotNull @NonNls String prefix,
      @Nullable @NonNls String suffix,
      boolean create,
      boolean deleteOnExit)
      throws IOException {
    return FileUtilRt.createTempFile(dir, prefix, suffix, create, deleteOnExit);
  }

  @NotNull
  public static String getTempDirectory() {
    return FileUtilRt.getTempDirectory();
  }

  @TestOnly
  public static void resetCanonicalTempPathCache(final String tempPath) {
    FileUtilRt.resetCanonicalTempPathCache(tempPath);
  }

  @NotNull
  public static File generateRandomTemporaryPath() throws IOException {
    return FileUtilRt.generateRandomTemporaryPath();
  }

  public static void setExecutableAttribute(@NotNull String path, boolean executableFlag)
      throws IOException {
    FileUtilRt.setExecutableAttribute(path, executableFlag);
  }

  public static void setLastModified(@NotNull File file, long timeStamp) throws IOException {
    if (!file.setLastModified(timeStamp)) {
      LOG.warn(file.getPath());
    }
  }

  @NotNull
  public static String loadFile(@NotNull File file) throws IOException {
    return FileUtilRt.loadFile(file);
  }

  @NotNull
  public static String loadFile(@NotNull File file, boolean convertLineSeparators)
      throws IOException {
    return FileUtilRt.loadFile(file, convertLineSeparators);
  }

  @NotNull
  public static String loadFile(@NotNull File file, @Nullable @NonNls String encoding)
      throws IOException {
    return FileUtilRt.loadFile(file, encoding);
  }

  @NotNull
  public static String loadFile(
      @NotNull File file, @Nullable @NonNls String encoding, boolean convertLineSeparators)
      throws IOException {
    return FileUtilRt.loadFile(file, encoding, convertLineSeparators);
  }

  @NotNull
  public static char[] loadFileText(@NotNull File file) throws IOException {
    return FileUtilRt.loadFileText(file);
  }

  @NotNull
  public static char[] loadFileText(@NotNull File file, @Nullable @NonNls String encoding)
      throws IOException {
    return FileUtilRt.loadFileText(file, encoding);
  }

  @NotNull
  public static char[] loadText(@NotNull Reader reader, int length) throws IOException {
    return FileUtilRt.loadText(reader, length);
  }

  @NotNull
  public static List<String> loadLines(@NotNull File file) throws IOException {
    return FileUtilRt.loadLines(file);
  }

  @NotNull
  public static List<String> loadLines(@NotNull File file, @Nullable @NonNls String encoding)
      throws IOException {
    return FileUtilRt.loadLines(file, encoding);
  }

  @NotNull
  public static List<String> loadLines(@NotNull String path) throws IOException {
    return FileUtilRt.loadLines(path);
  }

  @NotNull
  public static List<String> loadLines(@NotNull String path, @Nullable @NonNls String encoding)
      throws IOException {
    return FileUtilRt.loadLines(path, encoding);
  }

  @NotNull
  public static List<String> loadLines(@NotNull BufferedReader reader) throws IOException {
    return FileUtilRt.loadLines(reader);
  }

  /** @deprecated unclear closing policy, do not use (to remove in IDEA 14) */
  @SuppressWarnings({"UnusedDeclaration", "deprecation"})
  public static List<String> loadLines(@NotNull InputStream stream) throws IOException {
    return loadLines(new InputStreamReader(stream));
  }

  /** @deprecated unclear closing policy, do not use (to remove in IDEA 14) */
  @SuppressWarnings("UnusedDeclaration")
  public static List<String> loadLines(@NotNull Reader reader) throws IOException {
    BufferedReader bufferedReader = new BufferedReader(reader);
    try {
      return loadLines(bufferedReader);
    } finally {
      bufferedReader.close();
    }
  }

  @NotNull
  public static byte[] loadBytes(@NotNull InputStream stream) throws IOException {
    return FileUtilRt.loadBytes(stream);
  }

  @NotNull
  public static byte[] loadBytes(@NotNull InputStream stream, int length) throws IOException {
    return FileUtilRt.loadBytes(stream, length);
  }

  @NotNull
  public static List<String> splitPath(@NotNull String path) {
    ArrayList<String> list = new ArrayList<String>();
    int index = 0;
    int nextSeparator;
    while ((nextSeparator = path.indexOf(File.separatorChar, index)) != -1) {
      list.add(path.substring(index, nextSeparator));
      index = nextSeparator + 1;
    }
    list.add(path.substring(index, path.length()));
    return list;
  }

  @SuppressWarnings({"HardCodedStringLiteral"})
  public static boolean isJarOrZip(File file) {
    if (file.isDirectory()) {
      return false;
    }
    final String name = file.getName();
    return StringUtil.endsWithIgnoreCase(name, ".jar")
        || StringUtil.endsWithIgnoreCase(name, ".zip");
  }

  public static boolean visitFiles(@NotNull File root, @NotNull Processor<File> processor) {
    if (!processor.process(root)) {
      return false;
    }

    File[] children = root.listFiles();
    if (children != null) {
      for (File child : children) {
        if (!visitFiles(child, processor)) {
          return false;
        }
      }
    }

    return true;
  }

  /** Like {@link Properties#load(java.io.Reader)}, but preserves the order of key/value pairs. */
  @NotNull
  public static Map<String, String> loadProperties(@NotNull Reader reader) throws IOException {
    final Map<String, String> map = ContainerUtil.newLinkedHashMap();

    new Properties() {
      @Override
      public synchronized Object put(Object key, Object value) {
        map.put(String.valueOf(key), String.valueOf(value));
        //noinspection UseOfPropertiesAsHashtable
        return super.put(key, value);
      }
    }.load(reader);

    return map;
  }

  public static boolean isRootPath(@NotNull File file) {
    return isRootPath(file.getPath());
  }

  public static boolean isRootPath(@NotNull String path) {
    return path.equals("/") || path.matches("[a-zA-Z]:[/\\\\]");
  }
}