/*
 * 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 jdk.javadoc.doclet.DocletEnvironment	 ;
// import jdk.javadoc.doclet.Doclet  ;
// import jdk.javadoc.doclet.Reporter ;
// import javax.lang.model.element.Element	;
import javax.lang.model.element.Modifier ;
import javax.lang.model.element.TypeElement;
// import javax.lang.model.element.ElementKind	;
// import javax.lang.model.element.VariableElement;
import javax.lang.model.element.ExecutableElement;
// import javax.lang.model.SourceVersion ;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.type.TypeKind;
import javax.lang.model.util.ElementFilter ;
import javax.lang.model.util.Elements ;
import javax.tools.Diagnostic.Kind ;
import com.sun.source.doctree.DocCommentTree  ;
import com.sun.source.util.DocTrees  ;
// import com.sun.source.util.DocSourcePositions  ;
// import com.sun.source.doctree.DocTree  ;

// import java.util.Locale ;
import java.util.Set;
import java.util.Map;
// import java.util.HashMap;
import java.util.List;
import java.util.HashSet;
import java.util.Arrays;
// import java.util.stream.Stream;										// 6.4.3.4 (2016/03/11)
// import java.util.stream.Collectors;									// 6.4.3.4 (2016/03/11)

// import java.io.IOException;
// import java.io.File;
// import java.io.PrintWriter;

// import org.opengion.fukurou.util.FileUtil;
// import org.opengion.fukurou.util.StringUtil;

/**
 * ソースコメントから、パラメータ情報を取り出す Doclet クラスです。
 * og.paramLevel タグと og.cryptography タグを切り出します。
 * これらは、ｼｽﾃﾑパラメータとしてGE12ﾃｰﾌﾞﾙに設定される値をクラスより抽出する
 * のに使用します。
 *
 * @version  7.3
 * @author	Kazuhiko Hasegawa
 * @since	 JDK11.0,
 */
public class DocTreeTaglib extends AbstractDocTree {
	private static final String OG_FOR_SMPL	= "og.formSample";
	private static final String OG_TAG_NAME	= "og.tag";
	private static final String OG_GROUP	= "og.group";

	private static final String	DOC_PARAM	= "param";		// 6.1.1.0 (2015/01/17)

	private static final String OG_TAG_CLASS = "org.opengion.hayabusa.taglib";

	private String	version		;
	private String	outfile		;

//	private DocTrees docUtil;
//	private Elements eleUtil ;

	/**
	 * デフォルトコンストラクター
	 *
	 * @og.rev 7.3.0.0 (2021/01/06) PMD refactoring. Each class should declare at least one constructor.
	 */
	public DocTreeTaglib() { super(); }		// これも、自動的に呼ばれるが、空のメソッドを作成すると警告されるので、明示的にしておきます。

	/**
	 * Doclet のエントリポイントメソッドです(昔の startメソッド)。
	 *
	 * @og.rev 7.3.0.0 (2021/01/06) 新しいJavaDoc対応
	 *
	 * @param docEnv ドックレットを1回呼び出す操作環境
	 *
	 * @return 正常実行時 true
	 */
	@Override
	public boolean run( final DocletEnvironment docEnv ) {
		try( DocTreeWriter writer = new DocTreeWriter( outfile,ENCODE ) ) {
			writer.printTag( "<?xml version=\"1.0\" encoding=\"", ENCODE , "\" ?>" );
			writer.printTag( "<javadoc>" );
			writer.printTag( "  <version>",version,"</version>" );
			writer.printTag( "  <description></description>" );
			writeContents( docEnv,writer );
			writer.printTag( "</javadoc>" );
		}
		catch( final Throwable th ) {
			reporter.print(Kind.ERROR, th.getMessage());
		}

		return true;
	}

	/**
	 * DocletEnvironmentよりコンテンツを作成します。
	 *
	 * @og.rev 7.3.0.0 (2021/01/06) 新しいJavaDoc対応
	 * @og.rev 8.0.2.1 (2021/12/10) tagGroupがdescriptionと合わせて出力されるため、【】でくくります。
	 * @og.rev 8.0.2.1 (2021/12/10) ｺﾒﾝﾄ分割で『。』と半角の『｡』の両方対応しておく。
	 *
	 * @param docEnv	ドックレットの最上位
	 * @param writer	DocTreeWriterオブジェクト
	 */
	private void writeContents( final DocletEnvironment docEnv, final DocTreeWriter writer ) {
//		docUtil = docEnv.getDocTrees();
//		eleUtil = docEnv.getElementUtils();

//		// get the DocTrees utility class to access document comments
		final DocTrees docUtil = docEnv.getDocTrees();
		final Elements eleUtil  = docEnv.getElementUtils();

		final Set<String> mtdClsSet = new HashSet<>();			//  6.4.3.1 (2016/02/12) 変数名も変えておきます。

		// クラス単位にループする。
		for( final TypeElement typEle : ElementFilter.typesIn(docEnv.getIncludedElements())) {
			final String fullName	= String.valueOf( typEle.getQualifiedName() ) ;
//			final String fullName = String.valueOf( typEle ) ;
			writer.setClassName( fullName );

			if( !typEle.getModifiers().contains( Modifier.PUBLIC ) ||
				!fullName.contains( OG_TAG_CLASS ) ) { continue; }		// public かつ taglib に絞る

			final DocCommentTree docTree = docUtil.getDocCommentTree(typEle);		// ﾄﾞｷｭﾒﾝﾃｰｼｮﾝ･ｺﾒﾝﾄが見つからない場合、null が返る。

//			final List<? extends DocTree> desc	= docTree == null ? EMPTY_LIST : docTree.getFirstSentence();
//			final List<? extends DocTree> cmnt	= docTree == null ? EMPTY_LIST : docTree.getFullBody();
			final String[] cmnts = getTitleCmnt( docTree );							// 8.0.2.1 (2021/12/10)

			final Map<String,List<String>> blkTagMap = blockTagsMap(docTree);
			final String smplTags	= getBlockTag( OG_FOR_SMPL, blkTagMap, ""  );
			final String grpTags	= getBlockTag( OG_GROUP   , blkTagMap, "," );

//			String smplTags		= "";
//			final StringBuilder grpBuf = new StringBuilder();
//			if( docTree != null ) {
//				for( final DocTree dt : docTree.getBlockTags() ) {
//					final String tag = String.valueOf(dt);
//					if(      tag.contains( OG_FOR_SMPL	) ) { smplTags	= cutTag( tag,OG_FOR_SMPL	); }
//					else if( tag.contains( OG_GROUP		) ) { grpBuf.append( '【' ).append( cutTag( tag,OG_GROUP ) ).append( "】," ); }
//				}
//			}
//			final String grpTags = grpBuf.length() == 0 ? "" : grpBuf.substring(0,grpBuf.length()-1);	// 最後のｶﾝﾏを削除

			// 5.7.1.1 (2013/12/13) タグのインデントを止める。
			writer.printTag( "<classDoc>" );
			writer.printTag( "  <tagClass>"		,fullName	,"</tagClass>"		);
			writer.printTag( "  <tagGroup>【"	,grpTags	,"】</tagGroup>"	);		// 8.0.2.1 (2021/12/10)
//			writer.printTag( "  <description>"	,desc		,"</description>"	);
			writer.printTag( "  <description>"	,cmnts[0]	,"</description>"	);		// 8.0.2.1 (2021/12/10)
//			writer.printTag( "  <contents>"		,cmnt		,"</contents>"		);
			writer.printTag( "  <contents>"		,cmnts[1]	,"</contents>"		);		// 8.0.2.1 (2021/12/10)
			writer.printTag( "  <formSample>"	,smplTags	,"</formSample>"	);

			mtdClsSet.clear();
			String className = String.valueOf( typEle );
			TypeElement loopEle = typEle;
			while( 	loopEle != null						&&
					className.contains( OG_TAG_CLASS ) ) {

				writer.setClassName( className );
				final String extendFlag = String.valueOf( className.contains( "HTMLTagSupport" ) );

				// メソッドのみフィルタリングして取得する
				for( final ExecutableElement ele : ElementFilter.methodsIn(loopEle.getEnclosedElements())) {		// メソッドだけに絞る
					if( !ele.getModifiers().contains( Modifier.PUBLIC ) ) { continue; }						// public だけに絞る

					final DocCommentTree mdoc = docUtil.getDocCommentTree(ele);		// ﾄﾞｷｭﾒﾝﾃｰｼｮﾝ･ｺﾒﾝﾄが見つからない場合、null が返る。
					if( mdoc == null ) { continue; }

					final Map<String,List<String>> blkTagMap2 = blockTagsMap(mdoc);
					final String tags = getBlockTag( OG_TAG_NAME , blkTagMap2, "" );

//					String tags = "";
//					for( final DocTree dt : mdoc.getBlockTags() ) {
//						final String tag = String.valueOf(dt);
//						if( tag.contains( OG_TAG_NAME ) ) { tags = cutTag( tag,OG_TAG_NAME	); }
//					}

					if( !tags.isEmpty() ) {
						final String fname = String.valueOf( ele );
						if( !mtdClsSet.add( fname ) ) { continue; }		// 継承もとに同じメソッド名がある場合は、無視する。

						final String methodName = removeSetter( String.valueOf( ele.getSimpleName() ) );

//						final List<? extends DocTree> ftag	= mdoc.getFirstSentence();
//						final List<? extends DocTree> mcmnt	= mdoc.getFullBody();
						final String[] fcmnts = getTitleCmnt( mdoc );							// 8.0.2.1 (2021/12/10)

						final String[] keylblCd = methodLabelCode( mdoc );

//						final List<? extends DocTree> doc1	= mdoc.getPostamble();
//						final List<? extends DocTree> doc2	= mdoc.getPreamble();

						// 5.7.1.1 (2013/12/13) タグのインデントを止める。
						writer.printTag( "  <method>" );
						writer.printTag( "    <name>"		,methodName	,"</name>" );
						writer.printTag( "    <label>"		,keylblCd[1],"</label>" );		// 6.1.1.0 (2015/01/17)
						writer.printTag( "    <comment>"	,keylblCd[2],"</comment>" );	// 6.2.5.0 (2015/06/05)
						writer.printTag( "    <code>"		,keylblCd[3],"</code>" );		// 6.1.1.0 (2015/01/17)
						writer.printTag( "    <htmlExtend>"	,extendFlag	,"</htmlExtend>" );
//						writer.printTag( "    <description>",ftag		,"</description>" );	// 8.0.2.1 (2021/12/10)
						writer.printTag( "    <description>",fcmnts[0]	,"</description>" );
//						writer.printTag( "    <contents>"	,mcmnt		,"" );
						writer.printTag( "    <contents>"	,fcmnts[0]	,"" );					// 8.0.2.1 (2021/12/10)
						writer.printTag( ""					,tags		,"</contents>" );
						writer.printTag( "  </method>");
					}
				}

				final TypeMirror superType 	= loopEle.getSuperclass();
				className = String.valueOf( superType );
				loopEle = null;							// while ループの終了
				if( !TypeKind.NONE.equals( superType.getKind() ) ) {
					for( final TypeElement tp : eleUtil.getAllTypeElements(className) ) {
						if( className.equals( String.valueOf(tp) ) ) {
							loopEle = tp ; break;
						}
					}
				}
			}
			writer.printTag( "</classDoc>" );
		}
	}

	/**
	 * セッターメソッドの setXXXX の set を削除し、次の文字を小文字化します。
	 * つまり、セッターメソッドから属性値を推測します。
	 * (超特殊処理)セッターメソッドのset以下２文字目が大文字の場合は、
	 * １文字目も大文字と考えて小文字化を行いません。
	 * 例えば、setSYS や setUSER など、RequestValueTag.javaに使用するケースです。
	 *
	 * @param target 処理対象となる文字列
	 *
	 * @return オプション文字列
	 */
	private String removeSetter( final String target ) {
		if( target != null && target.startsWith( "set" ) ) {
			char[] chs = target.toCharArray();
			if( chs.length > 4 && !( chs[4] >= 'A' && chs[4] <= 'Z' ) ) {
				chs[3] = Character.toLowerCase( chs[3] ) ;
			}
			return new String( chs,3,chs.length-3 );
		}
		return target;
	}

	/**
	 * MethodDocを受け取り、0:ﾊﾟﾗﾒｰﾀ、1:ﾗﾍﾞﾙ、2:ｺﾒﾝﾄ、3:ｺｰﾄﾞを文字列配列に入れて返します。
	 *
	 * これは、タグリブのラベルリソース登録時に使用する情報です。
	 * タグリブのローカルルールとして、0:ﾊﾟﾗﾒｰﾀ 1:ﾗﾍﾞﾙ 2:ﾗﾍﾞﾙ以降の解説 3:ｺｰﾄﾞ
	 * という記述を行う事を前提にしています。
	 *
	 * 0:ﾊﾟﾗﾒｰﾀは、引数です。メソッド名ではありませんので、ご注意ください。
	 * 1:ﾗﾍﾞﾙは、ﾊﾟﾗﾒｰﾀの次の空白文字から、次の空白文字、または、"[" までの文字です。
	 * 2:ｺﾒﾝﾄは、ﾗﾍﾞﾙ以降の文字列で、ｺｰﾄﾞの記述部分も含みます。
	 * 3:ｺｰﾄﾞ は、特別な処理を行います。"[" と "]" 内に記述された内容を取り出します。
	 *
	 * @og.rev 7.3.0.0 (2021/01/06) 新しいJavaDoc対応
	 *
	 * @param	mdoc DocCommentTreeｵﾌﾞｼﾞｪｸﾄ
	 *
	 * @return	0:ﾊﾟﾗﾒｰﾀ、1:ﾗﾍﾞﾙ、2:ｺﾒﾝﾄ、3:ｺｰﾄﾞを文字列配列に詰めた値(長さ４の配列)
	 * @og.rtnNotNull
	 */
	private String[] methodLabelCode( final DocCommentTree mdoc ) {
		final String[] arys = new String[] { "","","","" } ;	// 後で内容を更新する。

		final Map<String,List<String>> blkTagMap = blockTagsMap(mdoc);
		final String prmTag = getBlockTag( DOC_PARAM, blkTagMap, "" );

//		String prmTag = "";
//		for( final DocTree dt : mdoc.getBlockTags() ) {
//			final String tag = String.valueOf(dt);
//			if( tag.contains( DOC_PARAM ) ) {
//				prmTag = cutTag( tag,DOC_PARAM ); break;		// Taglibのsetter は、paramは一つだけのハズ
//			}
//		}

		// 最大３つ と指定しているが、0:ﾊﾟﾗﾒｰﾀと1:ﾗﾍﾞﾙの２つしか保証されていない。
		final String[] temp = prmTag.split( "[\\s]+",3 );		// 空白文字で３つに分解する。
		System.arraycopy( temp,0,arys,0,temp.length );			// 6.3.6.0 (2015/08/16)

		// 3:ｺｰﾄﾞ があれば、2:ｺﾒﾝﾄから取り出す。
		final int st1 = arys[2].indexOf( '[' );
		if( st1 >= 0 ) {
			final int st2 = arys[2].indexOf( ']',st1 );
			if( st2 > 0 ) {
				// 前後の [] は、取り除き、'/' があれば、' ' に置換する。(ｺｰﾄﾞ文字列化)
				arys[3] = arys[2].substring( st1+1,st2 ).replace( '/' , ' ' );
			}
		}

		return arys ;
	}

	/**
	 * サポートされているすべてのオプションを返します。
	 *
	 * @return サポートされているすべてのオプションを含むセット、存在しない場合は空のセット
	 */
	@Override
	public Set<? extends Option> getSupportedOptions() {
		final Option[] options = {
			new AbstractOption( "-outfile", "-version" ) {

				/**
				 * 必要に応じてオプションと引数を処理します。
				 *
				 * @param  opt オプション名
				 * @param  arguments 引数をカプセル化したリスト
				 * @return 操作が成功した場合はtrue、そうでない場合はfalse
				 */
				@Override
				public boolean process(final String opt, final List<String> arguments) {
					if( "-outfile".equalsIgnoreCase(opt) ) {
						outfile = arguments.get(0);
					}
					else if( "-version".equalsIgnoreCase(opt) ) {
						version = arguments.get(0);
					}
					return true;
				}
			}
		};
		return new HashSet<>(Arrays.asList(options));
	}
}
