/*
 * FileNewerThanTargetFilter class.
 *
 * Copyright (C) 2007 SATOH Takayuki All Rights Reserved.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc.,
 * 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 */
package ts.util.file;

import java.io.File;
import java.io.FileFilter;
import java.io.IOException;
import java.util.List;
import java.util.LinkedList;
import ts.util.Duo;
import ts.util.Trio;
import ts.util.text.StringOperation;

/**
 * ターゲットのファイルよりも最終更新日が新しいことを条件とする
 * ファイル・フィルタ・クラス。
 * <br>
 * ターゲットのファイルは、{@link #accept(File) accept(File)}メソッドで指定
 * されたファイルのパスを、このオブジェクトに設定された変換方法によって変換
 * されたパスによって特定される。
 * <br>
 * ファイル・パスの変換は、このオブジェクトに追加した順番に従って、変換方法が
 * 適用される。
 * <br>
 * ターゲットのファイルと、引数のファイルの間で最終更新日の比較を行い、
 * 引数のファイルの方が新しい場合に真とする。
 * <br>
 * ターゲットのファイルが存在しない場合は真を返す。
 *
 * @author  佐藤隆之.
 * @version $Revision: 1.1.1.1 $, $Date: 2010-10-16 00:03:46 $
 */
public class FileNewerThanTargetFilter implements FileFilter
{
  /** ファイル・パスの変換に使用される変換元の基底ディレクトリ。 */
  private File baseDir_ ;

  /** ファイル・パスの変換に使用される変換先の基底ディレクトリ。 */
  private File destDir_ ;

  /** ファイル名の変換に使用される正規表現と置換文字列の組を格納するリスト。 */
  private List<Trio<String,String,Boolean>> nameReplaceLst_ =
    new LinkedList<Trio<String,String,Boolean>>();

  /** ディレクトリ・パスの変換に使用される正規表現と置換文字列の組を格納する
   * リスト。 */
  private List<Trio<String,String,Boolean>> dirReplaceLst_ =
    new LinkedList<Trio<String,String,Boolean>>();

  /**
   * ターゲット・ファイルのパスを求めるために使用される変換元と変換先の
   * 基底ディレクトリを引数にとるコンストラクタ。
   * <br>
   * 変換元とは、{@link #accept(File) accept(File)}メソッドで指定されるファイル
   * のことであり、変換先とはターゲット・ファイルのことである。
   * <br>
   * 変換元の基底ディレクトリが存在しない場合は、{@link #accept(File) 
   * accept(File)}メソッド実行結果が偽となる。
   *
   * @param  baseDir 変換元の基底ディレクトリ。
   * @param  destDir 変換元の基底ディレクトリ。
   * @throws AssertionError 引数がヌルの場合（デバッグ・モードのみ）。
   */
  public FileNewerThanTargetFilter(File baseDir, File destDir)
  {
    assert (baseDir != null) : "@param:baseDir is null.";
    assert (destDir != null) : "@param:destDir is null.";

    baseDir_ = baseDir;
    destDir_ = destDir;
  }

  /**
   * ファイル名の変換に使用される正規表現と置換文字列を追加する。
   * <br>
   * 正規表現に合致した文字列の変換は、最初に合致したものだけが変換される。
   *
   * @param  regex 正規表現。
   * @param  replacement 置換文字列。
   * @throws AssertionError 引数がヌルの場合（デバッグ・モードのみ）。
   */
  public void addNameReplaceFirst(String regex, String replacement)
  {
    assert (regex != null) : "@param:regex is null.";
    assert (replacement != null) : "@param:replacement is null.";

    nameReplaceLst_.add(new Trio<String,String,Boolean>(
      regex, replacement, Boolean.FALSE));
  }

  /**
   * ファイル名の変換に使用される正規表現と置換文字列を追加する。
   * <br>
   * 第３引数に正規表現に合致した全ての文字列を変換する。
   *
   * @param  regex 正規表現。
   * @param  replacement 置換文字列。
   * @throws AssertionError 引数がヌルの場合（デバッグ・モードのみ）。
   */
  public void addNameReplaceAll(String regex, String replacement)
  {
    assert (regex != null) : "@param:regex is null.";
    assert (replacement != null) : "@param:replacement is null.";

    nameReplaceLst_.add(new Trio<String,String,Boolean>(
      regex, replacement, Boolean.TRUE));
  }

  /**
   * ディレクトリ・パスの変換に使用される正規表現と置換文字列を追加する。
   * <br>
   * 正規表現に合致した文字列の変換は、最初に合致したものだけが変換される。
   * <br>
   * 変換されるパスは、変換先の基底ディレクトリのパスから下の部分のみである。
   *
   * @param  regex 正規表現。
   * @param  replacement 置換文字列。
   * @throws AssertionError 引数がヌルの場合（デバッグ・モードのみ）。
   */
  public void addDirReplaceFirst(String regex, String replacement)
  {
    assert (regex != null) : "@param:regex is null.";
    assert (replacement != null) : "@param:replacement is null.";

    dirReplaceLst_.add(new Trio<String,String,Boolean>(
      regex, replacement, Boolean.FALSE));
  }

  /**
   * ディレクトリ・パスの変換に使用される正規表現と置換文字列を追加する。
   * <br>
   * 正規表現に合致した全ての文字列を変換する。
   * <br>
   * 変換されるパスは、変換先の基底ディレクトリのパスから下の部分のみである。
   *
   * @param  regex 正規表現。
   * @param  replacement 置換文字列。
   * @throws AssertionError 引数がヌルの場合（デバッグ・モードのみ）。
   */
  public void addDirReplaceAll(String regex, String replacement)
  {
    assert (regex != null) : "@param:regex is null.";
    assert (replacement != null) : "@param:replacement is null.";

    dirReplaceLst_.add(new Trio<String,String,Boolean>(
      regex, replacement, Boolean.TRUE));
  }

  /**
   * 指定されたファイルの最終更新日が、ターゲット・ファイルの最終更新日よりも
   * 新しいかどうかを判定する。
   * <br>
   * ターゲット・ファイルのパスは、引数のファイルのパスを元に、このオブジェクト
   * に設定されている変換方法を使って求められる。
   * <br>
   * ターゲット・ファイルが存在しない場合は<tt>true</tt>を返す。
   * <br>
   * 引数のファイルが存在しない場合は<tt>false</tt>を返す。
   * <br>
   * ターゲット・ファイルのパスへの変換に失敗した場合は<tt>false</tt>を返す。
   *
   * @param file  判定対象のファイル。
   * @return 指定されたファイルの最終更新日がターゲット・ファイルよりも新しい
   *         か、ターゲット・ファイルが存在しない場合は<tt>true</tt>を返す。
   * @throws AssertionError 引数がヌルの場合（デバッグ・モードのみ）。
   */
  public boolean accept(File file)
  {
    assert (file != null) : "@param:file is null.";

    try {
      if (! file.exists()) {
        return false;
      }

      File target = getTargetFile(file);
      if (! target.exists()) {
        return true;
      }
      else if (target.lastModified() >= file.lastModified()) {
        return false;
      }
      else {
        return true;
      }
    }
    catch (Exception e) {
      return false;
    }
  }

  /**
   * ターゲット・ファイルを求める。
   *
   * @param  file 判定対象のファイル。
   * @throws IOException ターゲット・ファイルを求めることに失敗した場合。
   * @throws AssertionError 引数がヌルの場合。
   */
  protected File getTargetFile(File file) throws IOException
  {
    Duo<String,String> duo = getRelativePaths(file, baseDir_);

    String name = duo.getSecond();
    for (Trio<String,String,Boolean> trio : nameReplaceLst_) {
      if (trio.getThird().booleanValue()) {
        name = name.replaceAll(trio.getFirst(), trio.getSecond());
      }
      else {
        name = name.replaceFirst(trio.getFirst(), trio.getSecond());
      }
    }

    String dir = duo.getFirst();
    for (Trio<String,String,Boolean> trio : dirReplaceLst_) {
      if (trio.getThird().booleanValue()) {
        dir = dir.replaceAll(trio.getFirst(), trio.getSecond());
      }
      else {
        dir = dir.replaceFirst(trio.getFirst(), trio.getSecond());
      }
    }

    File target = destDir_;
    if (! StringOperation.isEmpty(dir)) {
      target = new File(target, dir);
    }
    if (! StringOperation.isEmpty(name)) {
      target = new File(target, name);
    }
    return target;
  }

  /**
   * 指定されたファイルの、基底ファイルからの相対パスを取得し、それを
   * 親ディレクトリ・パスとファイル名に分けて取得する。
   * <br>
   * 返される{@link ts.util.Duo Duo}オブジェクトの
   * 第１要素に親ディレクトリ・パス、第２要素にファイル名が格納される。
   *
   * @param  file ファイル。
   * @param  baseFile 基底ファイル。
   * @return 相対パスの親ディレクトリ・パスとファイル名を格納した
   *           {@link ts.util.Duo Duo}オブジェクト。
   * @throws IOException 相対パスの取得に失敗した場合。
   * @throws AssertionError 引数がヌルの場合。
   */
  protected Duo<String,String> getRelativePaths(File file, File baseFile)
    throws IOException
  {
    assert (file != null) : "@param:file is null.";
    assert (baseFile != null) : "@param:baseFile is null.";

    String path = file.getCanonicalPath();
    String basePath = baseFile.getCanonicalPath();

    if (StringOperation.startsWith(path, basePath)) {
      int basePathLen = StringOperation.length(basePath);
      String relPath = StringOperation.substring(path, basePathLen);

      if (StringOperation.isEmpty(relPath)) {
        return new Duo<String,String>("", "");
      }
      else if (StringOperation.startsWith(relPath, File.separator)) {
        int sepLen = StringOperation.length(File.separator);
        int endIndex = StringOperation.lastIndexOf(relPath, File.separator);
        if (endIndex < 0) {
          return new Duo<String,String>("", relPath);
        }
        else if (endIndex == 0) {
          return new Duo<String,String>(
            "", StringOperation.substring(relPath, sepLen));
        }
        else {
          return new Duo<String,String>(
            StringOperation.substring(relPath, sepLen, endIndex),
            StringOperation.substring(relPath, endIndex + sepLen)
          );
        }
      }
    }

    throw new IllegalArgumentException(
      "@param:file is not a child path of @param:baseFile.");
  }
}

