package org.phosphoresce.commons.util;

import java.io.File;
import java.io.FileFilter;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.URI;
import java.nio.channels.FileChannel;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;

/**
 * ファイル関係ユーティリティクラス。<br>
 *
 * @author Kitagawa<br>
 *
 *<!--
 * 更新日		更新者			更新内容
 * 2005/05/24	Kitagawa		新規作成
 * 2007/07/14	Kitagawa		copy(File, File)メソッド追加
 * 2007/10/17	Kitagawa		replacaInvalidateChar(String)メソッド追加
 * 2008/11/21	Kitagawa		connectPathメソッド追加
 * 2009/06/25	Kitagawa		copyメソッドにおいてディレクトリの場合に再帰処理を行うように修正
 * 2009/06/25	Kitagawa		mkdirメソッド追加
 * 2009/06/25	Kitagawa		delete、copyメソッドにStringパラメータを持つメソッドを追加
 * 2009/06/25	Kitagawa		copy、deleteメソッドにおいてファイルフィルタを有効にするメソッドを追加
 * 2010/01/28	Kitagawa		getFileメソッド追加
 * 2010/04/06	Kitagawa		getDirectories、getFiles追加
 *-->
 */
public final class FileUtil {

	/**
	 * コンストラクタ<br>
	 */
	private FileUtil() {
		//
	}

	/**
	 * 指定したパスのディレクトリを作成します。<br>
	 * java.io.Fileクラスが持つメソッドですが、mkdir(String)との一貫性のために設けられました。<br>
	 * @param directory 作成するディレクトリパス
	 */
	public static void mkdir(File directory) {
		if (directory != null) {
			directory.mkdirs();
		}
	}

	/**
	 * 指定したパスのディレクトリを作成します。<br>
	 * @param directory 作成するディレクトリパス
	 */
	public static void mkdir(String directory) {
		if (!StringUtil.isEmpty(directory)) {
			mkdir(new File(directory));
		}
	}

	/**
	 * 指定されたファイルオブジェクトを削除します。<br>
	 * ファイルオブジェクトが保持する配下のリソースに対しても再帰的に削除します。<br>
	 * @param file 削除対象ファイルオブジェクト
	 * @param fileFilter 削除処理時のファイルフィルタオブジェクト
	 */
	public static void delete(File file, FileFilter fileFilter) {
		if (file != null) {
			if (file.isDirectory()) {
				File[] files = fileFilter != null ? file.listFiles(fileFilter) : file.listFiles();
				if (file.listFiles().length > 0) {
					for (int i = 0; file != null && i <= files.length - 1; i++) {
						delete(files[i--], fileFilter);
					}
					if (file.listFiles().length == 0) {
						file.delete();
					}
				} else {
					file.delete();
				}
			} else {
				file.delete();
			}
		}
	}

	/**
	 * 指定されたファイルオブジェクトを削除します。<br>
	 * ファイルオブジェクトが保持する配下のリソースに対しても再帰的に削除します。<br>
	 * @param file 削除対象ファイルオブジェクト
	 */
	public static void delete(File file) {
		delete(file, null);
	}

	/**
	 * 指定されたファイルオブジェクトを削除します。<br>
	 * ファイルオブジェクトが保持する配下のリソースに対しても再帰的に削除します。<br>
	 * @param file 削除対象ファイルオブジェクト
	 */
	public static void delete(String file) {
		if (!StringUtil.isEmpty(file)) {
			delete(new File(file));
		}
	}

	/**
	 * 指定されたファイルを指定されたファイルにコピーします。<br>
	 * @param source コピー元ファイルオブジェクト
	 * @param destination コピー先ファイルオブジェクト
	 * @param fileFilter コピー処理時のファイルフィルタオブジェクト
	 * @throws IOException コピー中に入出力例外が発生した場合にスローされます
	 */
	public static void copy(File source, File destination, FileFilter fileFilter) throws IOException {
		if (source == null) {
			return;
		}
		if (destination == null) {
			destination = new File("");
		}
		if (source.isDirectory()) {
			File[] files = fileFilter != null ? source.listFiles(fileFilter) : source.listFiles();
			for (int i = 0; i <= files.length - 1; i++) {
				File file = files[i];
				String fileRelative = file.getAbsolutePath().substring(source.getAbsolutePath().length());
				String destAbsolute = connectPath(destination.getAbsolutePath(), fileRelative);
				File destFile = new File(destAbsolute);
				if (file.isDirectory()) {
					destFile.mkdirs();
				}
				copy(file, destFile, fileFilter);
			}
		} else {
			if (fileFilter == null || fileFilter.accept(source)) {
				FileChannel src = new FileInputStream(source).getChannel();
				FileChannel dst = new FileOutputStream(destination).getChannel();
				dst.transferFrom(src, 0, src.size());
			}
		}
	}

	/**
	 * 指定されたファイルを指定されたファイルにコピーします。<br>
	 * @param source コピー元ファイルオブジェクト
	 * @param destination コピー先ファイルオブジェクト
	 * @throws IOException コピー中に入出力例外が発生した場合にスローされます
	 */
	public static void copy(File source, File destination) throws IOException {
		copy(source, destination, null);
	}

	/**
	 * 指定されたファイルを指定されたファイルにコピーします。<br>
	 * @param source コピー元ファイルオブジェクト
	 * @param destination コピー先ファイルオブジェクト
	 * @throws IOException コピー中に入出力例外が発生した場合にスローされます
	 */
	public static void copy(String source, String destination) throws IOException {
		if (StringUtil.isEmpty(source)) {
			return;
		}
		if (StringUtil.isEmpty(destination)) {
			destination = "";
		}
		copy(new File(source), new File(destination));
	}

	/**
	 * 指定されたファイルオブジェクトのディレクトリパスを取得します。<br>
	 * @param file ファイルオブジェクト
	 * @return ディレクトリパスを持つファイルオブジェクト
	 */
	public static File getDirectory(File file) {
		if (file.isDirectory()) {
			return new File(file.getAbsolutePath());
		} else {
			String path = file.getAbsolutePath();
			path = path.substring(0, path.lastIndexOf(File.separator));
			return new File(path);
		}
	}

	/**
	 * 指定されたファイル名に[x]を付加してファイル名のインクリメントを行います。<br>
	 * 指定されたファイル名が存在しない場合はそのまま返却します。<br>
	 * @param filename ファイル名
	 * @return インクリメント処理をしたファイル名
	 */
	public static String getAutoIncrementFilename(String filename) {
		File file = new File(filename);
		String incrementedFilename = filename;

		if (file.exists()) {
			if (file.isDirectory()) {
				int counter = 1;
				for (File check = new File(filename + " [" + counter + "]"); check.exists(); counter++)
					;
				incrementedFilename = filename + " [" + counter + "]";
			} else {
				String basename = filename;
				String extension = "";

				if (filename.lastIndexOf('.') > 0) {
					basename = filename.substring(0, filename.lastIndexOf('.'));
					extension = filename.substring(filename.lastIndexOf('.'));
				}

				int counter = 1;
				for (File check = new File(filename + " [" + counter + "]" + extension); check.exists(); counter++)
					;
				incrementedFilename = filename + " [" + counter + "]" + extension;
			}
		}

		return incrementedFilename;
	}

	/**
	 * 指定されたファイルが存在する場合、日付をポストフィクスとして付加し退避します。<br>
	 * @param filename 退避対象ファイル名
	 */
	public static void renameWithDatePostfix(String filename) {
		File file = new File(filename);

		if (file.exists()) {
			if (file.isDirectory()) {
				int counter = 1;
				String rename = filename + new SimpleDateFormat(".yyyyMMdd.hhmmss").format(new Date());
				for (File check = new File(rename); check.exists(); counter++) {
					rename = rename + " [" + counter + "]";
					check = new File(rename);
				}

				file.renameTo(new File(rename));
			} else {
				String basename = filename;
				String extension = "";

				if (filename.lastIndexOf('.') > 0) {
					basename = filename.substring(0, filename.lastIndexOf('.'));
					extension = filename.substring(filename.lastIndexOf('.'));
				}

				int counter = 1;
				String head = basename + new SimpleDateFormat(".yyyyMMdd.hhmmss").format(new Date());
				String rename = head;
				for (File check = new File(rename + extension); check.exists(); counter++) {
					rename = head + " [" + counter + "]";
					check = new File(rename + extension);
				}

				file.renameTo(new File(rename + extension));
			}
		}
	}

	/**
	 * 指定されたディレクトリパスに対して対象のパス(ファイルパス、ディレクトリパス)を接続します。<br>
	 * 但し、接続対象パスがnullの場合は元のディレクトリパス、元のディレクトリパスがnullの場合は
	 * ユーザーが指定した接続対象パスがそのまま返却されます。<br>
	 * また、接続対象パスがFile.separatorから始まるルートを示すパスである場合、接続元のパスは
	 * 無視された接続対象のパスがそのまま返却されます。<br>
	 * 当メソッドはパス接続時のファイルセパレータの補完処理を都度考慮せずに作業を行うために
	 * 設けられました。また、ファイルセパレータに関しては当メソッドを介すことで、
	 * プラットフォームに依存したパスに変換されます。即ちUnix環境にてパス指定が\を使用している場合、
	 * 提供されるパスセパレータは/となって返却されます。<br>
	 * <br>
	 * 動作例：<br>
	 * <pre>
	 * connectPath("foo", "bar") -> "foo/bar"
	 * connectPath("foo/", "bar") -> "foo/bar"
	 * connectPath("foo/", "/bar") -> "foo/bar"
	 * connectPath("foo/", "bar/") -> "foo/bar"
	 * </pre>
	 * @param directory 接続元ディレクトリ
	 * @param target 接続対象パス
	 * @return 接続されたパス
	 */
	public static String connectPath(String directory, String target) {
		if (directory == null) {
			return new File(target).getPath();
		}
		if (target == null) {
			return new File(directory).getPath();
		}

		File directoryFile = new File(directory);
		File targetFile = new File(target);
		String directoryPath = directoryFile.getPath();
		String targetPath = targetFile.getPath();

		if (directory.endsWith(File.separator) && targetPath.startsWith(File.separator)) {
			targetPath = targetPath.substring(1);
		}

		StringBuffer buffer = new StringBuffer();
		buffer.append(directoryPath);
		if (buffer.length() > 0 && !buffer.toString().endsWith(File.separator) && !targetPath.startsWith(File.separator)) {
			buffer.append(File.separator);
		}
		buffer.append(targetPath);

		return buffer.toString();
	}

	/**
	 * 指定されたパスが定義されているパス配列を全て接続して提供します。<br>
	 * 動作仕様についてはconnectPath(String, String)に依存します。<br>
	 * @param paths パス配列
	 * @return 接続されたパス
	 */
	public static String connectPath(String[] paths) {
		if (paths == null || paths.length == 0) {
			return "";
		}
		String render = "";
		for (int i = 0; i <= paths.length - 1; i++) {
			render = connectPath(render, paths[i]);
		}
		return render;
	}

	/**
	 * 指定されたファイル名から不正な文字を除去して返却します。<br>
	 * @param filename 補正対象ファイル名
	 * @return 補正後ファイル名
	 */
	public static String replacaInvalidateChar(String filename) {
		String buffer = filename;
		buffer = buffer.replaceAll("[\"]", "");
		//buffer = buffer.replaceAll("[=]", "");
		buffer = buffer.replaceAll("[|]", "");
		buffer = buffer.replaceAll("[\\]", "");
		//buffer = buffer.replaceAll("[;]", "");
		buffer = buffer.replaceAll("[:]", "");
		//buffer = buffer.replaceAll("[+]", "");
		buffer = buffer.replaceAll("[*]", "");
		buffer = buffer.replaceAll("[<]", "");
		buffer = buffer.replaceAll("[>]", "");
		//buffer = buffer.replaceAll("[.]", "");
		//buffer = buffer.replaceAll("[,]", "");
		buffer = buffer.replaceAll("[/]", "");
		buffer = buffer.replaceAll("[?]", "");
		return buffer;
	}

	/**
	 * 指定されたパスからファイルオブジェクトを取得します。<br>
	 * ここで指定できるパスはシステムパス又は、クラスリソースパスとなります。<br>
	 * @param path システムパス又は、クラスリソースパス
	 * @return ファイルオブジェクト
	 * @throws IOException 指定されたパスからファイルオブジェクト生成が行えなかった場合にスローされます
	 */
	public static File getFile(String path) throws IOException {
		try {
			//return new File(new File(ResourceUtil.getURL(path).toURI()).getAbsolutePath()); // <- after JDK1.5
			return new File(new File(new URI(ResourceUtil.getURL(path).toString())).getAbsolutePath());
		} catch (Throwable e) {
			if (e instanceof IOException) {
				throw (IOException) e;
			} else {
				throw new IOException("failed to create File object");
			}
		}
	}

	/**
	 * 指定された基底パス配下のディレクトリを取得します。<br>
	 * 提供されるディレクトリ配列には基底パスとして指定されたディレクトリ自体も含まれます。<br>
	 * @param base 基底パス
	 * @param filter 検索フィルタ
	 * @param hierarchical 階層検索フラグ
	 * @return 基底パス配下のディレクトリ
	 */
	public static File[] getDirectories(File base, final FileFilter filter, boolean hierarchical) {
		if (base == null) {
			throw new NullPointerException("base");
		}
		File[] dirs = base.listFiles(new FileFilter() {

			public boolean accept(File target) {
				if (filter == null) {
					return target.isDirectory();
				} else {
					return target.isDirectory() && filter.accept(target);
				}
			}
		});
		List result = new LinkedList();
		result.add(base);
		if (dirs != null) {
			result.addAll(Arrays.asList(dirs));
			if (hierarchical) {
				for (int i = 0; i <= dirs.length - 1; i++) {
					//list.addAll(Arrays.asList(getDirectories(dirs[i], hierarchical)));
					for (Iterator iterator = Arrays.asList(getDirectories(dirs[i], filter, hierarchical)).iterator(); iterator.hasNext();) {
						File file = (File) iterator.next();
						if (!result.contains(file)) {
							result.add(file);
						}
					}
				}
			}
		}
		return (File[]) result.toArray(new File[0]);
	}

	/**
	 * 指定された基底パス配下のディレクトリを取得します。<br>
	 * 提供されるディレクトリ配列には基底パスとして指定されたディレクトリ自体も含まれます。<br>
	 * @param base 基底パス
	 * @param hierarchical 階層検索フラグ
	 * @return 基底パス配下のディレクトリ
	 */
	public static File[] getDirectories(File base, boolean hierarchical) {
		return getDirectories(base, null, hierarchical);
	}

	/**
	 * 指定された基底パス配下のファイルを取得します。<br>
	 * @param base 基底パス
	 * @param filter 検索フィルタ
	 * @param hierarchical 階層検索フラグ
	 * @return 基底パス配下のディレクトリ
	 */
	public static File[] getFiles(File base, FileFilter filter, boolean hierarchical) {
		if (base == null) {
			throw new NullPointerException("base");
		}
		if (base.isFile()) {
			return new File[] { base };
		}
		File[] files = base.listFiles(filter);
		if (!hierarchical) {
			return files;
		}
		List list = new LinkedList();
		File[] dirs = getDirectories(base, true);
		if (dirs != null) {
			for (int i = 0; i <= dirs.length - 1; i++) {
				File[] childs = getFiles(dirs[i], filter, false);
				if (childs != null) {
					for (int j = 0; j <= childs.length - 1; j++) {
						File child = childs[j];
						if (child.isFile() && !list.contains(child)) {
							list.add(child);
						}
					}
				}
			}
		}
		return (File[]) list.toArray(new File[0]);
	}

	/**
	 * 指定された基底パス配下のファイルを取得します。<br>
	 * @param base 基底パス
	 * @param hierarchical 階層検索フラグ
	 * @return 基底パス配下のディレクトリ
	 */
	public static File[] getFiles(File base, boolean hierarchical) {
		return getFiles(base, null, hierarchical);
	}
}
