package ash.reverse.uml;
import java.util.List;
import java.util.ArrayList;
import java.util.Map;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Set;
import java.util.HashSet;
import java.io.PrintStream;
import java.awt.Font;
import ash.reverse.parser.Visibility;
import ash.reverse.struct.ObjectRecord;
import ash.reverse.struct.FileRecord;
import ash.reverse.struct.ClassRecord;

/**
 * 複数のファイル記録から、そこで定義されているパッケージ情報を抽出し、
 * パッケージ図を出力する。
 */
public class PkgGenerator extends UMLDiagram {
	/**
	 * パッケージ図を出力する。
	 * @param out パッケージ図情報の出力先
	 * @param fileList ファイル情報を管理するリスト
	 * @param font レイアウト計算で使用するフォント
	 * @param rflag 外部パッケージ参照
	 * @param dflag 定義されている型の表示
	 */
	public static void generate(PrintStream out, List<FileRecord> fileList,
								Font font, boolean rflag, boolean dflag) {
		PkgGenerator gen = new PkgGenerator();
		// レイアウト計算で使用するフォントを設定する。
		gen.setFont(font);			// UMLDiagramのメソッド

		// パッケージ図生成のパラメータを設定する。
		gen.setParams(rflag, dflag);

		// パッケージ図のグラフ構造の情報（nodeList と relList）を生成する。
		gen.generatePkgInfo(fileList);

		// グラフ構造情報からパッケージ図を生成する。
		gen.generateDiagram(out);	// UMLDiagramのメソッド
	}

	/**
	 * パッケージレポートを出力する。
	 * @param out パッケージ図情報の出力先
	 * @param fileList ファイル情報を管理するリスト
	 */
	public static void generateRep(PrintStream out, List<FileRecord> fileList) {
		PkgGenerator gen = new PkgGenerator();
		gen.setParams(true, true);
		gen.generatePkgInfo(fileList);
		gen.generateReport(out);
	}

	/**
	 * パッケージ図生成のパラメータを設定する。
	 * @param rflag 外部パッケージ参照
	 * @param dflag 定義されている型の表示
	 */
	void setParams(boolean rflag, boolean dflag) {
		this.rflag = rflag;
		this.dflag = dflag;
	}

	private boolean rflag;				// 外部パッケージ参照
	private boolean dflag;				// 定義されている型の表示

	private static final String DEFAULTPKG = "*default*";
	private static final String ON_DEMAND = "  *  ";

	/** パッケージ情報の識別子の連番 */
	private int pkgseq = 0;

	/** 依存関係のラベルの連番 */
	private int refseq = 0;

	/** 宣言パッケージと参照パッケージの名前と記録のマップ */
	private Map<String,PackageRec>
		packageMap = new LinkedHashMap<String,PackageRec>();

	/** 二つの型の間の依存関係の単一化 */
	private Set<String> dependsSet = new HashSet<String>();

	/**
	 * パッケージ図のグラフ構造の情報（nodeList と relList）を生成する。
	 * @param fileList ファイル情報を管理するリスト
	 */
	void generatePkgInfo(List<FileRecord> fileList) {
		// package文で宣言されたパッケージを定義パッケージとして登録する。
		// 定義パッケージを最初に登録しておく。
		for(FileRecord filerec : fileList) {
			PackageRec prec = findPackage(filerec.pkgname());
			prec.defined = true;
			// パッケージに含まれる型を登録する。
			for(ObjectRecord rec : filerec.members())
				registerClass(prec, (ClassRecord)rec);
		}
		// import文で型宣言しているパッケージを登録する。
		for(FileRecord filerec : fileList) {
			// オンデマンド宣言のパッケージ参照を登録する。
			for(String pkgname : filerec.onDemandSet()) {
				findPackage(pkgname).referedOnDemand = true;
			}
			// 参照パッケージに参照している型名を登録し、依存関係を設定する。
			PackageRec prec = findPackage(filerec.pkgname());
			for(Map.Entry<String,String> e : filerec.typePkgMap().entrySet()) {
				String typeName = e.getKey();
				String pkgname = e.getValue();
				prec.addReference(findPackage(pkgname), typeName);
			}
		}

		// 解析したソースコードからパッケージ図のグラフ構造情報を生成する。
		for(PackageRec prec : packageMap.values()) {
			// 参照されているが定義されていない型を登録する。
			prec.appendRef();
			if(prec.defined || rflag) generateGraphInfo(prec);
		}
	}

	/**
	 * パッケージ名から対応するパッケージ情報を返す。
	 * パッケージ名が空文字列の場合は DEFAULTPKG とする。
	 * 未登録の場合は登録する。
	 * @param name パッケージ名(ex. java.util)
	 * @return パッケージ情報
	 */
	private PackageRec findPackage(String name) {
		if(name == null || name.isEmpty()) name = DEFAULTPKG;
		PackageRec prec = packageMap.get(name);
		if(prec == null) {
			prec = new PackageRec(++pkgseq, name);
			packageMap.put(name, prec);
		}
		return prec;
	}

	/**
	 * 解析対象のパッケージの中で定義された型を登録する。
	 * @param prec 解析対象のパッケージ
	 * @param crec 解析した型情報
	 */
	private void registerClass(PackageRec prec, ClassRecord crec) {
		prec.addType(crec);
		for(ObjectRecord rec : crec.members()) {
			if(rec instanceof ClassRecord) {
				registerClass(prec, (ClassRecord)rec);
				//System.err.println(((ClassRecord)rec).qcn());
			}
		}
	}

	/**
	 * 指定されたパッケージに関するパッケージ図情報を生成する。
	 * パッケージをクラス、依存パッケージを依存先型名、型名をフィールド変数
	 * とするクラス図としてグラフ構造を生成する。
	 * @param prec パッケージ情報
	 */
	private void generateGraphInfo(PackageRec prec) {
		// nodeListにパッケージノードを登録する。
		String pinfo = "CLASS" + SEP + prec.id + SEP + prec.name + SEP + "null";
		ClassNode node = new ClassNode(prec.name, pinfo, 0);
		nodeList.add(node);
		node.setFontMetrics(fm);

		// 依存関係を生成し登録する。
		for(PackageRec ref : prec.references) {
			if(ref.defined || rflag) {
				String arcID = "" + prec.id + "," + ref.id;
				if(!dependsSet.contains(arcID)) {
					dependsSet.add(arcID);
					String label = "ref" + (++refseq);
					relList.add(new Depends(prec.id, ref.id, label));
				}
			}
		}
		if(dflag) {
			// node に型定義情報を登録する。
			for(ClassRecord trec : prec.typeList) {
				Visibility vis = trec.visibility();
				String name = trec.symbol() + trec.qcn();
				String cinfo = "ATTRIB" + SEP + name + SEP + "Class" + SEP + vis;
				node.addAttribute(name, cinfo, 0);
			}
			if(prec.referedOnDemand) {
				String cinfo =
					"ATTRIB" + SEP + ON_DEMAND + SEP + "Class" + SEP + "null";
				node.addAttribute(ON_DEMAND, cinfo, 0);
			}
		}
	}

	/**
	 * パッケージレポートを出力する。
	 * @param out パッケージレポートの出力先
	 */
	void generateReport(PrintStream out) {
		for(PackageRec prec : packageMap.values()) {
			out.println(prec.name);
			for(ClassRecord trec : prec.typeList) {
				String spec = trec.visibility().symbol() + " " + trec.symbol();
				out.println("\t" + spec + "\t" + trec.qcn());
			}
			if(prec.referedOnDemand) out.println("\t\t*");

			for(PackageRec ref : prec.references)
				out.println("\t-->\t" + ref.name);
		}
	}
}

/**
 * 1つのパッケージに関する情報を保持する。
 */
class PackageRec {
	int id;							// パッケージの識別子
	String name;					// パッケージの名前(ex. java.util)
	boolean defined = false;		// package文で宣言されたパッケージならtrue
	boolean referedOnDemand = false;	// オンデマンド参照(*)があるならばtrue

	/** このパッケージが参照しているパッケージのリスト */
	List<PackageRec> references = new ArrayList<PackageRec>();

	/** このパッケージに含まれる型のリスト */
	List<ClassRecord> typeList = new ArrayList<ClassRecord>();
	private Set<String> defSet = new HashSet<String>();
	private Set<String> refSet = new HashSet<String>();

	/**
	 * パッケージ情報のコンストラクタ
	 * @param id パッケージ識別子
	 * @param name パッケージ名(ex. java.util)
	 */
	PackageRec(int id, String name) {
		this.id = id;
		this.name = name;
	}

	/**
	 * このパッケージの中で定義された型を登録する。
	 * @param trec 型情報
	 */
	public void addType(ClassRecord trec) {
		defSet.add(trec.qcn());
		typeList.add(trec);
	}

	/**
	 * 参照しているパッケージが未登録であったら登録する。
	 * @param ref 参照しているパッケージ
	 * @param typeName 参照している型名
	 */
	public void addReference(PackageRec ref, String typeName) {
		if(this != ref && !references.contains(ref)) references.add(ref);
		ref.refSet.add(typeName);
	}

	/**
	 * 参照されているが定義されていない型をこのパッケージに登録する。
	 */
	public void appendRef() {
		for(String ref : refSet) {
			if(!defSet.contains(ref)) {
				ClassRecord crec = new ClassRecord("undef", ref, 0, null);
				crec.visibility(Visibility.PUBLIC);
				typeList.add(crec);
			}
		}
	}
}
