/*
 * 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.util.ArrayList;
import java.util.List;

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

/**
 * Calc帳票システムでタグのパースを行うためのクラスです。
 *
 * 主に開始タグ、終了タグを指定したパースのループ処理を行うための機能を提供します。
 * 具体的には、{@link #doParse(String, String, String)}により、パース文字列、開始タグ、終了タグを
 * 指定し、パースを行います。
 * パース後の文字列は、{@link #doParse(String, String, String)}の戻り値になります。
 *
 * パース実行中に、発見された開始タグから終了タグまでの間の文字列の処理は、{@link #exec(String, StringBuilder, int)}を
 * オーバーライドすることにより定義します。
 *
 * また、このクラスでは、パースに必要な各種ユーティリティメソッドについても同様に定義されています。
 *
 * @og.group 帳票システム
 *
 * @version  4.0
 * @author   Hiroki.Nakamura
 * @since    JDK1.6
 */
class TagParser {

	private int preOffset	;
	private int curOffset	;

	/**
	 * パース処理を行います。
	 *
	 * パース中に取り出された開始タグから終了タグまでの文字列の処理は、
	 * {@link #exec(String, StringBuilder, int)}で定義します。
	 *
	 * また、isAddTagをtrueにした場合、{@link #exec(String, StringBuilder, int)}に渡される
	 * 文字列に、開始タグ、終了タグが含まれます。
	 * 逆にfalseにした場合は、開始タグ、終了タグを除き、{@link #exec(String, StringBuilder, int)}に渡されます。
	 *
	 * @og.rev 5.2.2.0 (2010/11/01) 読み飛ばしをした場合に、開始タグが書き込まれないバグを修正
	 *
	 * @param content パース対象文字列
	 * @param startTag 開始タグ
	 * @param endTag 終了タグ
	 * @param isAddTag 開始タグ・終了タグを含むか
	 *
	 * @return パース後の文字列
	 * @og.rtnNotNull
	 * @see #exec(String, StringBuilder, int)
	 */
	public String doParse( final String content, final String startTag, final String endTag, final boolean isAddTag ) {
		final StringBuilder buf = new StringBuilder( BUFFER_MIDDLE );
		String errMsg = null;

		while( ( preOffset = content.indexOf( startTag, Math.max( preOffset, curOffset ) ) ) >= 0 ) {
			buf.append( content.substring( curOffset, preOffset ) );
			curOffset = content.indexOf( endTag, preOffset + startTag.length() );

			if( checkIgnore( preOffset, curOffset ) ) {
				if( curOffset < 0 ){
					errMsg = "[ERROR]PARSE:開始タグを終了タグの整合性が不正です。[開始タグ=" + startTag + ":終了タグ=" + endTag + "]";
					throw new HybsSystemException( errMsg );
				}
				preOffset += startTag.length();
				curOffset += endTag.length();

				String str = null;
				if( isAddTag ) {
					str = content.substring( preOffset - startTag.length(), curOffset );
				}
				else {
					str = content.substring( preOffset, curOffset - endTag.length() );
				}

				exec( str, buf, curOffset );
			}
			else {
				// 5.2.2.0 (2010/11/01) 開始タグが書き込まれないバグを修正
				buf.append( startTag );
				preOffset += startTag.length();
				curOffset = preOffset;
			}
		}
		buf.append( content.substring( curOffset, content.length() ) );

		return buf.toString();
	}

	/**
	 * パース処理を行います。
	 *
	 * 詳細は、{@link #doParse(String, String, String, boolean)}のJavadocを参照して下さい。
	 *
	 * @param content パース対象文字列
	 * @param startTag 開始タグ
	 * @param endTag 終了タグ
	 *
	 * @return パース後の文字列
	 * @see #doParse(String, String, String, boolean)
	 */
	public String doParse( final String content, final String startTag, final String endTag ) {
		return doParse( content, startTag, endTag, true );
	}

	/**
	 * 開始タグから終了タグまでの文字列の処理を定義します。
	 *
	 * この実装では、何も処理を行いません。(切り出した文字列はアペンドされません)
	 * サブクラスでオーバーライドして実際の処理を実装して下さい。
	 *
	 * @param str 開始タグから終了タグまでの文字列(開始タグ・終了タグを含む)
	 * @param buf 出力を行う文字列バッファ
	 * @param offset 終了タグのオフセット
	 */
	protected void exec( final String str, final StringBuilder buf, final int offset ) {
		// Document empty method 対策
	}

	/**
	 * 開始タグから終了タグまでの文字列の処理を実行するかどうかを定義します。
	 *
	 * falseが返された場合、何も処理されず({@link #exec(String, StringBuilder, int)}が実行されない)、
	 * 元の文字列がそのまま出力されます。
	 *
	 * @param strOffset 開始タグのオフセット
	 * @param endOffset 終了タグのオフセット
	 *
	 * @return 処理を行うかどうか(true:処理を行う false:処理を行わない)
	 */
	protected boolean checkIgnore( final int strOffset, final int endOffset ) {
		return true;
	}

	/**
	 * パース実行中のoffset値を外部からセットします。
	 *
	 * このメソッドは、{@link #exec(String, StringBuilder, int)}で、処理結果により、offset値を
	 * 進めておく必要がある場合に利用されます。(つまり通常は利用する必要はありません)
	 *
	 * @param offset オフセット
	 * @see #exec(String, StringBuilder, int)
	 */
	public void setOffset( final int offset ) {
		curOffset = offset;
	}

	/**
	 * 引数の文字列を指定された開始タグ、終了タグで解析し配列として返す、ユーティリティメソッドです。
	 *
	 * 開始タグより前の文字列は0番目に、終了タグより後の文字列は1番目に格納されます。
	 * 2番目以降に、開始タグ、終了タグの部分が格納されます。
	 *
	 * @param str		解析する文字列
	 * @param startTag	開始タグ
	 * @param endTag	終了タグ
	 *
	 * @return 解析結果の配列
	 */
	public static String[] tag2Array( final String str, final String startTag, final String endTag ) {
		String header = null;
		String footer = null;
		final List<String> body = new ArrayList<>();

		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 解析結果の文字
	 */
	public static String getValueFromTag( final String str, final String start, final String end ) {
		int startOffset = str.indexOf( start );
		// 4.2.4.0 (2008/06/02) 存在しない場合はnullで返す
		if( startOffset == -1 ) {
			return null;
		}
		startOffset += start.length();

		final int endOffset = str.indexOf( end, startOffset );
//		final String value = str.substring( startOffset, endOffset );

		// 7.2.9.5 (2020/11/28) PMD:Consider simply returning the value vs storing it in local variable 'XXXX'
		return str.substring( startOffset, endOffset );

//		return value;
	}

	/**
	 * 引数のキーから不要なキーを取り除く、ユーティリティメソッドです。
	 *
	 * @og.rev 5.1.8.0 (2010/07/01) spanタグを削除
	 *
	 * @param key	オリジナルのキー
	 * @param sb	キーの外に含まれるaタグを削除するための、バッファ
	 *
	 * @return 削除後のキー
	 * @og.rtnNotNull
	 */
	public static String checkKey( final String key, final StringBuilder sb ) {
		if( key.indexOf( '<' ) < 0 && key.indexOf( '>' ) < 0 ) { return key; }

		final StringBuilder rtn = new StringBuilder( key );
		final String tagEnd = ">";
		int rtnOffset = -1;

		// <text:a ...>{@XXX</text:a>の不要タグを削除
		final String delTagStart1 = "<text:a ";
		final String delTagEnd1 = "</text:a>";
		while( ( rtnOffset = rtn.lastIndexOf( delTagEnd1 ) ) >= 0 ) {
			boolean isDel = false;
			// キー自身に含まれるaタグを削除
			int startOffset = rtn.lastIndexOf( delTagStart1, rtnOffset );
			if( startOffset >= 0 ) {
				final int endOffset = rtn.indexOf( tagEnd, startOffset );
				if( endOffset >= 0 ) {
					rtn.delete( rtnOffset, rtnOffset + delTagEnd1.length() );
					rtn.delete( startOffset, endOffset + tagEnd.length() );
					isDel = true;
				}
			}
			else {
				// キーの外に含まれるaタグを削除
				startOffset = sb.lastIndexOf( delTagStart1 );
				if( startOffset >= 0 ) {
					final int endOffset = sb.indexOf( tagEnd, startOffset );
					if( endOffset >= 0 ) {
						rtn.delete( rtnOffset, rtnOffset + delTagEnd1.length() );
						sb.delete( startOffset, endOffset + tagEnd.length() );
						isDel = true;
					}
				}
			}
			if( !isDel ) { break; }
		}

		// 5.1.8.0 (2010/07/01) spanタグを削除
		final String delTagStart2 = "<text:span ";
		final String delTagEnd2 = "</text:span>";
		while( ( rtnOffset = rtn.lastIndexOf( delTagEnd2 ) ) >= 0 ) {
			boolean isDel = false;
			// キー自身に含まれるspanタグを削除
			int startOffset = rtn.lastIndexOf( delTagStart2, rtnOffset );
			if( startOffset >= 0 ) {
				final int endOffset = rtn.indexOf( tagEnd, startOffset );
				if( endOffset >= 0 ) {
					rtn.delete( rtnOffset, rtnOffset + delTagEnd2.length() );
					rtn.delete( startOffset, endOffset + tagEnd.length() );
					isDel = true;
				}
			}
			else {
				// キーの外に含まれるspanタグを削除
				startOffset = sb.lastIndexOf( delTagStart2 );
				if( startOffset >= 0 ) {
					final int endOffset = sb.indexOf( tagEnd, startOffset );
					if( endOffset >= 0 ) {
						rtn.delete( rtnOffset, rtnOffset + delTagEnd2.length() );
						sb.delete( startOffset, endOffset + tagEnd.length() );
						isDel = true;
					}
				}
			}
			if( !isDel ) { break; }
		}

		return rtn.toString();
	}
}
