/*
 * 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.util.ElementFilter ;
// import javax.lang.model.util.Elements	 ;
import javax.tools.Diagnostic.Kind ;
import com.sun.source.doctree.DocCommentTree  ;
import com.sun.source.doctree.DocTree  ;
import com.sun.source.util.DocTrees  ;

// import java.util.Locale ;
import java.util.Set;
import java.util.Map;
import java.util.List;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.HashMap;
import java.util.Arrays;

// 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 クラスです。
 * パラメータの version に一致する og.rev タグの 書かれたメソッドを、
 * ピックアップします。
 * og.rev タグ で、まとめて表示します。
 *
 * ルールとしては、X.X.X.X だけが引数で渡されますので、 X.X.X.X (YYYY/MM/DD) が
 * コメントとして記載されているとして、処理します。
 * そして、それ以降の記述の、「。」までを、キーワードとして、まとめます。
 * キーワード以下は、それぞれのコメントとして、各メソッドの直前に表示します。
 * このクラスは、RELEASE-NOTES.txt を作成する場合に、変更箇所をピックアップするのに使用します。
 *
 * @version  7.3
 * @author	Kazuhiko Hasegawa
 * @since	 JDK11.0,
 */
public class DocTreeVerCheck extends AbstractDocTree {
	private static final String SELECT_PACKAGE	= "org.opengion." ;

	private static final String OG_REV			= "og.rev";

	private String	version	;
	private String	outfile	;
	private int omitPackage	= SELECT_PACKAGE.length()	;	// パッケージ名の先頭をカットするため

	private DocTrees docUtil;

	/**
	 * デフォルトコンストラクター
	 *
	 * @og.rev 7.3.0.0 (2021/01/06) PMD refactoring. Each class should declare at least one constructor.
	 */
	public DocTreeVerCheck() { 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 ) ) {
			writeContents( docEnv,writer );
		}
		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) ｺﾒﾝﾄ分割で『。』と半角の『｡』の両方対応しておく。
	 *
	 * @param docEnv	ドックレットの最上位
	 * @param writer	DocTreeWriterオブジェクト
	 */
	private void writeContents( final DocletEnvironment docEnv, final DocTreeWriter writer ) {
		docUtil = docEnv.getDocTrees();

		final Map<String,List<String>> verMap = new HashMap<>();
		String revYMD = null;	// 初めてのRev.日付をセットする。

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

		// クラス単位にループする。
		for( final TypeElement typEle : ElementFilter.typesIn(docEnv.getIncludedElements())) {
			final String fullName = String.valueOf(typEle).substring( omitPackage );		// パッケージ名を簡略化しておきます。
			writer.setClassName( fullName );

	//		// クラスに書かれている private static final String VERSION フィールドを先に取得する。
	//		String clsVer = null;
	//		for( final VariableElement ele : ElementFilter.fieldsIn(typEle.getEnclosedElements())) {		// フィールドだけに絞る
	//			final Set<Modifier> modi = ele.getModifiers();
	//			final String typ = String.valueOf( ele.asType() );
	//			if( modi.contains(  Modifier.PRIVATE  ) && modi.contains( Modifier.STATIC ) && typ.contains( "String" )
	//							&& "VERSION".equals( ele.getSimpleName() ) ) {
	//				clsVer = String.valueOf( ele.getConstantValue() );
	//			}
	//		}

			// 5.6.6.0 (2013/07/05) VERSION staticフィールドと、@og.rev コメントの比較チェック
			// while 以下で、fullName と classDoc を順番に上にさかのぼっているので、先にチェックします。
			final String clsVer = checkTag2( typEle );

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

				for( final DocTree dt : doc.getBlockTags() ) {
					final String tag = String.valueOf(dt);
					if( tag.contains( OG_REV ) ) {
						final String[] tags = tag.split( " ",4 );		// ﾀｸﾞ , ﾊﾞｰｼﾞｮﾝ , 日付 , ｺﾒﾝﾄ (@og.rev 7.3.0.0 (2021/01/06) 新しいJavaDoc対応)
						if( tags.length >= 2 ) {						// 最低限、ﾊﾞｰｼﾞｮﾝ は記載されている
							final String tagVer = tags[1];				// ﾊﾞｰｼﾞｮﾝ

							// Ver チェックで、旧の場合
							if( clsVer != null && clsVer.compareTo( tagVer ) < 0 ) {
								final String msg = "旧Ver：" + fullName + ":" + version + " : " + tagVer ;
								reporter.print(Kind.WARNING, msg );					// NOTE:情報 WARNING:警告 ERROR:エラー
							}

							if( tags.length >= 4 && tagVer.equals( version ) ) {	// ﾊﾞｰｼﾞｮﾝが一致して、ｺﾒﾝﾄ まで記載済み
								// ｺﾒﾝﾄを「。」で分割する。まずは、convertToOiginal で、ｵﾘｼﾞﾅﾙに戻しておく
								final String cmnt = writer.convertToOiginal( tags[3] );
								// 8.0.2.1 (2021/12/10) ｺﾒﾝﾄ分割で『。』と半角の『｡』の両方対応しておく。
//								final int idx = cmnt.indexOf( '。' );				// '。' の区切り文字の前半部分が、タイトル。
								final int idx = Math.max( cmnt.indexOf( '。' ) , cmnt.indexOf( '｡' ) );
								final String key = idx > 0 ? cmnt.substring( 0,idx ).trim() : cmnt ;

//								final String key = tags[3];							// ｺﾒﾝﾄがｷｰ
								final String val = fullName + "#" + ele ;			// ﾒｿｯﾄﾞが値
								verMap.computeIfAbsent( key, k -> new ArrayList<>() ).add( val );

								if( revYMD == null ) {								// 一番最初だけセットする
									revYMD = tagVer + " " + tags[2];				// ﾊﾞｰｼﾞｮﾝ + 日付
								}
							}
						}
					}
				}
			}
		}

		// 書き出し
		if( revYMD != null ) {
			writer.printTag( revYMD );
			for( final Map.Entry<String,List<String>> entry : verMap.entrySet() ) {
				final String msg = entry.getKey().trim();
	//			writer.printTag( "\n\t[" , entry.getKey() , "]" );
				writer.printTag( "\n\t[" , msg , "]" );						// 8.0.0.0 (2021/07/31)
				for( final String str : entry.getValue() ) {				// リスト
					writer.printTag( "\t\t" , str );
				}
			}
		}
	}

	/**
	 * PMDで、チェックしている処理のうち、Docletでフォローできる分をチェックします。
	 *
	 * ※ このチェックは、警告レベル5 のみ集約していますので、呼出元で、制限します。
	 *
	 * ※ DocTreeSpecific から、移植しました。
	 *
	 * @og.rev 7.3.0.0 (2021/01/06) 新しいJavaDoc対応
	 *
	 * @param typEle TypeElementオブジェクト
	 * @return VERSIONフィールドの値
	 */
	private String checkTag2( final TypeElement typEle ) {
		String cnstVar = null ;		// 初期値
		String seriUID = null ;

		// フィールドのみフィルタリングして取得する
		for( final VariableElement varEle : ElementFilter.fieldsIn(typEle.getEnclosedElements())) {		// フィールドだけに絞る
			final Set<Modifier> modi = varEle.getModifiers();
			if( modi.contains( Modifier.PRIVATE ) && modi.contains( Modifier.STATIC ) ) {
				final String key = String.valueOf( varEle.getSimpleName() );
				if( "VERSION".equals( key ) ) {
					cnstVar = String.valueOf( varEle.getConstantValue() );
				}
				else if( "serialVersionUID".equals( key ) ) {
					seriUID = varEle.getConstantValue() + "L";			// 旧JavaDocと違い、"L" まで取ってこれないみたい
				}
			}
		}

		if( cnstVar == null ) { return null; }			// VERSION が未定義のクラスは処理しない

		String maxRev = cnstVar ;						// 5.7.1.1 (2013/12/13) 初期値
		boolean isChange = false;						// max が入れ替わったら、true

		// メソッドのみフィルタリングして取得する
		for( final ExecutableElement exEle : ElementFilter.methodsIn(typEle.getEnclosedElements())) {
			final DocCommentTree dct = docUtil.getDocCommentTree(exEle);		// ﾄﾞｷｭﾒﾝﾃｰｼｮﾝ･ｺﾒﾝﾄが見つからない場合、null が返る。
			final Map<String,List<String>> blkTagMap = blockTagsMap(dct);
			final List<String> revTags = blkTagMap.get("og.rev");

			if( revTags != null ) {
				for( final String tag :revTags ) {								// 複数存在しているはず
					final String[] tags = tag.split( " ",3 );					// 最小3つに分割する。

					if( tags.length >= 2 ) {
						final String rev = ( tags[0] + ' ' + tags[1] ).trim();
						if( maxRev.compareTo( rev ) < 0 ) {						// revTags の og.rev が大きい場合
							maxRev = rev ;
							isChange = true;
						}
					}
				}
			}
		}

		final String src = "\tsrc/" + String.valueOf(typEle).replace('.','/') + ".java:100" ;			// 行が判らないので、100行目 決め打ち

		// VERSION 文字列 の定義があり、かつ、max の入れ替えが発生した場合のみ、警告4:VERSIONが古い
		if( isChange ) {			// 5.7.1.1 (2013/12/13) 入れ替えが発生した場合
			System.err.println( "警告4:VERSIONが古い=\t" + cnstVar + " ⇒ " + maxRev + src );
		}

		// serialVersionUID の定義がある。
		if( seriUID != null ) {
			final StringBuilder buf = new StringBuilder();
			// maxRev は、最大の Revか、初期のVERSION文字列 例:5.6.6.0 (2013/07/05)
			for( int i=0; i<maxRev.length(); i++ ) {	//
				final char ch = maxRev.charAt( i );
				if( ch >= '0' && ch <= '9' ) { buf.append( ch ); }	// 数字だけ取り出す。 例:566020130705
			}
			buf.append( 'L' );	// 強制的に、L を追加する。
			final String maxSeriUID = buf.toString() ;

			// 5.7.1.1 (2013/12/13) 値の取出し。Long型を表す "L" も含まれている。
			if( !maxSeriUID.equals( seriUID ) ) {	// 一致しない
				System.err.println( "警告4:serialVersionUIDが古い=\t" + seriUID + " ⇒ " + maxSeriUID + src );
			}
		}

		return cnstVar ;
	}

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

				/**
				 * 必要に応じてオプションと引数を処理します。
				 *
				 * @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);
					}
					else if( "-omitPackage".equalsIgnoreCase(opt) ) {
						omitPackage = arguments.get(0).length()+1;
					}
					return true;
				}
			}
		};
		return new HashSet<>(Arrays.asList(options));
	}
}
