/*
 * Copyright (c) 2009 The openGion Project.
 *
 * Licensed 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.opengion.fukurou.util;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.io.UnsupportedEncodingException;
import java.io.Writer;
// import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

/**
 * FileUtil.java は、共通的に使用される File関連メソッドを集約した、クラスです。
 *
 * 全変数は、public static final 宣言されており、全メソッドは、public static synchronized 宣言されています。
 *
 * @version  4.0
 * @author	 Kazuhiko Hasegawa
 * @since    JDK5.0,
 */
public final class FileUtil {
	private final static NonClosePrintWriter outWriter = new NonClosePrintWriter( System.out );
	private final static NonClosePrintWriter errWriter = new NonClosePrintWriter( System.err );

	/**
	 * すべてが staticメソッドなので、コンストラクタを呼び出さなくしておきます。
	 *
	 */
	private FileUtil() {}

	/** システム依存の改行記号をセットします。	*/
	private static final String CR = System.getProperty("line.separator");

	/**
	 * Fileオブジェクトとエンコードより PrintWriterオブジェクトを作成します。
	 *
	 * @param	file File 出力するファイルオブジェクト
	 * @param	encode String ファイルのエンコード
	 * @return	PrintWriterオブジェクト
	 * @throws RuntimeException 何らかのエラーが発生した場合
	 */
	public static PrintWriter getPrintWriter( final File file,final String encode ) {
		return getPrintWriter( file,encode,false );
	}

	/**
	 * Fileオブジェクトとエンコードより PrintWriterオブジェクトを作成します。
	 *
	 * @param	file File 出力するファイルオブジェクト
	 * @param	encode String ファイルのエンコード
	 * @param	append boolean ファイルを追加モード(true)にするかどうか
	 * @return	PrintWriterオブジェクト
	 * @throws RuntimeException 何らかのエラーが発生した場合
	 */
	public static PrintWriter getPrintWriter( final File file,final String encode,final boolean append ) {
		final PrintWriter writer ;

		try {
			writer = new PrintWriter(new BufferedWriter(new OutputStreamWriter(
					new FileOutputStream(file,append) ,encode )));
		}
		catch( UnsupportedEncodingException ex ) {
			String errMsg = "指定されたエンコーディングがサポートされていません。" + CR
							+ ex.getMessage() + CR
							+ "File=[" + file + " , encode=[" + encode + "]" ;
			throw new RuntimeException( errMsg,ex );
		}
		catch( FileNotFoundException ex ) {		// 3.6.1.0 (2005/01/05)
			String errMsg = "ファイル名がオープン出来ませんでした。" + CR
							+ ex.getMessage() + CR
							+ "File=[" + file + " , encode=[" + encode + "]" ;
			throw new RuntimeException( errMsg,ex );
		}

		return writer ;
	}

	/**
	 * ファイル名より、PrintWriterオブジェクトを作成する簡易メソッドです。
	 *
	 * これは、ファイル名は、フルパスで、追加モードで、UTF-8 エンコードの
	 * ログファイルを出力する場合に使用します。
	 * また、ファイル名に、"System.out" と、"System.err" を指定できます。
	 * その場合は、標準出力、または、標準エラー出力に出力されます。
	 * "System.out" と、"System.err" を指定した場合は、NonClosePrintWriter
	 * オブジェクトが返されます。これは、close() 処理が呼ばれても、何もしない
	 * クラスです。また、常に内部キャッシュの同じオブジェクトが返されます。
	 *
	 * @param	file String 出力するファイル名
	 * @throws RuntimeException 何らかのエラーが発生した場合
	 * @throws IllegalArgumentException ファイル名が null の場合
	 */
	public static PrintWriter getLogWriter( final String file ) {
		if( file == null ) {
			String errMsg = "ファイル名に、null は指定できません。";
			throw new IllegalArgumentException( errMsg );
		}

		final PrintWriter writer ;
		if( "System.out".equalsIgnoreCase( file ) ) {
			writer = outWriter ;
		}
		else if( "System.err".equalsIgnoreCase( file ) ) {
			writer = errWriter ;
		}
		else {
			writer = getPrintWriter( new File( file ),"UTF-8",true );
		}

		return writer ;
	}

	/**
	 * PrintWriter を継承した、JspWriterなどの Writer 用のクラスを定義します。
	 *
	 * 例えば、JspWriterなどの JSP/Servlet等のフレームワークで使用される
	 * Writer では、flush や close 処理は、フレームワーク内で行われます。
	 * その場合、通常のファイルと同じ用に、flush や close をアプリケーション側で
	 * 行うと、内部処理的に不整合が発生したり、最悪の場合エラーになります。
	 * このクラスは、NonFlushPrintWriter クラスのオブジェクトを返します。
	 * これは、通常の、new PrintWriter( Writer ) で、求めるのと、ほとんど同様の
	 * 処理を行いますが、close() と flush() メソッドが呼ばれても、何もしません。
	 *
	 * @param	writer Writer 出力するWriteオブジェクト(NonFlushPrintWriterクラス)
	 */
	public static PrintWriter getNonFlushPrintWriter( final Writer writer ) {
		return new NonFlushPrintWriter( writer );
	}

	/**
	 * Fileオブジェクトとエンコードより BufferedReaderオブジェクトを作成します。
	 *
	 * @param	file File 入力するファイルオブジェクト
	 * @param	encode String ファイルのエンコード
	 * @return	BufferedReaderオブジェクト
	 * @throws RuntimeException 何らかのエラーが発生した場合
	 */
	public static BufferedReader getBufferedReader( final File file,final String encode ) {
		final BufferedReader reader ;

		try {
			reader = new BufferedReader(new InputStreamReader(
							new FileInputStream( file ) ,encode ));
		}
		catch( UnsupportedEncodingException ex ) {
			String errMsg = "指定されたエンコーディングがサポートされていません。" + CR
							+ ex.getMessage() + CR
							+ "FIle=[" + file + " , encode=[" + encode + "]" ;
			throw new RuntimeException( errMsg,ex );
		}
		catch( FileNotFoundException ex ) {
			String errMsg = "ファイル名がオープン出来ませんでした。" + CR
							+ ex.getMessage() + CR
							+ "FIle=[" + file + " , encode=[" + encode + "]" ;
			throw new RuntimeException( errMsg,ex );
		}

		return reader ;
	}

	/**
	 * 指定のファイル名が、実際に存在しているかどうかをチェックします。
	 * 存在しない場合は、２秒毎に、３回確認します。
	 * それでも存在しない場合は、エラーを返します。
	 * return されるFileオブジェクトは、正規の形式(CanonicalFile)です。
	 *
	 * @param  dir String
	 * @param  filename String
	 * @return File (なければ null/あれば、CanonicalFile)
	 */
	public static File checkFile( final String dir, final String filename ) {
		return checkFile( dir,filename,3 );
	}

	/**
	 * 指定のファイル名が、実際に存在しているかどうかをチェックします。
	 * 存在しない場合は、２秒毎に、指定の回数分確認します。
	 * それでも存在しない場合は、エラーを返します。
	 * return されるFileオブジェクトは、正規の形式(CanonicalFile)です。
	 *
	 * @param  dir String
	 * @param  filename String
	 * @param  count int
	 * @return File (なければ null/あれば、CanonicalFile)
	 */
	public static File checkFile( final String dir, final String filename,final int count ) {
		File file = null;

		int cnt = count;
		while( cnt > 0 ) {
			file = new File( dir,filename );
			if( file.exists() ) { break; }
			else {
				if( cnt == 1 ) { return null; }		// 残り１回の場合は、２秒待機せずに即抜ける。
				try { Thread.sleep( 2000 );	}	// ２秒待機
				catch ( InterruptedException ex ) {
					System.out.println( "InterruptedException" );
				}
				System.out.println();
				System.out.print( "CHECK File Error! CNT=" + cnt );
				System.out.print( " File=" + file.getAbsolutePath() );
			}
			cnt--;
		}

		// ファイルの正式パス名の取得
		try {
			return file.getCanonicalFile() ;
		}
		catch( IOException ex ) {
			String errMsg = "ファイルの正式パス名が取得できません。[" + file.getAbsolutePath() + "]";
			throw new RuntimeException( errMsg,ex );
		}
	}

	/**
	 * ファイルのコピーを行います。
	 *
	 * このファイルコピーは、バイナリコピーです。
	 *
	 * @og.rev 4.2.3.0 (2008/05/26) changeCrLf 属性対応
	 *
	 * @param  fromFile String
	 * @param  toFile   String
	 */
	public static void copy( final String fromFile,final String toFile ) {
		copy( new File( fromFile ), new File( toFile ), false );
	}

	/**
	 * ファイルのコピーを行います。
	 *
	 * このファイルコピーは、バイナリコピーです。
	 * changeCrLf = true の場合は、バイナリファイルの 改行コードを
	 * CR+LF に統一します。また、UTF-8 の BOM(0xef,0xbb,0xbf) があれば、
	 * 取り除きます。
	 *
	 * @og.rev 4.2.3.0 (2008/05/26) changeCrLf 属性対応
	 *
	 * @param  fromFile   String
	 * @param  toFile     String
	 * @param  changeCrLf boolean
	 */
	public static void copy( final String fromFile,final String toFile,final boolean changeCrLf ) {
		copy( new File( fromFile ), new File( toFile ), changeCrLf );
	}

	/**
	 * ファイルのコピーを行います。
	 *
	 * このファイルコピーは、バイナリコピーです。
	 *
	 * @og.rev 4.2.3.0 (2008/05/26) changeCrLf 属性対応
	 *
	 * @param  fromFile File
	 * @param  toFile   File
	 */
	public static void copy( final File fromFile,final File toFile ) {
		copy( fromFile, toFile, false );
	}

	private static final byte B_CR = (byte)0x0d ;	// '\r'
	private static final byte B_LF = (byte)0x0a ;	// '\n'

	/**
	 * ファイルのコピーを行います。
	 *
	 * このファイルコピーは、バイナリコピーです。
	 * changeCrLf = true の場合は、バイナリファイルの 改行コードを
	 * CR+LF に統一します。また、UTF-8 の BOM(0xef,0xbb,0xbf) があれば、
	 * 取り除きます。
	 *
	 * @og.rev 4.2.3.0 (2008/05/26) changeCrLf 属性対応
	 *
	 * @param  fromFile File
	 * @param  toFile   File
	 * @param  changeCrLf boolean
	 */
	public static void copy( final File fromFile,final File toFile,final boolean changeCrLf ) {
		BufferedInputStream 	fromStream = null;
		BufferedOutputStream	toStream   = null;
		File tempToFile = toFile ;
		try {
			// ディレクトリの場合は、そのパスでファイル名をfromFileから取り出す。
			if( toFile.isDirectory() ) {
				tempToFile = new File( toFile,fromFile.getName() );
			}
			fromStream = new BufferedInputStream( new FileInputStream( fromFile ) );
			toStream   = new BufferedOutputStream( new FileOutputStream( tempToFile ) );

			int BUFSIZE = 8192 ;
			byte[] buf = new byte[BUFSIZE];
			int len ;
			// 4.2.3.0 (2008/05/26) changeCrLf 属性対応
			if( changeCrLf ) {
				boolean bomCheck = true;	// 最初の一回だけ、ＢＯＭチェックを行う。
				byte    bt = (byte)0x00;	// バッファの最後と最初の比較時に使用
				while( (len = fromStream.read(buf,0,BUFSIZE)) != -1 ) {
					int st = 0;
					if( bomCheck && len >= 3 &&
						buf[0] == (byte)0xef &&
						buf[1] == (byte)0xbb &&
						buf[2] == (byte)0xbf  ) {
							st = 3;
					}
					else {
						// バッファの最後が CR で、先頭が LF の場合、LF をパスします。
						if( bt == B_CR && buf[0] == B_LF ) {
							st = 1 ;
						}
					}
					bomCheck = false;

					for( int i=st;i<len;i++ ) {
						bt = buf[i] ;
						if( bt == B_CR || bt == B_LF ) {
							toStream.write( (int)B_CR );		// CR
							toStream.write( (int)B_LF );		// LF
							// CR+LF の場合
							if( bt == B_CR && i+1 < len && buf[i+1] == B_LF ) {
								i++;
								bt = buf[i] ;
							}
						}
						else {
							toStream.write( (int)bt );
						}
					}
				}
				// 最後が改行コードでなければ、改行コードを追加します。
				// テキストコピーとの互換性のため
				if( bt != B_CR && bt != B_LF ) {
					toStream.write( (int)B_CR );		// CR
					toStream.write( (int)B_LF );		// LF
				}
			}
			else {
				while( (len = fromStream.read(buf,0,BUFSIZE)) != -1 ) {
					toStream.write( buf,0,len );
				}
			}
		}
		catch ( IOException e ) { System.out.println(e.getMessage()); }
		finally {
			Closer.ioClose( fromStream ) ;
			Closer.ioClose( toStream ) ;
		}
	}

	/**
	 * 再帰処理でディレクトリのコピーを行います。
	 *
	 * @og.rev 4.3.0.0 (2008/07/24) 追加
	 * @param fromDirectry 
	 * @param toDirectry 
	 */
	public static void copyDirectry( final String fromDirectry, final String toDirectry ) {
		copyDirectry( new File( fromDirectry ), new File( toDirectry ) );
	}

	/**
	 * 再帰処理でディレクトリをコピーします。
	 * 
	 * @og.rev 4.3.0.0 (2008/07/24) 追加
	 * @param fromDirectry 
	 * @param toDirectry 
	 * @return boolean 指定されたコピー元ディレクトリがディレクトリでなかったり存在しないときはfalseを返す。
	 * @throws IOException 
	 */
	public static boolean copyDirectry( final File fromDirectry, final File toDirectry ) {
		// コピー元がディレクトリでない場合はfalseを返す
		// 4.3.4.4 (2009/01/01)
		if( !fromDirectry.exists() || !fromDirectry.isDirectory() ) { return false; }

		// 4.3.4.4 (2009/01/01)
		boolean flag = true;
		if( !toDirectry.exists() ) {
			// ディレクトリを作成する
			flag = toDirectry.mkdirs();
			if( ! flag ) { System.err.println( toDirectry.getName() + " の ディレクトリ作成に失敗しました。" ); }
		}

		// ディレクトリ内のファイルをすべて取得する
		File[] files = fromDirectry.listFiles();

		// ディレクトリ内のファイルに対しコピー処理を行う
		for( int i = 0; files.length>i; i++ ){
			if( files[i].isDirectory() ){ // ディレクトリだった場合は再帰呼び出しを行う
				copyDirectry(
				new File( fromDirectry.toString(), files[i].getName() ), 
				new File( toDirectry.toString(), files[i].getName()));
			}
			else{ // ファイルだった場合はファイルコピー処理を行う
				copy(
				new File( fromDirectry.toString(), files[i].getName() ), 
				new File( toDirectry.toString(), files[i].getName()) );			
			}			
		}
		return true;
	}

	/**
	 * 指定されたファイル及びディレクトを削除します。
	 * ディレクトリの場合はサブフォルダ及びファイルも削除します。
	 * １つでもファイルの削除に失敗した場合、その時点で処理を中断しfalseを返します。
	 * 
	 * @param file
	 * @return ファイルの削除に成功したか
	 */
	public static boolean deleteFiles( final File file ) {
		if( file.exists() ) {
			if( file.isDirectory() ) {
				File[] list = file.listFiles();
				for( int i=0; i<list.length; i++ ) {
					deleteFiles( list[i] );
				}
			}
			if( !file.delete() ) { return false; }
		}
		return true;
	}

	/**
	 * 指定されたディレクトリを基点としたファイル名(パスを含む)の一覧を返します。
	 * 
	 * @og.rev 4.3.6.6 (2009/05/15) 新規作成
	 * 
	 * @param dir 基点となるディレクトリ
	 * @param sort ファイル名でソートするか
	 * @param list ファイル名一覧を格納するList
	 */
	public static void getFileList( final File dir, final boolean sort, final List<String> list ) {
		if( list == null ) { return; }
		if( dir.isFile() ) {
			list.add( dir.getAbsolutePath() );
		}
		else if( dir.isDirectory() ) {
			File[] files = dir.listFiles();
			for( int i=0; i<files.length; i++ ) {
				getFileList( files[i], sort, list );
			}
		}
		if( sort ) {
			Collections.sort( list );
		}
	}

	/**
	 * PrintWriter を継承した、System.out/System.err 用のクラスを定義します。
	 *
	 * 通常の、new PrintWriter( OutputStream ) で、求めるのと、ほとんど同様の
	 * 処理を行います。
	 * ただ、close() メソッドが呼ばれても、何もしません。
	 *
	 */
	private static final class NonClosePrintWriter extends PrintWriter {
		/**
		 * コンストラクター
		 *
		 * new PrintWriter( OutputStream ) を行います。
		 *
		 * @param out OutputStream
		 */
		public NonClosePrintWriter( final OutputStream out ) {
			super( out );
		}

		/**
		 * close() メソッドをオーバーライドします。
		 *
		 * 何もしません。
		 *
		 */
		public void close() {
			// ここでは処理を行いません。
		}
	}

	/**
	 * PrintWriter を継承した、JspWriterなどの Writer 用のクラスを定義します。
	 *
	 * 例えば、JspWriterなどの JSP/Servlet等のフレームワークで使用される
	 * Writer では、flush や close 処理は、フレームワーク内で行われます。
	 * その場合、通常のファイルと同じ用に、flush や close をアプリケーション側で
	 * 行うと、内部処理的に不整合が発生したり、最悪の場合エラーになります。
	 * このクラスは、単に、通常の、new PrintWriter( Writer ) で、求めるのと、
	 * ほとんど同様の処理を行います。
	 * ただ、close() と flush() メソッドが呼ばれても、何もしません。
	 *
	 */
	private static final class NonFlushPrintWriter extends PrintWriter {
		/**
		 * コンストラクター
		 *
		 * new PrintWriter( Writer ) を行います。
		 *
		 * @param writer Writer
		 */
		public NonFlushPrintWriter( final Writer writer ) {
			super( writer );
		}

		/**
		 * close() メソッドをオーバーライドします。
		 *
		 * 何もしません。
		 *
		 */
		public void close() {
			// ここでは処理を行いません。
		}

		/**
		 * flush() メソッドをオーバーライドします。
		 *
		 * 何もしません。
		 *
		 */
		public void flush() {
			// ここでは処理を行いません。
		}
	}

	/**
	 * ファイルをコピーします。<br />
	 *
	 * 引数に <file1> <encode1> <file2> <encode2> を指定します。
	 * file1 を読み込み、file2 にコピーします。
	 * file2 がすでに存在した場合は、削除されます。
	 * file1 と file2 が同じ場合は、動作しません。
	 *
	 * @og.rev 4.0.0.0 (2007/11/28) メソッドの戻り値をチェックします。
	 *
	 * @param	args 引数配列 <file1> <encode1> <file2> <encode2>
	 */
	public static void main( final String[] args ) throws Throwable {
		if( args.length != 4 ) {
			LogWriter.log("Usage: java FileUtil <file1> <encode1> <file2> <encode2>" );
			return ;
		}

		File   file1   = new File( args[0] );
		String encode1 = args[1];
		File   file2   = new File( args[2] );
		String encode2 = args[3];

		File tempFile = new File( args[2] + "_backup" );

		BufferedReader reader = FileUtil.getBufferedReader( file1 ,encode1 );
		PrintWriter    writer = FileUtil.getPrintWriter( tempFile ,encode2 );

		try {
			String line1;
			while((line1 = reader.readLine()) != null) {
				writer.println( line1 );
			}
		}
		finally {
			Closer.ioClose( reader ) ;
			Closer.ioClose( writer ) ;
		}

		if( !file2.delete() ) {
			String errMsg = "所定のファイルを削除できませんでした。[" + file2 + "]" ;
			throw new RuntimeException( errMsg );
		}
		if( !tempFile.renameTo( file2 ) ) {
			String errMsg = "所定のファイルをリネームできませんでした。[" + tempFile + "]" ;
			throw new RuntimeException( errMsg );
		}
	}
}
