/**
 * Copyright 2014 Hanei Management Co.,Ltd. 
 * 
 * This file is part of Jaxcel
 * 
 *  Jaxcel is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU Lesser General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  Jaxcel is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU Lesser General Public License for more details.
 *
 *  You should have received a copy of the GNU Lesser General Public License
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package org.hanei.jaxcel.report;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Map;


import org.apache.poi.hssf.usermodel.HSSFSheet;
import org.apache.poi.hssf.usermodel.HSSFWorkbook;
import org.apache.poi.openxml4j.opc.OPCPackage;
import org.apache.poi.poifs.filesystem.NPOIFSFileSystem;
import org.apache.poi.poifs.filesystem.OfficeXmlFileException;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.ss.usermodel.WorkbookFactory;
import org.apache.poi.ss.util.CellReference;
import org.apache.poi.xssf.usermodel.XSSFChartSheet;
import org.apache.poi.xssf.usermodel.XSSFDialogsheet;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.hanei.jaxcel.exception.JaxcelInputException;
import org.hanei.jaxcel.exception.JaxcelOutputException;
import org.slf4j.LoggerFactory;
import org.slf4j.Logger;

/**
 * Excel帳票生成クラス<br>
 * テンプレートのExcelファイルにデータを挿入することでExcel帳票を生成する。
 * 
 * <h4>テンプレートの書式について</h4>
 * <h5>${expression}</h5>
 * <p>値出力。expressionを解析し値を出力する。</p>
 * 
 * <h5>#foreach(${object in aryObject}[ rows:"number"][ cols:"number"][ direction:"row|col"][ style:"copy|^copy"][ shift:"true|false"][ block:"true|false"][ start:"number"][ end:"number"])</h5>
 * <p>foreach句。配列やListオブジェクト、Mapオブジェクトの繰返し出力を行う。<br>
 * プロパティにより、繰返し出力方向の指定や、繰返し回数、出力範囲以降のセルのシフト方法等を指定可能。</p>
 * <table border="1" cellspacing="1">
 * <tr><th>属性</th><th>説明</th></tr>
 * <tr>
 * 		<td>${object in aryObject}</td>
 * 		<td>"${" 要素オブジェクト in 配列オブジェクト "}" のフォーマットで指定する。配列オブジェクトには配列、List、Mapが使用可能。<br>
 * 		rows・colsオプションで指定した範囲内で、要素オブジェクトで指定した名称で各インデックスの要素が出力可能。</td>
 * </tr>
 * <tr>
 * 		<td>rows</td>
 * 		<td>繰返し元の行数を正の整数で指定する。"1"指定で#foreach指示子が記述された行のみとなる。省略すると規定値である "1"となる。</td>
 * </tr>
 * <tr>
 * 		<td>cols</td>
 * 		<td>繰返し元の列数を正の整数で指定する。"1"指定で#foreach指示子が記述された列のみとなる。省略すると規定値である "1"となる。</td>
 * </tr>
 * <tr>
 * 		<td>direction</td>
 * 		<td>繰返し処理の方向を指定する。省略すると規定値である"row"となる。<br>
 * 		<table>
 * 		<tr><td>row</td><td>行方向に繰返す。</td></tr>
 * 		<tr><td>col</td><td>列方向に繰返す。</td></tr>
 * 		</table></td>
 * </tr>
 * <tr>
 * 		<td>style</td>
 * 		<td>繰返し元範囲(rows・colsオプションで指定する範囲)のセルスタイルのコピーについて指定する。省略すると規定値である"copy"となる。<br>
 * 		<table>
 * 		<tr><td>copy</td><td>セルスタイルをコピーする。</td></tr>
 * 		<tr><td>copy以外</td><td>セルスタイルをコピーしない。</td></tr>
 * 		</table></td>
 * </tr>
 * <tr>
 * 		<td>shift</td>
 * 		<td>繰返し範囲より後方の範囲のシフト方法について指定する。省略すると規定値である"true"となる。<br>
 * 		<table>
 * 		<tr><td>true</td><td>繰返し範囲より後方の範囲をdirectionオプションで指定した方向にシフトする。</td></tr>
 * 		<tr><td>false</td><td>シフトしない。</td></tr>
 * 		</table></td>
 * </tr>
 * <tr>
 * 		<td>block</td>
 * 		<td>矩形範囲での繰返し処理を行うかについて指定する。省略すると規定値である"true"となる。<br>
 * 		<table>
 * 		<tr><td>true</td><td>繰返し範囲を矩形範囲(rows・colsオプションで指定する範囲)とする。</td></tr>
 * 		<tr><td>false</td><td>繰返し範囲を繰返し元範囲に含まれる行・列全体とする。<br>
 * 		directionオプションが"row"の場合、繰返し元範囲に含まれる行全体が繰返し範囲となる。<br>
 * 		directionオプションが"col"の場合、繰返し元範囲に含まれる列全体が繰返し範囲となる。</td></tr>
 * 		</table></td>
 * </tr>
 * <tr>
 * 		<td>start</td>
 * 		<td>配列オブジェクトの出力開始インデックス(1起点)を正の整数、もしくは、正の整数を返却する関数で指定する。省略すると規定値である"1"となる。</td>
 * </tr>
 * <tr>
 * 		<td>end</td>
 * 		<td>配列オブジェクトの出力終了インデックス(1起点)を正の整数、もしくは、正の整数を返却する関数で指定する。省略すると配列オブジェクトの要素数となる。</td>
 * </tr>
 * <tr>
 * 		<td colspan="2">startオプションよりendオプションの指定値が小さい場合、配列オブジェクトを降順で出力する。<br>
 * 		配列オブジェクトの要素数は"size(配列オブジェクト)"関数で取得可能。</td>
 * </tr>
 * </table>
 * 
 * <h5>#if(${expression}[ rows:"number"][ cols:"number"][ delete:"left|up|clear"][ block:"true|false"])</h5>
 * <p>if句。expression判定式がtrue判定の場合は指定範囲の出力、false判定の場合は指定の方法で範囲を削除する。</p>
 * <table border="1" cellspacing="1">
 * <tr><th>属性</th><th>説明</th></tr>
 * <tr>
 * 		<td>${expression}</td>
 * 		<td>判定式。"${" 式 "}" のフォーマットで指定する。Bool値を返す式を指定する。Bool値を返さない式の場合は、値がnullでなければtrueと判断する。</td>
 * </tr>
 * <tr>
 * 		<td>rows</td>
 * 		<td>制御範囲の行数を正の整数で指定する。"1"指定で#if指示子が記述された行のみとなる。省略すると規定値である "1"となる。</td>
 * </tr>
 * <tr>
 * 		<td>cols</td>
 * 		<td>制御範囲の列数を正の整数で指定する。"1"指定で#if指示子が記述された列のみとなる。省略すると規定値である "1"となる。</td>
 * </tr>
 * <tr>
 * 		<td>delete</td>
 * 		<td>判定式がflse判定の場合の制御範囲に対する操作を指定する。省略すると規定値である"left"となる。<br>
 * 		<table>
 * 		<tr><td>left</td><td>制御範囲セルを削除し、左に詰める。</td></tr>
 * 		<tr><td>up</td><td>制御範囲セルを削除し、上に詰める。</td></tr>
 * 		<tr><td>clear</td><td>制御範囲セルの値・計算式をクリアするのみで詰めない。</td></tr>
 * 		</table>
 * </tr>
 * <tr>
 * 		<td>block</td>
 * 		<td>矩形範囲での制御を行うかについて指定する。省略すると規定値である"true"となる。<br>
 * 		<table>
 * 		<tr><td>true</td><td>制御範囲を矩形範囲(rows・colsオプションで指定する範囲)とする。</td></tr>
 * 		<tr><td>false</td><td>制御範囲を矩形範囲に含まれる行・列全体とする。<br>
 * 		deleteオプションが"left"の場合、矩形範囲に含まれる列全体が制御範囲となる。<br>
 * 		deleteオプションが"up"の場合、矩形範囲に含まれる行全体が制御範囲となる。<br>
 * 		deleteオプションが"clear"の場合は無効。</td></tr>
 * 		</table></td>
 * </tr>
 * </table>
 * 
 * @version 1.00.00
 * @author Noboru Saito
 */
public class ReportMaker {

	private static final Logger log = LoggerFactory.getLogger(ReportMaker.class);

	/**
	 * POIファイルシステム<br>
	 * Excel 2003 形式（.xls）
	 */
	private NPOIFSFileSystem npoifs = null;
	
	/**
	 * POIファイルシステム<br>
	 * Excel 2007 形式（.xlsx, .xlsm）
	 */
	private OPCPackage pkg = null;
	
	/**
	 *  EL式マネージャ
	 */
	private ELManager elMgr = null;
	
	/**
	 *  コンテキスト
	 */
	private JaxcelContext context  = null;

	/**
	 * コンストラクタ
	 */
	public ReportMaker() {}

	/**
	 * 入力ストリームのExcelテンプレートファイルにデータを挿入することでExcel帳票を生成、Workbookオブジェクトを返却する。<br>
	 * 返却されたWorkbookオブジェクトはPOIを使用し、加工・出力が可能。<br>
	 * 入力ストリームは別途クローズが必要。
	 * 
	 * @param template Excelテンプレートファイル入力ストリーム
	 * @param parameter テンプレートに挿入するデータ
	 * 
	 * @return Workbookオブジェクト
	 * 
	 * @throws JaxcelInputException 入力例外発生時
	 */
	public Workbook makeReport(InputStream template, Map<String, Object> parameter) {
		log.trace("makeReport start");

		// 引数チェック
		if(template == null) {
			log.error("template is null");
			throw new JaxcelInputException("template is null");
		}
		if(parameter == null) {
			log.debug("parameter is null");
		}

		// Excelテンプレートファイルオープン
		Workbook book = openWorkbook(template);
		
		// Excel帳票生成
		makeReport(book, parameter);
		
		log.trace("makeReport end");
		return book;
	}

	/**
	 * 入力ストリームのExcelテンプレートファイルにデータを挿入することでExcel帳票を生成、出力ストリームにExcel帳票を出力する。<br>
	 * 入出力ストリームは別途クローズが必要。
	 * 
	 * @param template Excelテンプレートファイル入力ストリーム
	 * @param parameter テンプレートに挿入するデータ
	 * @param output Excel帳票出力ストリーム
	 *
	 * @throws JaxcelInputException 入力例外発生時
 	 * @throws JaxcelOutputException 出力例外発生時
	 */
	public void makeReport(InputStream template, Map<String, Object> parameter, OutputStream output) {
		log.trace("makeReport start");
		
		// Excel帳票生成
		Workbook book = makeReport(template, parameter);
		
		// 出力
		outputReport(book, output);

		// テンプレートファイルクローズ
		close();

		log.trace("makeReport end");
	}

	 /**
	 * 入力ストリームのExcelテンプレートファイルにデータを挿入することでExcel帳票を生成、Excel帳票ファイルを出力する。<br>
	 * 入力ストリームは別途クローズが必要。
	 * 
	 * @param template Excelテンプレートファイル入力ストリーム
	 * @param parameter テンプレートに挿入するデータ
	 * @param output Excel帳票出力ファイル
	 *
	 * @throws JaxcelInputException 入力例外発生時
 	 * @throws JaxcelOutputException 出力例外発生時
	 */
	public void makeReport(InputStream template, Map<String, Object> parameter, File output) {
		log.trace("makeReport start");
		
		// Excel帳票生成
		Workbook book = makeReport(template, parameter);
		
		// 出力ストリーム
		FileOutputStream _output;
		try {
			_output = new FileOutputStream(output);
		}
		catch(Exception e) {
			log.error("output file open error: {}", e.getMessage(), e);
			throw new JaxcelOutputException("output file open error");
		}

		// 出力
		outputReport(book, _output);
		try {
			_output.close();
		} catch (IOException e) {
			log.error("output file close error: {}", e.getMessage(), e);
			throw new JaxcelOutputException("output file close error");
		}

		// テンプレートファイルクローズ
		close();

		log.trace("makeReport end");
	}
	
	/**
	 * Excelテンプレートファイルにデータを挿入することでExcel帳票を生成、Workbookオブジェクトを返却する。<br>
	 * 返却されたWorkbookオブジェクトはPOIを使用し、加工・出力が可能。<br>
	 * Excelテンプレートファイルは別途クローズが必要。
	 * 
	 * @param template Excelテンプレートファイル
	 * @param parameter テンプレートに挿入するデータ
	 * 
	 * @return Workbookオブジェクト
	 * 
	 * @throws JaxcelInputException 入力例外発生時
	 */
	public Workbook makeReport(File template, Map<String, Object> parameter) {
		log.trace("makeReport start");

		// 引数チェック
		if(template == null) {
			log.error("template file is null");
			throw new JaxcelInputException("template file is null");
		}
		else if(!template.exists()) {
			log.error("template file does not exist: {}", template.getAbsolutePath());
			throw new JaxcelInputException("template file does not exist");
		}
		else if(!template.canRead()) {
			log.error("template file can not read: {}", template.getAbsolutePath());
			throw new JaxcelInputException("template file can not read");
		}
		if(parameter == null) {
			log.debug("parameter is null");
		}

		// Excelテンプレートファイルオープン
		Workbook book = openWorkbook(template);
		
		// Excel帳票生成
		makeReport(book, parameter);
		
		log.trace("makeReport end");
		return book;
	}

	/**
	 * Excelテンプレートファイルにデータを挿入することでExcel帳票を生成、出力ストリームにExcel帳票を出力する。<br>
	 * 出力ストリームは別途クローズが必要。
	 * 
	 * @param template Excelテンプレートファイル
	 * @param parameter テンプレートに挿入するデータ
	 * @param output Excel帳票出力ストリーム
	 * 
	 * @throws JaxcelInputException 入力例外発生時
	 * @throws JaxcelOutputException 出力例外発生時
	 */
	public void makeReport(File template, Map<String, Object> parameter, OutputStream output) {
		log.trace("makeReport start");

		// Workbook生成
		Workbook book = makeReport(template, parameter);

		// 出力
		outputReport(book, output);
	
		// テンプレートファイルクローズ
		close();

		log.trace("makeReport end");
	}

	/**
	 * Excelテンプレートファイルにデータを挿入することでExcel帳票を生成、Excel帳票ファイルを出力する。<br>
	 * 
	 * @param template Excelテンプレートファイル
	 * @param parameter テンプレートに挿入するデータ
	 * @param output Excel帳票出力ファイル
	 * 
	 * @throws JaxcelInputException 入力例外発生時
	 * @throws JaxcelOutputException 出力例外発生時
	 */
	public void makeReport(File template, Map<String, Object> parameter, File output) {
		log.trace("makeReport start");

		// Workbook生成
		Workbook book = makeReport(template, parameter);

		// 出力ストリーム
		FileOutputStream _output;
		try {
			_output = new FileOutputStream(output);
		}
		catch(Exception e) {
			log.error("output file open error: {}", e.getMessage(), e);
			throw new JaxcelOutputException("output file open error");
		}

		// 出力
		outputReport(book, _output);
		try {
			_output.close();
		} catch (IOException e) {
			log.error("output file close error: {}", e.getMessage(), e);
			throw new JaxcelOutputException("output file close error");
		}
		
		// テンプレートファイルクローズ
		close();

		log.trace("makeReport end");
	}
	
	/**
	 * ExcelテンプレートのWorkbookオブジェクトにデータを挿入することでExcel帳票を生成する。<br>
	 * Excelテンプレートファイルは別途クローズが必要。
	 * 
	 * @param book Workbookオブジェクト
	 * @param parameter テンプレートに挿入するデータ
	 * 
	 * @throws JaxcelInputException 入力例外発生時
	 */
	public void makeReport(Workbook book, Map<String, Object> parameter) {
		log.trace("makeReport start");

		// 引数チェック
		if(book == null) {
			log.error("workbook is null");
			throw new JaxcelInputException("Workbook is null");
		}
		else if(!(book instanceof HSSFWorkbook) && !(book instanceof XSSFWorkbook)) {
			log.error("Workbook is unsupport type: {}", book.getClass().getName());
			throw new JaxcelInputException("Workbook is unsupported type");
		}
		if(parameter == null) {
			log.debug("parameter is null");
		}

		// Jaxlsコンテキスト生成
		context = new JaxcelContext();

		// EL式マネージャ生成。パラメータ設定
		elMgr = new ELManager();
		elMgr.setParameter(parameter);
		
		// JaxlsコンテキストにEL式マネージャ設定
		context.setElManager(elMgr);

		// Book生成
		makeBook(book);
		
		log.trace("makeReport end");
	}

	/**
	 * ワークブックのオープン
	 * 
	 * @param template Excelテンプレートファイル 入力ストリーム or ファイル
	 * 
	 * @throws JaxcelInputException Excelテンプレートファイルオープン失敗時
	 */
	private Workbook openWorkbook(Object template) {
		log.trace("openWorkbook start");

		// Workbookオブジェクト 
		Workbook book = null;
	
		// Excelテンプレートファイルオープン
		
		try {
			// Excel2003以前
			if(template instanceof File) {
				npoifs = new NPOIFSFileSystem((File) template);
			}
			else {
				npoifs = new NPOIFSFileSystem((InputStream) template);
			}
			book = WorkbookFactory.create(npoifs);
		} catch(OfficeXmlFileException | IOException e1) {
	
			// Excel2007以降
			try {
				if(template instanceof File) {
					pkg = OPCPackage.open((File) template);
				}
				else {
					pkg = OPCPackage.open((InputStream) template);
				}
				book = WorkbookFactory.create(pkg);
			} catch (Exception e2) {
				log.error("template file open error: {}. {}", e1.getMessage(), e2.getMessage());
			}
		}

		// チェック
		if(book == null) {
			throw new JaxcelInputException("template file open error");
		}
	
		log.trace("openWorkbook end");
		return book;
	}

	
	/**
	 * 出力ストリームにワークブックを出力する
	 * 
	 * @version 1.00.00
	 * @param book	Workbookオブジェクト
	 * @param output	出力ストリーム
	 *
 	 * @throws JaxcelOutputException 出力例外発生時
	 */
	private void outputReport(Workbook book, OutputStream output) {
		log.trace("outputReport start");

		try {
			book.write(output);
		} catch (Exception e) {
			log.error("workbook output error: {}", e.getMessage(), e);
			throw new JaxcelOutputException("workbook output error");
		}
		
		log.trace("outputReport end");
	}

	/**
	 * Excelテンプレートファイルのクローズ<br>
	 * テンプレートファイルの変更は保存しません。
	 * 
	 * @throws JaxcelOutputException 出力例外発生時
	 */
	public void close() {
		log.trace("close start");

		try {
			if (npoifs != null) {
				npoifs.close();
				log.debug("template file close.");
				npoifs = null;
			}
			if (pkg != null) {
				pkg.revert();
				log.debug("template file close.");
				pkg = null;
			}
		} catch (IOException e) {
			log.error("template file close error: {}", e.getMessage(), e);
			throw new JaxcelOutputException("template file close error");
		}

		log.trace("close end");
	}

	/**
	 * Workbook生成
	 * @param book Workbookオブジェクト
	 */
	private void makeBook(Workbook book) {
		log.trace("makeBook start");
		
		// シートでループ
		log.debug("sheet count: {}", book.getNumberOfSheets());
		for(int i = 0; i < book.getNumberOfSheets(); i++) {
			// カレントシート取得
			Sheet sheet = book.getSheetAt(i);
			if(sheet == null) {
				log.warn("sheet[{}] is null. skip", i);
				continue;
			}
			else if((sheet instanceof HSSFSheet && ((HSSFSheet)sheet).getDialog()) || (sheet instanceof XSSFDialogsheet)) {
				log.debug("sheet[{}] is dialog sheet. skip", i);
				continue;
			}
			else if(sheet instanceof XSSFChartSheet) {
				log.debug("sheet[{}] is chart sheet. skip", i);
				continue;
			}
			
			log.debug("sheet[{}] name: {}", i, sheet.getSheetName());

			// シート生成
			makeSheet(sheet);
		}
		
		// 再計算
		book.setForceFormulaRecalculation(true);
		
		log.trace("makeBook end");
	}
	
	/**
	 * ワークシート生成
	 * @param sheet ワークシートオブジェクト
     */
	private void makeSheet(Sheet sheet) {
		log.trace("makeSheet start");
		
		Row row;						// 行オブジェクト
		Cell cell;						// セルブジェクト
		TLParser tlParser;				// パーサ

		// カレントシート設定、パーサ生成
		context.setCurrentSheet(sheet);
		tlParser = new TLParser(context);

		//最大行数
		int lastRowNum = sheet.getLastRowNum();
		log.debug("lastRowNum: {}", lastRowNum);
		
		//最大セル数
		int maxColNum = 0;
		
		// 行方向にループ
		for(int rowIdx = 0; rowIdx <= lastRowNum; rowIdx++) {
			// 行取得
			row = sheet.getRow(rowIdx);

			// チェック
			if(row == null) {
				log.debug("row[{}] is null", (rowIdx + 1));
				continue;
			}

			// 列最終取得・最大列数更新
			if(row.getLastCellNum() > maxColNum) {
				maxColNum = row.getLastCellNum();
				log.debug("maxColNum: {}", maxColNum);
			}

			// 列方向にループ
			for(int cellIdx = 0; cellIdx <= maxColNum; cellIdx++) {
				// セル取得
				cell = row.getCell(cellIdx);

				// チェック
				if(cell == null) {
					log.debug("cell[{}] is null", (new CellReference(rowIdx, cellIdx)).formatAsString());
					continue;
				}
				
				// セルタイプにより分岐
				switch (cell.getCellType()) {
				// 文字列セル、計算式セル
				case Cell.CELL_TYPE_STRING:
				case Cell.CELL_TYPE_FORMULA:
					// パース
					tlParser.parse(cell);
					// 再パースフラグONなら
					if(tlParser.isReParseCell()) {
						// もう一度そのセルからループする
						cellIdx--;
					}
					break;
				// 文字列セル、計算式セル以外
				default:
					log.debug("cell type is not string or formula");
					continue;
				}

				// 列最終取得・最大列数更新
				if(row.getLastCellNum() > maxColNum) {
					maxColNum = row.getLastCellNum();
					log.debug("maxColNum update: {}", maxColNum);
				}
			}
			//最大行数更新
			if(lastRowNum < sheet.getLastRowNum()) {
				lastRowNum = sheet.getLastRowNum();
				log.debug("lastRowNum update: {}", lastRowNum);
			}
		}
		log.trace("transformSheet end");
	}
}
