/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you 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 org.apache.uima.util;

import java.io.BufferedWriter;
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.OutputStreamWriter;
import java.io.Reader;
import java.nio.charset.Charset;
import java.nio.file.CopyOption;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardOpenOption;
import java.nio.file.attribute.FileAttribute;
import java.util.ArrayList;
import java.util.Random;

/**
 * Some utilities for handling files.
 * 
 * 
 */
public class FileUtils {

  /**
   * Get a list of all files in a directory. Optionally, get files in subdirectories as well.
   * 
   * @param directory
   *          The directory for which to get the files.
   * @param getRecursive
   *          Should we get files from subdirectories?
   * @return ArrayList A list of <code>File</code> objects. <code>null</code> if
   *         <code>directory</code> does not exist, or is not a directory.
   */
  public static final ArrayList<File> getFiles(File directory, boolean getRecursive) {
    if (!directory.exists() || !directory.isDirectory()) {
      return null;
    }
    ArrayList<File> fileList = new ArrayList<>();
    for (File file : directory.listFiles()) {
      if (file.isDirectory()) {
        if (getRecursive) {
          fileList.addAll(getFiles(file, getRecursive));
        }
      } else {
        fileList.add(file);
      }
    }
    return fileList;
  }

  /**
   * Get a list of all files in a directory, ignoring subdirectories.
   * 
   * @param directory
   *          The directory for which to get the files.
   * @return ArrayList A list of <code>File</code> objects. <code>null</code> if
   *         <code>directory</code> does not exist, or is not a directory.
   */
  public static final ArrayList<File> getFiles(File directory) {
    return getFiles(directory, false);
  }

  /**
   * Get a list of all subdirectories in a directory, ignoring regular files.
   * 
   * @param directory
   *          The directory for which to get the subdirectories.
   * @return ArrayList A list of <code>File</code> objects. <code>null</code> if
   *         <code>directory</code> does not exist, or is not a directory.
   */
  public static final ArrayList<File> getSubDirs(File directory) {
    if (!directory.exists() || !directory.isDirectory()) {
      return null;
    }
    ArrayList<File> dirList = new ArrayList<>();

    for (File file : directory.listFiles()) {
      if (file.isDirectory()) {
        dirList.add(file);
      }
    }
    return dirList;
  }

  /**
   * Read a bufferedReader into a string, using the default platform encoding.
   * 
   * @param reader
   *          to be read in
   * @return String The contents of the stream.
   * @throws IOException
   *           Various I/O errors.
   */
  public static String reader2String(Reader reader) throws IOException {
    StringBuilder sb = new StringBuilder();
    char[] buf = new char[10000];
    int charsRead;
    try {
      while ((charsRead = reader.read(buf)) >= 0) {
        sb.append(buf, 0, charsRead);
      }
    } finally {
      reader.close();
    }
    return sb.toString();
  }

  /**
   * Read the contents of a file into a string, using the default platform encoding.
   * 
   * @param file
   *          The file to be read in.
   * @return String The contents of the file.
   * @throws IOException
   *           Various I/O errors.
   */
  public static String file2String(File file) throws IOException {
    return reader2String(new FileReader(file));
  }

  /**
   * Read the contents of a file into a string using a specific character encoding.
   * 
   * @param file
   *          The input file.
   * @param fileEncoding
   *          The character encoding of the file (see your Java documentation for supported
   *          encodings).
   * @return String The contents of the file.
   * @throws IOException
   *           Various I/O errors.
   */
  public static String file2String(File file, String fileEncoding) throws IOException {
    if (fileEncoding == null) { // use default
      return file2String(file);
    }
    return reader2String(new InputStreamReader(new FileInputStream(file), fileEncoding));
  }

  /**
   * Write a string to a file, using platform encoding. If the file exists, it is overwritten.
   * 
   * @param fileContents
   *          The file contents.
   * @param file
   *          The file to save to.
   * @throws IOException
   *           If for any reason the file can't be written.
   */
  public static void saveString2File(String fileContents, File file) throws IOException {
    saveString2File(fileContents, file, Charset.defaultCharset().name());
  }

  /**
   * Write a string to a file using the specified encoding. If the file exists, it is overwritten.
   * 
   * @param s
   *          The file contents.
   * @param file
   *          The file to save to.
   * @param encoding
   *          The character encoding to use.
   * @throws IOException
   *           If for any reason the file can't be written.
   */
  public static void saveString2File(String s, File file, String encoding) throws IOException {
    try (BufferedWriter writer = new BufferedWriter(
            new OutputStreamWriter(new FileOutputStream(file), encoding))) {
      writer.write(s);
    }
  }

  /**
   * Efficiently Writes string data as UTF-8 characters to path added for backwards compatibility
   * with v2
   * 
   * @param path
   *          where to write to, creating a new file if it doesn't already exist
   * @param data
   *          the data to write
   */
  public static void writeToFile(Path path, String data) {
    // try with resources, closes bw at end
    try (BufferedWriter bw = Files.newBufferedWriter(path, StandardOpenOption.CREATE)) {
      bw.write(data);
    } catch (IOException e) {
      throw new RuntimeException(e);
    }
  }

  /**
   * Delete all files in a directory (not recursive).
   * 
   * @param directory
   *          The directory that contains the files to be deleted.
   */
  public static final void deleteAllFiles(File directory) {
    File[] fileList = directory.listFiles();
    // If file does not exist, or is not a directory, do nothing.
    if (fileList == null) {
      return;
    }
    File file;
    for (int i = 0; i < fileList.length; i++) {
      file = fileList[i];
      if (file.isFile()) {
        file.delete();
      }
    }
  }

  /**
   * Recursively delete possibly non-empty directory or file.
   * 
   * @param file
   *          The file or directory to be deleted.
   * @return <code>true</code> iff the file/directory could be deleted.
   */
  public static final boolean deleteRecursive(File file) {
    if (!file.exists()) {
      return false;
    }
    boolean rc = true;
    if (file.isDirectory()) {
      File[] fileList = file.listFiles();
      for (int i = 0; i < fileList.length; i++) {
        rc &= deleteRecursive(fileList[i]);
      }
    }
    rc &= file.delete();
    return rc;
  }

  /**
   * Create a new directory. The parent directory must exist.
   * 
   * @param directory
   *          The directory to be created.
   * @return boolean Will fail if directory already exists, or File.mkdir() returns
   *         <code>false</code>.
   */
  public static final boolean mkdir(File directory) {
    // Check if directory already exists. The documentation is silent on
    // what
    // could actually cause File.mkdir() to fail.
    if (directory.exists()) {
      return false;
    }
    return directory.mkdir();
  }

  /**
   * Create a temporary directory using random numbers as name suffixes.
   * 
   * @param parent
   *          Where the directory is created.
   * @param prefix
   *          Prefix of the directory names to be created.
   * @return A file object corresponding to the newly created dir, or <code>null</code> if none
   *         could be created for some reason (e.g., if the parent is not writable).
   * @deprecated use Java 7 methods for this see
   *             {@link java.nio.file.Files#createTempDirectory(Path, String, FileAttribute...)}
   */
  @Deprecated
  public static final File createTempDir(File parent, String prefix) {
    Random rand = new Random();
    File tempDir;
    while (true) {
      tempDir = new File(parent, prefix + rand.nextInt());
      if (!tempDir.exists()) {
        if (tempDir.mkdirs()) {
          tempDir.deleteOnExit();
          return tempDir;
        }
        return null;
      }
    }
  }

  /**
   * @deprecated use Java 7 methods for this see
   *             {@link java.io.File#createTempFile(String, String, File)}
   * @param prefix
   *          -
   * @param suffix
   *          -
   * @param tempDir
   *          -
   * @return the file
   * @throws IOException
   *           -
   */
  @Deprecated
  public static final File createTempFile(String prefix, String suffix, File tempDir)
          throws IOException {
    File file = File.createTempFile(prefix, suffix, tempDir);
    file.deleteOnExit();
    return file;
  }

  /**
   * Copy file <code>file</code> to location <code>dir</code>. This method will not fail silently.
   * Anything that prevents the copy from happening will be treated as an error condition. If the
   * method does not throw an exception, the copy was successful.
   * 
   * <p>
   * <b>Note: </b> this is a completely brain-dead implementation of file copy. The complete file is
   * read into memory before it is copied. If you need something more sophisticated for copying
   * large files, feel free to add your improvements.
   * 
   * @param file
   *          The file to copy.
   * @param dir
   *          The destination directory.
   * @throws IOException
   *           For various reason: if <code>file</code> does not exist or is not readable, if the
   *           destination directory does not exist or isn't a directory, or if the file can't be
   *           copied for any reason.
   * @deprecated use Java 7 for this see {@link java.nio.file.Files#copy(Path, Path, CopyOption...)}
   */
  @Deprecated
  public static final void copyFile(File file, File dir) throws IOException {
    if (!file.exists() || !file.canRead()) {
      throw new IOException("File does not exist or is not readable: " + file.getAbsolutePath());
    }
    if (!dir.exists() || !dir.isDirectory()) {
      throw new IOException(
              "Destination does not exist or is not a directory: " + dir.getAbsolutePath());
    }
    File outFile = new File(dir, file.getName());
    if (outFile.exists() && !outFile.canWrite()) {
      throw new IOException("Can't write output file: " + outFile);
    }
    byte[] bytes = new byte[(int) file.length()];
    try (FileInputStream is = new FileInputStream(file);
            FileOutputStream os = new FileOutputStream(outFile)) {

      while (true) {
        int count = is.read(bytes);
        if (0 > count) {
          break;
        }
        os.write(bytes, 0, count);
      }
    }
  }

  /**
   * Finds a relative path to a given file, relative to a specified directory.
   * 
   * @param file
   *          file that the relative path should resolve to
   * @param relativeToDir
   *          directory that the path should be relative to
   * @return a relative path. This always uses / as the separator character.
   * @throws IOException
   *           -
   */
  public static String findRelativePath(File file, File relativeToDir) throws IOException {
    String canonicalFile = file.getCanonicalPath();
    String canonicalRelTo = relativeToDir.getCanonicalPath();
    String[] filePathComponents = getPathComponents(canonicalFile);
    String[] relToPathComponents = getPathComponents(canonicalRelTo);
    int i = 0;
    while (i < filePathComponents.length && i < relToPathComponents.length
            && filePathComponents[i].equals(relToPathComponents[i])) {
      i++;
    }
    StringBuilder sb = new StringBuilder();
    for (int j = i; j < relToPathComponents.length; j++) {
      sb.append("../");
    }
    for (int j = i; j < filePathComponents.length - 1; j++) {
      sb.append(filePathComponents[j]).append('/');
    }
    sb.append(filePathComponents[filePathComponents.length - 1]);
    return sb.toString();
  }

  /**
   * Splits a path into components using the OS file separator character. This can be used on the
   * results of File.getCanonicalPath().
   * 
   * @param canonicalPath
   *          a file path that uses the OS file separator character
   * @return an array of strings, one for each component of the path
   */
  public static String[] getPathComponents(String canonicalPath) {
    String regex = File.separator;
    if (regex.equals("\\"))
      regex = "\\\\";
    return canonicalPath.split(regex);
  }

}
