// Loosely based on the workaround posted here:
  // http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4993360
  // XXX why isn't this in Apache Commons?
  public static boolean isFileWritable(File file) {
    boolean isWritable = false;

    if (file != null) {
      boolean fileAlreadyExists = file.isFile(); // i.e. exists and is a File

      if (fileAlreadyExists || !file.exists()) {
        try {
          // true: open for append: make sure the open
          // doesn't clobber the file
          new FileOutputStream(file, true).close();
          isWritable = true;

          if (!fileAlreadyExists) { // a new file has been "touch"ed; try to remove it
            try {
              if (!file.delete()) {
                LOGGER.warn("Can't delete temporary test file: {}", file.getAbsolutePath());
              }
            } catch (SecurityException se) {
              LOGGER.error("Error deleting temporary test file: " + file.getAbsolutePath(), se);
            }
          }
        } catch (IOException | SecurityException ioe) {
        }
      }
    }

    return isWritable;
  }
  /**
   * Detects charset/encoding for given file. Not 100% accurate for non-Unicode files.
   *
   * @param file File to detect charset/encoding
   * @return file's charset {@link org.mozilla.universalchardet.Constants} or null if not detected
   * @throws IOException
   */
  public static String getFileCharset(File file) throws IOException {
    byte[] buf = new byte[4096];
    final UniversalDetector universalDetector;
    try (BufferedInputStream bufferedInputStream =
        new BufferedInputStream(new FileInputStream(file))) {
      universalDetector = new UniversalDetector(null);
      int numberOfBytesRead;
      while ((numberOfBytesRead = bufferedInputStream.read(buf)) > 0
          && !universalDetector.isDone()) {
        universalDetector.handleData(buf, 0, numberOfBytesRead);
      }
    }
    universalDetector.dataEnd();
    String encoding = universalDetector.getDetectedCharset();

    if (encoding != null) {
      LOGGER.debug("Detected encoding for {} is {}.", file.getAbsolutePath(), encoding);
    } else {
      LOGGER.debug("No encoding detected for {}.", file.getAbsolutePath());
    }

    universalDetector.reset();

    return encoding;
  }
  public static File getFileNameWithAddedExtension(File parent, File f, String ext) {
    File ff = new File(parent, f.getName() + ext);

    if (ff.exists()) {
      return ff;
    }

    return null;
  }
  /**
   * Converts UTF-16 inputFile to UTF-8 outputFile. Does not overwrite existing outputFile file.
   *
   * @param inputFile UTF-16 file
   * @param outputFile UTF-8 file after conversion
   * @throws IOException
   */
  public static void convertFileFromUtf16ToUtf8(File inputFile, File outputFile)
      throws IOException {
    String charset;
    if (inputFile == null || !inputFile.canRead()) {
      throw new FileNotFoundException("Can't read inputFile.");
    }

    try {
      charset = getFileCharset(inputFile);
    } catch (IOException ex) {
      LOGGER.debug("Exception during charset detection.", ex);
      throw new IllegalArgumentException("Can't confirm inputFile is UTF-16.");
    }

    if (isCharsetUTF16(charset)) {
      if (!outputFile.exists()) {
        BufferedReader reader = null;

        try {
          if (equalsIgnoreCase(charset, CHARSET_UTF_16LE)) {
            reader =
                new BufferedReader(new InputStreamReader(new FileInputStream(inputFile), "UTF-16"));
          } else {
            reader =
                new BufferedReader(
                    new InputStreamReader(new FileInputStream(inputFile), "UTF-16BE"));
          }
        } catch (UnsupportedEncodingException ex) {
          LOGGER.warn("Unsupported exception.", ex);
          throw ex;
        }

        BufferedWriter writer =
            new BufferedWriter(new OutputStreamWriter(new FileOutputStream(outputFile), "UTF-8"));
        int c;

        while ((c = reader.read()) != -1) {
          writer.write(c);
        }

        writer.close();
        reader.close();
      }
    } else {
      throw new IllegalArgumentException("File is not UTF-16");
    }
  }
  public static boolean isSubtitlesExists(File file, DLNAMediaInfo media, boolean usecache) {
    boolean found = false;
    if (file.exists()) {
      found = browseFolderForSubtitles(file.getParentFile(), file, media, usecache);
    }
    String alternate = PMS.getConfiguration().getAlternateSubtitlesFolder();

    if (isNotBlank(alternate)) { // https://code.google.com/p/ps3mediaserver/issues/detail?id=737#c5
      File subFolder = new File(alternate);

      if (!subFolder.isAbsolute()) {
        subFolder = new File(file.getParent() + "/" + alternate);
        try {
          subFolder = subFolder.getCanonicalFile();
        } catch (IOException e) {
          LOGGER.debug("Caught exception", e);
        }
      }

      if (subFolder.exists()) {
        found = found || browseFolderForSubtitles(subFolder, file, media, usecache);
      }
    }

    return found;
  }
  public static boolean isFolderRelevant(
      File f, PmsConfiguration configuration, Set<String> ignoreFiles) {
    if (f.isDirectory() && configuration.isHideEmptyFolders()) {
      File[] children = f.listFiles();

      /**
       * listFiles() returns null if "this abstract pathname does not denote a directory, or if an
       * I/O error occurs". in this case (since we've already confirmed that it's a directory), this
       * seems to mean the directory is non-readable
       * http://www.ps3mediaserver.org/forum/viewtopic.php?f=6&t=15135
       * http://stackoverflow.com/questions/3228147/retrieving-the-underlying-error-when-file-listfiles-return-null
       */
      if (children == null) {
        LOGGER.warn("Can't list files in non-readable directory: {}", f.getAbsolutePath());
      } else {
        for (File child : children) {
          if (ignoreFiles.contains(child.getAbsolutePath())) {
            continue;
          }

          if (child.isFile()) {
            if (FormatFactory.getAssociatedFormat(child.getName()) != null
                || isFileRelevant(child, configuration)) {
              return true;
            }
          } else {
            if (isFolderRelevant(child, configuration, ignoreFiles)) {
              return true;
            }
          }
        }
      }
    }
    return false;
  }
  // XXX dir.canRead() has issues on Windows, so verify it directly:
  // http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6203387
  public static boolean isDirectoryReadable(File dir) {
    boolean isReadable = false;

    if (dir != null) {
      // new File("").isDirectory() is false, even though getAbsolutePath() returns the right path.
      // this resolves it
      dir = dir.getAbsoluteFile();

      if (dir.isDirectory()) {
        try {
          File[] files = dir.listFiles(); // null if an I/O error occurs
          isReadable = files != null;
        } catch (SecurityException se) {
        }
      }
    }

    return isReadable;
  }
  public static File isFileExists(File f, String ext) {
    int point = f.getName().lastIndexOf('.');

    if (point == -1) {
      point = f.getName().length();
    }

    File lowerCasedFile =
        new File(f.getParentFile(), f.getName().substring(0, point) + "." + ext.toLowerCase());
    if (lowerCasedFile.exists()) {
      return lowerCasedFile;
    }

    File upperCasedFile =
        new File(f.getParentFile(), f.getName().substring(0, point) + "." + ext.toUpperCase());
    if (upperCasedFile.exists()) {
      return upperCasedFile;
    }

    return null;
  }
  // XXX dir.canWrite() has issues on Windows, so verify it directly:
  // http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6203387
  public static boolean isDirectoryWritable(File dir) {
    boolean isWritable = false;

    if (dir != null) {
      // new File("").isDirectory() is false, even though getAbsolutePath() returns the right path.
      // this resolves it
      dir = dir.getAbsoluteFile();

      if (dir.isDirectory()) {
        File file =
            new File(
                dir,
                String.format(
                    "pms_directory_write_test_%d_%d.tmp",
                    System.currentTimeMillis(), Thread.currentThread().getId()));

        try {
          if (file.createNewFile()) {
            if (isFileWritable(file)) {
              isWritable = true;
            }

            if (!file.delete()) {
              LOGGER.warn("Can't delete temporary test file: {}", file.getAbsolutePath());
            }
          }
        } catch (IOException | SecurityException ioe) {
        }
      }
    }

    return isWritable;
  }
  // based on the workaround posted here:
  // http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4993360
  // XXX why isn't this in Apache Commons?
  public static boolean isFileReadable(File file) {
    boolean isReadable = false;

    if ((file != null) && file.isFile()) {
      try {
        new FileInputStream(file).close();
        isReadable = true;
      } catch (IOException ioe) {
      }
    }

    return isReadable;
  }
  public static boolean isFileRelevant(File f, PmsConfiguration configuration) {
    String fileName = f.getName().toLowerCase();
    if ((configuration.isArchiveBrowsing()
            && (fileName.endsWith(".zip")
                || fileName.endsWith(".cbz")
                || fileName.endsWith(".rar")
                || fileName.endsWith(".cbr")))
        || fileName.endsWith(".iso")
        || fileName.endsWith(".img")
        || fileName.endsWith(".m3u")
        || fileName.endsWith(".m3u8")
        || fileName.endsWith(".pls")
        || fileName.endsWith(".cue")) {
      return true;
    }

    return false;
  }
  // this is called from a static initialiser, where errors aren't clearly reported,
  // so do everything possible to return a valid reponse, even if the parameters
  // aren't sane
  public static FileLocation getFileLocation(
      String customPath, String defaultDirectory, String defaultBasename) {
    File customFile = null;
    File directory = null;
    File file = null;

    if (isBlank(defaultBasename)) {
      // shouldn't get here
      defaultBasename = DEFAULT_BASENAME;
    }

    if (defaultDirectory == null) {
      defaultDirectory = ""; // current directory
    }

    if (customPath != null) {
      customFile = new File(customPath).getAbsoluteFile();
    }

    if (customFile != null) {
      if (customFile.exists()) {
        if (customFile.isDirectory()) {
          directory = customFile;
          file = new File(customFile, defaultBasename).getAbsoluteFile();
        } else {
          directory = customFile.getParentFile();
          file = customFile;
        }
      } else {
        File parentDirectoryFile = customFile.getParentFile();
        if (parentDirectoryFile != null && parentDirectoryFile.exists()) {
          // parent directory exists: the file can be created
          directory = parentDirectoryFile;
          file = customFile;
        }
      }
    }

    if (directory == null || file == null) {
      directory = new File(defaultDirectory).getAbsoluteFile();
      file = new File(directory, defaultBasename).getAbsoluteFile();
    }

    return new FileLocation(directory, file);
  }
  private static synchronized boolean browseFolderForSubtitles(
      File subFolder, File file, DLNAMediaInfo media, boolean usecache) {
    boolean found = false;

    if (!usecache) {
      cache = null;
    }

    if (cache == null) {
      cache = new HashMap<>();
    }

    final Set<String> supported = SubtitleType.getSupportedFileExtensions();

    File[] allSubs = cache.get(subFolder);
    if (allSubs == null) {
      allSubs =
          subFolder.listFiles(
              new FilenameFilter() {
                @Override
                public boolean accept(File dir, String name) {
                  String ext = FilenameUtils.getExtension(name).toLowerCase();
                  if ("sub".equals(ext)) {
                    // Avoid microdvd/vobsub confusion by ignoring sub+idx pairs here since
                    // they'll come in unambiguously as vobsub via the idx file anyway
                    return isFileExists(new File(dir, name), "idx") == null;
                  }
                  return supported.contains(ext);
                }
              });

      if (allSubs != null) {
        cache.put(subFolder, allSubs);
      }
    }

    String fileName = getFileNameWithoutExtension(file.getName()).toLowerCase();
    if (allSubs != null) {
      for (File f : allSubs) {
        if (f.isFile() && !f.isHidden()) {
          String fName = f.getName().toLowerCase();
          for (String ext : supported) {
            if (fName.length() > ext.length()
                && fName.startsWith(fileName)
                && endsWithIgnoreCase(fName, "." + ext)) {
              int a = fileName.length();
              int b = fName.length() - ext.length() - 1;
              String code = "";

              if (a <= b) { // handling case with several dots: <video>..<extension>
                code = fName.substring(a, b);
              }

              if (code.startsWith(".")) {
                code = code.substring(1);
              }

              boolean exists = false;
              if (media != null) {
                for (DLNAMediaSubtitle sub : media.getSubtitleTracksList()) {
                  if (f.equals(sub.getExternalFile())) {
                    exists = true;
                  } else if (equalsIgnoreCase(ext, "idx")
                      && sub.getType() == SubtitleType.MICRODVD) { // sub+idx => VOBSUB
                    sub.setType(SubtitleType.VOBSUB);
                    exists = true;
                  } else if (equalsIgnoreCase(ext, "sub")
                      && sub.getType() == SubtitleType.VOBSUB) { // VOBSUB
                    try {
                      sub.setExternalFile(f);
                    } catch (FileNotFoundException ex) {
                      LOGGER.warn("Exception during external subtitles scan.", ex);
                    }

                    exists = true;
                  }
                }
              }

              if (!exists) {
                DLNAMediaSubtitle sub = new DLNAMediaSubtitle();
                sub.setId(
                    100
                        + (media == null
                            ? 0
                            : media.getSubtitleTracksList().size())); // fake id, not used
                if (code.length() == 0 || !Iso639.getCodeList().contains(code)) {
                  sub.setLang(DLNAMediaSubtitle.UND);
                  sub.setType(SubtitleType.valueOfFileExtension(ext));
                  if (code.length() > 0) {
                    sub.setFlavor(code);
                    if (sub.getFlavor().contains("-")) {
                      String flavorLang =
                          sub.getFlavor().substring(0, sub.getFlavor().indexOf('-'));
                      String flavorTitle =
                          sub.getFlavor().substring(sub.getFlavor().indexOf('-') + 1);
                      if (Iso639.getCodeList().contains(flavorLang)) {
                        sub.setLang(flavorLang);
                        sub.setFlavor(flavorTitle);
                      }
                    }
                  }
                } else {
                  sub.setLang(code);
                  sub.setType(SubtitleType.valueOfFileExtension(ext));
                }

                try {
                  sub.setExternalFile(f);
                } catch (FileNotFoundException ex) {
                  LOGGER.warn("Exception during external subtitles scan.", ex);
                }

                found = true;
                if (media != null) {
                  media.getSubtitleTracksList().add(sub);
                }
              }
            }
          }
        }
      }
    }

    return found;
  }
 public static File getFileNameWithNewExtension(File parent, File file, String ext) {
   return isFileExists(new File(parent, file.getName()), ext);
 }
 FileLocation(File directory, File file) {
   this.directoryPath = FilenameUtils.normalize(directory.getAbsolutePath());
   this.filePath = FilenameUtils.normalize(file.getAbsolutePath());
 }