/*
 * 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;

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

import java.util.Map;
import java.util.TreeMap;
import java.util.Set;							// 6.2.0.0 (2015/02/27)
import java.util.TreeSet;						// 6.2.0.0 (2015/02/27)
import java.io.IOException;

import com.sun.javadoc.RootDoc;
import com.sun.javadoc.ClassDoc;
import com.sun.javadoc.ExecutableMemberDoc;
import com.sun.javadoc.Type;
import com.sun.javadoc.Parameter;
import com.sun.javadoc.Tag;

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

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

	// 6.2.0.0 (2015/02/27) List → Set に変更して、TreeSet を使います。
	// 6.4.3.1 (2016/02/12) インスタンス変数をローカル変数に変更。
//	private static final Map<String,Set<String[]>> VER_INFO = new TreeMap<>();		// 6.4.1.1 (2016/01/16) verInfo → VER_INFO refactoring

	private static       int	debugLevel		;		// 6.2.0.0 (2015/02/27) 0:なし 1:ソース行出力

	/**
	 * すべてが staticメソッドなので、コンストラクタを呼び出さなくしておきます。
	 *
	 */
	private DocletVerCheck() {}

	/**
	 * Doclet のエントリポイントメソッドです。
	 *
	 * @og.rev 6.0.2.3 (2014/10/10) 新規作成
	 *
	 * @param	root	エントリポイントのRootDocオブジェクト
	 *
	 * @return 正常実行時 true
	 */
	public static boolean start( final RootDoc root ) {
		final String version	= DocletUtil.getOption( "-version" , root.options() );
		final String file		= DocletUtil.getOption( "-outfile" , root.options() );
		final String dbgLvl		= DocletUtil.getOption( "-debugLevel" , root.options() );		// 6.2.0.0 (2015/02/27)
		if( dbgLvl != null ) { debugLevel = Integer.parseInt( dbgLvl ); }

		DocletTagWriter writer = null;
		try {
			writer = new DocletTagWriter( file,ENCODE );

			writeContents( root.classes(),writer,version );
		}
		catch( IOException ex ) {
			LogWriter.log( ex );
		}
		finally {
			if( writer != null ) { writer.close(); }
		}
		return true;
	}

	/**
	 * TreeSet用の Comparator 実装クラス
	 *
	 * これは、String[] 配列を TreeSet でソートする為のComparatorです。
	 * ソートは、同一Set内で、配列の３番目の要素(ポジション)です。
	 * つまり、クラス名、メソッド名、行番号順になります。
	 *
	 * @og.rev 6.2.0.0 (2015/02/27) List → Set に変更して、TreeSet を使います。
	 * @og.rev 6.3.9.0 (2015/11/06) Comparator を実装する場合、Serializable も実装するべきかどうか検討するべきです(findbugs)。
	 *
//	 * @param classes	ClassDoc配列
//	 * @param writer	Tagを書き出すWriterオブジェクト
//	 * @param version	チェックする対象のバージョン
	 */
//	private static final class ObjComp implements java.util.Comparator<String[]> {
	private static final class ObjComp implements java.util.Comparator<String[]> , java.io.Serializable {
		private static final long serialVersionUID = 639020151030L ;

		/**
		 * 比較メソッド
		 *
		 * これは、String[] 配列を TreeSet でソートする為のComparatorです。
		 * ソートは、同一Set内で、配列の３番目の要素(ポジション)です。
		 * つまり、クラス名、メソッド名、行番号順になります。
		 *
		 * @og.rev 6.2.0.0 (2015/02/27) List → Set に変更して、TreeSet を使います。
		 *
		 * @param o1	文字列配列(比較対象の最初のオブジェクト)
		 * @param o2	文字列配列(比較対象の2番目のオブジェクト)
		 * @return		最初の引数が2番目の引数より小さい場合は負の整数、両方が等しい場合は0、最初の引数が2番目の引数より大きい場合は正の整数
		 */
		public int compare( final String[] o1, final String[] o2 ) {
			return o1[2].compareTo( o2[2] );
		}
	}

	/**
	 * ClassDoc 配列よりコンテンツを作成します。
	 *
	 * @og.rev 6.0.2.3 (2014/10/10) 新規作成
	 * @og.rev 6.2.0.0 (2015/02/27) List → Set に変更して、TreeSet を使います。
	 * @og.rev 6.3.9.0 (2015/11/06) Map.keySet() ではなく、Map.entrySet() を使う様に変更。
	 * @og.rev 6.4.3.1 (2016/02/12) インスタンス変数をローカル変数に変更。
	 *
	 * @param classes	ClassDoc配列
	 * @param writer	Tagを書き出すWriterオブジェクト
	 * @param version	チェックする対象のバージョン
	 */
	private static void writeContents( final ClassDoc[] classes,final DocletTagWriter writer,final String version ) {

		// 6.4.3.1 (2016/02/12) インスタンス変数をローカル変数に変更。
		final Map<String,Set<String[]>> VER_INFO_MAP = new TreeMap<>();			// 6.4.1.1 (2016/01/16) verInfo → VER_INFO_MAP refactoring

		ExecutableMemberDoc[][] docs = new ExecutableMemberDoc[2][];		// 6.1.0.0 (2014/12/26) refactoring
		for( int x=0; x<classes.length; x++ ) {
			final ClassDoc classDoc	= classes[x] ;
			String fullName		= classDoc.qualifiedName();

			fullName = fullName.substring( SELECT_PACKAGE.length()+1 );		// パッケージ名を簡略化しておきます。

			// コンストラクタ、メソッドから、対象の @og.rev の値を取得します。
			docs[0] = classDoc.constructors( false ) ;
			docs[1] = classDoc.methods( false ) ;

			for( int i=0; i<docs.length; i++ ) {
				for( int j=0; j<docs[i].length; j++ ) {
					final ExecutableMemberDoc doc = docs[i][j];
					final Tag[] revTags = doc.tags(OG_REV);
					for( int k=0 ; k<revTags.length; k++ ) {
						final String rev = revTags[k].text();
						if( rev.startsWith( version ) ) {
							// 6.2.0.0 (2015/02/27) List → Set に変更して、TreeSet を使います。
							Set<String[]> set = VER_INFO_MAP.get( rev );		// rev コメントでまとめます。
							if( set == null ) { set = new TreeSet<>( new ObjComp() ); }
							set.add( menberCheck( doc,fullName ) );
							VER_INFO_MAP.put( rev,set );

							break;
						}
					}
				}
			}
		}

		String revYMD = null;	// 初めてのRev.日付をセットする。
		String title = "";			// キーブレイクのマーカー
		// 6.3.9.0 (2015/11/06) keySet() ではなく、entrySet() を使う様に変更
//		for( final String key : VER_INFO.keySet() ) {
		for( final Map.Entry<String,Set<String[]>> entry : VER_INFO_MAP.entrySet() ) {		// 6.3.9.0 (2015/11/06)
			final String key = entry.getKey();											// 6.3.9.0 (2015/11/06)
			if( revYMD == null ) {
				final int idx = key.indexOf( ')' ) ;
				revYMD = key.substring( 0,idx+1 );
				writer.printTag( revYMD );									// 一番先頭の Rev-日付出力
			}

			// バージョン、日付以降のメッセージを取得。
			final String msg = key.substring( revYMD.length() ).trim();	// 日付以降のコメント
			final int idx = msg.indexOf( '。' );							// '。' の区切り文字の前半部分が、タイトル。

			if( idx >= 0 ) {		// 分割する場合
				final String newTitle = msg.substring( 0,idx ).trim() ;
				if( !title.equals( newTitle ) ) {
					writer.printTag( "\n\t[" , newTitle , "]" );
					title = newTitle ;
				}
				if( idx != msg.length()-1 ) {				// 分割する場合、最後の「。」は、分割対象外
					writer.printTag( "\t" , msg.substring( idx+1 ).trim() );
				}
			}
			else {
				writer.printTag( "\n\t[" , msg , "]" );
				title = msg ;
			}

//			for( final String[] vals : VER_INFO.get( key ) ) {
			for( final String[] vals : entry.getValue() ) {					// 6.3.9.0 (2015/11/06) keySet() ではなく、entrySet() を使う様に変更
				if( debugLevel > 0 ) {										// 6.2.0.0 (2015/02/27)
					writer.printTag( vals[2] );
				}
				writer.printTag( "\t\t" , vals[0] , "#" , vals[1] );
			}
		}
	}

	/**
	 * メンバークラス(コンストラクタ、メソッド)から、メソッド名、シグネチャを取得します。
	 *
	 * @og.rev 6.0.2.3 (2014/10/10) 新規作成
	 *
	 * @param	menber		ExecutableMemberDocオブジェクト
	 * @param	fullName	パッケージを含めたクラスのフル名称
	 * @return	配列にセットされた情報( fullName,signature,position )
	 * @og.rtnNotNull
	 */
	private static String[] menberCheck( final ExecutableMemberDoc menber,final String fullName ) {

		final String menberName = menber.name();

		// 6.0.2.5 (2014/10/31) char を append する。
		final StringBuilder sigBuf = new StringBuilder( BUFFER_MIDDLE );
		sigBuf.append( menberName ).append( '(' ) ;
		final Parameter[] prm = menber.parameters();

		for( int k=0; k<prm.length; k++ ) {
			final Type ptyp = prm[k].type();
			final String prmNm =prm[k].name();

			sigBuf.append( ptyp.typeName() ).append( ptyp.dimension() ).append( ' ' )
					.append( prmNm ).append( ',' );
		}

		if( prm.length > 0 ) { sigBuf.deleteCharAt( sigBuf.length()-1 ); }
		sigBuf.append( ')' );
		final String signature = sigBuf.toString();

		final String position = menber.position().toString();
		System.out.println( position );		// デバッグ情報

		return new String[] { fullName , signature , position };
	}

	/**
	 * カスタムオプションを使用するドックレットの必須メソッド optionLength(String) です。
	 *
	 * ドックレットに認識させる各カスタムオプションに、 optionLength がその
	 * オプションを構成する要素 (トークン) の数を返さなければなりません。
	 * このカスタムオプションでは、 -tag オプションそのものと
	 * その値の 2 つの要素で構成されるので、作成するドックレットの
	 * optionLengthメソッドは、 -tag オプションに対して 2 を返さなくては
	 * なりません。また、認識できないオプションに対しては、0 を返します。
	 *
	 * @param	option	オプション文字列
	 *
	 * @return	要素(トークン) の数
	 */
	public static int optionLength( final String option ) {
		if( "-version".equalsIgnoreCase(option) ) {
			return 2;
		}
		else if( "-outfile".equalsIgnoreCase(option) ) {
			return 2;
		}
		// 6.2.0.0 (2015/02/27) ソース行番号出力
		else if( "-debugLevel".equalsIgnoreCase(option) ) {
			return 2;
		}
		return 0;
	}
}
