/*
 * 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.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream;

/**
 * ZipFileUtil.java は、ZIPファイルの解凍・圧縮を行うためのUtilクラスです。
 *
 * @og.group ユーティリティ
 *
 * @version  4.1
 * @author	 Hiroki Nakamura
 * @since    JDK5.0,
 */
public final class ZipFileUtil {

	/** ファイル読み込み時のバッファサイズ */
	private static final int	BUF_SIZE	= 1024;

	/**
	 * 全てスタティックメソッドのためインスタンスの作成を禁止します。
	 */
	private ZipFileUtil() {};

	/**
	 * 引数に指定されたZIPファイルをフォルダに解凍します。
	 * 解凍先のファイルが存在する場合でも、上書きされますので注意下さい。
	 *
	 * @og.rev 4.1.0.2 (2008/02/01) 新規追加
	 * @og.rev 4.3.1.1 (2008/08/23) mkdirs の戻り値判定
	 * @og.rev 4.3.3.3 (2008/10/22) mkdirsする前に存在チェック
	 * @og.rev 5.1.9.0 (2010/08/01) 更新時刻の設定
	 *
	 * @param targetPath 解凍先のフォルダ
	 * @param zipFileName 解凍するZIPファイル
	 *
	 * @return 解凍されたZIPファイルの一覧
	 */
	public static List<String> unCompress( final String targetPath, final String zipFileName ) {
		// 解凍先フォルダの末尾が'/'又は'\'でなければ区切り文字を挿入
		String tmpPrefix = targetPath;
		if( File.separatorChar != targetPath.charAt( targetPath.length() - 1 ) ) {
			tmpPrefix = tmpPrefix + File.separator;
		}

		List<String> list = new ArrayList<String>();
		ZipInputStream zis = null;
		ZipEntry entry = null;
		BufferedOutputStream out = null;
		String fileName = null;
		File tmpFile = null;
		try {
			zis = new ZipInputStream( new BufferedInputStream( new FileInputStream( zipFileName ) ) );

			while( ( entry = zis.getNextEntry() ) != null ) {
				fileName = tmpPrefix + entry.getName().replace( '/', File.separatorChar );
				list.add( fileName );

				boolean flag = true;	// 4.3.1.1 (2008/08/23) mkdirs の戻り値判定
				tmpFile = new File( fileName );
				// ディレクトリの場合は、自身を含むディレクトリを作成
				if( entry.isDirectory() ) {
					// 4.3.3.3 (2008/10/22) 作成する前に存在チェック
					if( !tmpFile.exists() ) {
						flag = tmpFile.mkdirs();
					}
				}
				// ディレクトリの場合は、自身の親となるディレクトリを作成
				else {
					// 4.3.3.3 (2008/10/22) 作成する前に存在チェック
					if( !tmpFile.getParentFile().exists() ) {
						flag = new File( fileName ).getParentFile().mkdirs();
					}

					out = new BufferedOutputStream( new FileOutputStream( fileName ) );
					byte[] buf = new byte[BUF_SIZE];
					int count = 0;
					while( ( count = zis.read( buf ) ) != -1 ) {
						out.write( buf, 0, count );
					}
					out.close();
				}
				// 4.3.1.1 (2008/08/23) mkdirs の戻り値判定
				if( ! flag ) { System.err.println( fileName + " の ディレクトリ作成に失敗しました。" ); }
				// 5.1.9.0 (2010/08/01) 更新時刻の設定
				long lastTime = entry.getTime();
				if( lastTime >= 0 ) {
					flag = tmpFile.setLastModified( lastTime );
				}
				if( ! flag ) { System.err.println( fileName + " の 更新時刻の設定に失敗しました。" ); }
			}
		}
		catch( FileNotFoundException ex ) {
			String errMsg = "解凍ファイルが作成できません。[ファイル名=" + fileName + "]";
			throw new RuntimeException( errMsg, ex );
		}
		catch( IOException ex ) {
			String errMsg = "ZIPファイルの解凍に失敗しました。[ファイル名=" + fileName + "]";
			throw new RuntimeException( errMsg, ex );
		}
		finally {
			Closer.ioClose( zis );
			Closer.ioClose( out );
		}

		return list;
	}

	/**
	 * 引数に指定されたファイル又はフィルダ内に存在するファイルをZIPファイルに圧縮します。
	 * 圧縮レベルはデフォルトのDEFAULT_COMPRESSIONです。
	 * 圧縮ファイルのエントリー情報として本来は、圧縮前後のファイルサイズ、変更日時、CRCを登録する
	 * 必要がありますが、ここでは高速化のため、設定していません。(特に圧縮後ファイルサイズの取得は、
	 * 非常に不可がかかる。)
	 * このため、一部のアーカイバでは正しく解凍できない可能性があります。
	 * 既にZIPファイルが存在する場合でも、上書きされますので注意下さい。
	 *
	 * @og.rev 4.1.0.2 (2008/02/01) 新規追加
	 *
	 * @param targetPath 圧縮対象のファイル又はフォルダ
	 * @param zipFileName ZIPファイル名
	 *
	 * @return ZIPファイルのエントリーファイル名一覧
	 */
	public static List<String> compress( final String targetPath, final String zipFileName ) {
		List<String> list = new ArrayList<String>();
		ZipOutputStream zos = null;

		try {
			zos = new ZipOutputStream( new BufferedOutputStream ( new FileOutputStream( zipFileName ) ) );
			File target = new File( targetPath );

			// ZIP圧縮処理を行います
			addZipEntry( zos, list, target, "", 0 );

			zos.close();
		}
		catch( FileNotFoundException ex ) {
			String errMsg = "ZIPファイルが見つかりません。[ファイル名=" + zipFileName + "]";
			throw new RuntimeException( errMsg, ex );
		}
		catch( IOException ex ) {
			String errMsg = "ZIP圧縮に失敗しました。[ファイル名=" + zipFileName + "]";
			throw new RuntimeException( errMsg, ex );
		}
		finally {
			Closer.ioClose( zos );
		}

		return list;
	}

	/**
	 * ZIP圧縮処理を行います。
	 * 引数に指定されたFileオブジェクトがディレクトリであれば再帰的に呼び出し、
	 * 下層のファイルをエントリーします。但し、そのディレクトリ自身が空である場合は、
	 * ディレクトリをエントリー情報として設定します。
	 *
	 * @og.rev 4.1.0.2 (2008/02/01) 新規追加
	 * @og.rev 5.1.9.0 (2010/08/01) 更新時刻の設定 、BufferedInputStream のスコープを小さくする。
	 *
	 * @param zos ZIP用OutputStream
	 * @param list ZIPファイルのエントリーファイル名一覧
	 * @param target 圧縮対象のファイルオブジェクト
	 * @param prefix 処理中のディレクトリ
	 * @param depth 階層
	 */
	private static void addZipEntry( final ZipOutputStream zos, final List<String> list, final File target, final String prefix, final int depth ) {
//		BufferedInputStream in = null;

		try {
			// ターゲットがディレクトリの場合は、ファイルが含まれているかを
			// チェックし、空ならばそのディレクトリをエントリーに追加する。(最後に'/'が必要)
			// 空じゃなければ、再起呼び出し
			if( target.isDirectory() ) {
				File[] fileList = target.listFiles();
				if( fileList.length == 0 ) {
					list.add( prefix + target.getName() );
					ZipEntry entry = new ZipEntry( prefix + target.getName() + '/' );
					zos.putNextEntry( entry );
					zos.closeEntry(); // ディレクトリのエントリーは空で作成する必要がある。但し、FindBugsはエラー
				}
				else {
					for( int i = 0; i < fileList.length; i++ ) {
						// 再起呼び出しを行う際、圧縮対象にフォルダが指定された場合、
						// 最初の再起処理時は、エントリーにフォルダのパスを含めないようにする。
						String nextPrefix = "";
						if( depth > 0 ) {
							nextPrefix = prefix + target.getName() + '/';
						}
						addZipEntry( zos, list, fileList[i], nextPrefix, depth + 1 );
					}
				}
			}
			// ターゲットがファイルの場合
			else {
				list.add( prefix + target.getName() );
				ZipEntry entry = new ZipEntry( prefix + target.getName() );
				entry.setTime( target.lastModified() );			// 5.1.9.0 (2010/08/01) 更新時刻の設定
				zos.putNextEntry( entry );
				BufferedInputStream in = null;
				try {
					in = new BufferedInputStream( new FileInputStream( target.getAbsolutePath() ) );
					byte[] buf = new byte[BUF_SIZE];
					int count;
					while( ( count = in.read( buf, 0, BUF_SIZE ) ) != -1 ) {
						zos.write( buf, 0, count );
					}
				}
				finally {
//					in.close();
					Closer.ioClose( in );
				}
				zos.closeEntry();
			}
		}
		catch( FileNotFoundException ex ) {
			String errMsg = "圧縮対象のファイルが見つかりません。[ファイル名=" + target.getName() + "]";
			throw new RuntimeException( errMsg, ex );
		}
		catch( IOException ex ) {
			String errMsg = "ZIP圧縮に失敗しました。[ファイル名=" + target.getName() + "]";
			throw new RuntimeException( errMsg, ex );
		}
//		finally {
//			Closer.ioClose( in );
//		}
	}

	/**
	 * ファイルの圧縮または解凍を行います。
	 *
	 * @og.rev 4.1.0.2 (2008/02/01) 新規追加
	 *
	 * 使用方法 : java [comp or uncomp] [targetPath] [zipFileName]
	 * 第1引数 : comp:圧縮 uncomp:解凍
	 * 第2引数 : 圧縮時:圧縮対象のファイル又はフォルダ 解凍時:解凍先のフォルダ
	 * 第3引数 : ZIPファイル名
	 *
	 * @param args パラメータ
	 */
	public static void main( final String[] args ) {
		String usage = "Usage: java [comp or uncomp] [targetPath] [zipFileName]";
		if( args.length < 3 ) {
			System.out.println( usage );
			return;
		}

		// 開始時間
		long start = System.currentTimeMillis();

		List<String> list = null;
		if( "comp".equals( args[0] ) ) {
			list = compress( args[1], args[2] );
		}
		else if( "uncomp".equals( args[0] ) ) {
			list = unCompress( args[1], args[2] );
		}
		else {
			System.out.println( usage );
			return;
		}

		if( list != null ) {
			// 結果を表示
			for( String fileName : list ) {
				System.out.println( fileName );
			}
			// 処理時間を表示
//			System.out.println( "処理時間 : " + String.valueOf( System.currentTimeMillis() - start ) + "(ms)" );
			System.out.println( "処理時間 : " + ( System.currentTimeMillis() - start ) + "(ms)" );
		}
	}
}
