/*
 * 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.model;

// import java.util.Map;											// 6.2.0.0 (2015/02/27)
// import java.util.HashMap;									// 6.2.0.0 (2015/02/27)
import java.util.concurrent.ConcurrentMap;						// 6.4.3.4 (2016/03/11)
import java.util.concurrent.ConcurrentHashMap;					// 6.4.3.1 (2016/02/12) refactoring

import org.apache.poi.ss.usermodel.BuiltinFormats;				// 6.2.0.0 (2015/02/27)
import org.apache.poi.xssf.model.StylesTable;					// 6.2.0.0 (2015/02/27)
import org.apache.poi.xssf.usermodel.XSSFCellStyle;				// 6.2.0.0 (2015/02/27)

import java.util.Locale;										// 6.2.0.0 (2015/02/27)
import java.util.Date;											// 6.2.0.0 (2015/02/27)
import java.text.DateFormat;									// 6.2.0.0 (2015/02/27)
import java.text.SimpleDateFormat;								// 6.2.0.0 (2015/02/27)
import org.apache.poi.ss.usermodel.DateUtil;					// 6.2.0.0 (2015/02/27)
import org.apache.poi.ss.util.NumberToTextConverter;			// 6.2.0.0 (2015/02/27)
import org.apache.poi.hssf.record.ExtendedFormatRecord;			// 6.2.0.0 (2015/02/27)
import org.apache.poi.hssf.record.FormatRecord;					// 6.2.0.0 (2015/02/27)
import org.apache.poi.hssf.record.NumberRecord;					// 6.2.0.0 (2015/02/27)

/**
 * POI による、Excel(xlsx)の読み取りクラスです。
 *
 * xlsx形式のEXCELを、イベント方式でテキストデータを読み取ります。
 * このクラスでは、XSSF(.xlsx)形式のファイルを、TableModelHelper を介したイベントで読み取ります。
 * TableModelHelperイベントは、openGion形式のファイル読み取りに準拠した方法をサポートします。
 * ※ openGion形式のEXCELファイルとは、#NAME 列に、カラム名があり、#で始まる
 *    レコードは、コメントとして判断し、読み飛ばす処理の事です。
 *
 * @og.rev 6.0.3.0 (2014/11/13) 新規作成
 * @og.rev 6.2.0.0 (2015/02/27) パッケージ変更(util → model),クラス名変更(ExcelReader_XSSF → EventReader_XLSX)
 * @og.group ファイル入力
 *
 * @version  6.0
 * @author   Kazuhiko Hasegawa
 * @since    JDK7.0,
 */
public final class ExcelStyleFormat {
	/** このプログラムのVERSION文字列を設定します。	{@value} */
	private static final String VERSION = "6.4.3.3 (2016/03/04)" ;

	// 6.2.0.0 (2015/02/27) 日付型の処理(DATE=0,DATETIME=1,TIME=2)
	private static final String[] DATE_TYPE = { "yyyyMMdd" , "yyyyMMddHHmmss" , "HHmmss" };
	// 6.3.1.0 (2015/06/28) 外部からも使えるように、static化します。
//	private final DateFormat[]	dtFormat = new DateFormat[DATE_TYPE.length];
	private static final DateFormat[] DT_FORMAT = new DateFormat[DATE_TYPE.length];			// 6.4.1.1 (2016/01/16) dtFormat → DT_FORMAT refactoring
	private final StylesTable			stylesTable ;

	// 6.2.0.0 (2015/02/27) 日付型の処理(DATE=0,DATETIME=1,TIME=2)
	// private final List<Integer>			extFmtIdx = new ArrayList<>();	// ExtendedFormatRecord のｱﾄﾞﾚｽ(順番) の順番に、FormatIndexを設定する。
	/** 6.4.3.1 (2016/02/12) PMD refactoring. HashMap → ConcurrentHashMap に置き換え。  */
//	private final Map<Integer,String>	fmtStrMap = new HashMap<>();	// FormatIndex をキーに、Format文字列 を管理
	private final ConcurrentMap<Integer,String> fmtStrMap = new ConcurrentHashMap<>();	// FormatIndex をキーに、Format文字列 を管理
	/** 6.4.3.1 (2016/02/12) PMD refactoring. HashMap → ConcurrentHashMap に置き換え。  */
//	private final Map<Integer,Integer>	extFmtIdx = new HashMap<>();	// ExtendedFormatRecord のｱﾄﾞﾚｽ(順番) をキーに、FormatIndexを設定する。
//	private final ConcurrentMap<Integer,Integer> extFmtIdx = new ConcurrentHashMap<>();	// ExtendedFormatRecord のｱﾄﾞﾚｽ(順番) をキーに、FormatIndexを設定する。
	private final ConcurrentMap<Integer,Integer> extFmtIdxMap = new ConcurrentHashMap<>();	// ExtendedFormatRecord のｱﾄﾞﾚｽ(順番) をキーに、FormatIndexを設定する。
	private int   extFmtCnt ;

	/**
	 * XSL系 コンストラクター
	 *
	 * XSL 処理では、HSSFListener のイベント処理のうち、NumberRecord の値取得に
	 * 必要な内部処理を、実行します。
	 * 具体的には、ExtendedFormatRecord レコードから、FormatIndex と 並び順を
	 * 管理するMapと、FormatRecord レコードから、IndexCode と フォーマット文字列を
	 * 管理するMap を作成し、NumberRecordレコードの XFIndex から、ExtendedFormatRecord を
	 * 経由して、FormatRecord のフォーマット文字列 を取得し、日付フォーマットの場合は、
	 * 日付文字列に、それ以外は、数字文字列に変換する手助けを行います。
	 *
	 * @og.rev 6.2.0.0 (2015/02/27) 日付型の処理(DATE=0,DATETIME=1,TIME=2)
	 */
	public ExcelStyleFormat() {
		stylesTable = null;
	}

	/**
	 * XSLX系 コンストラクター
	 *
	 * StylesTable は、日付型をはじめとする、EXCELのスタイルのフォーマットを管理しています。
	 * XSLX形式のEXCELをパースする場合に、このコンストラクタを使用して、StylesTableオブジェクトを
	 * 設定します。
	 *
	 * @og.rev 6.2.0.0 (2015/02/27) 日付型の処理(DATE=0,DATETIME=1,TIME=2)
	 *
	 * @param	styles StylesTableオブジェクト
	 */
	public ExcelStyleFormat( final StylesTable styles ) {
		stylesTable = styles;
	}

	/**
	 * XSL系 ExtendedFormatRecordレコードの設定。
	 *
	 * @og.rev 6.2.0.0 (2015/02/27) 日付型の処理(DATE=0,DATETIME=1,TIME=2)
	 *
	 * @param	extFmtRec ExtendedFormatRecordレコード
	 */
	public void addExtFmtRec( final ExtendedFormatRecord extFmtRec ) {
		final short fmtIdx = extFmtRec.getFormatIndex();
		final short xfType = extFmtRec.getXFType();
		// Listに ｱﾄﾞﾚｽ(順番) の順番に、FormatIndexを設定する。
	//	extFmtIdx.add( Integer.valueOf( fmtIdx ) );

		// タイプを判別して、ｱﾄﾞﾚｽ(順番)をキーに、Mapに登録することで、データ件数を削減します。
		if( xfType == ExtendedFormatRecord.XF_CELL ) {
			//                              ｱﾄﾞﾚｽ(順番)               FormatIndex
			extFmtIdxMap.put( Integer.valueOf( extFmtCnt ),Integer.valueOf( fmtIdx ) );
	//		System.out.println( "fmtIdx=[" + fmtIdx + "] , xfType=[" + xfType + "] , CNT=" + extFmtCnt );
		}
		extFmtCnt++;
	}

	/**
	 * XSL系 FormatRecordレコードの設定。
	 *
	 * @og.rev 6.2.0.0 (2015/02/27) 日付型の処理(DATE=0,DATETIME=1,TIME=2)
	 *
	 * @param	fmtRec FormatRecordレコード
	 */
	public void addFmtRec( final FormatRecord fmtRec ) {
		final int    idxc = fmtRec.getIndexCode();
		final String fmt  = fmtRec.getFormatString();

		// IndexCode をキーに、Format文字列を登録する。
		fmtStrMap.put( Integer.valueOf( idxc ) , fmt );
	//	System.out.println( "fmtRec=[" + idxc + "], fmt=[" + fmt + "]" );
	}

	/**
	 * XSLX系 セルスタイル文字列(スタイル番号)から、データフォーマットを取得します。
	 *
	 * StylesTable は、日付型をはじめとする、EXCELのスタイルのフォーマットを管理しています。
	 * XSLX形式のEXCELのフォーマット文字列を取得する場合に、使用します。
	 *
	 * @og.rev 6.2.0.0 (2015/02/27) 日付型の処理(DATE=0,DATETIME=1,TIME=2)
	 *
	 * @param	cellStyleStr	セルスタイル文字列(スタイル番号)
	 * @param	val				endElement時の値文字列
	 * @return	日付データか、数値データかを判別した結果の文字列
	 */
	public String getNumberValue( final String cellStyleStr , final String val ) {
		String fmtStr = null;

		if( stylesTable != null && cellStyleStr != null && !cellStyleStr.isEmpty() ) {
			final int stIdx = Integer.parseInt( cellStyleStr );
			final XSSFCellStyle style = stylesTable.getStyleAt( stIdx );
			fmtStr = style.getDataFormatString();

			// 必要かどうか不明。テスト時は、ユーザー定義フォーマットも、上記処理で取得できていた。
			if( fmtStr == null ) {
				final int fmtIdx = style.getDataFormat();
				fmtStr = BuiltinFormats.getBuiltinFormat( fmtIdx );
			}

	//		if( fmtStr != null ) {
	//			System.out.println( "style=[" + cellStyleStr + "], stIdx=[" + stIdx + "],  fmtStr=[" + fmtStr + "]" );
	//		}
		}

		return getNumberValue( fmtStr , Double.parseDouble( val ) ) ;
	}

	/**
	 * XSL系 Numberレコードから、日付データか、数値データかを判別して返します。
	 *
	 * 日付フォーマットの判定処理を #isDateFormat(String) で行い、日付の場合は、
	 * 各タイプ(日付、日時、時刻)に応じた、文字列を返します。
	 * 日付フォーマットでない場合は、数字化文字列を返します。
	 *
	 * @og.rev 6.2.0.0 (2015/02/27) 日付型の処理(DATE=0,DATETIME=1,TIME=2)
	 *
	 * @param	numrec	NumberRecordレコード
	 * @return	日付データか、数値データかを判別した結果の文字列
	 */
	public String getNumberValue( final NumberRecord numrec ) {
		final int xfIdx  = numrec.getXFIndex();								// extFmtCnt の事
		final int fmtIdx = extFmtIdxMap.get( Integer.valueOf( xfIdx ) );

	//	final String fmtStr = fmtIdx < HSSFDataFormat.getNumberOfBuiltinBuiltinFormats()
	//							? HSSFDataFormat.getBuiltinFormat( (short)fmtIdx )
	//							: fmtStrMap.get( Integer.valueOf( fmtIdx ) );

		final String fmtStr = fmtIdx < BuiltinFormats.FIRST_USER_DEFINED_FORMAT_INDEX
								? BuiltinFormats.getBuiltinFormat( fmtIdx )
								: fmtStrMap.get( Integer.valueOf( fmtIdx ) );

	//		if( fmtStr != null ) {
	//			System.out.println( "xfIdx=[" + xfIdx + "], fmtIdx=[" + fmtIdx + "],  fmtStr=[" + fmtStr + "]" );
	//		}

		return getNumberValue( fmtStr , numrec.getValue() ) ;
	}

	/**
	 * フォーマット情報と値から、日付データか、数値データかを判別して返します。
	 *
	 * 日付フォーマットの判定処理を #isDateFormat(String) で行い、日付の場合は、
	 * 各タイプ(日付、日時、時刻)に応じた、文字列を返します。
	 * 日付フォーマットでない場合は、数字文字列を返します。
	 *
	 * @og.rev 6.2.0.0 (2015/02/27) 日付型の処理(DATE=0,DATETIME=1,TIME=2)
	 * @og.rev 6.3.1.0 (2015/06/28) 外部からも使えるように、static化します。
	 *
	 * @param	fmtStr	フォーマット情報
	 * @param	val		Numberレコードのデータ
	 * @return	日付データか、数値データかを判別した結果の文字列
	 */
//	private String getNumberValue( final String fmtStr , final double val ) {
	public static String getNumberValue( final String fmtStr , final double val ) {
		return isDateFormat( fmtStr )
					? dateFormat( val )							// 日付
					: NumberToTextConverter.toText( val ) ;		// 数字
	}

	/**
	 * フォーマット文字列から、日付型フォーマットかどうかの判定を行います。
	 *
	 * ここでは、日本式のフォーマットや、ユーザー定義の日付フォーマットでも、
	 * ある程度判定できるように、処理しています。
	 * 以下の文字列を含む場合は、true を返し、それ以外は、false です。
	 *   "年","月","日","yy","y/m","m/d","h:m"
	 * ただし、General(標準) は、除外しています。
	 *
	 * @og.rev 6.2.0.0 (2015/02/27) 日付型の処理(DATE=0,DATETIME=1,TIME=2)
	 * @og.rev 6.3.1.0 (2015/06/28) 外部からも使えるように、static化します。
	 * @og.rev 6.4.3.3 (2016/03/04) PMD refactoring. These nested if statements could be combined
	 *
	 * @param	fmt	フォーマット文字列
	 * @return	判定結果 [true:日付型/false:それ以外]
	 */
//	private boolean isDateFormat( final String fmt ) {
	private static boolean isDateFormat( final String fmt ) {
		return ( fmt != null && !fmt.isEmpty() && !"General".equalsIgnoreCase( fmt ) ) &&
			(	fmt.contains( "年" ) || fmt.contains( "月"  ) || fmt.contains( "日"  ) ||
				fmt.contains( "yy" ) || fmt.contains( "y/m" ) || fmt.contains( "m/d" ) ||
				fmt.contains( "h:m" ) ) ;

//		boolean isDf = false;
//		// 特殊処理：General(標準)は、除外します。
//		if( fmt != null && !fmt.isEmpty() && !"General".equalsIgnoreCase( fmt ) ) {
//			// これ以外のパターンもあるかも。逆に、日付フォームでない別のフォームと一致するかも。
//			if( fmt.contains( "年" ) || fmt.contains( "月"  ) || fmt.contains( "日"  ) ||
//				fmt.contains( "yy" ) || fmt.contains( "y/m" ) || fmt.contains( "m/d" ) ||
//				fmt.contains( "h:m" ) ) {
//					isDf = true;
//			}
//		}
//
//		return isDf;
	}

	/**
	 * 日付型の値を、最適なフォーマットで変換して返します。
	 *
	 * 日付データは、(DATE=0,DATETIME=1,TIME=2) に分類できます。
	 * DATE とは、日付のみの状態で、引数の val は、整数に変換できます。
	 * その場合、"yyyyMMdd" フォーマットで変換します。
	 * DATETIME とは、日付＋時刻なので、"yyyyMMddHHmmss" に変換します。
	 * TIME は、日付情報を持っていないため、"HHmmss" に変換します。
	 *
	 * @og.rev 6.2.0.0 (2015/02/27) 日付型の処理(DATE=0,DATETIME=1,TIME=2)
	 * @og.rev 6.3.1.0 (2015/06/28) 外部からも使えるように、static化します。
	 *
	 * @param	val	日付型の値
	 * @return	日付型の変換結果
	 */
//	private String dateFormat( final double val ) {
	public static String dateFormat( final double val ) {
		int dtType = 0;		// 日付型の処理(DATE=0,DATETIME=1,TIME=2)
		if( val < 1.0d ) {												// 日付部が無い → TIME=2
			dtType = 2;
		}
		// 6.3.9.0 (2015/11/06) Avoid if (x != y) ..; else ..; (PMD)
//		else if( Double.compare( val , Math.floor( val ) ) != 0 ) {		// 整数でない → DATETIME=1
//			dtType = 1;
//		}
//		else {															// Long 相当 → DATE=0
//			dtType = 0;
//		}
		else if( Double.compare( val , Math.floor( val ) ) == 0 ) {		// 整数(Long 相当) → DATE=0
			dtType = 0;
		}
		else {															// 整数でない → DATETIME=1
			dtType = 1;
		}

		DateFormat dtfmt = DT_FORMAT[dtType];			// 各タイプ別にキャッシュしている。
		if( dtfmt == null ) {
			dtfmt = new SimpleDateFormat( DATE_TYPE[dtType] , Locale.JAPAN );		// 初めての場合は新規作成
			DT_FORMAT[dtType] = dtfmt;
		}

		final Date dt = DateUtil.getJavaDate( val );
		return dtfmt.format( dt );
	}
}
