/*
 * 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
import static org.opengion.fukurou.system.HybsConst.CR ;			// 8.0.3.0 (2021/12/17)

/**
 * 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 static final String VAR_START = "{@";		// 8.0.3.0 (2021/12/17) splitSufix で使います。
	private static final char   VAR_END   = '}';		// 8.0.3.0 (2021/12/17) splitSufix で使います。
	private static final char   VAR_CON   = '_';		// 8.0.3.0 (2021/12/17) splitSufix,SplitKey で使います。
	private static final char   TAG_START = '<';		// 8.1.1.1 (2022/02/18) 検索時に、ﾘﾝｸ等のﾀｸﾞ情報があれば無視します。

	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 );

		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 ){
					final String errMsg = "[ERROR]PARSE:開始ﾀｸﾞを終了ﾀｸﾞの整合性が不正です｡" + CR
										+ "[開始ﾀｸﾞ=" + 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();
	}

	/**
	 * "{&#064;" + key + '_' と、'}' の間の文字列を返します。
	 *
	 * '_' を含まない場合は、ゼロ文字列を返します。
	 * ここでは、簡易的に処理しているため、タグ等の文字列が含まれる場合は、
	 * 上手くいかない可能性があります。
	 *
	 * "{&#064;" + key + '_' と、'}' の間の文字列を返します。
	 * '_' が存在しない場合は、空文字列を返します。
	 * "{&#064;" + key が存在しない場合は、null を返します。
	 * "{&#064;" + key が存在しており、'}' が存在しない場合は、Exception が throw されます。
	 *
	 * @og.rev 8.0.3.0 (2021/12/17) 新規追加
	 * @og.rev 8.1.1.1 (2022/02/18) 検索時に、ﾘﾝｸ等のﾀｸﾞ情報があれば無視します。
	 *
	 * @param row	検索元の文字列
	 * @param key	検索対象のｷｰ
	 *
	 * @return "{&#064;" + key + '_' と、'}' の間の文字列を返します。
	 */
	public static String splitSufix( final String row,final String key ) {
		final int st1 = row.indexOf( VAR_START + key );				// "{@" + key
		if( st1 >= 0 ) {
			final int ed1 = row.indexOf( VAR_END  ,st1 );			// '}' を探す
			final int ed2 = row.indexOf( TAG_START,st1 );			// '<' を探す
			if( ed1 >= 0 ) {
				final int st2 = row.lastIndexOf​( VAR_CON,ed1 );	// '_' を逆順で探す
				if( st2 < 0 ) {		// '_' が無い場合は、空文字列を返す。
					return "";
				}
				else {
					// 8.1.1.1 (2022/02/18) '}' より前に'<'があれば、'<'の値を使用する。
					if( ed2 >= 0 && ed2 < ed1 ) {
						return row.substring( st2+1,ed2 );			// '_'の次の文字から、'<' 手前まで
					}
					else {
						return row.substring( st2+1,ed1 );			// '_'の次の文字から、'}' 手前まで
					}
				}
			}
			else {
				final String errMsg = "[ERROR]SHEET:{@と}の整合性が不正です｡" + CR
									+ "変数内の特定の文字列に書式設定がされている可能性があります｡ｷｰ=" + key;
				throw new HybsSystemException( errMsg );
			}
		}
		return null;
	}

	/**
	 * ｱﾝﾀﾞｰﾊﾞｰで､ｷｰと行番号の分離を行います｡
	 *
	 * @og.rev 8.0.3.0 (2021/12/17) ｱﾝﾀﾞｰﾊﾞｰで､ｷｰと行番号の分離を､ｲﾝﾅｰｸﾗｽ化します｡
	 */
	/* default */ static final class SplitKey {
		/** 分割後のｷｰ */
		public final String name ;
		/** 分割後の行番号 */
		public final int	rownum ;

		/**
		 * ｺﾝｽﾄﾗｸﾀで､分割､設定
		 *
		 * @param key 分割処理対象のｷｰ
		 */
		public SplitKey( final String key ) {
			final int idx = key.lastIndexOf( VAR_CON );

			int num = -1;
			if( idx >= 0 ) {
				try {
					num = Integer.parseInt( key.substring( idx+1 ) );
				}
				// '_'以降の文字が数字でない場合は､'_'以降の文字もｶﾗﾑ名の一部として扱う
				catch( final NumberFormatException ex ) {
					// 6.4.1.1 (2016/01/16) PMD refactoring. Avoid empty catch blocks
					final String errMsg = "'_'以降の文字をｶﾗﾑ名の一部として扱います｡" + CR
											+ "ｶﾗﾑ名=[" + key + "]" + CR
											+ ex.getMessage() ;
					System.err.println( errMsg );
				}
			}
			if( num >= 0 ) {
				name   = key.substring( 0, idx );
				rownum = num ;
			}
			else {
				name   = key;
				rownum = num ;
			}
		}

		/**
		 * XXX_番号の番号部分を引数分追加して返します｡
		 * 番号部分が数字でない場合や､_が無い場合はそのまま返します｡
		 *
		 * @param inc	ｶｳﾝﾀ部
		 *
		 * @return 変更後ｷｰ
		 */
		public String incrementKey( final int inc ) {
			return rownum < 0 ? name : name + VAR_CON + (rownum + inc) ;
//			return new StringBuilder().append(name).append(VAR_CON).append( rownum+inc ).toString();
		}

//		/**
//		 * rownumが無効(-1)ならcntを､有効なら､rownumを返します｡
//		 *
//		 * @param cnt	ﾃﾞﾌｫﾙﾄのｶｳﾝﾄ値
//		 *
//		 * @return rownumか､引数のcntを返します｡
//		 */
//		public int count( final int cnt ) {
//			return rownum < 0 ? cnt : rownum;
//		}
	}
}
