/*
 * 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.util.LogWriter;

import com.sun.javadoc.RootDoc;
import com.sun.javadoc.ClassDoc;
import com.sun.javadoc.MethodDoc;
import com.sun.javadoc.ConstructorDoc;
import com.sun.javadoc.ExecutableMemberDoc;
import com.sun.javadoc.Type;
import com.sun.javadoc.Parameter;
import com.sun.javadoc.Tag;
import com.sun.javadoc.SourcePosition;
import java.util.Set;
import java.util.HashSet;
import java.io.IOException;

/**
 * ソースコメントから、タグ情報を取り出す Doclet クラスです。
 * クラスファイルの仕様を表現する為、og.formSample , og.rev , og.group ,
 * version , author , since の各タグコメントより値を抽出します。
 * また、各クラスの継承関係、インターフェース、メソッドなども抽出します。
 * これらの抽出結果をDB化し、EXCELファイルに帳票出力する事で、クラスファイルの
 * ソースから仕様書を逆作成します。
 *
 * @version  4.0
 * @author   Kazuhiko Hasegawa
 * @since    JDK5.0,
 */
public final class DocletSpecific {
	private static final String  SELECT_PACKAGE	= "org.opengion" ;
	private static final boolean NOT_PRIVATE	= false ;
	private static final String ENCODE = "UTF-8";

	private static final String	OG_FOR_SMPL		= "og.formSample";
	private static final String	OG_REV			= "og.rev";
	private static final String	OG_GROUP		= "og.group";
	private static final String	DOC_VERSION		= "version";
	private static final String	DOC_AUTHOR		= "author";
	private static final String	DOC_SINCE		= "since";

	private static final String	DOC_PARAM		= "param";		// 5.1.9.0 (2010/08/01) チェック用
	private static final String	DOC_RETURN		= "return";		// 5.1.9.0 (2010/08/01) チェック用

	private static final String	CONSTRUCTOR		= "コンストラクタ" ;
	private static final String	METHOD			= "メソッド" ;
	private static final Set<String> methodSet	= new HashSet<String>();

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

	/**
	 * Doclet のエントリポイントメソッドです。
	 *
	 * @param root RootDoc
	 * @return 正常実行時 true
	 */
	public static boolean start( final RootDoc root ) {
		String version = DocletUtil.getOption( "-version" , root.options() );
		String file    = DocletUtil.getOption( "-outfile" , root.options() );

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

			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 配列よりコンテンツを作成します。
	 *
	 * @param classes ClassDoc[]
	 * @param writer DocletTagWriter
	 */
	private static void writeContents( final ClassDoc[] classes,final DocletTagWriter writer ) {
		for(int i=0; i< classes.length; i++) {
			ClassDoc classDoc	= classes[i] ;
			String className	= classDoc.name();
			String fullName		= classDoc.qualifiedName() ;
			String modifiers	= (classDoc.modifiers()
								+ ( classDoc.isClass() ? " class" : "" ) ).trim();

			Type superType = classDoc.superclassType();
			String superClass = ( superType == null ) ? "" : superType.qualifiedTypeName();

			Type[] interfaceTypes = classDoc.interfaceTypes();
			StringBuilder buf = new StringBuilder( 200 );
			for( int j=0; j<interfaceTypes.length; j++ ) {
				buf.append( interfaceTypes[j].qualifiedTypeName() ).append( "," );
			}
			if( interfaceTypes.length > 0 ) { buf.deleteCharAt( buf.length()-1 ); }
			String intFase = buf.toString();

			Tag[]  desc = classDoc.firstSentenceTags();
			String cmnt = DocletUtil.htmlFilter( classDoc.commentText() );
			Tag[] smplTags	= classDoc.tags(OG_FOR_SMPL);
			Tag[] revTags	= classDoc.tags(OG_REV);
			Tag[] createVer	= classDoc.tags(DOC_VERSION);
			Tag[] author	= classDoc.tags(DOC_AUTHOR);
			Tag[] since		= classDoc.tags(DOC_SINCE);
			Tag[] grpTags	= classDoc.tags(OG_GROUP);

			writer.printTag( "<classDoc>" );
			writer.printTag( "  <fullName>"		,fullName		,"</fullName>"		);
			writer.printTag( "  <modifiers>"	,modifiers		,"</modifiers>"		);
			writer.printTag( "  <className>"	,className		,"</className>"		);
			writer.printTag( "  <superClass>"	,superClass		,"</superClass>"	);
			writer.printTag( "  <interface>"	,intFase		,"</interface>"		);
			writer.printTag( "  <createVer>"	,createVer		,"</createVer>"		);
			writer.printTag( "  <author>"		,author			,"</author>"		);
			writer.printTag( "  <since>"		,since			,"</since>"			);
			writer.printTag( "  <description>"	,desc			,"</description>"	);
			writer.printTag( "  <contents>"		,cmnt			,"</contents>"		);
			writer.printTag( "  <classGroup>"	);
			writer.printCSVTag(		grpTags		);
			writer.printTag(   "</classGroup>"	);
			writer.printTag( "  <formSample>"	,smplTags		,"</formSample>"	);
			writer.printTag( "  <history>"		,revTags		,"</history>"		);

			// 5.1.9.0 (2010/08/01) ソースチェック用(コメントや概要が無い場合。スーパークラスは省く)
			if( ( cmnt.length() == 0 || desc.length == 0 ) && superClass.length() == 0 ) {
				System.out.println( "警告:コメントC=\t" + classDoc.position() );
			}

			methodSet.clear();
			int extendFlag = 0;		// 0:オリジナル 1:org.opengion関連Extend 2:Java関連Extend
	//		while( fullName.startsWith( SELECT_PACKAGE ) ) {
			while( true ) {
//				ConstructorDoc[] cnstrctrs = classDoc.constructors();
				ConstructorDoc[] cnstrctrs = classDoc.constructors( false );	// 5.1.9.0 (2010/08/01) チェック用
				for(int j=0; j < cnstrctrs.length; j++) {
					if( isAction( cnstrctrs[j],extendFlag ) ) {
						menberTag( cnstrctrs[j],CONSTRUCTOR,writer,extendFlag );
					}
				}

//				MethodDoc[] methods = classDoc.methods();
				MethodDoc[] methods = classDoc.methods( false );	// 5.1.9.0 (2010/08/01) チェック用
				for(int j=0; j < methods.length; j++) {
					if( isAction( methods[j],extendFlag ) ) {
						menberTag( methods[j],METHOD,writer,extendFlag );
					}
				}
				// 対象クラス(オリジナル)から、上に上がっていく。
				Type type = classDoc.superclassType();
				if( type == null ) { break; }
				classDoc  = type.asClassDoc() ;
				fullName = classDoc.qualifiedName();
				// java.lang.Object クラスは対象が多いため、処理しません。
				if( "java.lang.Object".equals( fullName ) || classDoc.isEnum() ) {
					break;
				}
				else if( fullName.startsWith( SELECT_PACKAGE ) ) {
					extendFlag = 1;
				}
				else {
					extendFlag = 2;
				}
			}
			writer.printTag( "  </classDoc>" );
		}
	}

	/**
	 * メンバークラスのXML化を行うかどうかを判定します。
	 *
	 * 以下の条件に合致する場合は、処理を行いません。(false を返します。)
	 *
	 * １．同一クラスを処理中にEXTENDで継承元をさかのぼる場合、すでに同じシグネチャのメソッドが
	 *     存在している。
	 * ２．NOT_PRIVATE が true の時の private メソッド
	 * ３．extendFlag が 0以上(1,2)の時の private メソッド
	 * ４．メソッド名におかしな記号(&lt;など)が含まれている場合
	 *
	 * @param menber ExecutableMemberDoc
	 * @param extendFlag int 0:オリジナル 1:org.opengion関連Extend 2:Java関連Extend
	 */
	private static boolean isAction( final ExecutableMemberDoc menber,final int extendFlag ) {
		String menberName = menber.name() ;
		String signature  = menberName + menber.signature();

		boolean rtn = 	( ! methodSet.add( signature ) )
					||	( NOT_PRIVATE    && menber.isPrivate() )
					||	( extendFlag > 0 && menber.isPrivate() )
					||	( menberName.charAt(0) == '<' ) ;

		return ! rtn ;
	}

	// 5.1.9.0 (2010/08/01) ソースチェック用(半角文字＋空白文字のみ)
	private static java.util.regex.Pattern PTN = java.util.regex.Pattern.compile("[\\w\\s]+");

	/**
	 * メンバークラス(コンストラクタ、メソッド)をXML化します。
	 *
	 * @param menber ExecutableMemberDoc
	 * @param menberType String
	 * @param writer DocletTagWriter
	 * @param extendFlag int 0:オリジナル 1::org.opengion関連Extend 2:Java関連Extend
	 */
	private static void menberTag(	final ExecutableMemberDoc menber,
									final String menberType,
									final DocletTagWriter writer,
									final int extendFlag ) {

		final String modifiers ;
		if( menber instanceof MethodDoc ) {
			// メソッドの処理
			Type rtnType = ((MethodDoc)menber).returnType();
			StringBuilder modifyBuf = new StringBuilder( 200 );
			modifyBuf.append( menber.modifiers() );
	//		modifyBuf.append( " " ).append( rtnType.qualifiedTypeName() );
			modifyBuf.append( " " ).append( rtnType.typeName() );
			if( rtnType.dimension() != null ) { modifyBuf.append( rtnType.dimension() ); }

			modifiers = modifyBuf.toString();

			// 5.1.9.0 (2010/08/01) ソースチェック用(@return との整合性チェック)
			Tag[] docReturn	= menber.tags(DOC_RETURN);		// 5.1.9.0 (2010/08/01) チェック用
			if( docReturn.length > 0 ) {
				String data = docReturn[0].text();
				SourcePosition posi = docReturn[0].position();
				// ｢@return String 解説｣ の形式かどうかチェック。String が無ければ警告
			//	if( data.indexOf( rtnType.typeName() ) < 0 ) {
			//		System.out.println( "警告:return=" + "\t" + modifiers + "\t" + data + "\t" + posi );
			//	}
				// ｢@return 解説｣ の形式で、解説に日本語がなく、スペースやタブもなければ、警告
				if( PTN.matcher( data ).matches() && data.indexOf( ' ' ) < 0 && data.indexOf( '\t' ) < 0 ) {
					System.out.println( "警告:日本語R=" + "\t" + modifiers + "\t" + data + "\t" + posi );
				}
			}
		}
		else {
			// コンストラクター処理
			modifiers  = menber.modifiers();
		}

		String menberName = menber.name();

		StringBuilder sigBuf = new StringBuilder( 200 );
		sigBuf.append( menberName ).append( "(" ) ;
		Parameter[] prm = menber.parameters();

		// 5.1.9.0 (2010/08/01) ソースチェック用(@param と引数の個数が異なる場合)
		Tag[] docParam	= menber.tags(DOC_PARAM);		// 5.1.9.0 (2010/08/01) チェック用
		if( docParam.length != prm.length && docParam.length > 0 ) {
			System.out.println( "警告:個数違い=" + "\t" + docParam[0].position() );
		}

		for( int k=0; k<prm.length; k++ ) {
//			sigBuf.append( prm[k].toString() ).append( "," );
			Type ptyp = prm[k].type();
			String prmNm =prm[k].name();

			sigBuf.append( ptyp.typeName() ).append( ptyp.dimension() ).append( " " )
					.append( prmNm ).append( "," );

			// 5.1.9.0 (2010/08/01) ソースチェック用(@param と引数の個数が異なる場合)
			if( docParam.length > k ) {
				String data  = docParam[k].text();
				SourcePosition posi  = docParam[k].position();
				// 引数の文字列の長さが、１文字の場合
				if( prmNm.length() == 1 ) {
					System.out.println( "警告:短いM=" + "\t" + prmNm + "\t" + data + "\t" + posi );
				}
				// ｢@param aaa String 解説｣形式で、aaa(引数名)がない場合
				if( data.indexOf( prmNm ) < 0 ) {
					System.out.println( "警告:引数名=" + "\t" + prmNm + "\t" + data + "\t" + posi );
				}
				// ｢@param aaa String 解説｣形式で、解説に日本語がなく、スペースやタブもなければ、警告
				if( PTN.matcher( data ).matches() && data.indexOf( ' ' ) < 0 && data.indexOf( '\t' ) < 0 ) {
					System.out.println( "警告:日本語M=" + "\t" + prmNm + "\t" + data + "\t" + posi );
				}
			}
		}
		if( prm.length > 0 ) { sigBuf.deleteCharAt( sigBuf.length()-1 ); }
		sigBuf.append( ")" );
		String signature = sigBuf.toString();

		Tag[]	desc	= menber.firstSentenceTags();
		String	cmnt	= DocletUtil.htmlFilter( menber.commentText() );
		Tag[]	tags	= menber.tags();
		Tag[]	revTags = menber.tags(OG_REV);
		String	extend  = String.valueOf( extendFlag );
		String	extClass = ( extendFlag == 0 ) ? "" : menber.containingClass().qualifiedName() ;

		String	position = String.valueOf( menber.position().line() );

		writer.printTag( "  <menber>" );
		writer.printTag( "    <type>"		,menberType	,"</type>"			);
		writer.printTag( "    <name>"		,menberName	,"</name>"			);
		writer.printTag( "    <modifiers>"	,modifiers	,"</modifiers>"		);
		writer.printTag( "    <signature>"	,signature	,"</signature>"		);
		writer.printTag( "    <position>"	,position	,"</position>"		);
		writer.printTag( "    <extendClass>",extClass	,"</extendClass>"	);
		writer.printTag( "    <extendFlag>"	,extend		,"</extendFlag>"	);
		writer.printTag( "    <description>",desc		,"</description>"	);
		writer.printTag( "    <contents>"	,cmnt		,"</contents>"		);
		writer.printTag( "    <tagText>" );
		writer.printTagsInfo(   tags );
		writer.printTag( "    </tagText>" );
		writer.printTag( "    <history>"	,revTags	,"</history>" );
		writer.printTag( "  </menber>");

		// 5.1.9.0 (2010/08/01) ソースチェック用
		if( ( cmnt.length() == 0 || desc.length == 0 ) 		// コメントや概要が無い
				&& extClass.length() == 0					// 拡張クラスが存在しない
				&& ( menber instanceof MethodDoc )			// メソッドに限定
				&& !menber.isSynthetic()					// コンパイラによって合成されていない場合
				&& !menber.isNative() ) {					// ネイティブメソッドでない

			// さらに、親が Enum クラス以外
			Type prntType = menber.containingClass().superclassType();
			String prntClass = ( prntType == null ) ? "" : prntType.qualifiedTypeName();
			if( !"java.lang.Enum".equals( prntClass ) ) {
				System.out.println( "警告:コメントM=" + "\t" + signature + "\t" + menber.position() );
			}
		}
	}

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