/*
 * 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.fukurou.taglet;		// 7.4.4.0 (2021/06/30) openGionV8事前準備(taglet2→taglet)

import com.sun.source.doctree.DocTree;

import org.opengion.fukurou.util.FileUtil;
import org.opengion.fukurou.util.StringUtil;
import static org.opengion.fukurou.system.HybsConst.CR;				// 6.1.0.0 (2014/12/26) refactoring

import java.io.File;
import java.io.PrintWriter;
import java.io.IOException;
import java.util.List;

import java.lang.reflect.Field;
// import java.security.AccessController;				// 6.1.0.0 (2014/12/26) findBugs
// import java.security.PrivilegedAction;				// 6.1.0.0 (2014/12/26) findBugs

/**
 * DocTree 情報を出力する PrintWriter 相当クラスです。
 *
 * @version  7.3
 * @author	Kazuhiko Hasegawa
 * @since	 JDK11.0,
 */
public final class DocTreeWriter implements AutoCloseable {
	private static final String OG_VALUE  = "{@og.value" ;
	private static final String OG_DOCLNK = "{@og.doc03Link" ;
	private static final String TAG_LNK   = "{@link" ;

//	private static final String CLS = "org.opengion.fukurou.system.BuildNumber";		// package.class
//	private static final String FLD = "VERSION_NO";										// field
//	private static final String VERSION_NO = getStaticField( CLS , FLD );				// 6.4.1.1 (2016/01/16) versionNo → VERSION_NO refactoring

	// 8.0.2.1 (2021/12/10) JavaDocのVersionは、最後の一桁が X になっている。
	private static final String VERSION_NO ;
	static {
		final String CLS = "org.opengion.fukurou.system.BuildNumber";	// package.class
		final String FLD = "VERSION_NO";								// field
		final String VER = getStaticField( CLS , FLD );					// VERSION_NO refactoring
		VERSION_NO = VER.substring( 0,VER.length()-1 ) + "X" ;			// 1桁取り除いて、X を追加
	}

	private String clsName ;

	private final PrintWriter outFile ;

	/**
	 * Doclet のエントリポイントメソッドです。
	 *
	 * @og.rev 7.3.0.0 (2021/01/06) 新しいJavaDoc対応
	 *
	 * @param file		出力ファイル名
	 * @param encode	エンコード
	 * @throws IOException なんらかのエラーが発生した場合。
	 */
	public DocTreeWriter( final String file,final String encode ) throws IOException {
		outFile = FileUtil.getPrintWriter( new File( file ),encode );
	}

	/**
	 * try-with-resourcesブロックで、自動的に呼ばれる AutoCloseable の実装。
	 *
	 * コンストラクタで渡された ResultSet を close() します。
	 *
	 * @og.rev 7.3.0.0 (2021/01/06) 新しいJavaDoc対応
	 *
	 * @see		java.lang.AutoCloseable#close()
	 */
	@Override
	public void close() {
		if( outFile != null ) {
			outFile.close();
		}
	}

	/**
	 * 現在処理中のクラス名をセットします。
	 * これは、og.value の値所得時の自身のクラス名を求める場合に使用します。
	 *
	 * @og.rev 7.3.0.0 (2021/01/06) 新しいJavaDoc対応
	 *
	 * @param str 現在処理中のクラス名
	 */
	public void setClassName( final String str ) {
		clsName = str;
	}

	/**
	 * 可変長の文字列引数を取り、文字列を出力します。
	 * 文字列の最後に改行が入ります。
	 *
	 * @og.rev 7.3.0.0 (2021/01/06) 新しいJavaDoc対応
	 *
	 * @param str String...
	 */
	public void printTag( final String... str ) {
		if( str.length == 3 ) {								// 3つの場合だけ、真ん中をconvertToOiginal 処理する。
			final StringBuilder buf = new StringBuilder( str[1] );
			valueTag(		buf );
			doc03LinkTag(	buf );
			linkTag(		buf );

			outFile.print( str[0] );
			outFile.print( convertToOiginal( buf.toString() ) );
			outFile.println( str[2] );
		}
		else {
			outFile.println( String.join( "",str ) );		// それ以外は単純な連結
		}
	}

	/**
	 * 文字列引数を ２つと、タグ配列を受け取り、タグ出力します。
	 *
	 * @og.rev 7.3.0.0 (2021/01/06) 新しいJavaDoc対応
	 * @og.rev 8.0.0.0 (2021/07/31) Avoid instantiating new objects inside loops
	 *
	 * @param str1	第一文字列
	 * @param doc	DocTreeリスト
	 * @param str3	第三文字列
	 */
	public void printTag( final String str1,final List<? extends DocTree> doc, final String str3 ) {
		final StringBuilder tmp = new StringBuilder( 1000 );
		final StringBuilder buf = new StringBuilder();					// 8.0.0.0 (2021/07/31) Avoid instantiating new objects inside loops
		for( final DocTree dt : doc ) {
//			final StringBuilder buf = new StringBuilder( String.valueOf(dt) );
			buf.setLength( 0 );											// 8.0.0.0 (2021/07/31)
			buf.append( String.valueOf(dt) );							// 8.0.0.0 (2021/07/31)
			valueTag(		buf );
			doc03LinkTag(	buf );
			linkTag(		buf );

			tmp.append( buf );
		}

		outFile.print( str1 );
		outFile.print( convertToOiginal( tmp.toString() ) );
		outFile.println( str3 );
	}

	/**
	 * Unicode文字列から元の文字列に変換する ("￥u3042" → "あ")。
	 *
	 * @og.rev 7.3.0.0 (2021/01/06) 新しいJavaDoc対応
	 *
	 * @param unicode Unicode文字列("\u3042")
	 *
	 * @return	通常の文字列
	 */
	/* default */ String convertToOiginal( final String unicode ) {
		final StringBuilder rtn = new StringBuilder( unicode );

		int st = rtn.indexOf( "\\u" );
		while( st >= 0 ) {
			final int ch = Integer.parseInt( rtn.substring( st+2,st+6 ),16 );
			rtn.replace( st,st+6, Character.toString( (char)ch ) );

			st = rtn.indexOf( "\\u",st + 1 );
		}

		return StringUtil.htmlFilter( rtn.toString() ).trim();
	}

	/**
	 * {&#064;og.value package.class#field} 形式のvalueタグを文字列に置き換えます。
	 *
	 * 処理的には、リフレクションで、値を取得します。値は、staticフィールドのみ取得可能です。
	 *
	 * @og.rev 7.3.0.0 (2021/01/06) 新しいJavaDoc対応
	 *
	 * @param buf Tagテキストを連結させるStringBuilder
	 *
	 * @return valueタグの解析結果のStringBuilder
	 */
	private StringBuilder valueTag( final StringBuilder buf ) {
		int st = buf.indexOf( OG_VALUE );
		while( st >= 0 ) {
			final int ed = buf.indexOf( "}", st+OG_VALUE.length() );		// 終了の "}" を探す
			if( ed < 0 ) {
				final String errMsg = "警告:{@og.value package.class#field} 形式の終了マーカー'}'がありません。" + CR
										+ "[" + clsName + "],[" + buf + "]" ;
				System.err.println( errMsg );
				break ;
			}

			final String val = buf.substring( st+OG_VALUE.length(),ed ).trim();

			String cls = null;		// package.class
			String fld = null;		// field
			// package.class#field 形式の解析。
			final int adrs = val.indexOf( '#' ) ;
			if( adrs > 0 ) {
				cls = val.substring( 0,adrs );		// package.class
				fld = val.substring( adrs+1 );		// field

				if( cls.indexOf( '.' ) < 0 ) {		// cls に . がない場合は、特殊な定数クラスか、自身のパッケージ内のクラス
					if( "HybsSystem".equals( cls ) || "SystemData".equals( cls ) ) {
						cls = "org.opengion.hayabusa.common." + cls ;
					}
					else if( "HybsConst".equals( cls ) ) {
						cls = "org.opengion.fukurou.system." + cls ;
					}
					else {
						final int sep = clsName.lastIndexOf( '.' );
						if( sep > 0 ) {
							cls = clsName.substring( 0,sep+1 ) + cls ;		// ピリオドも含めるので、sep+1 とする。
						}
					}
				}
			}
			else if( adrs == 0 ) {
				cls = clsName;						// 現在処理中のクラス名
				fld = val.substring( 1 );			// #field
			}
			else {
				final String errMsg = "警告:{@og.value package.class#field} 形式のフィールド名 #field がありません。" + CR
										+ "[" + clsName + "],[" + val + "]" ;
				System.err.println( errMsg );

				// # を付け忘れたと考え、自分自身のクラスを利用
				cls = clsName;						// 現在処理中のクラス名
				fld = val;							// field
			}

			final String text = getStaticField( cls,fld );
			buf.replace( st,ed+1,text );
			st = buf.indexOf( OG_VALUE,st+text.length() );		// 置き換えた文字列の長さ文だけ、後ろから再検索開始
		}
		return buf ;
	}

	/**
	 * {&#064;og.doc03Link queryType Query_****クラス} 形式のdoc03Linkタグをリンク文字列に置き換えます。
	 *
	 * &lt;a href="/gf/jsp/DOC03/index.jsp?command=NEW&amp;GAMENID=DOC03&amp;VERNO=X.X.X.X&amp;VALUENAME=queryType"
	 *          target="CONTENTS" &gt;Query_****クラス&lt;/a&gt;
	 * のようなリンクを作成します。
	 * 第一引数は、VALUENAME の引数です。
	 * それ以降のテキストは、リンク文字列のドキュメントになります。
	 * DOC03 画面へのリンクを作成するに当たり、バージョンが必要です。
	 * org.opengion.fukurou.system.BuildNumber#VERSION_NO から取得しますが、
	 * パッケージの優先順の関係で、リフレクションを使用します。
	 *
	 * @og.rev 7.3.0.0 (2021/01/06) 新しいJavaDoc対応
	 * @og.rev 8.0.2.1 (2021/12/10) 後で、htmlFilter処理するので、通常のhtmlにしておく。
	 *
	 * @param buf Tagテキストを連結させるStringBuilder
	 *
	 * @return valueタグの解析結果のStringBuilder
	 * @og.rtnNotNull
	 */
	private StringBuilder doc03LinkTag( final StringBuilder buf ) {
		int st = buf.indexOf( OG_DOCLNK );
		while( st >= 0 ) {
			final int ed = buf.indexOf( "}", st+OG_DOCLNK.length() );		// 終了の "}" を探す
			if( ed < 0 ) {
				final String errMsg = "警告:{@og.doc03Link queryType Query_****クラス} 形式の終了マーカー'}'がありません。" + CR
										+ "[" + clsName + "],[" + buf + "]" ;
				System.err.println( errMsg );
				break ;
			}

			final String val = buf.substring( st+OG_DOCLNK.length(),ed ).trim();

			String link = "" ;
			final int adrs = val.indexOf(' ') ;			// 最初のスペースで分離します。
			if( adrs > 0 ) {
				final String valnm = val.substring( 0,adrs ).trim();	// VALUENAME
				final String body  = val.substring( adrs+1 ).trim();	// ドキュメント

				// 8.0.2.1 (2021/12/10) 後で、htmlFilter処理するので、通常のhtmlにしておく。
//				link = "&lt;a href=\"/gf/jsp/DOC03/index.jsp?command=NEW&amp;GAMENID=DOC03"
//						+ "&amp;VERNO="     + VERSION_NO
//						+ "&amp;VALUENAME=" + valnm
//						+ "\" target=\"CONTENTS\"&gt;"
//						+ body
//						+ "&lt;/a&gt;" ;

				link = "<a href=\"/gf/jsp/DOC03/index.jsp?command=NEW&GAMENID=DOC03"
						+ "&VERNO="     + VERSION_NO
						+ "&VALUENAME=" + valnm
						+ "\" target=\"CONTENTS\">"
						+ body
						+ "</a>" ;
			}
			else {
				link = OG_DOCLNK + " 【不明】:" + val ;
				final String errMsg = "[" + clsName + "],[" + link + "]" ;
				System.err.println( errMsg );
			}

			buf.replace( st,ed+1,link );
			st = buf.indexOf( OG_DOCLNK,st+link.length() );		// 置き換えた文字列の長さ文だけ、後ろから再検索開始
		}
		return buf ;
	}

	/**
	 * このタグレットがインラインタグで {&#064;link XXXX YYYY} を処理するように
	 * 用意された、カスタムメソッドです。
	 *
	 * @og.rev 7.3.0.0 (2021/01/06) 新しいJavaDoc対応
	 *
	 * @param buf Tagテキストを連結させるStringBuilder
	 *
	 * @return valueタグの解析結果のStringBuilder
	 * @og.rtnNotNull
	 */
	private StringBuilder linkTag( final StringBuilder buf ) {
		int st = buf.indexOf( TAG_LNK );
		while( st >= 0 ) {
			final int ed = buf.indexOf( "}", st+TAG_LNK.length() );		// 終了の "}" を探す
			if( ed < 0 ) {
				final String errMsg = "警告:{@link XXXX YYYY} 形式の終了マーカー'}'がありません。" + CR
										+ "[" + clsName + "],[" + buf + "]" ;
				System.err.println( errMsg );
				break ;
			}

			final String val = buf.substring( st+TAG_LNK.length(),ed ).trim();

			String link = "" ;
			final int adrs = val.indexOf(' ') ;			// 最初のスペースで分離します。
			if( adrs > 0 ) {
				final String xxx = val.substring( 0,adrs ).trim();	// 前半：アドレス変換
				final String yyy = val.substring( adrs   ).trim();	// 後半：ラベル
				final String zzz = xxx.replace( '.','/' );

				link = "<a href=\"../../../../" + zzz + ".html\" " + "title=\"" + xxx + "\">" + yyy + "</a>" ;
			}
			else {
				link = TAG_LNK + " " + val ;		// 元に戻す
	//			final String errMsg = "[" + clsName + "],[" + link + "]" ;
	//			System.err.println( errMsg );
			}

			buf.replace( st,ed+1,link );
			st = buf.indexOf( TAG_LNK,st+link.length() );		// 置き換えた文字列の長さ文だけ、後ろから再検索開始
		}
		return buf ;
	}

	/**
	 * パッケージ.クラス名 と、フィールド名 から、staticフィールドの値を取得します。
	 *
	 * Field fldObj = Class.forName( cls ).getDeclaredField( fld ); で、Fieldオブジェクトを呼出し、
	 * String.valueOf( fldObj.get( null ) ); で、値を取得しています。
	 * static フィールドは、引数 null で値を取得できます。
	 *
	 * ※ 超特殊処理として、cls名が、HybsSystem とSystemData のみ、パッケージ無しで処理
	 *    できるように対応します。
	 *
	 * 例；
     *      String cls = "org.opengion.fukurou.system.BuildNumber";        // package.class
     *      String fld = "VERSION_NO";                                     // field
	 *
	 * @og.rev 7.3.0.0 (2021/01/06) 新しいJavaDoc対応
	 *
	 * @param cls パッケージ.クラス名
	 * @param fld フィールド名
	 * @return 取得値
	 */
	private static String getStaticField( final String cls , final String fld ) {
		String txt = "";
		try {
			final Field fldObj = Class.forName( cls ).getDeclaredField( fld );
			if( !fldObj.canAccess( null ) ) {
	// JDK1.8 警告:[removal] java.securityのAccessControllerは推奨されておらず、削除用にマークされています
	//			AccessController.doPrivileged( new PrivilegedAction<DocTreeWriter>() {
	//				/**
	//				 * 特権を有効にして実行する PrivilegedAction<T> の run() メソッドです。
	//				 *
	//				 * このメソッドは、特権を有効にしたあとに AccessController.doPrivileged によって呼び出されます。
	//				 *
	//				 * @return  DocTreeWriterオブジェクト
	//				 */
	//				public DocTreeWriter run() {
	//					// privileged code goes here
						fldObj.setAccessible( true );
	//					return null; // nothing to return
	//				}
	//			});
			}
			txt = String.valueOf( fldObj.get( null ) );		// static フィールドは、引数 null で値を取得
		}
		catch( final Throwable th ) {
			final String errMsg = "package.class=[" + cls + "],field=[" + fld + "] の取得に失敗しました。" + CR
							+ th ;
			System.err.println( errMsg );
		}

		return txt ;
	}
}
