/*
 * 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 static org.opengion.fukurou.util.HybsConst.CR ;		// 6.1.0.0 (2014/12/26)
import org.opengion.fukurou.util.LogWriter;
import org.opengion.fukurou.util.StringUtil;
import org.opengion.fukurou.util.FileUtil;
import org.opengion.fukurou.util.Closer ;
import org.opengion.hayabusa.common.HybsSystem;
import org.opengion.hayabusa.common.HybsSystemException;
import org.opengion.hayabusa.db.DBTableModel;
import org.opengion.hayabusa.db.DBColumn;
import org.opengion.hayabusa.resource.ResourceManager;

import java.io.File;
import java.io.BufferedReader;
import java.io.PrintWriter;

/**
 * DBTableReport インターフェース のデフォルト実装クラスです。
 * writeReport() を、オーバーライドすれば，各種出力フォーマットに合わせた
 * サブクラスを実現する事が可能です。
 *
 * @og.group 帳票システム
 *
 * @version  4.0
 * @author	 Kazuhiko Hasegawa
 * @since    JDK5.0,
 */
public abstract class AbstractDBTableReport implements DBTableReport {
	private static final String ENCODE		= HybsSystem.REPORT_ENCODE ;
	private static final String PAGEBREAK	= "PAGEBREAK";

	protected String[]		headerKeys	;		// 固定部の key 部分を指定する。カンマで複数指定できる。
	protected String[]		headerVals	;		// 固定部の key に対応する値を指定する。
	protected String[]		footerKeys	;		// 繰り返し部の終了後に表示する key 部分を指定する。カンマで複数指定できる。
	protected String[]		footerVals	;		// 繰り返し部の終了後に表示する key に対する値を指定する。
	protected boolean		pageEndCut	;		// ボディー部(繰り返し部)がなくなったときに、それ以降のページを出力するか指定する。
	protected int			maxRowCount	;		// 自動計算方式を採用
	protected int			pageRowCount;		// 過去のページの最大件数。	3.7.0.1 (2005/01/31)
	protected int			lineCopyCnt	;		// LINE_COPY した際の加算行番号。	4.0.0 (2007/06/08)
	protected ResourceManager resource  ;		// 4.0.0 (2005/01/31)
	protected PrintWriter	writer		;
	protected BufferedReader reader		;
	protected File			templateFile	 ;	// 3.8.0.0 (2005/06/07)
	protected File			firstTemplateFile;	// 3.8.0.0 (2005/06/07)
	protected String		htmlDir		;
	protected String		htmlFileKey	;
	protected String		ykno		;		// 3.8.5.1 (2006/04/28) 追加
	protected DBTableModel	table		;

	protected int			pageCount	;
	protected int			maxPageCount = 1000;
	protected boolean		rowOver		;		// データ件数分のカラムを要求されると、true にセットされる。
	protected boolean		dataOver	;		// 3.8.1.2 (2005/12/19) データがなくなると、true にセットされる。
	protected String		listId		;		// 3.6.1.0 (2005/01/05) 帳票ＩＤ追加

	// 3.6.0.0 (2004/09/24) フォーマットエラーの判定(formatErr)を、子クラスから移動します。
	private boolean  formatErr			;		// フォーマットエラーの判定

	// 3.7.0.1 (2005/01/31) ページブレイク時の処理
	private int				pbClmNo		= -1;	// PAGEBREAK カラムの番号
	private boolean			pageBreak	;		// PAGEBREAK = "1" が見つかった時、true
	private int				tableSize	;

	/**
	 * DBTableModel から データを作成して,PrintWriter に書き出します。
	 *
	 */
	public void writeReport() {
		setHeaderFooter();
		initReader();
		initWriter();
		String str ;
		while( (str = readLine()) != null ) {
			println( changeData( str ) );
		}
		close();
	}

	/**
	 * 入力文字列 を読み取って、出力します。
	 * tr タグを目印に、１行(trタグ間)ずつ取り出します。
	 * 読み取りを終了する場合は、null を返します。
	 * 各サブクラスで実装してください。
	 *
	 * @return	出力文字列
	 */
	abstract protected String readLine() ;

	/**
	 * 入力文字列 を加工して、出力します。
	 * {&#064;XXXX} をテーブルモデルより読み取り、値をセットします。
	 * 各サブクラスで実装してください。
	 *
	 * @param	inLine	入力文字列
	 *
	 * @return	出力文字列
	 */
	abstract protected String changeData( final String inLine ) ;

	/**
	 * 入力文字列 を読み取って、出力します。
	 * 各サブクラスで実装してください。
	 *
	 * @param line 出力文字列
	 */
	abstract protected void println( final String line ) ;

	/**
	 * リソースマネージャーをセットします。
	 * これは、言語(ロケール)に応じた DBColumn をあらかじめ設定しておく為に
	 * 必要です。
	 * リソースマネージャーが設定されていない、または、所定のキーの DBColumn が
	 * リソースに存在しない場合は、内部で DBColumn オブジェクトを作成します。
	 *
	 * @og.rev 4.0.0.0 (2005/01/31) lang ⇒ ResourceManager へ変更
	 *
	 * @param  resource リソースマネージャー
	 */
	public void setResourceManager( final ResourceManager resource ) {
		this.resource = resource;
	}

	/**
	 * 帳票ＩＤ をセットします。
	 * この帳票ＩＤを利用して、画像ファイル等のセーブディレクトリを求めます。
	 *
	 * @og.rev 3.6.1.0 (2005/01/05) 新規作成
	 *
	 * @param   listId 帳票ＩＤ
	 */
	public void setListId( final String listId ) {
		this.listId = listId ;
	}

	/**
	 * DBTableModel をセットします。
	 *
	 * @og.rev 3.7.0.1 (2005/01/31) ページブレイク時の処理
	 *
	 * @param	table	DBTableModelオブジェクト
	 */
	public void setDBTableModel( final DBTableModel table ) {
		this.table = table;
		// 3.7.0.1 (2005/01/31) ページブレイク時の処理
		tableSize = table.getRowCount();
		pbClmNo   = table.getColumnNo( PAGEBREAK,false );		// 存在しない場合は、-1
	}

	/**
	 * 雛型ファイル名をセットします。
	 *
	 * @og.rev 3.6.0.0 (2004/09/17) メソッド名の変更。setInputFile ⇒ setTemplateFile
	 * @og.rev 3.8.0.0 (2005/06/07) 引数を String  ⇒ File に変更
	 *
	 * @param   inFile 雛型ファイル名
	 */
	public void setTemplateFile( final File inFile ) {
		templateFile = inFile;
	}

	/**
	 * 最初のページのみに使用する雛型ファイル名をセットします。
	 *
	 * @og.rev 3.6.0.0 (2004/09/17) 新規追加
	 * @og.rev 3.8.0.0 (2005/06/07) 引数を String  ⇒ File に変更
	 *
	 * @param   inFile 最初のページの雛型ファイル名
	 */
	public void setFirstTemplateFile( final File inFile ) {
		firstTemplateFile = inFile;
	}

	/**
	 * 変換後ファイルを出力するディレクトリ名をセットします。
	 * ディレクトリが存在しない場合は、新規に作成します。
	 *
	 * @og.rev 3.7.1.1 (2005/05/23) フォルダがない場合は、複数階層分のフォルダを自動で作成します。
	 *
	 * @param   outDir 出力ディレクトリ
	 */
	public void setOutputDir( final String outDir ) {
		htmlDir = outDir;

		final File dir = new File(htmlDir);
		if( ! dir.exists() && ! dir.mkdirs() ) {
			final String errMsg = "ディレクトリの作成に失敗しました。[" + htmlDir + "]";
			throw new HybsSystemException( errMsg );
		}
	}

	/**
	 * 変換後ファイルキーをセットします。
	 * キーとは、拡張子の無い状態までのファイル名です。
	 * 変換後ファイルは、複数発生します。
	 * 実際に出力されるファイル名は、outFile + "_連番.html" となります。
	 *
	 * @param   outFile 出力ファイル名の共通部
	 */
	public void setOutputFileKey( final String outFile ) {
		htmlFileKey = outFile;
	}

	/**
	 * 帳票起動された要求番号をセットします。
	 *
	 * @og.rev 3.8.5.1 (2006/04/28) 新規追加
	 *
	 * @param   ykno 要求番号
	 */
	public void setYkno( final String ykno ) {
		this.ykno = ykno;
	}

	/**
	 * 固定部の key 部分を指定します。
	 * カンマで複数指定できます。
	 *
	 * @og.rev 3.5.6.0 (2004/06/18) 配列の設定は、arraycopy して取り込みます。
	 *
	 * @param   hKeys 固定部のキー配列(可変長引数)
	 */
	public void setHeaderKeys( final String... hKeys ) {
		if( hKeys != null && hKeys.length > 0 ) {		// 6.1.1.0 (2015/01/17) 可変長引数でもnullは来る。
			final int size = hKeys.length ;
			headerKeys = new String[size];
			System.arraycopy( hKeys,0,headerKeys,0,size );
		}
		else {
			headerKeys = null;
		}
	}

	/**
	 * 固定部のkey に対応する値を指定します。
	 * カンマで複数指定で、リクエスト情報でも設定できます。
	 *
	 * @og.rev 3.5.6.0 (2004/06/18) 配列の設定は、arraycopy して取り込みます。
	 *
	 * @param   hVals 固定部の値配列(可変長引数)
	 */
	public void setHeaderVals( final String... hVals ) {
		if( hVals != null && hVals.length > 0 ) {		// 6.1.1.0 (2015/01/17) 可変長引数でもnullは来る。
			final int size = hVals.length ;
			headerVals = new String[size];
			System.arraycopy( hVals,0,headerVals,0,size );
		}
		else {
			headerVals = null;
		}
	}

	/**
	 * 雛型帳票に対する、実際の行番号を求めます。
	 * これは、雛型の複数回読みをサポートする為、実際の雛型のrow番号と
	 * DBTableModel から取得すべき row番号が、異なる為です。
	 * オーバーフロー時は、Exception を避ける為、-1 を返します。
	 *
	 * @og.rev 3.5.6.0 (2004/06/18) noDataflag の追加。
	 * @og.rev 3.5.6.3 (2004/07/12) noDataflag の廃止。
	 * @og.rev 3.6.0.4 (2004/10/14) FIRST 雛型時の対応追加。
	 * @og.rev 3.7.0.1 (2005/01/31) ページブレイク処理に対応。
	 * @og.rev 3.8.1.2 (2005/12/19) PAGE_END_CUT用にdataOverフラグを追加
	 *
	 * @param	row 固定部の値(オーバーフロー時は、-1 )
	 *
	 * @return	実際の行番号
	 */
	protected int getRealRow( final int row ) {

		// 3.7.0.1 (2005/01/31) ページブレイク処理に対応。
		int realRow = pageRowCount + row + lineCopyCnt ;
		if( maxRowCount <= realRow ) { maxRowCount = realRow + 1; }

		if( realRow >= (tableSize-1) ) {			// 行番号が最大値と同じ(データは存在)
			rowOver = true;
			if( realRow >= tableSize ) {	// さらに、データは存在しない。
				realRow = -1;		// 3.5.6.3 (2004/07/12) オーバーフロー
				dataOver = true;	// 3.8.1.2 (2005/12/19)
			}
		}

		return realRow ;
	}

	/**
	 * 指定のキーについて、その値を取得します。
	 * 値の取得方法として、
	 * 　　{&#064;xxx_no} 形式の場合は、DBTableModel から、
	 * 　　{&#064;XXXX} 形式で、かつ、rowOver が false の場合は、ヘッダーから、
	 * 　　{&#064;XXXX} 形式で、かつ、rowOver が true の場合は、フッターから、
	 * 取得します。
	 * rowOver は、{&#064;xxx_no} 形式の番号欄(no)が、DBTableModel のデータ件数よりも
	 * 大きい場合に、セットされます。
	 *
	 * @og.rev 3.5.6.0 (2004/06/18) noDataflag の追加。
	 * @og.rev 3.5.6.3 (2004/07/12) noDataflag の廃止。
	 * @og.rev 3.6.0.0 (2004/09/24) フォーマットエラーの判定(formatErr)を、子クラスから移動します。
	 * @og.rev 3.7.0.1 (2005/01/31) ページブレイク時の処理追加。
	 * @og.rev 3.7.0.2 (2005/02/18) HTML のエスケープ文字対応
	 * @og.rev 3.7.1.1 (2005/05/09) セル内の改行 <br> は、エスケープしない。
	 * @og.rev 3.8.0.0 (2005/06/07) Shift-JIS で中国語を扱う。(Unicodeエスケープ文字は、エスケープしない)
	 * @og.rev 3.8.5.1 (2006/04/28) YKNO を特別扱いする。
	 * @og.rev 6.1.1.0 (2015/01/17) getRendererValue の代わりに、getWriteValue を使うように変更。
	 *
	 * @param   key 指定のキー
	 *
	 * @return  指定のキーの値
	 * @og.rtnNotNull
	 */
	protected String getValue( final String key ) {
		if( pageBreak ) { return ""; }		// 3.7.0.1 (2005/01/31) ページブレイク時の処理

		final int sp = key.lastIndexOf( '_' );
		if( sp >= 0 ) {
			try {
				final int row = Integer.parseInt( key.substring( sp+1 ) );
				final int realRow = getRealRow( row );

				if( realRow >= 0 ) {	// 3.5.6.3 (2004/07/12)
					formatErr = false;	// 3.6.0.0 (2004/09/24)
					final int col = table.getColumnNo( key.substring( 0,sp ),false );
					if( col < 0 ) {
						// 超暫定対策：I 変数で、行番号を出力する。
						if( "I".equals( key.substring( 0,sp ) ) ) {
							return String.valueOf( realRow+1 );		// 行番号は物理行＋１
						}
						else {
							final String errMsg = "カラム名が存在しません:[" + key + "]" ;
							System.out.println( errMsg );
							LogWriter.log( errMsg );
							return "" ;
						}
					}

					String val = table.getValue( realRow,col );

					// 3.7.0.1 (2005/01/31) ページブレイク時の処理追加
					if( pbClmNo == col ) {
						if( ! rowOver ) {
							final String val2 = table.getValue( realRow+1,pbClmNo );	// 先読み
							if( val != null && ! val.equals( val2 ) ) {
								pageBreak = true;
							}
						}
						return "";	// ページブレイクカラムは、すべて""に変換する。
					}
					// 3.7.1.1 (2005/05/09) セル内の改行 <br> は、エスケープしない。
					val = StringUtil.htmlFilter( val );
					val = StringUtil.replace( val,"&lt;br&gt;","<br>" );
					// 3.8.0.0 (2005/06/07) Shift-JIS で中国語を扱う。(Unicodeエスケープ文字は、エスケープしない)
					val = StringUtil.replace( val,"&amp;#","&#" );	// 中国語変換対応 &amp;# は変換しない
					// 6.1.1.0 (2015/01/17) getRendererValue の代わりに、getWriteValue を使うように変更。
					return table.getDBColumn( col ).getWriteValue( val );
				}
			}
			catch ( NumberFormatException ex ) {	// 4.0.0 (2005/01/31)
				final String errMsg = "警告：ヘッダーに'_'カラム名が使用  "
							+ "key=[" + key + "]  "
							+ ex.getMessage() ;
				LogWriter.log( errMsg );
				// フォーマットエラーは、何もしない。
				// 通常のカラム名にアンダーバーが使用されている可能性があるため。
			}
			catch ( RuntimeException ex ) {
				final String errMsg = "カラムデータ取得処理で、エラーが発生しました。  "
							+ "key=[" + key + "]  "
							+ ex.getMessage() ;
				LogWriter.log( errMsg );
				// フォーマットエラーは、何もしない。
			}
		}

		// 3.8.5.1 (2006/04/28) YKNO を特別扱いする。
		if( "YKNO".equals( key ) ) { return ykno; }

		String rtnVal ;
		if( rowOver ) { rtnVal = getFooterValue( key ); }
		else          { rtnVal = getHeaderValue( key ); }

		if( rtnVal == null ) { rtnVal = ""; }
		return rtnVal ;
	}

	/**
	 * 固定部のkey に対応する値を取得します。
	 *
	 * @param   key String
	 *
	 * @return   固定部の値
	 */
	private String getHeaderValue( final String key ) {
		if( headerKeys == null ||
			headerVals == null ||
			key        == null ) { return null; }

		for( int i=0; i<headerKeys.length; i++ ) {
			if( key.equals( headerKeys[i] ) ) { return headerVals[i]; }
		}
		return null;
	}

	/**
	 * 繰り返し部の終了後に表示する key 部分を指定します。
	 * カンマで複数指定できます。
	 *
	 * @og.rev 3.5.6.0 (2004/06/18) 配列の設定は、arraycopy して取り込みます。
	 *
	 * @param   fKeys 繰り返し部の終了後に表示するキー配列(可変長引数)
	 */
	public void setFooterKeys( final String... fKeys ) {
		if( fKeys != null && fKeys.length > 0 ) {		// 6.1.1.0 (2015/01/17) 可変長引数でもnullは来る。
			final int size = fKeys.length ;
			footerKeys = new String[size];
			System.arraycopy( fKeys,0,footerKeys,0,size );
		}
		else {
			footerKeys = null;
		}
	}

	/**
	 * 繰り返し部の終了後に表示する key 部分を取得します。
	 *
	 * @param   key String
	 *
	 * @return   繰り返し部の終了後に表示する key
	 */
	private String getFooterValue( final String key ) {
		if( footerKeys == null ||
			footerVals == null ||
			key        == null ) { return null; }

		for( int i=0; i<footerKeys.length; i++ ) {
			if( key.equals( footerKeys[i] ) ) { return footerVals[i]; }
		}
		return null;
	}

	/**
	 * 固定部のkey に対応する値を指定します。
	 * カンマで複数指定で、リクエスト情報でも設定できます。
	 *
	 * @og.rev 3.5.6.0 (2004/06/18) 配列の設定は、arraycopy して取り込みます。
	 *
	 * @param   fVals 繰り返し部の終了後に表示する値配列(可変長引数)
	 */
	public void setFooterVals( final String... fVals ) {
		if( fVals != null && fVals.length > 0 ) {		// 6.1.1.0 (2015/01/17) 可変長引数でもnullは来る。
			final int size = fVals.length ;
			footerVals = new String[size];
			System.arraycopy( fVals,0,footerVals,0,size );
		}
		else {
			footerVals = null;
		}
	}

	/**
	 * ボディー部(繰り返し部)がなくなったときに、それ以降を表示するかどうかを指定します。
	 * true では、それ以降を出力しません。
	 * デフォルト "true" (なくなった時点で、出力しない。)です。
	 *
	 * @param   pageEndCut 繰り返し部の終了後に継続処理するかどうか (true:処理しない/false:処理する)
	 */
	public void setPageEndCut( final boolean pageEndCut ) {
		this.pageEndCut = pageEndCut ;
	}

	/**
	 * BufferedReader を、初期化します。
	 * これは、雛型ファイルの終端まで読取り、処理した場合、もう一度
	 * 初めから読み込みなおす処理を行います。
	 * 基本的に、書き込みも初期化する必要があります。
	 *
	 * メモリ上に読み込んで、繰り返し利用するかどうかは、実装依存です。
	 *
	 * @og.rev 3.1.3.0 (2003/04/10) "DEFAULT" エンコーディング名のサポートを廃止。
	 * @og.rev 3.5.5.9 (2004/06/07) FileUtil.getBufferedReader を使用
	 * @og.rev 3.6.0.0 (2004/09/17) 最初のページのみに使用する雛型ファイル名を追加します。
	 * @og.rev 3.6.0.0 (2004/09/24) フォーマットエラーの判定(formatErr)を、子クラスから移動します。
	 *
	 */
	protected void initReader() {
		Closer.ioClose( reader );		// 4.0.0 (2006/01/31) close 処理時の IOException を無視

		if( reader == null && firstTemplateFile != null ) {
			reader = FileUtil.getBufferedReader(firstTemplateFile,ENCODE);
		}
		else {
			if( formatErr ) {
				final String errMsg = "Error in HTML File. " + CR
								+ "Excel containing two or more sheets is not supporting."
								+ CR
								+ "or HTML template File is not in '{@xxxx_0}' key word." ;
				throw new HybsSystemException( errMsg );
			}
			reader = FileUtil.getBufferedReader(templateFile,ENCODE);
			formatErr = true;		// 初期化します。クリアしなければエラー
		}
	}

	/**
	 * PrintWriter を、初期化します。
	 * これは、雛型ファイルを終端まで読取り、処理した場合、出力ファイル名を
	 * 変えて、別ファイルとして出力する為のものです。
	 * 基本的に、読取も初期化する必要があります。
	 *
	 * メモリ上に読み込んで、繰り返し利用するかどうかは、実装依存です。
	 *
	 * @og.rev 3.0.0.1 (2003/02/14) ページの最大ページ数の制限を追加。暴走停止用
	 * @og.rev 3.1.3.0 (2003/04/10) "DEFAULT" エンコーディング名のサポートを廃止。
	 * @og.rev 3.5.5.9 (2004/06/07) FileUtil.getPrintWriter メソッドを使用
	 * @og.rev 3.7.0.1 (2005/01/31) ページブレイク処理に対応。
	 * @og.rev 3.8.0.0 (2005/06/07) FileUtil#getPrintWriter を利用。
	 * @og.rev 3.8.5.3 (2006/06/30) EXCEL最大シート数のエラーメッセージを変更。
	 *
	 */
	protected void initWriter() {
		if( writer != null ) {
			writer.flush();
			writer.close();
			writer = null;
			pageCount++ ;
			if( pageCount >= maxPageCount ) {
				final String errMsg = "EXCELのページ(シート)が最大ページ数(1000)をオーバーしました。"
								+ CR
								+ "この数は、DB_MAX_ROW_COUNT ではなく、LISTID_999.htmlのオーバーを意味します。"
								+ CR;
				throw new HybsSystemException( errMsg );
			}
		}

		final int pgCnt = pageCount + 1000;		// 桁合わせの為、下３桁を利用します。

		final String subName = String.valueOf( pgCnt ).substring( 1 );
		final String filename = htmlFileKey + "_" + subName + ".html" ;

	 	// 3.8.0.0 (2005/06/07) FileUtil#getPrintWriter を利用。
		writer = FileUtil.getPrintWriter( new File( htmlDir,filename ),ENCODE );

		// 3.7.0.1 (2005/01/31) ページブレイク時の処理
		pageRowCount = maxRowCount ;	// そのページの頭のデータ行数をセット
		pageBreak    = false;			// pageBreak フラグを元に戻す。
	}

	/**
	 * ヘッダーフッターのレンデラーデータを設定します。
	 * カンマで複数指定で、リクエスト情報でも設定できます。
	 *
	 * @og.rev 6.1.1.0 (2015/01/17) getRendererValue の代わりに、getWriteValue を使うように変更。
	 */
	protected void setHeaderFooter() {

		DBColumn clm ;
		if( headerKeys != null ) {
			for( int i=0; i<headerKeys.length; i++ ) {
				clm = resource.getDBColumn( headerKeys[i] );
				if( clm != null ) {
					// 6.1.1.0 (2015/01/17) getRendererValue の代わりに、getWriteValue を使うように変更。
					headerVals[i] = clm.getWriteValue( headerVals[i] );
				}
			}
		}

		if( footerKeys != null ) {
			for( int i=0; i<footerKeys.length; i++ ) {
				clm = resource.getDBColumn( footerKeys[i] );
				if( clm != null ) {
					// 6.1.1.0 (2015/01/17) getRendererValue の代わりに、getWriteValue を使うように変更。
					footerVals[i] = clm.getWriteValue( footerVals[i] );
				}
			}
		}
	}

	/**
	 * リーダー、ライターの終了処理を行います。
	 *
	 */
	private void close() {
		if( writer != null ) {
			writer.flush();
			writer.close();
			writer = null;
		}
		Closer.ioClose( reader );		// 4.0.0 (2006/01/31) close 処理時の IOException を無視
		reader = null;
	}
}
