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

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.List;

import org.opengion.fukurou.util.Closer;
import org.opengion.fukurou.util.FileUtil;
import org.opengion.fukurou.util.QrcodeImage;
import org.opengion.hayabusa.common.HybsSystem;
import org.opengion.hayabusa.common.HybsSystemException;

/**
 * 指定されたパスに存在するODSの各XMLファイルをパースし、帳票定義及び
 * 帳票データから書き換えます。
 * 書き換えは読み取り先と同じファイルであるため、一旦読み取った各XMLを
 * メモリ上に格納したからパース後のXMLファイルの書き込みを行います。
 * 
 * パース対象となるファイルは以下の3つです。
 *  content.xml シートの中身を定義
 *  meta.xml    メタデータを定義
 *  style.xml   帳票ヘッダーフッターを定義
 *  
 * content.xmlのパース処理として、まずxmlファイルをシート+行単位に分解します。
 * その後、分解された行毎に帳票データを埋め込み、出力先のXMLに書き込みを行います。
 * 書き込みは行単位に行われます。
 * 
 * また、Calcの特性として、関数の引数に不正な引数が指定された場合、(Text関数の
 * 引数にnullが指定された場合等)、エラー:XXXという文字が表示されます。
 * ここでは、これを回避するため、全ての関数にisError関数を埋め込み、エラー表示を
 * 行わないようにしています。
 *
 * @og.group 帳票システム
 *
 * @version  4.0
 * @author   Hiroki.Nakamura
 * @since    JDK1.6
 */
public class OdsContentParser {

	//======== content.xmlのパースで使用 ========================================
	/* シートの開始終了タグ */
	private static final String BODY_START_TAG = "<table:table ";
	private static final String BODY_END_TAG = "</table:table>";
	
	/* 行の開始終了タグ */
	private static final String ROW_START_TAG = "<table:table-row ";
	private static final String ROW_END_TAG = "</table:table-row>";

	/* ページエンドカットの際に、行を非表示にするためのテーブル宣言 */
	private static final String ROW_START_TAG_INVISIBLE = "<table:table-row table:visibility=\"collapse\" ";

	/* シート名を取得するための開始終了文字 */
	private static final String SHEET_NAME_START = "table:name=\"";
	private static final String SHEET_NAME_END = "\"";
	
	/* オブジェクトの終了位置(シート名)を見つけるための開始文字 */
	private static final String OBJECT_SEARCH_STR = "table:end-cell-address=\"";
	
	/* 印刷範囲指定の開始終了文字 */
	// 4.3.3.5 (2008/11/08) 空白ページ対策で追加
	private static final String PRINT_RANGE_START = "table:print-ranges=\"";
	private static final String PRINT_RANGE_END = "\"";

	/* 表紙印刷用のページ名称 */
	private static final String FIRST_PAGE_NAME = "FIRST";
	
	/* 変数定義の開始終了文字及び区切り文字 */
	private static final String VAL_START = "{@";
	private static final String VAR_END = "}";
	private static final String VAR_CON = "_";
	
	/* ページエンドカットのカラム文字列 */
	private static final String PAGE_END_CUT = "PAGEENDCUT";
	
	/* ページブレイクのカラム文字列 */
	private static final String PAGE_BREAK = "PAGEBREAK";
	
	/* ラインコピー文字列 5.0.0.2 (2009/09/15) */
	private static final String LINE_COPY = "LINECOPY";

	/* 画像のリンクを取得するための開始終了文字 */
	private static final String DRAW_IMG_START_TAG = "<draw:image xlink:href=\"";
	private static final String DRAW_IMG_END_TAG = "</draw:image>";
	private static final String DRAW_IMG_HREF_END = "\"";

	/* 画像ファイルを保存するためのパス */
	private static final String IMG_DIR = "Pictures";

	/* QRコードを処理するためのカラム名 */
	private static final String QRCODE_PREFIX = "QRCODE.";

	/* 作成したQRコードのフォルダ名及び拡張子 */
	private static final String QRCODE_FILETYPE = ".png";
	
	/* 4.3.3.5 (2008/11/08) 動的に画像を入れ替えるためのパスを記述するカラム名 */
	private static final String IMG_PREFIX = "IMG.";

	/* ファンクション定義を見つけるための開始終了文字 */
	private static final String OOOC_FUNCTION_START = "oooc:=";
	private static final String OOOC_FUNCTION_START_3 = "of:="; // 4.3.7.2 (2009/06/15) ODS仕様変更につき追加
//	private static final String OOOC_FUNCTION_END = "\"";
	private static final String OOOC_FUNCTION_END = ")\" ";
	
	/* セル内の改行を定義する文字列 5.0.2.0 (2009/11/01) */
	private static final String OOO_CR = "</text:p><text:p>";
	//===========================================================================

	//======== meta.xmlのパースで使用 ===========================================
	/* 総シートカウント数 */
	private static final String TABLE_COUNT_START_TAG = "meta:table-count=\"";
	private static final String TABLE_COUNT_END_TAG = "\"";

	/* 総セルカウント数 */
	private static final String CELL_COUNT_START_TAG = "meta:cell-count=\"";
	private static final String CELL_COUNT_END_TAG = "\"";

	/* 総オブジェクトカウント数 */
	private static final String OBJECT_COUNT_START_TAG = "meta:object-count=\"";
	private static final String OBJECT_COUNT_END_TAG = "\"";
	//===========================================================================

	/*
	 * 処理中の行番号の状態
	 * NORMAL : 通常
	 * LASTROW : 最終行
	 * OVERFLOW : 終了
	 */
	private static final int NORMAL = 0;
	private static final int LASTROW = 1;
	private static final int OVERFLOW = 2;
	private int status = NORMAL;

	/*
	 * 各雛形ファイルを処理する際の基準となる行数
	 * 初期>0 2行({@XXX_1}まで)処理後>2 ･･･
	 * 各雛形で定義されている行番号 + [baseRow] の値がDBTableModel上の行番号に相当する
	 * currentMaxRowは各シート処理後の[baseRow]と同じ
	 */
	private int currentBaseRow = 0;
	private int currentMaxRow = 0;

	/* 処理したページ数 */
	private int pages = 0;

	/* 処理行がページエンドカットの対象かどうか */
//	private boolean isPageEndCut = false;			// 4.3.1.1 (2008/08/23) ローカル変数化
	
	/* ページブレイクの処理中かどうか */
	private boolean isPageBreak = false;

	/* XML宣言の文字列。各XMLで共通なのでクラス変数として定義 */
	private String xmlHeader = null;

	private final ExecQueue queue;
	private final String path;

	/**
	 * コンストラクタ
	 * 
	 * @og.rev 5.1.2.0 (2010/01/01) 処理した行数をQueueオブジェクトから取得(シート数が256を超えた場合の対応)
	 * 
	 * @param qu ExecQueue
	 * @param pt String
	 */
	OdsContentParser( final ExecQueue qu, final String pt ) {
		queue = qu;
		path = pt;

		currentBaseRow = queue.getExecRowCnt();
	}
	
	/**
	 * パース処理を実行します
	 */
	public void exec() {
		/*
		 * 雛形ヘッダーフッターの定義
		 * OOoではページ毎にヘッダーフッターが設定できないよう。
		 * なので、全てヘッダー扱いで処理
		 */
		execStyles(); 
		
		/* 中身の変換 */
		execContent();
		
		/* メタデータの変換 */
		execMeta();
	}

	/**
	 * 帳票処理キューを元に、content.xmlを書き換えます。
	 * まず、XMLを一旦メモリ上に展開した後、シート単位に分解し、データの埋め込みを行います。
	 * 
	 * @og.rev 4.3.0.0 (2008/07/18) ページ数が256を超えた場合のエラー処理
	 * @og.rev 5.0.0.2 (2009/09/15) LINECOPY機能追加
	 * @og.rev 5.1.2.0 (2010/01/01) 処理したページ数、行数をQueueオブジェクトにセット(シート数が256を超えた場合の対応)
	 */
	private void execContent() {
		String fileName = path + "content.xml";
		String content = readOOoXml( fileName );

		// ファイルの解析し、シート+行単位に分解
		String[] tags = tag2Array( content, BODY_START_TAG, BODY_END_TAG );
		String contentHeader = tags[0];
		String contentFooter = tags[1];
		List<OOoCalcSheet> sheets = new ArrayList<OOoCalcSheet>();
		for( int i = 2; i < tags.length; i++ ) {
			OOoCalcSheet sheet = new OOoCalcSheet();
			// sheet.analyze( tags[i] );
			sheet.analyze( tags[i],queue.getBody().getRowCount() ); // 5.0.0.2 (2009/09/15)
			sheets.add( sheet );
		}

		// content.xmlの書き出し
		BufferedWriter bw = null;
		try {
			bw = getWriter( fileName );

			bw.write( xmlHeader );
			bw.write( '\n' );

			// ヘッダー
			bw.write( contentHeader );

			// ボディー部分
			for ( OOoCalcSheet sheet : sheets ) {
//				// 4.3.0.0 (2008/07/18) ページ数が256を超えた場合にエラーとする
//				if( pages > 256 ){
//					queue.addMsg( "[ERROR]PARSE:シート数が256枚を超えました。" + HybsSystem.CR );
//					throw new HybsSystemException();
//				}

				// 表紙
				if ( sheet.getSheetName().startsWith( FIRST_PAGE_NAME ) ) {
					// 5.1.2.0 (2010/01/01) 256シートを超えた場合でも、初めのファイルの初めのページのみ表紙として処理する
					if( queue.getExecPagesCnt() == 0 ) {
						writeParsedSheet( sheet, bw );
					}
				}
				else {
					// 表紙以外
					while ( currentBaseRow < queue.getBody().getRowCount() ) {
						// 4.3.0.0 (2008/07/18) ページ数が256を超えた場合にエラーとする
						// 5.1.2.0 (2010/01/01) 256シートを超えた場合の対応
//						if( pages >= 256 ){
						if( pages >= ExecQueue.MAX_SHEETS_PER_FILE ) {
//							queue.addMsg( "[ERROR]PARSE:シート数が256枚を超えました。" + HybsSystem.CR );
//							throw new HybsSystemException();
							queue.setEnd( false );
							break;
						}

						writeParsedSheet( sheet, bw );
					}
				}
			}

			// 5.1.2.0 (2010/01/01) 256シートを超えた場合の対応
			queue.addExecPageCnt( pages );
			queue.setExecRowCnt( currentBaseRow );

			// フッター
			bw.write( contentFooter );
			bw.flush();
		}
		catch ( IOException ex ) {
			queue.addMsg( "[ERROR]PARSE:error occurer while write Parsed Sheet " + fileName + HybsSystem.CR );
			throw new HybsSystemException( ex );
		}
		finally {
			Closer.ioClose( bw );
		}
	}

	/**
	 * シート単位にパースされた文書データを書き込みます
	 * 出力されるシート名には、ページ番号と基底となる行番号をセットします。
	 * 
	 * @og.rev 4.2.4.0 (2008/07/04) 行単位にファイルに書き込むように変更
	 * 
	 * @param sheet
	 * @param bw
	 * @throws IOException 書き込みに失敗した場合
	 */
	private void writeParsedSheet( final OOoCalcSheet sheet, final BufferedWriter bw ) throws IOException {
		// シート名
//		String outputSheetName = "Page" + pages + "_" + "Row" + currentBaseRow + "";
		String outputSheetName = "Page" + ( queue.getExecPagesCnt() + pages ) + "_" + "Row" + currentBaseRow + "";

		// ページブレイク変数を初期化
		isPageBreak = false;
		
		// シートのヘッダー部分を書き込み(シート名も書き換え)
		// 4.3.3.5 (2008/11/08) 空白ページ出力の対策。印刷範囲のシート名書き換えを追加
//		bw.write( sheet.getHeader().replace( SHEET_NAME_START + sheet.getSheetName(), SHEET_NAME_START + outputSheetName ) );
		
		String headerStr = sheet.getHeader();
		// シート名書き換え
		headerStr = headerStr.replace( SHEET_NAME_START + sheet.getSheetName(), SHEET_NAME_START + outputSheetName );

		// 印刷範囲指定部分のシート名を変更
		int printRangeStart = headerStr.indexOf( PRINT_RANGE_START );
		if( printRangeStart >= 0 ) {
			int printRangeEnd = headerStr.indexOf( PRINT_RANGE_END, printRangeStart + PRINT_RANGE_START.length() );
			String rangeStr = headerStr.substring( printRangeStart, printRangeEnd );
			rangeStr = rangeStr.replace( sheet.getSheetName(), outputSheetName );
			headerStr = headerStr.substring( 0, printRangeStart ) + rangeStr + headerStr.substring( printRangeEnd );
		}
		
		bw.write( headerStr );

		// シートのボディ部分を書き込み
		String[] rows = sheet.getRows();
		for( int i = 0; i < rows.length; i++ ) {
			// 4.3.4.4 (2009/01/01)
			writeParsedRow( rows[i], bw, sheet.getSheetName(), outputSheetName );
		}
		// {@XXXX}が埋め込まれていない場合はエラー
		if( currentBaseRow == currentMaxRow ) {
			queue.addMsg( "[ERROR]PARSE:No Data defined on Template ODS(" + queue.getListId() + ")" + HybsSystem.CR );
			throw new HybsSystemException();
		}
		currentBaseRow = currentMaxRow;

		// シートのフッター部分を書き込み
		bw.write( sheet.getFooter() );
	
		pages++;
	}

	/**
	 * 行単位にパースされた文書データを書き込みます
	 * 
	 * @og.rev 4.2.3.1 (2008/06/19) 関数エラーを表示させないため、ISERROR関数を埋め込み
	 * @og.rev 4.2.4.0 (2008/07/04) 行単位にファイルに書き込むように変更
	 * @og.rev 4.3.0.0 (2008/07/17) ｛＠と｝の整合性チェック追加
	 * @og.rev 4.3.0.0 (2008/07/22) 行最後の｛＠｝整合性エラーハンドリング追加
	 * @og.rev 4.3.3.5 (2008/11/08) 画像の動的な入れ替えに対応
	 * 
	 * @param row
	 * @param bw
	 * @param sheetNameOrig
	 * @param sheetNameNew
	 * @throws IOException 書き込みに失敗した場合
	 */
	private void writeParsedRow( final String row, final BufferedWriter bw, final String sheetNameOrig, final String sheetNameNew ) throws IOException {
		StringBuilder tmpBuf = new StringBuilder();

		// ページエンドカット変数初期化
		boolean isPageEndCut = false;

		int preOffset = -1;
		int curOffset = 0;
		while( ( preOffset = row.indexOf( VAL_START, preOffset + 1 ) ) >= 0 ) {
			tmpBuf.append( row.substring( curOffset, preOffset ) );
			curOffset = row.indexOf( VAR_END, preOffset + 1 ) + VAR_END.length();

			// {@XXXX_XX}のXXXX_XXの部分
			// 4.3.0.0 (2008/07/22) {@よりも}のindexOfの方が小さい場合に不整合エラーを出す
			if( preOffset + VAL_START.length() > curOffset - VAR_END.length() ){
				queue.addMsg( "[ERROR]PARSE:{@と}の整合性が不正です。" + HybsSystem.CR );
				throw new HybsSystemException();
			}
			String key = row.substring( preOffset + VAL_START.length(), curOffset - VAR_END.length() );

			key = checkKey( key, tmpBuf );
			
			// 4.3.0.0 (2008/07/15) "<"が入っていた場合には{@不整合}エラー
			int keyCheck = key.indexOf( '<' ); 
			if( keyCheck >= 0 ){
				queue.addMsg( "[ERROR]PARSE:{@と}の整合性が不正です。キー=" + key.substring( 0, keyCheck ) + HybsSystem.CR );
				throw new HybsSystemException();
			}

			// QRコードの処理、処理後はoffsetが進むため、offsetを再セット
			if( key.startsWith( QRCODE_PREFIX ) ) {
				curOffset = makeQRImage( row, curOffset, key.substring( QRCODE_PREFIX.length() ), tmpBuf );
			}
			else if( key.startsWith( IMG_PREFIX  ) ) {
				curOffset = changeImage( row, curOffset, key.substring( IMG_PREFIX.length() ), tmpBuf );
			}

			tmpBuf.append( getValue( key ) );

			// 処理行がページエンドカットの対象か
			if( queue.isFgcut() && PAGE_END_CUT.equals( key ) ) {
				isPageEndCut = true;
			}
		}

		tmpBuf.append( row.substring( curOffset, row.length() ) );

		//==== ここからは後処理 =========================================================
		/*
		 * ページエンドカットの判定は最後で処理する。
		 * {@PAGEENDCUT}が行の最初に書かれている場合は、OVERFLOWになっていない可能性が
		 * あるため行の途中では判断できない
		 */
		String rowStr = null;
		if( isPageEndCut && status == OVERFLOW ) {
			// ページエンドカットの場合は、非表示状態にする。
			rowStr = tmpBuf.toString().replace( ROW_START_TAG, ROW_START_TAG_INVISIBLE ) ;
		}
		else {
			rowStr = tmpBuf.toString();
		}
		
		/*
		 * オブジェクトで定義されているテーブル名を変更
		 */
		if( rowStr.indexOf( OBJECT_SEARCH_STR ) >= 0 ) {
			rowStr = rowStr.replace( OBJECT_SEARCH_STR + sheetNameOrig, OBJECT_SEARCH_STR + sheetNameNew );
		}

		/*
		 * 関数エラーを表示されないため、ISERROR関数を埋め込み 4.2.3.1 (2008/06/19)
		 */
		rowStr = replaceOoocError( rowStr );
		//==============================================================================
		
		bw.write( rowStr );
	}

	/**
	 * 変換後の行データで定義されている関数にISERROR関数を埋め込みます。
	 *
	 * これは、OOoの関数の動作として、不正な引数等が入力された場合(null値など)に、
	 * エラー:xxxと表示されてしまうため、これを防ぐために関数エラーのハンドリングを行い、
	 * エラーの場合は、空白文字を返すようにします。
	 * 
	 * @og.rev 4.3.7.2 (2009/06/15) 開始文字が変更になったため対応
	 * @og.rev 5.0.2.0 (2009/11/01) 関数内の"(quot)は、メタ文字に変換する
	 * 
	 * @param row
	 * @return 変換後の行データ
	 */
	private String replaceOoocError( final String row ) {
		// 4.3.7.2 (2009/06/15) OOOC_FUNCTION_START3の条件判定追加。どちらか分からないので変数で受ける。
		String functionStart = null;
		int ooocFunctionStart = row.indexOf( OOOC_FUNCTION_START );
		int ooocFunctionStart3 = row.indexOf( OOOC_FUNCTION_START_3 );
		if( ooocFunctionStart < 0 && ooocFunctionStart3 < 0 ) { return row; }
		else if( ooocFunctionStart >= 0){
			functionStart = OOOC_FUNCTION_START;
		}
		else{
			functionStart = OOOC_FUNCTION_START_3;
		}
		
		StringBuilder tmpBuf = new StringBuilder();
		int preOffset = -1;
		int curOffset = 0;
//		while( ( preOffset = row.indexOf( OOOC_FUNCTION_START, preOffset + 1 ) ) >= 0 ) {
//			tmpBuf.append( row.substring( curOffset, preOffset ) );
//			curOffset = row.indexOf( OOOC_FUNCTION_END, preOffset + 1 ) + OOOC_FUNCTION_END.length();
//			String key = row.substring( preOffset + OOOC_FUNCTION_START.length(), curOffset - OOOC_FUNCTION_END.length() );
//
//			tmpBuf.append( OOOC_FUNCTION_START + "IF(ISERROR(" + key + ");&quot;&quot;;" + key + ")" + OOOC_FUNCTION_END );
//		}
		while( ( preOffset = row.indexOf( functionStart, preOffset + 1 ) ) >= 0 ) {
			tmpBuf.append( row.substring( curOffset, preOffset ) );
			curOffset = row.indexOf( OOOC_FUNCTION_END, preOffset + 1 ) + OOOC_FUNCTION_END.length();
			String key = row.substring( preOffset + functionStart.length(), curOffset - OOOC_FUNCTION_END.length() + 1 );
			// 5.0.2.0 (2009/11/01)
			key = key.replace( "\"", "&quot;&quot;" ).replace( OOO_CR, "" );
			tmpBuf.append( functionStart + "IF(ISERROR(" + key + ");&quot;&quot;;" + key + OOOC_FUNCTION_END );
		}
		tmpBuf.append( row.substring( curOffset, row.length() ) );

		return tmpBuf.toString();
	}

	/**
	 * QRコードを作成します。
	 * この処理では、offsetを進めるため、戻り値として処理後のoffsetを返します。
	 * 
	 * @og.rev 4.3.1.1 (2008/08/23) mkdirs の戻り値判定
	 * @og.rev 4.3.3.5 (2008/11/08) ↑の判定は存在チェックを行ってから処理する。ファイル名に処理行を付加
	 * 
	 * @param row
	 * @param curOffset
	 * @param key
	 * @param sb
	 * @return 処理後のオフセット
	 */
	private int makeQRImage( final String row, final int curOffset, final String key, final StringBuilder sb ) {
		int offset = curOffset;

		// {@QRCODE.XXXX}から実際に画像のパスが書かれている部分までを書き込む
		offset = row.indexOf( DRAW_IMG_START_TAG, offset ) + DRAW_IMG_START_TAG.length();
		sb.append( row.substring( curOffset, offset ) );
		// 画像のパスの終了インデックスを求める
		offset = row.indexOf( DRAW_IMG_HREF_END, offset ) + DRAW_IMG_HREF_END.length();

		// QRCODEの画像ファイル名を求め書き込む
		// 4.3.3.5 (2008/11/08) ファイル名に処理行を付加
		String fileName = IMG_DIR + '/' + key + "_" + currentBaseRow + QRCODE_FILETYPE;
		sb.append( fileName ).append( DRAW_IMG_HREF_END );

		// QRCODEに書き込む値を求める
		String value = getValue( key );

		// QRCODEの作成
		// 4.3.3.5 (2008/11/08) ファイル名に処理行を付加
		String fileNameAbs =
			new File( path ).getAbsolutePath() + File.separator + IMG_DIR + File.separator + key + "_" + currentBaseRow + QRCODE_FILETYPE;

		// 画像リンクが無効となっている場合は、Picturesのフォルダが作成されていない可能性がある
		// 4.3.1.1 (2008/08/23) mkdirs の戻り値判定
		// 4.3.3.5 (2008/11/08) 存在チェック追加
		if( !new File( fileNameAbs ).getParentFile().exists() ) {
			if( new File( fileNameAbs ).getParentFile().mkdirs() ) {
				System.err.println( fileNameAbs + " の ディレクトリ作成に失敗しました。" );
			}
		}

		QrcodeImage qrImage = new QrcodeImage();
		qrImage.init( value, fileNameAbs );
		qrImage.saveImage();

		// 読み込みOffsetを返します
		return offset;
	}

	/**
	 * DBTableModelに設定されたパスから画像データを取得し、内部に取り込みます
	 * この処理では、offsetを進めるため、戻り値として処理後のoffsetを返します。
	 * 
	 * @og.rev 4.3.3.5 (2008/11/08) 新規追加
	 * @og.rev 4.3.3.6 (2008/11/15) 画像パスが存在しない場合は、リンクタグ(draw:image)自体を削除
	 * 
	 * @param row
	 * @param curOffset
	 * @param key
	 * @param sb
	 * @return 処理後のオフセット
	 */
	private int changeImage( final String row, final int curOffset, final String key, final StringBuilder sb ) {
		int offset = curOffset;
		File imgFile = null;

		// 画像ファイルを読み込むパスを求める
		String value = getValue( key );

		if( value != null && value.length() > 0 ) {
			imgFile = new File( HybsSystem.url2dir( value ) );
		}

		// 画像ファイルのパスが入っていて、実際に画像が存在する場合
		if( imgFile != null && imgFile.exists() ) {
			// {@IMG.XXXX}から実際に画像のパスが書かれている部分までを書き込む
			offset = row.indexOf( DRAW_IMG_START_TAG, offset ) + DRAW_IMG_START_TAG.length();
			sb.append( row.substring( curOffset, offset ) );

			// 画像のパスの終了インデックスを求める
			offset = row.indexOf( DRAW_IMG_HREF_END, offset ) + DRAW_IMG_HREF_END.length();

			String fileNameOut = IMG_DIR + '/' + imgFile.getName();
			sb.append( fileNameOut ).append( DRAW_IMG_HREF_END );;

			String fileNameOutAbs =
				new File( path ).getAbsolutePath() + File.separator + IMG_DIR + File.separator + imgFile.getName();
			if( !new File( fileNameOutAbs ).getParentFile().exists() ) {
				if( new File( fileNameOutAbs ).getParentFile().mkdirs() ) {
					System.err.println( fileNameOutAbs + " の ディレクトリ作成に失敗しました。" );
				}
			}
			FileUtil.copy( imgFile, new File( fileNameOutAbs ) );
		}
		// 画像パスが設定されていない、又は画像が存在しない場合
		else {
			// {@IMG.XXXX}から見て、<draw:image> ... </draw:image>までをスキップする
			offset = row.indexOf( DRAW_IMG_START_TAG, offset );
			sb.append( row.substring( curOffset, offset ) );

			offset = row.indexOf( DRAW_IMG_END_TAG, offset ) + DRAW_IMG_END_TAG.length();
		}

		// 読み込みOffsetを返します
		return offset;
	}

	/**
	 * 指定されたキーの値を返します。
	 * 
	 * @og.rev 4.3.0.0 (2008/07/18) アンダースコアの処理変更
 	 * @og.rev 4.3.5.0 (2008/02/01) カラム名と行番号文字の位置は最後から検索する 4.3.3.4 (2008/11/01) 修正分
	 * @param key
	 * @return 値
	 */
	private String getValue( final String key ) {
//		int conOffset = key.indexOf( VAR_CON );
		int conOffset = key.lastIndexOf( VAR_CON );

		String value = null;

		if( conOffset < 0 ) {
			value = getHeaderFooterValue( key );
		}
		else {
			String name = key.substring( 0, conOffset );
			int rownum = -1;
			try {
				rownum = Integer.valueOf( key.substring( conOffset + VAR_CON.length(), key.length() ) ) + currentBaseRow;
			}
			catch ( NumberFormatException ex ) {
				// 4.3.0.0 (2008/07/18) エラーが起きてもなにもしない。
				// queue.addMsg( "[ERROR]雛形の変数定義が誤っています。カラム名=" + name + HybsSystem.CR );
				// throw new Exception( ex );
			}

			// 4.3.0.0 (2008/07/18) アンダースコア後が数字に変換できない場合はヘッダフッタとして認識
			if( rownum < 0 ){
				value = getHeaderFooterValue( key );
			}
			else{
				value = getBodyValue( name, rownum );
			}
		}
		
		return checkValue( value );
	}

	/**
	 * 指定されたキーのヘッダー、フッター値を返します。
	 * 
	 * @og.rev 4.3.6.0 (2009/04/01) レンデラー適用されていないバグを修正
	 * @og.rev 5.0.2.0 (2009/11/01) ローカルリソースフラグを使用しない場合は、リソース変換を行わない。
	 * 
	 * @param key
	 * @return 値
	 */
	private String getHeaderFooterValue( final String key ) {
		String value = "";

		// 最後の行かオーバーフロー時はフッター
		if( status >= LASTROW ) {
			if( queue.getFooter() != null ) {
				int clmno = queue.getFooter().getColumnNo( key, false );
				if( clmno >= 0 ) {
					value = queue.getFooter().getValue( 0, clmno );
					// 5.0.2.0 (2009/11/01) ローカルリソースフラグを使用しない場合は、リソース変換を行わない。
					if( queue.isFglocal() ) {
						// 4.3.6.0 (2009/04/01)
						value = queue.getFooter().getDBColumn( clmno ).getRendererValue( value );
					}
				}
			}
		}
		// 最後の行にきていない場合はヘッダー
		else {
			if( queue.getHeader() != null ) {
				int clmno = queue.getHeader().getColumnNo( key, false );
				if( clmno >= 0 ) {
					value = queue.getHeader().getValue( 0, clmno );
					// 5.0.2.0 (2009/11/01) ローカルリソースフラグを使用しない場合は、リソース変換を行わない。
					if( queue.isFglocal() ) {
						// 4.3.6.0 (2009/04/01)
						value = queue.getHeader().getDBColumn( clmno ).getRendererValue( value );
					}
				}
			}
		}

		return value;
	}

	/**
	 * 指定された行番号、キーのボディー値を返します。
	 * 
	 * @og.rev 4.3.6.0 (2009/04/01) レンデラー適用されていないバグを修正
	 * @og.rev 4.3.6.2 (2009/04/15) 行番号のより小さいカラム定義を読んだ際に、内部カウンタがクリアされてしまうバグを修正
	 * @og.rev 4.3.6.2 (2009/04/15) 一度オーバーフローした場合に移行が全て空文字で返ってしまうバグを修正
	 * @og.rev 5.0.2.0 (2009/11/01) ローカルリソースフラグを使用しない場合は、リソース変換を行わない。
	 * 
	 * @param key
	 * @param rownum
	 * @return 値
	 */
	private String getBodyValue( final String key, final int rownum ) {
		// if( status == OVERFLOW || isPageBreak ) { return ""; }
		if( isPageBreak ) { return ""; } // 4.3.6.2 (2009/04/15) OVERFLOW時バグ修正

		int clmno = queue.getBody().getColumnNo( key, false );
		if( clmno < 0 ) { return ""; }

		// ページブレイク判定、先読みして判断
		if( PAGE_BREAK.equals( key ) ) {
			if( rownum <= queue.getBody().getRowCount() - 2 ) {
				if( !( queue.getBody().getValue( rownum, clmno ).equals( queue.getBody().getValue( rownum + 1, clmno ) ) ) ) {
					isPageBreak = true;
				}
			}
			return "";
		}

		if( rownum >= queue.getBody().getRowCount() ) {
			status = OVERFLOW;
			return "";
		}

		if( rownum == queue.getBody().getRowCount() - 1 ) {
			// status = LASTROW;
			status = Math.max( LASTROW, status ); // 4.3.6.2 (2009/04/15) 自身のステータスと比べて大きい方を返す
		}
		String value = queue.getBody().getValue( rownum, clmno );
		// 5.0.2.0 (2009/11/01) ローカルリソースフラグを使用しない場合は、リソース変換を行わない。
		if( queue.isFglocal() ) {
			// 4.3.6.0 (2009/04/01)
			value = queue.getBody().getDBColumn( clmno ).getRendererValue( value );
		}

		// 4.3.6.2 (2009/04/15)
		if( currentMaxRow < rownum + 1 ) {
			currentMaxRow = rownum + 1;
		}

		return value;
	}
	

	/**
	 * 引数のキーから不要なキーを取り除きます。
	 * 
	 * @param key
	 * @param sb
	 * @return 削除後のキー
	 */
	private static String checkKey( final String key, final StringBuilder sb ) {
		if( key.indexOf( '<' ) < 0 && key.indexOf( '>' ) < 0 ) { return key; }

		String rtn = key;
		String tagEnd = ">";

		// <text:a ...>{@XXX</text:a>の不要タグを削除
		String delTagStart1 = "<text:a ";
		String delTagEnd1 = "</text:a>";
		if( key.indexOf( delTagEnd1 ) >= 0 ) {
			rtn = rtn.replace( delTagEnd1, "" );
			int startOffset = sb.lastIndexOf( delTagStart1 );
			int endOffset = sb.indexOf( tagEnd, startOffset );
			sb.delete( startOffset, endOffset + tagEnd.length() );
		}

		return rtn;
	}
	
	/**
	 * 値に'<'や'>','&'が含まれていた場合にメタ文字に変換します。
	 * 
	 * @og.rev 5.0.2.0 (2009/11/01) 改行Cの変換ロジックを追加
	 * @og.rev 5.0.2.0 (2009/11/01) リソース変換時のspanタグを除去
	 * 
	 * @param value
	 * @return 変換後の値
	 */
	private String checkValue( final String value ) {
		String rtn = value;

		// 5.0.2.0 (2009/11/01)
		if( queue.isFglocal() ) {
			int idx = -1;
			if( ( idx = rtn.indexOf( "<span" ) ) >= 0 ) {
				String spanStart = rtn.substring( idx, rtn.indexOf( '>', idx ) + 1 ); 
				rtn = rtn.replace( spanStart, "" ).replace( "</span>", "" );
			}
		}

		if( rtn.indexOf( '&' ) >= 0 ) {
			rtn = rtn.replace( "&", "&amp;" );
		}
		if( rtn.indexOf( '<' ) >= 0 ) {
			rtn = rtn.replace( "<", "&lt;" );
		}
		if( rtn.indexOf( '>' ) >= 0 ) {
			rtn = rtn.replace( ">", "&gt;" );
		}
		if( rtn.indexOf( "\n" ) >= 0 ) {
			rtn = rtn.replace( "\r\n", "\n" ).replace( "\n", OOO_CR );
		}

		return rtn;
	}
	
	/**
	 * 引数の文字列を指定された開始タグ、終了タグで解析し配列として返します。
	 * 開始タグより前の文字列は0番目に、終了タグより後の文字列は1番目に格納されます。
	 * 2番目以降に、開始タグ、終了タグの部分が格納されます。
	 * 
	 * @param str
	 * @param startTag
	 * @param endTag
	 * @return 解析結果の配列
	 */
	private static String[] tag2Array( final String str, final String startTag, final String endTag ) {
		String header = null;
		String footer = null;
		List<String> body = new ArrayList<String>();
		
		int preOffset = -1;
		int curOffset = 0;
			
		while( true ) {
			curOffset = str.indexOf( startTag, preOffset + 1 );
			if( curOffset < 0 ) {
				curOffset = str.lastIndexOf( endTag ) + endTag.length();
				body.add( str.substring( preOffset, curOffset ) );
				
				footer = str.substring( curOffset );
				break;
			}
			else if( preOffset == -1 ) {
				header = str.substring( 0, curOffset );
			}
			else {
				body.add( str.substring( preOffset, curOffset ) );
			}
			preOffset = curOffset;
		}
		
		String[] arr = new String[body.size()+2];
		arr[0] = header;
		arr[1] = footer;
		for( int i=0; i<body.size(); i++ ) {
			arr[i+2] = body.get(i);
		}
		
		return arr;
	}
	
	/**
	 * 引数の文字列の開始文字と終了文字の間の文字列を取り出します。
	 * 
	 * @param str
	 * @param start
	 * @param end
	 * @return 解析結果の文字
	 */
	protected static String getValueFromTag( final String str, final String start, final String end ) {
		int startOffset = str.indexOf( start );
//		int startOffset = str.indexOf( start ) + start.length();
		// 4.2.4.0 (2008/06/02) 存在しない場合はnullで返す
		if( startOffset == -1 ) {
			return null;
		}
		startOffset +=  start.length();
		
		int endOffset = str.indexOf( end, startOffset );
		String value = str.substring( startOffset, endOffset );

		return value;
	}
	
	/**
	 * シート単位のcontent.xmlを管理するための内部クラスです。
	 * シートのヘッダー、行の配列、フッター及びシート名を管理します。
	 * 
	 * @og.rev 5.0.0.2 (2009/09/15) LINE_COPY機能を追加
	 * 
	 * @author Hiroki.Nakamura
	 */
	private static class OOoCalcSheet {

		private String			sheetHeader;
		private List<String>	sheetRows	= new ArrayList<String>();
		private String			sheetFooter;
		private String			sheetName;

		/**
		 * シートを行単位に分解します。
		 * 
		 * @og.rev 5.0.0.2 (2009/09/15) ボディ部のカウントを引数に追加し、LINECOPY機能実装。
		 * @param sheet
		 * @param bodyRowCount 
		 */
		// public void analyze( final String sheet ) {
		public void analyze( final String sheet, final int bodyRowCount ) {
			String[] tags = tag2Array( sheet, ROW_START_TAG, ROW_END_TAG );
			sheetHeader = tags[0];
			sheetFooter = tags[1];
			for( int i = 2; i < tags.length; i++ ) {
				sheetRows.add( tags[i] );
				lineCopy( tags[i], bodyRowCount ); // 5.0.0.2 (2009/09/15)
			}
			sheetName = getValueFromTag( sheetHeader, SHEET_NAME_START, SHEET_NAME_END );
		}

		/**
		 * ラインコピーに関する処理を行います。
		 * 
		 * {@LINE_COPY}が存在した場合に、テーブルモデル分だけ
		 * 行をコピーします。
		 * その際、{@xxx_y}のyをカウントアップしてコピーします。
		 * 
		 * 整合性等のエラーハンドリングはこのメソッドでは行わず、
		 * 実際のパース処理中で行います。
		 * 
		 * @og.rev 5.0.0.2 (2009/09/15) 追加
		 * @param row
		 * @param rowCount
		 */
		private void lineCopy( final String row, final int rowCount ) {
			int preOffset = -1;
			int curOffset = -1;

			preOffset = row.indexOf( VAL_START + LINE_COPY );
			// この段階で存在しなければ即終了
			if( preOffset < 0 ) { return; }

			curOffset = row.indexOf( VAR_END, preOffset );
			String lcKey = row.substring( preOffset + VAL_START.length(), curOffset );
			StringBuilder tmpBuf = new StringBuilder( row );
			if( LINE_COPY.equals( checkKey( lcKey, tmpBuf ) ) ){
				// 存在すればテーブルモデル行数-1回ループ(自身を除くため）
				for( int i = 1; i < rowCount; i++ ) {
					tmpBuf = new StringBuilder();
					preOffset = -1;
					curOffset = 0;
					while( ( preOffset = row.indexOf( VAL_START, preOffset + 1 ) ) >= 0 ) {
						tmpBuf.append( row.substring( curOffset, preOffset ) );
						curOffset = row.indexOf( VAR_END, preOffset + 1 ) + VAR_END.length();

						// {@XXXX_XX}のXXXX_XXの部分
						String key = row.substring( preOffset + VAL_START.length(), curOffset - VAR_END.length() );
						key = checkKey( key, tmpBuf );
						// 不整合はreturnで何もしないで返しておく
						int keyCheck = key.indexOf( '<' ); 
						if( keyCheck >= 0 ){
							return ;
						}
						tmpBuf.append( VAL_START ).append( incrementKey( key, i ) ).append( VAR_END );
					}
					tmpBuf.append( row.substring( curOffset, row.length() ) );

					sheetRows.add( tmpBuf.toString() ); // シートに追加
				}
			}

			return;
		}
		
		/**
		 * xxx_yのy部分を引数分追加して返します。
		 * yが数字でない場合や、_が無い場合はそのまま返します。
		 * 
		 * @og.rev 5.0.0.2 LINE_COPYで利用するために追加
		 * @param key
		 * @param inc
		 * @return 変更後キー
		 */
		private String incrementKey( final String key, final int inc ) {
			int conOffset = key.lastIndexOf( VAR_CON );
			String rtn = null;

			if( conOffset < 0 ) {
				rtn = key;
			}
			else {
				String name = key.substring( 0, conOffset );
				int rownum = -1;
				try {
					rownum = Integer.valueOf( key.substring( conOffset + VAR_CON.length(), key.length() ) );
				}
				catch ( NumberFormatException ex ) {
					// エラーが起きてもなにもしない。
				}

				// アンダースコア後が数字に変換できない場合はヘッダフッタとして認識
				if( rownum < 0 ) {
					rtn = key;
				}
				else {
					rtn = name + VAR_CON + ( rownum + inc );
				}
			}
			return rtn;
		}

		/**
		 * シートのヘッダー部分を返します。
		 * 
		 * @return ヘッダー
		 */
		public String getHeader() {
			return sheetHeader;
		}

		/**
		 * シートのフッター部分を返します。
		 * 
		 * @return フッター
		 */
		public String getFooter() {
			return sheetFooter;
		}

		/**
		 * シート名称を返します。
		 * 
		 * @return シート名称
		 */
		public String getSheetName() {
			return sheetName;
		}

		/**
		 * シートの各行を配列で返します。
		 * 
		 * @og.rev 4.3.1.1 (2008/08/23) あらかじめ、必要な配列の長さを確保しておきます。
		 * 
		 * @return シートの各行の配列
		 */
		public String[] getRows() {
//			return sheetRows.toArray( new String[0] );
			return sheetRows.toArray( new String[sheetRows.size()] );
		}
	}

	/**
	 * 帳票処理キューを元に、style.xml(ヘッダー、フッター)を書き換えます。
	 * 
	 * @throws Exception
	 */
	private void execStyles() {
		String fileName = path + "styles.xml";

		String styles = readOOoXml( fileName );

		StringBuilder sb = new StringBuilder();
		int preOffset = -1;
		int curOffset = 0;
		while( ( preOffset = styles.indexOf( VAL_START, preOffset + 1 ) ) >= 0 ) {
			sb.append( styles.substring( curOffset, preOffset ) );
			curOffset = styles.indexOf( VAR_END, preOffset + 1 ) + VAR_END.length();

			// {@XXXX_XX}のXXXX_XXの部分
			String key = styles.substring( preOffset + VAL_START.length(), curOffset - VAR_END.length() );

			sb.append( getHeaderFooterValue( key ) );
		}
		sb.append( styles.substring( curOffset, styles.length() ) );

		writeOOoXml( fileName, sb.toString() );
	}

	/**
	 * 帳票処理キューを元に、meta.xmlを書き換えます。
	 * 
	 * @throws Exception
	 */
	private void execMeta() {
		String fileName = path + "meta.xml";
		
		String meta = readOOoXml( fileName );

		// シート数書き換え
		String tableCount = OdsContentParser.getValueFromTag( meta, TABLE_COUNT_START_TAG, TABLE_COUNT_END_TAG );
		meta = meta.replace( TABLE_COUNT_START_TAG + tableCount, TABLE_COUNT_START_TAG + String.valueOf( pages ) );

		// セル数書き換え
		String cellCount = OdsContentParser.getValueFromTag( meta, CELL_COUNT_START_TAG, CELL_COUNT_END_TAG );
		meta = meta.replace( CELL_COUNT_START_TAG + cellCount, CELL_COUNT_START_TAG + String.valueOf( Integer.parseInt( cellCount ) * pages ) );

		// オブジェクト数書き換え
		String objectCount = OdsContentParser.getValueFromTag( meta, OBJECT_COUNT_START_TAG, OBJECT_COUNT_END_TAG );
		//4.2.4.0 (2008/06/02) 存在しない場合はnullで帰ってくるので無視する
		if( objectCount != null){
			meta = meta.replace( OBJECT_COUNT_START_TAG + objectCount, OBJECT_COUNT_START_TAG + String.valueOf( Integer.parseInt( objectCount ) * pages ) );
		}
		
		writeOOoXml( fileName, meta );
	}
	
	/**
	 * XMLファイルを読み取り、結果を返します。
	 * OOoのXMLファイルは全て1行めがxml宣言で、2行目が内容全体という形式であるため、
	 * ここでは、2行目の内容部分を返します。
	 * 
	 * @og.rev 4.3.6.0 (2009/04/01) meta.xmlでコンテンツの部分が改行されている場合があるため、ループを回して読込み
	 * 
	 * @param fileName
	 * @return 読み取った文字列
	 */
	private String readOOoXml( final String fileName ) {
		File file = new File ( fileName );

		BufferedReader br = null;
		String tmp = null;
		StringBuilder buf = new StringBuilder();
		try {
			br = new BufferedReader( new InputStreamReader( new FileInputStream( file ), "UTF-8" ) );
			xmlHeader = br.readLine();
//			str = br.readLine();
			while( ( tmp = br.readLine() ) != null ) { // 4.3.6.0 (2009/04/01)
				buf.append( tmp );
			}
		}
		catch( IOException ex ) {
			queue.addMsg( "[ERROR]PARSE:Failed to read " + fileName + HybsSystem.CR );
			throw new HybsSystemException( ex );
		}
		finally {
			Closer.ioClose( br );
		}

		String str = buf.toString();
		if( xmlHeader == null || xmlHeader.length() == 0 || str == null || str.length() == 0 ) {
			queue.addMsg( "[ERROR]PARSE:Maybe " + fileName + " is Broken!" + HybsSystem.CR );
			throw new HybsSystemException();
		}

		return str;
	}
	
	/**
	 * XMLファイルを書き込みます。
	 * OOoのXMLファイルは全て1行めがxml宣言で、2行目が内容全体という形式であるため、
	 * ここでは、2行目の内容部分を渡すことで、XMLファイルを作成します。
	 * 
	 * @param fileName
	 * @param str
	 * @throws Exception
	 */
	private void writeOOoXml( final String fileName, final String str ) {
		BufferedWriter bw = null;
		try {
			bw = getWriter( fileName );
			bw.write( xmlHeader );
			bw.write( '\n' );
			bw.write( str );
			bw.flush();
		}
		catch( IOException ex  ) {
			queue.addMsg( "[ERROR]PARSE:Failed to write " + fileName + HybsSystem.CR );
			throw new HybsSystemException( ex );
		}
		finally {
			Closer.ioClose( bw );
		}
	}
	
	
	/**
	 * XMLファイル書き込み用のライターを返します。
	 * 
	 * @param fileName
	 * @return ライター
	 * @throws Exception
	 */
	private BufferedWriter getWriter( final String fileName ) {
		File file = new File ( fileName );
		BufferedWriter bw;
		try {
			bw = new BufferedWriter( new OutputStreamWriter( new FileOutputStream( file ), "UTF-8" ) );
		}
		catch ( UnsupportedEncodingException ex ) {
			queue.addMsg( "[ERROR] Input File is written by Unsupported Encoding" );
			throw new HybsSystemException( ex );
		}
		catch ( FileNotFoundException ex ) {
			queue.addMsg( "[ERROR] File not Found" );
			throw new HybsSystemException( ex );
		}
		return bw;
	}
	
}
