/*
 * FilePathPatternFilter 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 ts.util.text.StringCutter;
import ts.util.text.StringOperation;
import java.io.File;
import java.io.FileFilter;
import java.io.IOException;
import java.util.Iterator;
import java.util.List;
import java.util.LinkedList;

/**
 * ファイル・パス・パターンを条件とするファイル・フィルタ・クラス。
 *
 * @author  佐藤隆之.
 * @version $Revision: 1.1.1.1 $, $Date: 2010-10-16 00:03:47 $
 */
public class FilePathPatternFilter implements FileFilter
{
  /** ベース・ディレクトリ・パス。 */
  private String basePath_ ;

  /**
   * パターン文字列を二重アスタリスク、パス区切り文字、アスタリスクで分割した
   * 要素を格納するリスト。
   */
  private List<List<List<String>>> pathPatternLLL_ ;

  /**
   * パス・パターン文字列とベース・ディレクトリを引数にとるコンストラクタ。
   *
   * @param  baseDir ベース・ディレクトリ。
   * @param  pathPattern パス・パターン文字列。
   * @throws AssertionError 引数がヌルの場合（デバッグ・モードのみ）。
   * @throws IllegalArgumentException <tt>baseDir</tt>の示すファイルがディレク
   *           トリでなかった場合。又はパス・パターン文字列が不正な場合。
   */
  public FilePathPatternFilter(File baseDir, String pathPattern)
    throws IllegalArgumentException
  {
    assert (baseDir != null) : "@param:baseDir is null.";

    if (! baseDir.isDirectory()) {
      throw new IllegalArgumentException("@param:baseDir is not directory.");
    }

    try {
      basePath_ = baseDir.getCanonicalPath();
      if (! StringOperation.endsWith(basePath_, File.separator)) {
        basePath_ += File.separator;
      }
    }
    catch (IOException e) {
      IllegalArgumentException exc = new IllegalArgumentException(e);
      exc.setStackTrace(e.getStackTrace());
      throw exc;
    }

    pathPatternLLL_ = parsePathPattern(pathPattern);
  }

  /**
   * パス・パターン文字列を解析して、内部データとして設定する。
   *
   * @param  pathPattern パス・パターン文字列。
   * @throws IllegalArgumentException　パス・パターン文字列が不正な場合。
   */
  private List<List<List<String>>> parsePathPattern(String pathPattern)
    throws IllegalArgumentException
  {
    assert (pathPattern != null) : "@param:pathPattern is null.";

    if (StringOperation.startsWith(pathPattern, "/")) {
      throw new IllegalArgumentException("@param:pathPattern must not start " +
        "with '/', because it is relative path.");
    }

    List<String> patternL = StringCutter.split(pathPattern, "/");
    return splitWithAsterisk(patternL);
  }

  /** 
   * ディレクトリ階層ごとに分割されたパス・パターン文字列のリストを、
   * 二重アスタリスクの要素を区切りとしてグルーピングする。
   * また、パス・パターン文字列自体も、アスタリスクを区切り文字として分割した
   * 文字列リストに変換する。
   *
   * @param  patternL　ディレクトリ階層ごとに分割されたパス・パターン文字列の
   *           リスト。
   * @return パス・パターンをアスタリスクで分割したリスト。
   * @throws NullPointerException 引数がヌルの場合。
   * @throws IllegalArgumentException 二重アスタリスクがファイル名に対する
   *           ワイルド・カードとして使用されていた場合。
   */
  private List<List<List<String>>> splitWithAsterisk(List<String> patternL)
  {
    List<List<List<String>>> patternLLL = new LinkedList<List<List<String>>>();
    List<List<String>> patternLL = new LinkedList<List<String>>();
    patternLLL.add(patternLL);

    int bgn = 0;
    for (String pattern : patternL) {
      if (StringOperation.isEmpty(pattern)) {
        throw new IllegalArgumentException(
          "Specified path pattern includes empty path elements.");
      }
      else if (StringOperation.contains(pattern, "**")) {
        if (pattern.equals("**")) {
          patternLL = new LinkedList<List<String>>();
          patternLLL.add(patternLL);
        }
        else {
          throw new IllegalArgumentException(
            "Wild card '**' cannot be used for a file name.");
        }
      }
      else {
        patternLL.add(StringCutter.split(pattern, "*"));
      }
    }

    return patternLLL;
  }

  /**
   * 指定されたファイルのパスが、このオブジェクトの示すパス・パターンに合致する
   * かどうかを判定する。
   *
   * @param  file 判定対象のファイル。
   * @return ファイルのパスがパス・パターンに合致する場合は<tt>true</tt>を返す。
   * @throws AssertionError 引数がヌルの場合（デバッグ・モードのみ）。
   */
  public boolean accept(File file)
  {
    assert (file != null) : "@param:file is null.";

    try {
      String absPath = file.getCanonicalPath();
      if (! StringOperation.startsWith(absPath, basePath_)) {
        return false;
      }
      int basePathLen = StringOperation.length(basePath_);
      String relPath = StringOperation.substring(absPath, basePathLen);
      relPath = relPath.replace(File.separator, "/");
      return matchPattern(relPath, pathPatternLLL_);
    }
    catch (Exception e) {
      return false;
    }
  }

  /**
   * ファイル・パスが、パターン・リストに合致するかどうかを判定する。
   *
   * @param  relativePath ベース・ディレクトリからの相対パス。
   * @param  patternLLL パターン文字列を分割したリスト。
   * @return 合致する場合は<tt>true</tt>を返す。
   */
  private boolean matchPattern(
    String relativePath, List<List<List<String>>> patternLLL)
  {
    List<String> pathL = StringCutter.split(relativePath, "/");

    Iterator<List<List<String>>> it = patternLLL.iterator();
    if (it.hasNext()) {
      List<List<String>> patternLL = it.next();
      if (! matchPatternElement(pathL, 0, patternLL)) {
        return false;
      }
      int pathIndex = patternLL.size();

      L1:while (it.hasNext()) {
        patternLL = it.next();
        if (patternLL.size() == 0) {
          continue L1;
        }

        while (pathIndex < pathL.size()) {
          if (matchPatternElement(pathL, pathIndex, patternLL)) {
            pathIndex += patternLL.size();
            continue L1;
          }
          else {
            pathIndex ++;
          }
        }

        return false;
      }

      if (pathIndex < pathL.size()) {
        if (patternLL.size() != 0) {
          return false;
        }
      }

      return true;
    }
    
    return false;
  }

  /**
   * ディレクトリ階層ごとに分割されたパス・リストの要素と、ディレクトリ、アス
   * タリスクごとに分割されたパターン・リストが合致するかどうかを判定する。
   *
   * @param  pathL ディレクトリ階層ごとに分割されたパス・リスト。
   * @param  pathIndex パス・リストのインデックス。
   * @param  patternLL ディレクトリ、アスタリスクごとに分割されたパターン・
   *           リスト。
   * @return 合致する場合は<tt>true</tt>を返す。
   */
  private boolean matchPatternElement(List<String> pathL, int pathIndex,
    List<List<String>> patternLL)
  {
    if (patternLL.size() + pathIndex > pathL.size()) {
      return false;
    }

    for (int i=0; i<patternLL.size(); i++) {
      String path = pathL.get(i + pathIndex);
      List<String> patternL = patternLL.get(i); 

      Iterator<String> patternIt = patternL.iterator();
      if (! patternIt.hasNext()) {
        return StringOperation.isEmpty(path) ? true : false;
      }

      int k = 0;
      String pattern = patternIt.next();
      if (! StringOperation.isEmpty(pattern)) {
        if (! StringOperation.startsWith(path, pattern)) {
          return false;
        }
        k = StringOperation.length(pattern);
      }
      else if (! patternIt.hasNext()) {
        return false;
      }

      while (patternIt.hasNext()) {
        pattern = patternIt.next();
        k = StringOperation.indexOf(path, pattern, k);
        if (k < 0) {
          return false;
        }
        k += StringOperation.length(pattern);
      }

      if (! StringOperation.isEmpty(pattern)) {
        if (k != StringOperation.length(path)) {
          return false;
        }
      }
    }

    return true;
  }
}
