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.TreeSet;
import java.util.HashSet;
import java.util.regex.Pattern;
import java.util.regex.Matcher;
import java.io.PrintStream;
import java.awt.Font;
import ash.reverse.parser.Visibility;
import ash.reverse.struct.ObjectVisitor;
import ash.reverse.struct.ClassVisitor;
import ash.reverse.struct.ObjectRecord;
import ash.reverse.struct.GroupRecord;
import ash.reverse.struct.FileRecord;
import ash.reverse.struct.ClassRecord;
import ash.reverse.struct.MethodRecord;
import ash.reverse.struct.FieldRecord;

/**
 * 複数のファイル記録から、そこで定義されているクラス情報を抽出し、
 * クラス図を生成する。
 */
public class ClsGenerator extends UMLDiagram {
	/**
	 * クラス図を出力する。
	 * @param out クラス図情報の出力先
	 * @param fileList ファイル情報を管理するリスト
	 * @param font レイアウト計算で使用するフォント
	 * @param selectLevel 可視性の選択レベル<p>
	 * 						1: public  2: protected  3: package  4: private
	 * @param aflag 引数の型参照依存関係の生成
	 * @param bflag メソッド本体の型参照依存関係の生成
	 */
	public static void generate(PrintStream out, List<FileRecord> fileList,
								Font font, int selectLevel,
								boolean aflag, boolean bflag) {
		ClsGenerator gen = new ClsGenerator();
		// レイアウト計算で使用するフォントを設定する。
		gen.setFont(font);			// UMLDiagramのメソッド

		// クラス図生成のパラメータを設定する。
		gen.setParams(selectLevel, aflag, bflag);

		// クラス図のグラフ構造の情報（nodeList と relList）を生成する。
		gen.generateClassInfo(fileList);

		// ファイルのヘッダ部分（クラス一覧とパッケージ定義）を出力する。
		gen.generateHeader(out);

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

	}
	/**
	 * クラス図生成のパラメータを設定する。
	 * @param selectLevel 可視性の選択レベル<p>
	 * 						1: public  2: protected  3: package  4: private
	 * @param aflag 引数の型参照依存関係の生成
	 * @param bflag メソッド本体の型参照依存関係の生成
	 */
	void setParams(int selectLevel, boolean aflag, boolean bflag) {
		this.selectLevel = selectLevel;
		this.aflag = aflag;
		this.bflag = bflag;
	}

	private int selectLevel;		// 可視性の選択レベル
									// 1:public 2:protected 3:package 4:private
	private boolean aflag;			// 引数の型参照依存関係の生成
	private boolean bflag;			// メソッド本体の型参照依存関係の生成
	
	private Map<String,Integer> packageMap;	// パッケージマップ
	private Map<String,ClassRecord> fqcnMap; // FQCNマップ
	private Map<Integer,ClassNode> idMap;	// IDマップ
	
	private FileRecord theFile;			// 現在解析中のファイル記録
	private ClassRecord theClassRec;	// 現在解析中のクラスの記録
	private ClassNode theClassNode;		// 現在解析中のクラスノード
	private int packageID;				// 現在解析中のパッケージのID

	/** 現在解析中の型のインタフェース */
	private List<ClassRecord> interfaceList = new ArrayList<ClassRecord>();

	/** 現在解析中の型が参照する型名からその依存関係へのマップ */
	private Map<ClassRecord,Depends>
		theRefMap = new HashMap<ClassRecord,Depends>();

	/**
	 * クラス図情報を生成する。
	 * @param out クラス図情報の出力先
	 * @param fileList ファイル情報をパッケージ記録として管理するリスト
	 */
	void generateClassInfo(List<FileRecord> fileList) {
		// クラス図生成に必要なマップを作成する。
		packageMap = makePackageMap(fileList);
		fqcnMap = new ClassVisitor(fileList).fqcnMap();
		idMap = new HashMap<Integer,ClassNode>();
		nodeList = new ArrayList<ClassNode>();
		relList = new ArrayList<Relationship>();

		// パーズしたソースコードからクラス図情報を生成する。
		for(FileRecord frec : fileList) genFile(frec);

		// 同一クラス間の再帰的関連に対してそのクラスへの参照を設定する。
		for(Relationship rel : relList) {
			if(rel instanceof Assoc) {
				Assoc assoc = (Assoc)rel;
				assoc.setRecursiveNode(idMap);
			}
		}

		// 同じ二つのクラスの間に関連が設定されている場合は依存関係を削除する。
		Set<String> linkSet = new HashSet<String>();
		for(Relationship rel : relList) {
			if(rel instanceof Assoc) linkSet.add(rel.linkID());
		}
		List<Relationship> duplLinks = new ArrayList<Relationship>();
		for(Relationship rel : relList) {
			if(rel instanceof Depends) {
				if(linkSet.contains(rel.linkID())) duplLinks.add(rel);
			}
		}
		relList.removeAll(duplLinks);

	}


	/**
	 * 全てのパッケージを定義・参照の順番で登録し一意番号を割り振る。
	 * @param パーズしたファイル情報のリスト
	 * @return パッケージ名と一意番号のマップ
	 */
	Map<String,Integer> makePackageMap(List<FileRecord> fileList) {
		// package文で宣言されたパッケージをdefpkgsに登録する。
		Set<String> defpkgs = new TreeSet<String>();
		for(FileRecord frec : fileList) {
			String pname = frec.pkgname();
			if(!pname.isEmpty()) defpkgs.add(pname);
		}

		// import文で参照しているパッケージをrefpkgsに登録する。
		Set<String> refpkgs = new TreeSet<String>();
		for(FileRecord frec : fileList) {
			for(String pkg : frec.onDemandSet()) refpkgs.add(pkg);
			for(String pkg : frec.typePkgMap().values()) refpkgs.add(pkg);
		}

		// 全てのパッケージを定義・参照の順番で pkgMapに登録する。
		// 各パッケージには１から始まる一意識別子を割り当てる。
		Map<String,Integer> pkgMap = new LinkedHashMap<String,Integer>();
		int seq = 0;
		for(String pkg : defpkgs) pkgMap.put(pkg, ++seq);
		for(String pkg : refpkgs) {
			if(!pkgMap.containsKey(pkg)) pkgMap.put(pkg, ++seq);
		}
		return pkgMap;
	}

	/**
	 * ファイルのヘッダ部分（クラス一覧とパッケージ定義）を出力する。
	 */
	void generateHeader(PrintStream out) {
		// クラス／インタフェースの一覧を表示する。
		for(String key : fqcnMap.keySet()) {	   // fqcnMap
			ClassRecord crec = fqcnMap.get(key);
			out.println("# " + crec.id() + ". " + key);
		}
		out.println();

		// パッケージIDの定義行を出力する。
		for(Map.Entry<String,Integer> e : packageMap.entrySet()) {
			out.println("PACKAGE" + SEP + e.getValue() + SEP + e.getKey());
		}
		if(!packageMap.isEmpty()) out.println();
	}

	/**
	 * ファイル記録からクラス図情報を生成する。
	 * @param frec ファイル記録
	 */
	private void genFile(FileRecord frec) {
		theFile = frec;
		String pname = frec.pkgname();
		packageID = pname.isEmpty() ? 0 : packageMap.get(pname);

		// このファイルで定義されている各クラスのクラス図情報を生成する。
		//frec.printTypes(frec.name() + ": ");
		for(ObjectRecord crec : frec.members()) genClass((ClassRecord)crec);
	}

	/**
	 * ひとつのクラスのクラス図情報を生成する。
	 * @param crec クラス記録
	 */
	private void genClass(ClassRecord crec) {

		// クラス情報を設定する
		theClassRec = crec;
		theClassNode = new ClassNode(crec.name(), classFormat(crec), packageID);
		idMap.put(crec.id(), theClassNode);
		nodeList.add(theClassNode);
		theClassNode.setFontMetrics(fm);
		theRefMap.clear();			// 依存関係にある型名を初期化する。
		interfaceList.clear();		// インタフェースリストを初期化する。

		// スーパクラスが定義されていたら拡張関係（継承）を追加する。
		if(crec.superClass() != null) {
			ClassRecord sc = findType(crec, crec.superClass());
			if(sc != null) relList.add(new Extends(crec.id(), sc.id(), null));
		}
		// インタフェースが定義されていたら拡張関係（実装）を登録する。
		for(String iname : crec.interfaces()) {
			ClassRecord ic = findType(crec, iname);
			if(ic != null) {
				relList.add(new Extends(crec.id(), ic.id(), null));
				interfaceList.add(ic);
			}
		}

		// 列挙型の値をフィールド値として登録する。
		if(selectLevel > 0) {
			for(String ev : crec.enumList()) {
				String line = ATTRIBUTE +SEP+ ev +SEP+ ENUM_TYPE + SEP + "null";
				theClassNode.addAttribute(ev, line, 0);
			}
		}

		// クラスのメンバーの記録を処理する。
		for(ObjectRecord rec : crec.members()) {
			if(rec instanceof ClassRecord) {
				// 内部クラスのクラス情報を生成し所有クラスとの関係を登録する。
				ClassRecord irec = (ClassRecord)rec;
				genClass(irec);
				relList.add(new InnerRel(irec.id(), crec.id(), null));
			} else {
				// フィールドまたはメソッドの情報を生成する。
				if(rec instanceof FieldRecord) {
					genField((FieldRecord)rec);
				} else {
					genMethod((MethodRecord)rec);
				}
			}
		}
	}

	/**
	 * メソッド記録からメソッド情報を生成する。
	 * @param mrec メソッド記録
	 */
	private void genMethod(MethodRecord mrec) {
		String method = mrec.getSignature();
		if(method.indexOf('"') >= 0) return; // enum 内のオブジェクト生成を排除
		int i1 = method.indexOf('(');
		int i2 = method.lastIndexOf(')');
		if(i1 < 0 || i2 < 0) return;

		// ex. method = MD MD T NAME(T1 V1, T2 V2, ...)
		String methodDecl = method.substring(0, i1);
		// methodDecl = MD MD T NAME
		if(aflag) addRefFromAPI(mrec, mrec.type(), Stereotype.RETURN);

		// メソッド情報を設定する
		MethodNode mn = null;
		if(mrec.visibility().level() <= selectLevel) {
			mn = new MethodNode(mrec.name(), memberFormat(METHOD, mrec),
								findPackageID(mrec.type()));
			theClassNode.addMethod(mn);
		}
		String params = method.substring(i1+1, i2);
		// params = T1 V1, T2 V2, ...
		if(!params.isEmpty()) {
			String[] pairs = params.split(", ");
			for(String param : pairs) {
				String var = param;
				String type = "null";
				for(String token : param.split(" ")) {
					type = var;
					var = token;
				}
				if(mn != null) {
					// メソッドパラメータの情報を設定する
					mn.addParam(var, PARAMETER + SEP + var + SEP + type,
								findPackageID(type));
				}
				if(aflag) addRefFromAPI(mrec, type, Stereotype.PARAM);
			}
		}
		if(bflag) parseBodyToken(mrec);	// メソッド本体記述での型参照
	}

	// 多値の多重度を表現するフィールドの型定義: List<T>, Set<T>, ...
	static final String MULTIPLICITY = //".*List<(.+)>";
		"(List|ArrayList|LinkedList|Set|HashSet|TreeSet)<(\\w+)>";
	static final Pattern MULTI_PAT = Pattern.compile(MULTIPLICITY);
	/**
	 * フィールド記録から属性情報と関連情報を生成する。
	 * このフィールドの型がfindType()メソッドで検索できる型ならば、
	 * 関連の定義と解釈し、関連情報を登録する。
	 * 型がList<T>の形式の場合は多重度が多値の関連とする。
	 * @param field フィールド記録
	 */
	private void genField(FieldRecord field) {
		if(bflag) parseBodyToken(field);	// フィールド初期化式での型参照

		ClassRecord source = (ClassRecord)field.owner();
		String fieldDecl = field.signature();
		ClassRecord target = findType(field, field.type());
		if(target != null) {
			// フィールドの型がクラスマップにあるならば、
			// 多重度がOPTIONAL(0..1)の関連として登録する。
			Assoc assoc = new Assoc(source.id(), target.id(), null);
			assoc.setTarget(OPTIONAL, true, field.name());
			relList.add(assoc);
			return;
		}
		Matcher m = MULTI_PAT.matcher(field.type());
		if(m.find()) {
			// フィールドの型が Link<T>の形式の場合
			target = findType(field, m.group(2));
			if(target != null) {
				// 多重度がMULTIVALUED(*)の関連として登録する。
				Assoc assoc = new Assoc(source.id(), target.id(), null);	
				assoc.setTarget(MULTIVALUED, true, field.name());
				relList.add(assoc);
				return;
			}
		}
		if(field.visibility().level() <= selectLevel) {
			theClassNode.addAttribute(field.name(),
									  memberFormat(ATTRIBUTE,field),
									  0);
		}
	}

	// 型パラメータを取り除いた型名の定義
	static final String TYPE_DECL = "(\\w+).*";
	static final Pattern TYPE_PAT = Pattern.compile(TYPE_DECL);

	private int findPackageID(String type) {
		Matcher m = TYPE_PAT.matcher(type);
		if(m.find()) {
			String pkg = theFile.getPackage(m.group(1));
			Integer id = packageMap.get(pkg);
			if(id != null) return id; 
		}
		return 0;
	}

	/**
	 * メソッドの戻り値またはパラメータの型に対する依存関係を登録する。
	 * @param mrec メソッド記録
	 * @param tname 型名
	 * @param tag 参照種別を示すタグ（return/param）
	 */
	private void addRefFromAPI(MethodRecord mrec, String tname, Stereotype tag) {
		String[] typeNames = tname.split("[<>,+]");
		for(String token : typeNames) {
			addDepends(mrec, token, tag);
		}
	}
	/**
	 * 本体記述のトークン列を解析し、型参照から依存関係を登録する。
	 * @param rec 解析対象のオブジェクト（フィールドかメソッド）の記録
	 */
	private void parseBodyToken(ObjectRecord rec) {
		Stereotype tag = Stereotype.BODY;
		for(String token : rec.getTokens()) {
			addDepends(rec, token, tag);
			tag = Stereotype.value(token);
			if(tag != Stereotype.NEW) tag = Stereotype.BODY;
		}
	}
	/**
	 * 解析対象のオブジェクトが参照している型名への依存関係があったら
	 * それを登録する。
	 * @param rec 解析対象のオブジェクト（フィールドかメソッド）の記録
	 * @param token 型名を表すトークン(ドット記法で表された名前)
	 */
	private void addDepends(ObjectRecord rec, String token, Stereotype tag) {
		ClassRecord target = fqcnMap.get(token); // FQCN表記の型を検索
		if(target != null) {
			addTarget(target, token, tag);
		} else {
			String[] items = token.split("\\.");
			for(String type : items) {
				target = findType(rec, type);
				if(target != null) addTarget(target, type, tag);
			}
		}
	}
	/**
	 * 依存関係を登録する。
	 * @param target 依存関係のターゲットとなる型
	 * @param type 型名
	 * @param tag 依存関係の種類
	 */
	private void addTarget(ClassRecord target, String type, Stereotype tag) {
		assert trace(tag.label + " " + type);
		Depends dep = theRefMap.get(target);
		if (dep == null) {
			dep = new Depends(theClassRec.id(), target.id(), tag);
			relList.add(dep);
			theRefMap.put(target, dep);
		} else {
			dep.count(tag);
		}
	}

	/**
	 * 名前から型を特定する。
	 * @param rec 名前を参照しているオブジェクト
	 * @param name 名前
	 * @return 名前が示す型情報
	 */
	private ClassRecord findType(ObjectRecord orec, String name) {
		ClassRecord crec = fqcnMap.get(name);
		if(crec != null) return crec;		// FQCNで参照している場合
		crec = orec.owner().findType(name);
		if(crec != null) return crec;		// 上位スコープで定義された型
		crec = findInterfaceType(name);
		if(crec != null) return crec;		// インタフェースで定義された型
		FileRecord frec = (FileRecord)orec.root();
		crec = fqcnMap.get(frec.fqcn(name));
		if(crec != null) return crec;		// import宣言のある場合
		crec = fqcnMap.get(frec.pkgname() + "." + name);
		if(crec != null) return crec;		// 同一パッケージにある場合
		return crec;
	}

	/**
	 * 指定した型名が実装しているインタフェースで宣言されていればそれを返す。
	 * @param name 型名
	 * @return インタフェースで宣言されている型情報
	 */
	private ClassRecord findInterfaceType(String name) {
		for(ClassRecord ifrec : interfaceList) {
			ClassRecord crec = ifrec.findType(name);
			if(crec != null) return crec;
		}
		return null;
	}

	/**
	 * 指定したクラス情報を出力する。
	 * @param crec クラス情報
	 */
	String classFormat(ClassRecord crec) {
		String tag = crec.type().toUpperCase();
		if(crec.isAbstract()) tag = "@" + tag;
		return tag + SEP + crec.id() + SEP + crec.name() + SEP
			+ crec.visibility();
	}
	
	/**
	 * 指定したクラスメンバー情報(属性またはメソッド)を出力する。
	 * @param tag メンバー種別を示すタグ（ATTRIB/METHOD）
	 */
	String memberFormat(String tag, ObjectRecord rec) {
		if(rec.isAbstract()) tag = "@" + tag;
		return tag + SEP + rec.name() + SEP + rec.type() + SEP
			+ rec.visibility();
	}

	boolean trace(Object msg) {
		System.err.println(msg);
		return true;
	}
}
