/*
 * Copyright 2003-2011 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.util;

import jetbrains.mps.vfs.IFile;
import jetbrains.mps.vfs.path.Path;
import org.apache.log4j.LogManager;
import org.apache.log4j.Logger;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.io.BufferedReader;
import java.io.Closeable;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.Reader;
import java.nio.charset.Charset;
import java.text.MessageFormat;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.jar.JarOutputStream;
import java.util.jar.Manifest;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

/**
 * @author Kostik
 */
public class FileUtil {
  private static final Logger LOG = LogManager.getLogger(FileUtil.class);

  private static final String[] IGNORED_DIRS = new String[]{".svn", ".git", "_svn"};
  public static final String DEFAULT_CHARSET_NAME = "UTF-8";
  public static final Charset DEFAULT_CHARSET = Charset.forName(DEFAULT_CHARSET_NAME);
  private static final String MPSTEMP = "mpstemp";
  private static final char DOT = '.';

  public static File createTmpDir() {
    File tmp = getTempDir();
    for (int i = 0;; ++i) {
      if (!new File(tmp, MPSTEMP + i).exists()) {
        File tmpDir = new File(tmp, MPSTEMP + i);
        boolean result = tmpDir.mkdir();
        if (!result) {
          throw new IllegalStateException("Could not create a directory " + tmpDir);
        }
        return tmpDir;
      }
    }
  }

  public static File createTmpFile() {
    File tmp = getTempDir();
    int i = 0;
    while (true) {
      if (!new File(tmp, MPSTEMP + i).exists()) {
        break;
      }
      i++;
    }

    File result = new File(tmp, MPSTEMP + i);
    try {
      result.createNewFile();
    } catch (IOException e) {
      e.printStackTrace();
    }
    return result;
  }

  public static File getTempDir() {
    return new File(System.getProperty("java.io.tmpdir"));
  }

  public static void jar(File dir, final Manifest mf, File to) {
    new Packer() {
      @Override
      protected ZipOutputStream createDeflaterStream(FileOutputStream fos) throws Exception {
        return new JarOutputStream(fos, mf);
      }
    }.pack(dir, to);
  }

  @SuppressWarnings({"UnusedDeclaration"})
  public static void zip(File dir, File to) {
    new Packer() {
      @Override
      protected ZipOutputStream createDeflaterStream(FileOutputStream fos) throws Exception {
        return new ZipOutputStream(fos);
      }
    }.pack(dir, to);
  }

  public static void zip(Map<String, File> entries, File to) {
    new Packer() {
      @Override
      protected ZipOutputStream createDeflaterStream(FileOutputStream fos) throws Exception {
        return new ZipOutputStream(fos);
      }
    }.pack(entries, to);
  }

  public static void copyDir(File what, File to) {
    assert what.isDirectory();
    if (!to.exists()) {
      to.mkdirs();
    }

    for (File f : what.listFiles()) {
      if (f.isDirectory()) {
        if (isIgnoredDir(f.getName())) continue;

        File fCopy = new File(to, f.getName());
        if (!fCopy.exists()) {
          fCopy.mkdir();
        }
        copyDir(f, fCopy);
      }

      if (f.isFile()) {
        copyFile(f, to);
      }
    }

  }

  public static void copyFile(File f, File to) {
    try {
      copyFileChecked(f, to);
    } catch (IOException e) {
      e.printStackTrace();
    }
  }

  public static void copyFileChecked(File f, File to) throws IOException {
    FileInputStream is = new FileInputStream(f);

    File target;
    if (to.isDirectory()) {
      target = new File(to, f.getName());
    } else {
      target = to;
    }

    if (!to.getParentFile().exists()) {
      to.getParentFile().mkdirs();
    }

    OutputStream os = new FileOutputStream(target);
    os.write(ReadUtil.read(is));

    is.close();
    os.close();
  }

  public static String getCanonicalPath(File file) {
    if (file == null) {
      return null;
    }
    try {
      return file.getCanonicalPath();
    } catch (IOException e) {
      return file.getAbsolutePath();
    }
  }

  public static String getCanonicalPath(String path) {
    if (path == null) {
      path = "";
    }
    path = normalize0(path, Path.SYSTEM_SEPARATOR);
    File file = new File(path);
    return getCanonicalPath(file);
  }

  @NotNull
  public static String normalize(@NotNull String path) {
    return stripLastSlashes(normalize0(getUnixPath(path), Path.UNIX_SEPARATOR));
  }

  // poor version of normalization
  // does not consider '..'; will be provided in the future release within new vfs API
  private static String normalize0(@NotNull String path, @NotNull String separator) {
    path = path.replaceAll("/+", "/").replaceAll("\\\\+", "\\\\");
    if (path.endsWith(separator + DOT)) {
      path = path.substring(0, path.length() - 1);
    }
    if (path.equals("" + DOT)) {
      return "";
    }
    // four backslashes are for windows file separator (escaping it twice), and two are escaping the dot
    path = path.replaceAll("\\\\\\.\\\\", "\\\\").replaceAll("/\\./", "/");
    return path;
  }


  public static boolean delete(File root) {
    boolean result = true;

    if (root.isDirectory()) {
      for (File child : root.listFiles()) {
        result = delete(child) && result;
      }
    }
    // !result means one of children was not deleted, hence you may not delete this directory
    return result && root.delete();
  }

  /**
   * deletes the file and all its parents above which happen to be empty after this file's removal
   * @return true iff the file has been removed
   */
  public static boolean deleteWithAllEmptyDirs(@NotNull IFile file) {
    if (file.exists()) { // exists is vital, see VirtualFile assert for file existence [AP]
      do {
        file.delete();
        file = file.getParent();
      } while (file != null && file.getChildren().isEmpty());
      return true;
    }
    return false;
  }

  public static boolean clear(File dir) {
    File[] files = dir.listFiles();
    if (files == null) return true;

    boolean result = true;

    for (File f : files) {
      boolean r = delete(f);
      result = result && r;
    }

    return result;
  }

  public static long getNewestFileTime(File dir) {
    return getNewestFileTime(dir, true);
  }

  public static long getNewestFileTime(File dir, boolean recursive) {
    File[] files = dir.listFiles();
    if (files == null) {
      return dir.lastModified();
    }
    long result = dir.lastModified();
    for (File file : files) {
      if (isIgnoredDir(file.getName())) {
        continue;
      }

      if (recursive) {
        result = Math.max(result, getNewestFileTime(file));
      } else {
        result = Math.max(result, file.lastModified());
      }
    }
    return result;
  }

  public static void writeFile(final File file, final String content) throws IOException {
    if (!file.getParentFile().exists()) {
      file.getParentFile().mkdirs();
    }

    if (file.exists()) {
      try {
        String existingContents = FileUtil.read(file);
        if (existingContents.equals(content)) {
          return;
        }
      } catch (RuntimeException ex) {
        /* ignore */
      }
      file.delete();
    }

    boolean fileCreated = false;
    IOException lastExc = null;
    for (int i = 1; i <= 20; i++) {
      try {
        file.createNewFile();
        fileCreated = true;
        break;
      } catch (IOException ex) {
        lastExc = ex;
        //sometimes:
        //java.io.IOException: Access is denied
        //at java.io.WinNTFileSystem.createFileExclusively(Native Method)
        //at java.io.File.createNewFile(File.java:850)
        // so we'll try 5(20) times
      }
      try {
        Thread.sleep(100);
      } catch (InterruptedException ie) {
        //ok
      }
    }

    if (fileCreated) {
      FileUtil.write(file, content);
    } else {
      throw lastExc == null ? new IOException("Can't create " + file.getPath()) : lastExc;
    }
  }

  /*
   * use writeFile
   */
  @Deprecated
  public static void write(File file, String content) {
    PrintWriter writer = null;
    try {
      writer = new PrintWriter(new OutputStreamWriter(new FileOutputStream(file), FileUtil.DEFAULT_CHARSET));
      writer.print(content);
      writer.flush();
    } catch (IOException e) {
      throw new RuntimeException(e);
    } finally {
      if (writer != null) {
        writer.close();
      }
    }
  }

  public static void write(File file, byte[] content) {
    OutputStream writer = null;
    try {
      writer = new FileOutputStream(file);
      writer.write(content);
    } catch (IOException e) {
      throw new RuntimeException(e);
    } finally {
      FileUtil.closeFileSafe(writer);
    }
  }

  public static String read(File file) {
    try {
      return read(new FileReader(file));
    } catch (IOException e) {
      throw new RuntimeException(e);
    }
  }

  public static String read(File file, String charset) {
    try {
      return read(new InputStreamReader(new FileInputStream(file), charset));
    } catch (IOException e) {
      throw new RuntimeException(e);
    }
  }

  public static String read(Reader reader) {
    BufferedReader r = null;
    try {
      r = new BufferedReader(reader);

      StringBuilder result = new StringBuilder();

      String line;
      while ((line = r.readLine()) != null) {
        result.append(line).append("\n");
      }

      return result.toString();
    } catch (IOException e) {
      throw new RuntimeException(e);
    } finally {
      FileUtil.closeFileSafe(r);
    }
  }

  public static String readLine(Reader reader, int lineNo) {
    BufferedReader r = null;
    try {
      r = new BufferedReader(reader);

      String line = null;
      int currentLine = 0;
      while ((line = r.readLine()) != null) {
        if (currentLine == lineNo) {
          return line;
        }
        currentLine++;
      }

      return null;
    } catch (IOException e) {
      throw new RuntimeException(e);
    } finally {
      try {
        if (r != null) {
          r.close();
        }
      } catch (IOException e) {
        e.printStackTrace();
      }
    }
  }

  public static boolean isParent(File parent, File child) {
    if (!parent.isDirectory()) {
      return false;
    }

    if (parent.equals(child)) return true;

    for (File f : parent.listFiles()) {
      if (isParent(f, child)) return true;
    }

    return false;
  }

  public static File getMaxContainingFile(List<File> files) {
    if (files.size() == 0) return null;
    Iterator<File> fileIterator = files.iterator();
    File max = fileIterator.next();
    while (fileIterator.hasNext()) {
      if (max == null) return null;
      max = getMaxContainingFile(max, fileIterator.next());
    }

    return max;
  }

  public static File getMaxContainingFile(File file1, File file2) {
    if (isParentUp(file1, file2)) return file1;
    if (isParentUp(file2, file1)) return file2;

    File parent1 = file1.getParentFile();
    File parent2 = file2.getParentFile();

    if ((parent1 == null) && (parent2 == null)) {
      return null;
    } else if (parent1 == null) {
      return getMaxContainingFile(file1, parent2);
    } else if (parent2 == null) {
      return getMaxContainingFile(parent1, file2);
    }

    return getMaxContainingFile(parent1, parent2);
  }

  public static boolean isParentUp(File parent, File child) {
    if (!parent.isDirectory()) {
      return false;
    }

    if (parent.getPath().equals(child.getPath())) return true;

    File parentOfChild = child.getParentFile();
    if (parentOfChild == null) return false;
    return isParentUp(parent, parentOfChild);
  }

  public static boolean isIgnoredDir(String name) {
    for (String ignoredDir : IGNORED_DIRS) {
      if (ignoredDir.equals(name)) {
        return true;
      }
    }
    return false;
  }

  @NotNull
  public static String getUnixPath(@NotNull String path) {
    return path.replace(Path.WIN_SEPARATOR, Path.UNIX_SEPARATOR);
  }

  public static boolean isAncestor(@NotNull String ancestorPath, @NotNull String path) {
    ancestorPath = getUnixPath(ancestorPath);
    path = getUnixPath(path);
    return path.startsWith(ancestorPath);
  }

  /**
   * @throws PathResolutionException if the paths do not intersect
   */
  public static String getRelativePath(@NotNull String targetPath, @NotNull String basePath, @NotNull String pathSeparator) {
    String[] base = basePath.split(Pattern.quote(pathSeparator));
    String[] target = targetPath.split(Pattern.quote(pathSeparator));

    StringBuilder common = new StringBuilder();
    int commonIndex = 0;
    while (commonIndex < target.length && commonIndex < base.length
        && target[commonIndex].equals(base[commonIndex])) {
      common.append(target[commonIndex]).append(pathSeparator);
      commonIndex++;
    }

    if (commonIndex == 0) {
      throw new PathResolutionException("No common path element found for '" + targetPath + "' and '" + basePath + "'");
    }

    if (target.length == commonIndex && base.length == target.length) {
      return "";
    }

    boolean baseIsFile = true;

    File baseResource = new File(basePath);

    if (baseResource.exists()) {
      baseIsFile = baseResource.isFile();

    } else if (basePath.endsWith(pathSeparator)) {
      baseIsFile = false;
    }

    StringBuilder relative = new StringBuilder();

    if (base.length != commonIndex) {
      int numDirsUp = baseIsFile ? base.length - commonIndex - 1 : base.length - commonIndex;

      for (int i = 0; i < numDirsUp; i++) {
        relative.append("..").append(pathSeparator);
      }
    }
    if (targetPath.length() > common.length()) {
      relative.append(targetPath.substring(common.length()));
    }
    return relative.toString();
  }

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

  public static String getAbsolutePath(String path) {
    return new File(path).getAbsolutePath();
  }

  public static void closeFileSafe(Closeable c) {
    if (c != null) {
      try {
        c.close();
      } catch (IOException ignored) {
      }
    }
  }

  public static boolean canWrite(File f) {
    while (!f.exists()) {
      f = f.getParentFile();
      if (f == null) {
        return false;
      }
    }
    return f.canWrite();
  }

  public final static class PathResolutionException extends RuntimeException {
    PathResolutionException(String msg) {
      super(msg);
    }
  }

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

  @NotNull
  public static String getNameWithoutExtension(@NotNull String name) {
    int i = name.lastIndexOf(DOT);
    if (i != -1) {
      name = name.substring(0, i);
    }
    return name;
  }

  @Nullable
  public static String getExtension(@NotNull String name) {
    int i = name.lastIndexOf(DOT);
    if (i != -1) {
      return name.substring(i + 1);
    }
    return null;
  }

  @Contract(value = "null -> null;!null->!null")
  @Nullable
  public static String stripLastSlashes(@Nullable String path) {
    if (path == null) return null;

    while (path.endsWith("/") || path.endsWith("\\")) {
      path = path.substring(0, path.length() - 1);
    }
    return path;
  }

  // not taking non-canonical paths into account
  public static boolean isSubPath(@NotNull String base, @NotNull String sub) {
    boolean startsWith = sub.startsWith(base);
    if (!startsWith) return false;
    int baseLen = base.length();
    if (sub.length() == baseLen) return true; // non-strict comparison: equal strings -> true

    char lastBaseChar = base.charAt(baseLen - 1);
    char nextChar = sub.charAt(baseLen);
    if (lastBaseChar == '/' || lastBaseChar == '\\' || nextChar == '/' || nextChar == '\\') {
      return true;
    }
    return false;
  }

  private abstract static class Packer {
    public void pack(File dir, File to) {
      FileOutputStream fos = null;
      ZipOutputStream out = null;

      try {
        fos = new FileOutputStream(to);
        out = createDeflaterStream(fos);
        _zip(dir, "", out);
      } catch (Exception e) {
        e.printStackTrace();
      } finally {
        try {
          if (out != null) {
            out.close();
          }
          if (fos != null) {
            fos.close();
          }
        } catch (IOException e) {
          e.printStackTrace();
        }
      }
    }

    public void pack(Map<String, File> entries, File to) {
      FileOutputStream fos = null;
      ZipOutputStream out = null;

      try {
        fos = new FileOutputStream(to);
        out = createDeflaterStream(fos);
        for (String key : entries.keySet()) {
          addZipEntry(out, key, entries.get(key));
        }
      } catch (Exception e) {
        e.printStackTrace();
      } finally {
        try {
          if (out != null) {
            out.close();
          }
          if (fos != null) {
            fos.close();
          }
        } catch (IOException e) {
          e.printStackTrace();
        }
      }
    }

    protected abstract ZipOutputStream createDeflaterStream(FileOutputStream fos) throws Exception;

    private static void addZipEntry(ZipOutputStream out, String path, File file) {
      ZipEntry entry = new ZipEntry(path);
      entry.setTime(file.lastModified());
      FileInputStream is = null;
      try {
        out.putNextEntry(entry);
        if (file.isFile()) {
          is = new FileInputStream(file);
          byte[] bytes = ReadUtil.read(is);
          out.write(bytes);
        }
        out.closeEntry();
      } catch (IOException e) {
        e.printStackTrace();
      } finally {
        if (is != null) {
          try {
            is.close();
          } catch (IOException e) {
            e.printStackTrace();
          }
        }
      }
    }

    private static void _zip(File base, String prefix, ZipOutputStream out) {
      File current = new File(base.getPath() + File.separator + prefix).getAbsoluteFile();

      if (prefix.length() > 0) {
        addZipEntry(out, prefix, current);
      }

      if (current.isDirectory()) {
        for (File file : current.listFiles()) {
          if (file.isFile()) {
            _zip(base, prefix + file.getName(), out);
          }
          if (file.isDirectory()) {
            _zip(base, prefix + file.getName() + "/", out);
          }
        }
      }
    }
  }
}
