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

import org.opengion.fukurou.system.OgRuntimeException ;		// 6.4.2.0 (2016/01/29)
import org.opengion.hayabusa.common.HybsSystemException;
import static org.opengion.fukurou.system.HybsConst.BUFFER_MIDDLE;	// 6.1.0.0 (2014/12/26) refactoring

import java.util.Map;
import java.util.HashMap;
import java.util.List;
import java.util.ArrayList;
import java.util.Iterator ;
import java.util.NoSuchElementException;
import java.util.Arrays ;

/**
 * 【EXCEL取込】雛形EXCELシートの {&#064;カラム} 解析データを管理、収集する 雛形レイアウト管理クラスです。
 * POIのHSSFListener などで、雛形情報を収集し、HSSFSheet などで、雛形情報のアドレス(行列)から
 * 必要な情報を取得し、このオブジェクトに設定しておきます。
 * EXCELシート毎に、INSERT文と、対応する文字列配列を取り出します。
 *
 * @og.rev 3.8.0.0 (2005/06/07) 新規追加
 * @og.group 帳票システム
 *
 * @version  4.0
 * @author   Kazuhiko Hasegawa
 * @since    JDK5.0,
 */
public class ExcelLayout {

	private final Map<String,String> headMap  = new HashMap<>();	// シート単位のヘッダーキーを格納します。
	private final Map<String,String> bodyMap  = new HashMap<>();	// シート単位のボディーキーを格納します。
	private final Map<Integer,Map<String,String>> dataMap  = new HashMap<>();	// シート単位のデータを格納するMapを格納します。(キーは、GEEDNO)

	private final List<ExcelLayoutData>[] model  ;	// シート毎にExcelLayoutDataが格納されます。

	private String loopClm	;						// 繰返必須カラム(なければnull))
	private ExcelLayoutDataIterator iterator	;	// ExcelLayoutData を返す、Iterator

	/**
	 * コンストラクター
	 *
	 * 雛形の最大シート数を設定します。
	 * ここでは、連番で管理している為、その雛形シート番号が処理対象外であっても、
	 * 雛形EXCEL上に存在するシート数を設定する必要があります。
	 * 具体的には、HSSFListener#processRecord( Record )で、BoundSheetRecord.sid の
	 * イベントの数を数えて設定します。
	 *
	 * @param	sheetSize	最大シート数
	 */
	@SuppressWarnings(value={"unchecked","rawtypes"})
	public ExcelLayout( final int sheetSize ) {
		model = new ArrayList[sheetSize];
		for( int i=0; i<sheetSize; i++ ) {
			model[i] = new ArrayList<>();
		}
	}

	/**
	 * 雛形EXCELの {&#064;カラム} 解析情報を設定します。
	 *
	 * 雛形EXCELは、HSSFListener を使用して、イベント駆動で取得します。その場合、
	 * {&#064;カラム}を含むセルを見つける都度、このメソッドを呼び出して、{&#064;カラム}の
	 * 位置(行列番号)を設定します。
	 * データEXCELからデータを読み出す場合は、ここで登録したカラムの行列より、読み込みます。
	 * 具体的には、HSSFListener#processRecord( Record )で、SSTRecord.sid の 情報をキープしておき、
	 * LabelSSTRecord.sid 毎に、{&#064;カラム}を含むかチェックし、含む場合に、このメソッドに
	 * 解析情報を設定します。
	 *
	 * @param	sheetNo	シート番号
	 * @param	key		処理カラム
	 * @param	rowNo	行番号
	 * @param	colNo	列番号
	 */
	public void addModel( final int sheetNo, final String key, final int rowNo, final short colNo ) {
		model[sheetNo].add( new ExcelLayoutData( key,rowNo,colNo ) );
	}

	/**
	 * 雛形EXCELの {&#064;カラム} 解析情報(ExcelLayoutData)を配列で取得します。
	 *
	 * 雛形EXCELは、イベント処理で取り込む為、すべての処理が終了してから、このメソッドで
	 * 処理結果を取り出す必要があります。
	 * 解析情報は、ExcelLayoutData オブジェクトにシート単位に保管されています。
	 * この ExcelLayoutData オブジェクト ひとつに、{&#064;カラム} ひとつ、つまり、
	 * ある特定の行列番号を持っています。
	 * データEXCELを読取る場合、この ExcelLayoutData配列から、行列情報を取り出し、
	 * addData メソッドで、キー情報と関連付けて登録する為に、使用します。
	 *
	 * @param	sheetNo	シート番号
	 * @param	loopClm	繰返必須カラム(なければ通常の１対１処理)
	 *
	 * @return	ExcelLayoutData配列
	 */
	public Iterator<ExcelLayoutData> getLayoutDataIterator( final int sheetNo, final String loopClm ) {
		this.loopClm = loopClm ;
		final ExcelLayoutData[] datas = model[sheetNo].toArray( new ExcelLayoutData[model[sheetNo].size()] );
		iterator = new ExcelLayoutDataIterator( datas,loopClm );
		return iterator ;
	}

	/**
	 * 解析情報(clm,edbn)と関連付けて、データEXCELの値を設定します。
	 *
	 * データEXCELは、雛形EXCELの解析情報を元に、行列番号から設定値を取り出します。
	 * その設定値は、取りだした ExcelLayoutData の clm,edbn と関連付けて、このメソッドで登録します。
	 * この処理は、シート毎に、初期化して使う必要があります。
	 * 初期化メソッドする場合は、dataClear() を呼び出してください。
	 *
	 * @og.rev 6.3.9.0 (2015/11/06) コンストラクタで初期化されていないフィールドを null チェックなしで利用している(findbugs)
	 *
	 * @param	clm		カラム名
	 * @param	edbn	枝番
	 * @param	value	データ値
	 */
	public void addData( final String clm, final int edbn, final String value ) {
		if( loopClm != null && loopClm.equals( clm ) && edbn >= 0 && ( value == null || value.isEmpty() ) ) {
			// 6.3.9.0 (2015/11/06) コンストラクタで初期化されていないフィールドを null チェックなしで利用している(findbugs)
			if( iterator == null ) {
				final String errMsg = "#getLayoutDataIterator(int,String)を先に実行しておいてください。" ;
				throw new OgRuntimeException( errMsg );
			}

			iterator.setEnd();
			final Integer edbnObj = Integer.valueOf( edbn );
			dataMap.remove( edbnObj );		// 枝番単位のMapを削除
			return ;
		}

		final Integer edbnObj = Integer.valueOf( edbn );
		Map<String,String> map = dataMap.get( edbnObj );		// 枝番単位のMapを取得
		if( map == null ) { map = new HashMap<>(); }
		map.put( clm,value );				// 枝番に含まれるキーと値をセット
		dataMap.put( edbnObj,map );			// そのMapを枝番に登録

		if( edbn < 0 ) {
			headMap.put( clm,null );
		}
		else {
			bodyMap.put( clm,null );
		}
	}

	/**
	 * データEXCELの設定情報を初期化します。
	 *
	 * データEXCELと、雛形EXCELの解析情報を関連付ける処理は、シート毎に行う必要があります。
	 * 処理終了時(シート切り替え時)このメソッドを呼び出して、初期化しておく必要があります
	 *
	 */
	public void dataClear() {
		dataMap.clear();
		headMap.clear();
		bodyMap.clear();
	}

	/**
	 * ヘッダー情報のINSERT用Query文字列を取得します。
	 *
	 * シート単位に、データEXCELより、INSERT用のQuery文字列を作成します。
	 * この、Query は、シート単位に登録したキー情報の最大数(使用されているすべてのキー)を
	 * 元に、PreparedStatement で処理できる形の INSERT文を作成します。
	 * シート単位に呼び出す必要があります。
	 *
	 * @param	table	ヘッダー情報を登録するデータベース名(HEADERDBID)
	 *
	 * @return	ヘッダー情報のINSERT用Query文字列
	 */
	public String getHeaderInsertQuery( final String table ) {
		// 6.4.1.1 (2016/01/16) PMD refactoring. A method should have only one exit point, and that should be the last statement in the method
		return table == null || table.isEmpty() || headMap.isEmpty() ? null : makeQuery( table,headMap );

//		if( table == null || table.isEmpty() || headMap.isEmpty() ) { return null; }
//		return makeQuery( table,headMap );
	}

	/**
	 * ボディ(明細)情報のINSERT用Query文字列を取得します。
	 *
	 * シート単位に、データEXCELより、INSERT用のQuery文字列を作成します。
	 * この、Query は、シート単位に登録したキー情報の最大数(使用されているすべてのキー)を
	 * 元に、PreparedStatement で処理できる形の INSERT文を作成します。
	 * シート単位に呼び出す必要があります。
	 *
	 * @param	table	ボディ(明細)情報を登録するデータベース名(BODYDBID)
	 *
	 * @return	ボディ(明細)情報のINSERT用Query文字列
	 */
	public String getBodyInsertQuery( final String table ) {
		// 6.4.1.1 (2016/01/16) PMD refactoring. A method should have only one exit point, and that should be the last statement in the method
		return table == null || table.isEmpty() || bodyMap.isEmpty() ? null : makeQuery( table,bodyMap );

//		if( table == null || table.isEmpty() || bodyMap.isEmpty() ) { return null; }
//		return makeQuery( table,bodyMap );
	}

	/**
	 * ヘッダー情報のINSERT用Queryに対応する、データ配列を取得します。
	 *
	 * getHeaderInsertQuery( String ) で取りだした PreparedStatement に設定する値配列です。
	 * シート単位に呼び出す必要があります。
	 *
	 * @param	systemId	システムID(SYSTEM_ID)
	 * @param	ykno	要求番号(YKNO)
	 * @param	sheetNo	登録するデータEXCELのシート番号(SHEETNO)
	 *
	 * @return	データ配列
	 * @og.rtnNotNull
	 */
	public String[] getHeaderInsertData( final String systemId,final int ykno,final int sheetNo ) {
		final String[] keys = headMap.keySet().toArray( new String[headMap.size()] );
		if( keys == null || keys.length == 0 ) { return new String[0]; }

		final Integer edbnObj = Integer.valueOf( -1 );		// ヘッダー
		final Map<String,String> map = dataMap.get( edbnObj );
		if( map == null ) { return new String[0]; }

		String[] rtnData = new String[keys.length+4];

		rtnData[0] = systemId;
		rtnData[1] = String.valueOf( ykno );
		rtnData[2] = String.valueOf( sheetNo );
		rtnData[3] = String.valueOf( -1 );		// 枝番

		for( int i=0; i<keys.length; i++ ) {
			rtnData[i+4] = map.get( keys[i] );
		}

		return rtnData;
	}

	/**
	 * ボディ(明細)情報のINSERT用Queryに対応する、データ配列のリスト(String[] のList)を取得します。
	 *
	 * getHeaderInsertQuery( String ) で取りだした PreparedStatement に設定する値配列です。
	 * シート単位に呼び出す必要があります。
	 *
	 * @param	systemId	システムID(SYSTEM_ID)
	 * @param	ykno	要求番号(YKNO)
	 * @param	sheetNo	登録するデータEXCELのシート番号(SHEETNO)
	 *
	 * @return	データ配列のリスト
	 */
	public List<String[]> getBodyInsertData( final String systemId,final int ykno,final int sheetNo ) {
		final String[] keys = bodyMap.keySet().toArray( new String[bodyMap.size()] );
		if( keys == null || keys.length == 0 ) { return null; }

		final List<String[]> rtnList = new ArrayList<>();

		final Integer[] edbnObjs = dataMap.keySet().toArray( new Integer[dataMap.size()] );
		for( int i=0; i<edbnObjs.length; i++ ) {
			final int edbn = edbnObjs[i].intValue();
			if( edbn < 0 ) { continue; }		// ヘッダーの場合は、読み直し

			String[] rtnData = new String[keys.length+4];	// 毎回、新規に作成する。
			rtnData[0] = systemId;
			rtnData[1] = String.valueOf( ykno );
			rtnData[2] = String.valueOf( sheetNo );
			rtnData[3] = String.valueOf( edbn );	// 枝番

			final Map<String,String> map = dataMap.get( edbnObjs[i] );
			for( int j=0; j<keys.length; j++ ) {
				rtnData[j+4] = map.get( keys[j] );
			}
			rtnList.add( rtnData );
		}

		return rtnList;
	}

	/**
	 * 内部情報Mapより、INSERT用Query文字列を取得します。
	 *
	 * シート単位に、データEXCELより、INSERT用のQuery文字列を作成します。
	 * この、Query は、シート単位に登録したキー情報の最大数(使用されているすべてのキー)を
	 * 元に、PreparedStatement で処理できる形の INSERT文を作成します。
	 * シート単位に呼び出す必要があります。
	 *
	 * @param	table	テーブル名
	 * @param	map		ボディ(明細)情報を登録する内部情報Map
	 *
	 * @return	INSERT用Query文字列
	 */
	private String makeQuery( final String table,final Map<String,String> map ) {
		final String[] keys = map.keySet().toArray( new String[map.size()] );

		if( keys == null || keys.length == 0 ) { return null; }

		final StringBuilder buf1 = new StringBuilder( BUFFER_MIDDLE )
			.append( "INSERT INTO " ).append( table ).append( " ( " )
			.append( "GESYSTEM_ID,GEYKNO,GESHEETNO,GEEDNO" );

		final StringBuilder buf2 = new StringBuilder( BUFFER_MIDDLE )
			.append( " ) VALUES (" )
			.append( "?,?,?,?" );

		for( int i=0; i<keys.length; i++ ) {
			buf1.append( ',' ).append( keys[i] );		// 6.0.2.5 (2014/10/31) char を append する。
			buf2.append( ",?" );
		}
		buf2.append( ')' );								// 6.0.2.5 (2014/10/31) char を append する。

		buf1.append( buf2 );

		return buf1.toString();
	}
}

/**
 * ExcelLayoutData (雛形解析結果)のシート毎のIteratorを返します。
 * ExcelLayout では、データEXCELは、シート毎に解析します。
 * 通常は、雛形とデータは１対１の関係で、雛形より多いデータは、読み取りませんし、
 * 少ないデータは、NULL値でデータ登録します。
 * ここで、繰返必須カラム(LOOPCLM)を指定することで、指定のカラムが必須であることを利用して、
 * データが少ない場合は、そこまでで処理を中止して、データが多い場合は、仮想的にカラムが
 * 存在すると仮定して、雛形に存在しない箇所のデータを読み取れるように、Iterator を返します。
 * データがオーバーする場合は、仮想的にカラムの存在するアドレスを求める必要があるため、
 * 最低 カラム_0 と カラム_1 が必要です。さらに、各カラムは、行方向に並んでおり、
 * 列方向は、同一であるという前提で、読み取るべき行列番号を作成します。
 *
 * @og.rev 3.8.0.0 (2005/06/07) 新規追加
 * @og.group 帳票システム
 *
 * @version  4.0
 * @author   Kazuhiko Hasegawa
 * @since    JDK5.0,
 */
class ExcelLayoutDataIterator implements Iterator<ExcelLayoutData> {
	private final ExcelLayoutData[] layoutDatas ;
	private final String	loopClm ;
	private int		incSize = 1;	// 行番号の増加数(段組などの場合は、１以上となる)
	private int		count	;		// 現在処理中の行番号
	private int		edbnCnt	;		// 処理中の枝番に相当するカウント値
	private int		stAdrs  = -1;	// 繰返し処理を行う開始アドレス
	private int		edAdrs  = -1;	// 繰返し処理を行う終了アドレス

	/**
	 * ExcelLayoutData の配列を受け取って、初期情報を設定します。
	 *
	 * 繰返必須カラム(LOOPCLM)がnullでない場合、枝番が０のカラムを繰り返します。
	 * 繰り返す場合、行番号と枝番を指定して、既存のExcelLayoutDataオブジェクトを作成し、
	 * 仮想的に繰返します。
	 *
	 * ※ 設定する ExcelLayoutData の配列 は、そのまま、内部配列に設定されます。(コピーされません)
	 *    よって、外部からパラメータに指定した ExcelLayoutData の配列を変更した場合の動作は保証されません。
	 *    また、受け取った配列は、ExcelLayoutData の自然順序(枝番順)にソートされます。
	 *
	 * @param	datas	ExcelLayoutDataの配列
	 * @param	lpClm	繰返必須カラム(LOOPCLM)
	 */
	public ExcelLayoutDataIterator( final ExcelLayoutData[] datas,final String lpClm ) {
		layoutDatas = datas;
		loopClm     = lpClm;

		final int size    = layoutDatas.length;		// 配列の最大値

		Arrays.sort( layoutDatas );		// 枝番順にソートされます。
		// loopClm を使う場合は、枝番 -1(ヘッダ)と、０のデータのみを使用する。枝番１は、増加数の取得のみに用いる。
		if( loopClm != null ) {
			int zeroRow = -1;
			for( int i=0; i<size; i++ ) {
				// System.out.println( "count=" + i + ":" + layoutDatas[i] );
				final int edbn = layoutDatas[i].getEdbn();
				if( stAdrs < 0 && edbn == 0 ) { stAdrs = i; }	// 初の枝番0アドレス＝開始(含む)
				if( edAdrs < 0 && edbn == 1 ) { edAdrs = i; }	// 初の枝番1アドレス＝終了(含まない)
				if( loopClm.equals( layoutDatas[i].getClm() ) ) {
					if( edbn == 0 ) {
						zeroRow = layoutDatas[i].getRowNo();	// loopClm の枝番0 の行番号
					}
					else if( edbn == 1 ) {
						incSize = layoutDatas[i].getRowNo() - zeroRow;	// 増加数=枝番1-枝番0
						break;
					}
				}
			}
			// 繰返がある場合(枝番が0以上)でloopClmが見つからない場合はエラー
			if( zeroRow < 0 && stAdrs >= 0 ) {
				final String errMsg = "繰返必須カラムがシート中に存在しません。[" + loopClm + "]";
				throw new HybsSystemException( errMsg );
			}
		}
		if( stAdrs < 0 ) { stAdrs = 0; }	// 開始(含む)
		if( edAdrs < 0 ) { edAdrs = size; }	// 終了(含まない)
	//	System.out.println( "stAdrs=" + stAdrs + " , edAdrs=" + edAdrs  );
	}

	/**
	 * 繰り返し処理でさらに要素がある場合に true を返します。
	 * つまり、next が例外をスローしないで要素を返す場合に true を返します。
	 *
	 * @return	反復子がさらに要素を持つ場合は true
	 */
	public boolean hasNext() {
		if( loopClm != null && count == edAdrs ) {
			count = stAdrs;
			edbnCnt++;
		}
	//	System.out.print( "count=[" + count + "]:" );
		return count < edAdrs ;
	}

	/**
	 * 繰り返し処理で次の要素を返します。
	 *
	 * @return 繰り返し処理で次の要素
	 * @throws NoSuchElementException 繰り返し処理でそれ以上要素がない場合
	 */
	public ExcelLayoutData next() throws NoSuchElementException {
		if( layoutDatas == null || layoutDatas.length == count ) {
			final String errMsg = "行番号がレイアウトデータをオーバーしました。" +
						" 行番号=[" + count + "]" ;
			throw new NoSuchElementException( errMsg );
		}

		ExcelLayoutData data = layoutDatas[count++];

		if( edbnCnt > 0 ) {	// 繰返必須項目機能が働いているケース
			final int rowNo = data.getRowNo() + edbnCnt * incSize ;
			data = data.copy( rowNo,edbnCnt );
	//		System.out.println( "row,edbn=[" + rowNo + "," + edbnCnt + "]:" + data );
		}

		return data;
	}

	/**
	 * このメソッドは、このクラスからは使用できません。
	 * ※ このクラスでは実装されていません。
	 * このメソッドでは、必ず、UnsupportedOperationException が、throw されます。
	 */
	public void remove() {
		final String errMsg = "このメソッドは、このクラスからは使用できません。";
		throw new UnsupportedOperationException( errMsg );
	}

	/**
	 * 繰返し処理を終了させます。
	 *
	 */
	public void setEnd() {
		edAdrs = -1;
	}
}
