/*
 * 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 org.opengion.fukurou.util.StringUtil;
// import static org.opengion.fukurou.system.HybsConst.BUFFER_MIDDLE;	// 6.1.0.0 (2014/12/26) refactoring

import com.sun.javadoc.RootDoc;
import com.sun.javadoc.ClassDoc;
import com.sun.javadoc.MethodDoc;
import com.sun.javadoc.Type;
import com.sun.javadoc.Tag;
import java.util.Map;
import java.util.HashMap;
import java.io.IOException;
import java.util.stream.Stream;									// 6.4.3.4 (2016/03/11)
import java.util.stream.Collectors;								// 6.4.3.4 (2016/03/11)

/**
 * ソースコメントから、タグ情報を取り出す Doclet クラスです。
 * この Doclet は、":org.opengion.hayabusa.taglib" のみ対象として処理します。
 * og.formSample , og.tag , og.group タグを切り出します。
 *
 * @version  4.0
 * @author   Kazuhiko Hasegawa
 * @since    JDK5.0,
 */
public final class DocletTaglib {
	// 6.4.3.1 (2016/02/12) インスタンス変数をローカル変数に変更。変数名も変えておきます。
//	private static Map<String,String> map = new HashMap<>();

	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 static final String ENCODE = "UTF-8";

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

	/**
	 * Doclet のエントリポイントメソッドです。
	 *
	 * @og.rev 5.7.1.1 (2013/12/13) タグのインデントを止める。
	 *
	 * @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() );

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

			// 5.7.1.1 (2013/12/13) タグのインデントを止める。
			writer.printTag( "<?xml version=\"1.0\" encoding=\"", ENCODE, "\" ?>" );
			writer.printTag( "<javadoc>" );
			writer.printTag( "  <version>",version,"</version>" );
			writer.printTag( "  <description></description>" );
			writeContents( root.classes(),writer );
			writer.printTag( "</javadoc>" );
		}
		catch( IOException ex ) {
			LogWriter.log( ex );
		}
		finally {
			if( writer != null ) { writer.close(); }
		}
		return true;
	}

	/**
	 * ClassDoc 配列よりコンテンツを作成します。
	 *
	 * @og.rev 5.5.4.1 (2012/07/06) コメントは文字列でなく、Tag配列として処理させる。
	 * @og.rev 5.6.6.1 (2013/07/12) og.group を、tagGroup として独立させる。
	 * @og.rev 5.7.1.1 (2013/12/13) cmnt と tags の間に改行をセット
	 * @og.rev 5.7.1.1 (2013/12/13) タグのインデントを止める。
	 * @og.rev 6.1.1.0 (2015/01/17) タグリブの param 属性取得(0:ﾊﾟﾗﾒｰﾀ、1:ﾗﾍﾞﾙ、2:ｺﾒﾝﾄ、3:ｺｰﾄﾞ)
	 * @og.rev 6.2.5.0 (2015/06/05) 2:ｺﾒﾝﾄ を、comment として出力します。
	 * @og.rev 6.4.3.1 (2016/02/12) インスタンス変数をローカル変数に変更。変数名も変えておきます。
	 *
	 * @param classes	ClassDoc配列
	 * @param writer	DocletTagWriterオブジェクト
	 */
	private static void writeContents( final ClassDoc[] classes,final DocletTagWriter writer ) {
		final Map<String,String> mtdClsMap = new HashMap<>();			//  6.4.3.1 (2016/02/12) 変数名も変えておきます。
		for( int i=0; i<classes.length; i++ ) {
			ClassDoc classDoc      = classes[i] ;
			final String   classFullName = classDoc.qualifiedName() ;

			if( ! classDoc.isPublic() ||
				classFullName.indexOf( OG_TAG_CLASS ) < 0 ) { continue; }

			final Tag[]  desc = classDoc.firstSentenceTags();
			Tag[]  cmnt = classDoc.inlineTags();									// 5.5.4.1 (2012/07/06)
			final Tag[] smplTags	= classDoc.tags(OG_FOR_SMPL);
			final Tag[] grpTags	= classDoc.tags(OG_GROUP);

			// 5.7.1.1 (2013/12/13) タグのインデントを止める。
			writer.printTag( "<classDoc>" );
			writer.printTag( "  <tagClass>"		,classFullName				,"</tagClass>" );
			// 5.6.6.1 (2013/07/12) og.group を、tagGroup として独立させる。
			writer.printTag( "  <tagGroup>"		,makeGroupTag( grpTags )	,"</tagGroup>" );
			writer.printTag( "  <description>"	,desc						,"</description>" );
			writer.printTag( "  <contents>"		,cmnt						,"</contents>" );
			writer.printTag( "  <formSample>"	,smplTags					,"</formSample>" );

			// 6.4.3.1 (2016/02/12) PMD refactoring. HashMap → ConcurrentHashMap に置き換え。変数名も変えておきます。
//			map.clear();
			mtdClsMap.clear();
			String className = classDoc.name();
			while( 	! "BodyTagSupport".equals( className ) &&
					! "TagSupport".equals( className ) ) {
				String extendFlag = "false";
				if( "HTMLTagSupport".equals( className ) ) {
					extendFlag = "true" ;
				}
				final MethodDoc[] methods = classDoc.methods();
				for( int j=0; j<methods.length; j++ ) {
					if( ! methods[j].isPublic() ) { continue; }
					final Tag[] tags = methods[j].tags(OG_TAG_NAME);
					if( tags.length > 0 ) {
						final String methodName = DocletUtil.removeSetter( methods[j].name() );
						// 6.4.3.1 (2016/02/12) PMD refactoring. HashMap → ConcurrentHashMap に置き換え。変数名も変えておきます。
//						if( map.containsKey( methodName ) ) { continue; }
//						map.put( methodName,className );
						if( mtdClsMap.containsKey( methodName ) ) { continue; }
						mtdClsMap.put( methodName,className );
						final Tag[] ftag = methods[j].firstSentenceTags();
						cmnt = methods[j].inlineTags();									// 5.5.4.1 (2012/07/06)

						// 6.1.1.0 (2015/01/17) タグリブの param 属性取得(0:ﾊﾟﾗﾒｰﾀ、1:ﾗﾍﾞﾙ、2:ｺﾒﾝﾄ、3:ｺｰﾄﾞ)
						final String[] keylblCd = methodLabelCode( methods[j] );

						// 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>" );
						// 5.7.1.1 (2013/12/13) cmnt と tags の間に改行をセット
						writer.printTag( "    <contents>"	,cmnt		,"" );
						writer.printTag( ""					,tags		,"</contents>" );
						writer.printTag( "  </method>");
					}
				}
				final Type type = classDoc.superclassType();
				if( type == null ) { break; }
				classDoc  = type.asClassDoc() ;
				className = classDoc.name();
			}
			writer.printTag( "  </classDoc>" );
		}
	}

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

		final Tag[] docParam = method.tags(DOC_PARAM);
		if( docParam.length == 1 ) {							// Taglibのsetter は、paramは一つだけのハズ
			final String data = docParam[0].text().trim();		// @param の説明文

			// 最大３つ と指定しているが、0:ﾊﾟﾗﾒｰﾀと1:ﾗﾍﾞﾙの２つしか保証されていない。
			final String[] temp = data.split( "[\\s]+",3 );		// 空白文字で３つに分解する。
			// 6.3.6.0 (2015/08/16) System.arraycopy が使える箇所は、置き換えます。
//			for( int i=0; i<temp.length; i++ ) {
//				arys[i] = temp[i];
//			}
			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 ;
	}

	/**
	 * タグ配列を受け取り、タグ出力します。
	 * 複数のタグを出力する場合に、CSV形式で連結します。
	 *
	 * @og.rev 5.5.4.1 (2012/07/06) DocletUtil.htmlFilter → StringUtil.htmlFilter に変更
	 * @og.rev 5.6.6.1 (2013/07/12) og.group の表示方法を変更する。
	 * @og.rev 6.4.3.4 (2016/03/11) CSV形式の文字連結を、stream 経由で行います。
	 *
	 * @param	tag タグ配列(可変長引数)
	 *
	 * @return	タグ出力文字列
	 * @og.rtnNotNull
	 */
	private static String makeGroupTag( final Tag... tag ) {

		return Stream.of( tag )
					.map( tg -> StringUtil.htmlFilter( tg.text() ) )
					.collect( Collectors.joining( "】,【" , "【" , "】" ) );	// 連結文字 , 最初 , 最後

//		final StringBuilder but = new StringBuilder( BUFFER_MIDDLE );
//		// 6.0.2.5 (2014/10/31) char を append する。
//		for( int i=0; i<tag.length; i++ ) {
//			final String data = StringUtil.htmlFilter( tag[i].text() );		// 5.5.4.1 (2012/07/06) DocletUtil → StringUtil に変更
//			if( i > 0 ) { but.append( ',' ); }
//			but.append( '【' ).append( data ).append( '】' );			// 5.6.6.1 (2013/07/12) og.group の表示方法を変更
//		}
//		return but.toString() ;											// 5.6.6.1 (2013/07/12)
	}

	/**
	 * カスタムオプションを使用するドックレットの必須メソッド 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;
		}
		return 0;
	}
}
