/*
 * 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 org.opengion.fukurou.system.OgRuntimeException ;		// 6.4.2.0 (2016/01/29)
import org.opengion.fukurou.system.Closer;						// 6.2.0.0 (2015/02/27)
import static org.opengion.fukurou.system.HybsConst.CR;			// 6.1.0.0 (2014/12/26) refactoring

import java.io.File;											// 6.2.0.0 (2015/02/27)
import java.io.InputStream;
import java.io.FileInputStream;
import java.io.BufferedInputStream;
import java.io.IOException;

import java.util.List;
import java.util.ArrayList;

import org.apache.poi.hssf.record.Record;
import org.apache.poi.hssf.record.CellRecord;
import org.apache.poi.hssf.record.SSTRecord;
import org.apache.poi.hssf.record.BOFRecord;
import org.apache.poi.hssf.record.EOFRecord;
import org.apache.poi.hssf.record.BoundSheetRecord;
import org.apache.poi.hssf.record.LabelSSTRecord;
import org.apache.poi.hssf.record.NumberRecord;
import org.apache.poi.hssf.record.BoolErrRecord;
import org.apache.poi.hssf.record.FormulaRecord;
import org.apache.poi.hssf.record.StringRecord;
import org.apache.poi.hssf.eventusermodel.HSSFEventFactory;
import org.apache.poi.hssf.eventusermodel.HSSFListener;
import org.apache.poi.hssf.eventusermodel.HSSFRequest;

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.ss.usermodel.Cell;
// import org.apache.poi.ss.usermodel.ErrorConstants;			// 6.3.1.0 (2015/06/28) Deprecated. 
import org.apache.poi.ss.usermodel.FormulaError;				// 6.3.1.0 (2015/06/28)
import org.apache.poi.ss.util.NumberToTextConverter;
import org.apache.poi.poifs.filesystem.POIFSFileSystem;

/**
 * POI による、Excel(xls)の読み取りクラスです。
 *
 * xls形式のEXCELを、イベント方式でテキストデータを読み取ります。
 * このクラスでは、HSSF(.xls)形式のファイルを、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 EventReader_XLS implements EventReader {
	/** このプログラムのVERSION文字列を設定します。	{@value} */
	private static final String VERSION = "6.2.0.0 (2015/02/27)" ;

	/**
	 * 引数ファイル(Excel)を、HSSFイベントモデルを使用してテキスト化します。
	 *
	 * TableModelHelperは、EXCEL読み取り処理用の統一されたイベント処理クラスです。
	 * openGion特有のEXCEL処理方法(#NAME , 先頭行#コメントなど)を実装しています。
	 * これは、HSSFやXSSFの処理を、統一的なイベントモデルで扱うためです。
	 * SSモデルが良いのですが、巨大なXSSF(.xlsx)ファイルを解析すると、OutOfMemoryエラーが
	 * 発生する為、個々に処理する必要があります。
	 * あくまで、読み取り限定であれば、こちらのイベントモデルで十分です。
	 *
	 * @og.rev 6.0.3.0 (2014/11/13) 新規作成
	 * @og.rev 6.2.0.0 (2015/02/27) staticメソッドをインスタンスメソッドに変更
	 *
	 * @param	file 入力ファイル
	 * @param	helper イベント処理するオブジェクト
	 */
	@Override
	public void eventReader( final File file , final TableModelHelper helper ) {
		InputStream fin  = null;
		InputStream din  = null;

		try {
			// 6.2.0.0 (2015/02/27) TableModelHelper 変更に伴う修正
			helper.startFile( file );

			fin = new BufferedInputStream( new FileInputStream( file ) );		// 6.2.0.0 (2015/02/27)
			final POIFSFileSystem poifs = new POIFSFileSystem( fin );

			din = poifs.createDocumentInputStream( "Workbook" );

			final HSSFRequest req = new HSSFRequest();
			req.addListenerForAllRecords( new ExcelListener( helper ) );
			final HSSFEventFactory factory = new HSSFEventFactory();

			factory.processEvents( req, din );
		}
		catch( IOException ex ) {
			final String errMsg = "ファイルの読取処理に失敗しました。"
								+ " filename=" + file + CR
								+ ex.getMessage() ;
			throw new OgRuntimeException( errMsg , ex );
		}
		finally {
			Closer.ioClose( din );
			Closer.ioClose( fin );
			helper.endFile( file );					// 6.2.0.0 (2015/02/27)
		}
	}

	/**
	 * HSSF(.xls)処理に特化したイベント処理を行う、HSSFListener の実装内部クラス。
	 *
	 * HSSFListener のイベント処理を、TableModelHelper に変換します。
	 * これは、HSSFやXSSFの処理を、統一的なイベントモデルで扱うためです。
	 * SSモデルが良いのですが、巨大なXSSF(.xlsx)ファイルを解析すると、OutOfMemoryエラーが
	 * 発生する為、個々に処理する必要があります。
	 * あくまで、読み取り限定であれば、こちらのイベントモデルで十分です。
	 *
	 * 読み書きも含めた EXCEL処理を行うには、ExcelModel クラスが別にあります。
	 *
	 * @og.rev 6.0.3.0 (2014/11/13) 新規作成
	 */
	private static final class ExcelListener implements HSSFListener {
		private final TableModelHelper	helper;
		private final ExcelStyleFormat	format;

		private final List<String> shtNms = new ArrayList<>();	// シート名の一括登録
		private SSTRecord sstrec;				// LabelSSTRecord のインデックスに対応した文字列配列

		private int		shtNo			= -1;	// 最初に見つけたときに、＋＋ するので初期値は －１にしておく
		private String	shtNm			;		// BOFRecord でキャッシュしておきます。
		private boolean isNextRecord	;		// FormulaRecord で、次のレコードに値があるかどうかの判定
		private boolean isReadSheet		= true;	// シートの読み取りを行うかどうか

		private int		rcdLvl	;				// BOFRecord で＋１、EOFRecord で－１ して、シートの EOFRecord の判定に使う。

		private int		rowNo	;				// 処理中の行番号(0～)
		private int		colNo	;				// 処理中の列番号(0～)

		private final boolean	useDebug	;	// デバッグフラグ

		/**
		 * TableModelHelper を引数に取るコンストラクタ
		 *
		 * HSSFListener のイベント処理を、TableModelHelper に変換します。
		 *
		 * @og.rev 6.0.3.0 (2014/11/13) 新規作成
		 * @og.rev 6.2.0.0 (2015/02/27) デバッグ情報の出力するかどうか。新規追加
		 *
		 * @param	helper イベント処理するオブジェクト
		 */
		public ExcelListener( final TableModelHelper helper ) {
			this.helper = helper ;
			useDebug	= helper.isDebug();						// 6.2.0.0 (2015/02/27) デバッグ情報の出力
			format		= new ExcelStyleFormat();				// 6.2.0.0 (2015/02/27) StylesTable 追加
		}

		/**
		 * HSSFListener のイベントを受け取るメソッド。
		 *
		 * @og.rev 6.1.0.0 (2014/12/26) シートの数のイベント
		 * @og.rev 6.3.1.0 (2015/06/28) ErrorConstants のDeprecated に伴う、FormulaError への置き換え。
		 * @og.rev 6.3.9.0 (2015/11/06) 未チェック/未確認のキャスト対応(findbugs)。
		 *
		 * @param record	イベント時に設定されるレコード
		 * @see	org.apache.poi.hssf.eventusermodel.HSSFListener
		 */
		@Override
		public void processRecord( final Record record ) {
			if( record instanceof CellRecord ) {
				final CellRecord crec = (CellRecord)record;
				rowNo = crec.getRow() ;
				if( helper.isSkip( rowNo ) ) { return; }		// 行のスキップ判定
				colNo = crec.getColumn();
			}

			String val = null;
			switch( record.getSid() ) {
				// the BOFRecord can represent either the beginning of a sheet or the workbook
				case BOFRecord.sid:				// Beginning Of File 
					// 6.3.9.0 (2015/11/06) 未チェック/未確認のキャスト対応(findbugs)
//					final BOFRecord bof = (BOFRecord)record;
//					if( bof.getType() == BOFRecord.TYPE_WORKSHEET ) {
					if( record instanceof BOFRecord && ((BOFRecord)record).getType() == BOFRecord.TYPE_WORKSHEET ) {
						// 6.1.0.0 (2014/12/26) シートの数のイベント
						// シート一覧の読み取り後、最初のレコードの判定に、shtNo を使います。
						if( shtNo < 0 ) { helper.sheetSize( shtNms.size() ); }

						shtNo++ ;						// 現在のシート番号。初期値が、-1 してあるので、先に ＋＋ する。
						shtNm = shtNms.get( shtNo ) ;	// 現在のシート名。
						rcdLvl = 0;						// シートの開始
						isReadSheet = helper.startSheet( shtNm,shtNo );
						if( useDebug ) { System.out.println( "① BOFRecord:" + record ); }
					}
					else {
						rcdLvl++;						// シート以外の開始
					}
					break;
				case EOFRecord.sid:				// End Of File record
					if( rcdLvl == 0 ) {					// シートの終了
						helper.endSheet( shtNo );
						isReadSheet = true;
						if( useDebug ) { System.out.println( "② EOFRecord" + record ); }
					}
					else {
						rcdLvl--;						// シート以外の終了
					}
					break;
				case BoundSheetRecord.sid:		// シート一覧(一括で最初にイベントが発生する)
					if( useDebug ) { System.out.println( "③ BoundSheetRecord" ); }
					// 6.3.9.0 (2015/11/06) 未チェック/未確認のキャスト対応(findbugs)
					if( record instanceof BoundSheetRecord ) {
						shtNms.add( ((BoundSheetRecord)record).getSheetname() );
					}
//					final BoundSheetRecord bsr = (BoundSheetRecord) record;
//					shtNms.add( bsr.getSheetname() );
					break;
				case SSTRecord.sid:				// Static String Table Record
					if( useDebug ) { System.out.println( "④ SSTRecord" ); }
					// 6.3.9.0 (2015/11/06) 未チェック/未確認のキャスト対応(findbugs)
					if( record instanceof SSTRecord ) {
						sstrec = (SSTRecord)record;		// LabelSSTRecord のインデックスに対応した文字列配列
					}
//					sstrec = (SSTRecord)record;			// LabelSSTRecord のインデックスに対応した文字列配列
		//			for( int k = 0; k < sstrec.getNumUniqueStrings(); k++ ) {
		//				System.out.println("table[" + k + "]=" + sstrec.getString(k));
		//			}
					break;
		//		case RowRecord.sid:				// stores the row information for the sheet
		//			if( useDebug ) { System.out.println( "⑤ RowRecord" ); }
		//			RowRecord rowrec = (RowRecord) record;
		//			System.out.println("Row=[" + rowrec.getRowNumber() + "],Col=["
		//					+ rowrec.getFirstCol() + "]-[" + rowrec.getLastCol() + "]" );
		//			break;

				// NumberRecord の XFIndex が、ExtendedFormatRecord の 番号になり、その値が、FormatIndex = FormatRecordのIndexCode
				case ExtendedFormatRecord.sid:
					// 6.3.9.0 (2015/11/06) 未チェック/未確認のキャスト対応(findbugs)
					if( record instanceof ExtendedFormatRecord ) {
						format.addExtFmtRec( (ExtendedFormatRecord)record );
					}
//					format.addExtFmtRec( (ExtendedFormatRecord)record );
					break;

				// IndexCode をキーに、FormatString を取り出す。
				case FormatRecord.sid:
					// 6.3.9.0 (2015/11/06) 未チェック/未確認のキャスト対応(findbugs)
					if( record instanceof FormatRecord ) {
						format.addFmtRec( (FormatRecord)record );
					}
//					format.addFmtRec( (FormatRecord)record );
					break;

				case NumberRecord.sid:			// extend CellRecord
					// 6.3.9.0 (2015/11/06) 未チェック/未確認のキャスト対応(findbugs)
					if( isReadSheet && record instanceof NumberRecord ) {
//					if( isReadSheet ) {
						val = format.getNumberValue( (NumberRecord)record );
					}
					break;
				// SSTRecords store a array of unique strings used in Excel.
				case LabelSSTRecord.sid:		// extend CellRecord
					// 6.3.9.0 (2015/11/06) 未チェック/未確認のキャスト対応(findbugs)
					if( isReadSheet && record instanceof LabelSSTRecord ) {
//					if( isReadSheet ) {
						final LabelSSTRecord lrec = (LabelSSTRecord)record;
						val = sstrec.getString(lrec.getSSTIndex()).getString();
					}
					break;
				case BoolErrRecord.sid:			// extend CellRecord
					// 6.3.9.0 (2015/11/06) 未チェック/未確認のキャスト対応(findbugs)
					if( isReadSheet && record instanceof BoolErrRecord ) {
//					if( isReadSheet ) {
						final BoolErrRecord berec = (BoolErrRecord)record;
						final byte errVal = berec.getErrorValue();
						val = errVal == 0 ? Boolean.toString( berec.getBooleanValue() )
											// 6.3.1.0 (2015/06/28)
//										  : ErrorConstants.getText( errVal );			// Deprecated
										  : FormulaError.forInt( errVal ).getString();
					}
					break;
				case FormulaRecord.sid:			// extend CellRecord
					// 6.3.9.0 (2015/11/06) 未チェック/未確認のキャスト対応(findbugs)
					if( isReadSheet && record instanceof FormulaRecord ) {
//					if( isReadSheet ) {
						final FormulaRecord frec = (FormulaRecord)record;
						switch (frec.getCachedResultType()) {
							case Cell.CELL_TYPE_NUMERIC:
								final double num = frec.getValue();
								if( Double.isNaN(num) ) {
									// Formula result is a string
									// This is stored in the next record
									isNextRecord = true;
								}
								else {
									val = NumberToTextConverter.toText( num );
								}
								break;
							case Cell.CELL_TYPE_BOOLEAN:
								val = Boolean.toString(frec.getCachedBooleanValue());
								break;
							case Cell.CELL_TYPE_ERROR:
								// 6.3.1.0 (2015/06/28)
//								val = ErrorConstants.getText(frec.getCachedErrorValue());	// Deprecated
								val = FormulaError.forInt( frec.getCachedErrorValue() ).getString();
								break;
							case Cell.CELL_TYPE_STRING:
								isNextRecord = true;
								break;
							default : break;
						}
					}
					break;
				case StringRecord.sid:			// FormulaRecord の場合の次のレコードに値が設定されている
					// 6.3.9.0 (2015/11/06) 未チェック/未確認のキャスト対応(findbugs)
					if( isReadSheet && isNextRecord && record instanceof StringRecord ) {
//					if( isReadSheet ) {
//						if( isNextRecord ) {
							// String for formula
							final StringRecord srec = (StringRecord)record;
							val = srec.getString();
							isNextRecord = false;
//						}
					}
					break;
		//		case TextObjectRecord.sid:		// 6.2.5.0 (2015/06/05) TextBox などの、非セルテキスト
		//			if( isReadSheet ) {
		//				if( useDebug ) { System.out.println( "⑥ TextObjectRecord" ); }
		//				final TextObjectRecord txrec = (TextObjectRecord)record;
		//				val = txrec.getStr().getString();
		//			}
		//			break;
				default :
					break ;
			}
			if( val != null ) {
				//           値   行(Row) 列(Col)
				helper.value( val, rowNo,  colNo );			// イベント処理
			}
		}
	}

	/**
	 * アプリケーションのサンプルです。
	 *
	 * 入力ファイル名 は必須で、第一引数固定です。
	 *
	 * Usage: java org.opengion.fukurou.model.EventReader_XLS 入力ファイル名
	 *
	 * @og.rev 6.0.3.0 (2014/11/13) 新規作成
	 * @og.rev 6.2.0.0 (2015/02/27) staticメソッドをインスタンスメソッドに変更
	 *
	 * @param	args	コマンド引数配列
	 */
	public static void main( final String[] args ) {
		final String usageMsg = "Usage: java org.opengion.fukurou.model.EventReader_XLS 入力ファイル名" ;
		if( args.length == 0 ) {
			System.err.println( usageMsg );
			return ;
		}

		final File file = new File( args[0] );
		final EventReader reader = new EventReader_XLS();

		reader.eventReader(					// 6.2.0.0 (2015/02/27)
			file,
			new TableModelHelper() {
				/**
				 * シートの読み取り開始時にイベントが発生します。
				 *
				 * @param   shtNm  シート名
				 * @param   shtNo  シート番号(0～)
				 * @return  true:シートの読み取り処理を継続します/false:このシートは読み取りません。
				 */
				public boolean startSheet( final String shtNm,final int shtNo ) {
					System.out.println( "S[" + shtNo + "]=" + shtNm );
					return super.startSheet( shtNm,shtNo );
				}

		//		public void columnNames( final String[] names ) {
		//			System.out.println( "NM=" + java.util.Arrays.toString( names ) );
		//		}

		//		public void values( final String[] vals,final int rowNo ) {
		//			System.out.println( "V[" + rowNo + "]=" + java.util.Arrays.toString( vals ) );
		//		}

		//		public boolean isSkip( final int rowNo ) {
		//			super.isSkip( rowNo );
		//			return false;
		//		}

				/**
				 * 読み取り状態の時に、rowNo,colNo にあるセルの値を引数にイベントが発生します。
				 *
				 * @param   val     文字列値
				 * @param   rowNo   行番号(0～)
				 * @param   colNo   列番号(0～)
				 * @return  読み取りするかどうか(true:読み取りする/false:読み取りしない)
				 */
				public boolean value( final String val,final int rowNo,final int colNo ) {
					System.out.println( "R[" + rowNo + "],C[" + colNo + "]=" + val );
					return super.value( val,rowNo,colNo );
				}
			}
		);
	}
}
