package pbl2011.common;

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

import pbl2011.model.Attribute;
import pbl2011.model.ClassNode;
import pbl2011.model.Method;
import pbl2011.model.PackageManager;
import pbl2011.model.Parameter;

public class JavaCodeGenerator {
	
	private static final String SRC_DIR = "src";
	
	private static final String EXTENTION_JAVA = ".java";

	private static final String SPACE = " ";

	private static final String PATH_JAVA_SOURCE_TEMPLATE = "conf/javaSource.template";

	private static final String PLACE_HOLDER_PACKAGE_PHRASE = "<<package>>";

	private static final String PLACE_HOLDER_IMPORT_PHRASE = "<<import>>";

	private static final String PLACE_HOLDER_CLASS_COMMENT = "<<classComment>>";

	private static final String PLACE_HOLDER_CLASS_DECLARATION = "<<classOpenPhrase>>";

	private static final String PLACE_HOLDER_ATTRIBUTES = "<<attributes>>";

	private static final String PLACE_HOLDER_METHODS = "<<methods>>";

	private static final String INPORT_PHRASE = "import <<packageName>>.<<className>>;";

	private static final String ATTRIBUTE_PHRASE = "        <<visibility>> <<typeName>> <<attrName>>;";

	private static final String METHOD_START_PHRASE = "        <<visibility>> <<returnType>> <<methodName>> (<<params>>) {";

	private static final String PARAM_PHRASE = "<<paramType>> <<paramName>>";

	public static void generate(File outputDir, List<ClassNode> classNodeList) {
		
		// チェック機能
		// TODO
		
		// ソースコード出力ディレクトリの作成
		makeOutputDir(outputDir);

		generateProcess(outputDir, classNodeList);
	}
	
	/**
	 * Javaソースコード出力で出力先となるディレクトリを作成する
	 * ディレクトリが存在した場合は、以下のファイルを削除し再作成する
	 */
	private static void makeOutputDir(File outputDir) {
		File file = new File(outputDir.getAbsolutePath() + File.separator + SRC_DIR); 
		if (file.exists()) {
			FileUtil.deleteDir(file);
		}
		file.mkdirs();
	}
	
	/**
	 * ソースコード出力処理
	 * @param classNodeList
	 */
	private static void generateProcess(File outputDir, List<ClassNode> classNodeList) {
		
		// 対象となるクラスの取得
		ArrayList<ClassNode> activeClassNodeList = getAllActiveClassData(classNodeList);

		for (ClassNode classNode : activeClassNodeList) {
			if (!classNode.isBuiltInClass) {
				FileWriter fw = null;
				String outputDirPath = null;
				String filePath = null;
				try {
					if (classNode.packageId == CommonConst.PACKAGE_DEFAULT_ID) {
						filePath = outputDir.getAbsolutePath() + File.separator + SRC_DIR + File.separator + classNode.className + EXTENTION_JAVA;
					} else {
						outputDirPath = outputDir.getAbsolutePath() + File.separator + SRC_DIR + File.separator + classNode.getPackageName().replace(CommonConst.DELIMITER_PACKAGE, File.separator);
						filePath = outputDirPath + File.separator + classNode.className + EXTENTION_JAVA;
						File outputSrcDir = new File(outputDirPath);
						if (!outputSrcDir.exists()) {
							outputSrcDir.mkdirs();
						}
					}
					fw = new FileWriter(filePath);
					fw.write(genarateClassCode(classNode, classNodeList));
					fw.flush();
				} catch (IOException e) {
					e.printStackTrace();
				} finally {
					try {
						fw.close();
					} catch (IOException e) {
						e.printStackTrace();
					}
				}
			}
		}
	}

	/**
	 * クラスのソースコードを作成する
	 * @param classNode
	 * @return クラスのソースコード
	 */
	private static String genarateClassCode(ClassNode classNode, List<ClassNode> classNodeList) {
	    ClassLoader cloader = Util.getClassLoader(JavaCodeGenerator.class);
		InputStream in = cloader.getResourceAsStream(PATH_JAVA_SOURCE_TEMPLATE);
		String template = FileUtil.readFile(in);
		Map<String, String> replaceMap = new HashMap<String, String>();
		
		replaceMap.put(PLACE_HOLDER_PACKAGE_PHRASE, classNode.getPackagePhrase());
		replaceMap.put(PLACE_HOLDER_IMPORT_PHRASE, genImportPhrase(classNode, classNodeList));
		replaceMap.put(PLACE_HOLDER_CLASS_COMMENT, null);
		replaceMap.put(PLACE_HOLDER_CLASS_DECLARATION, classNode.getClassdeclarationPhrase());
		replaceMap.put(PLACE_HOLDER_ATTRIBUTES, genAttributePhrase(classNode.attributeList, classNodeList));
		replaceMap.put(PLACE_HOLDER_METHODS, genMethodPhrase(classNode.methodList, classNodeList));

		return replace(template, replaceMap);
	}
	

	/**
	 * インポートのソースコードを作成する
	 * @param classNode
	 * @return インポートのソースコード
	 */
	private static String genImportPhrase(ClassNode classNode, List<ClassNode> classNodeList) {
		Set<String> importSet = new HashSet<String>();
		StringBuffer buf = new StringBuffer();
		for (Attribute attr : classNode.attributeList) {
			importSet.addAll(Util.getTypeList(attr.typeId));
		}
		for (Method method : classNode.methodList) {
			importSet.addAll(Util.getTypeList(method.dataTypeId));
			for (Parameter param : method.parameterList) {
				importSet.addAll(Util.getTypeList(param.dataTypeId));
			}
		}
		
		for (String typeId : importSet) {
			ClassNode importClassNode = getClassData(Integer.valueOf(typeId), classNodeList);
			if (PackageManager.isNeedImport(importClassNode.packageId) && importClassNode.packageId != classNode.packageId) {
				String phrase = INPORT_PHRASE;
				phrase = replace(phrase, "<<packageName>>", importClassNode.getPackageName());
				phrase = replace(phrase, "<<className>>", importClassNode.className);
				buf.append(phrase);
				buf.append(System.getProperty("line.separator"));
			}
		}
		return buf.toString();
	}

	/**
	 * 属性のソースコードを作成する
	 * @param attributeList
	 * @return 属性のソースコード
	 */
	private static String genAttributePhrase(List<Attribute> attributeList, List<ClassNode> classNodeList) {
		StringBuffer buf = new StringBuffer();
		// 属性
		for (Attribute attr : attributeList) {
			String phrase = ATTRIBUTE_PHRASE;
			phrase = replace(phrase, "<<visibility>>", attr.visibility.code());
			phrase = replace(phrase, "<<typeName>>",getTypeNameByTypeId(attr.typeId, classNodeList));
			phrase = replace(phrase, "<<attrName>>", attr.attributeName);
			buf.append(phrase);
			buf.append(System.getProperty("line.separator"));
			buf.append(System.getProperty("line.separator"));
		}
		return buf.toString();
	}
	
	/**
	 * 操作のソースコードを作成する
	 * @param methodList
	 * @return 操作のソースコード
	 */
	private static String genMethodPhrase(List<Method> methodList, List<ClassNode> classNodeList) {
		StringBuffer buf = new StringBuffer();

		// 操作
		for (Method method : methodList) {
			String phrase = METHOD_START_PHRASE;
			phrase = replace(phrase, "<<visibility>>", method.visibility.code());
			phrase = replace(phrase, "<<returnType>>", getTypeNameByTypeId(method.dataTypeId, classNodeList));
			phrase = replace(phrase, "<<methodName>>", method.methodName);
			phrase = phrase.replaceAll("<<params>>", genParamPhrase(classNodeList, method));
			buf.append(phrase);

			buf.append(System.getProperty("line.separator"));
			buf.append("            // TODO");
			buf.append(System.getProperty("line.separator"));
			// 戻り値がvoid以外の場合はデフォルトでreturn null;を出力
			if (!CommonConst.DEFAULT_RETURN_TYPE_NAME .equals(getTypeNameByTypeId(method.dataTypeId, classNodeList))) {
				buf.append("            return null;");
				buf.append(System.getProperty("line.separator"));
			}
			buf.append("        }");
			buf.append(System.getProperty("line.separator"));
			buf.append(System.getProperty("line.separator"));
		}
		return buf.toString();
	}

	private static String genParamPhrase(List<ClassNode> classNodeList,
			Method method) {
		StringBuffer buf = new StringBuffer();
		for (Parameter param : method.parameterList) {
			String phrase = PARAM_PHRASE;
			phrase = replace(phrase, "<<paramType>>",getTypeNameByTypeId(param.dataTypeId, classNodeList));
			phrase = replace(phrase, "<<paramName>>", param.parameterName);
			buf.append(phrase);
			if (!param.equals(method.parameterList.get(method.parameterList.size() - 1))) {
				buf.append(", ");
			}
		}
		return buf.toString();
	}

	/**
	 * <p>
	 * クラス情報取得
	 * 
	 * @return クラス情報
	 */
	private static ClassNode getClassData(int classId, List<ClassNode> classList) {

		for (ClassNode n : classList) {
			if (n.classId == classId) {
				return n;
			}
		}
		return null;
	}

	/**
	 * <p>
	 * クラス情報取得
	 * 
	 * @return クラス情報リスト
	 */
	private static ArrayList<ClassNode> getAllActiveClassData(List<ClassNode> classList) {

		ArrayList<ClassNode> list = new ArrayList<ClassNode>();
		for (ClassNode n : classList) {
			if (!n.delFlag && !n.isBuiltInClass && !(n.p == null)) {
				list.add(n);
			}
		}
		return list;
	}

	/**
	 * 型IDから型名に変換する
	 * @param typeId
	 * @return
	 */
	private static String getTypeNameByTypeId(String typeId, List<ClassNode> classList) {
		if (Util.hasSymbol(typeId)) {
			String tmpTypeName = typeId.replace(SPACE, "");
			List<String> list = Util.getTypeList(tmpTypeName);
			for (String id : list) {
				tmpTypeName = tmpTypeName.replace(id,getClassData(Integer.valueOf(id), classList).className);
			}
			return tmpTypeName;
		} else {
			return getClassData(Integer.valueOf(typeId), classList).className;
		}
	}

	/**
	 * プレースホルダーを置換する
	 * 値がnull or 空文字の場合はプレースホルダーの次の半角スペースを除去する
	 * @param phrase 置換対象の文字列
	 * @param placeHolder　置換するプレースポルダー
	 * @param value　置換する値
	 * @return 置換後の文字列
	 */
	public static String replace(String phrase, String placeHolder, String value) {
		if (value == null || value.length() == 0) {
			return phrase.replaceAll(placeHolder + " ", "");
		} else {
			return phrase.replaceAll(placeHolder, value);
		}
	}

	/**
	 * テンプレート内のプレースホルダーを置換する
	 * @param template テンプレート
	 * @param replaceMap　置換するプレースポルダーをkey、置換する値をvalueとしたマップオブジェクト
	 * @return replaceMapで置換された文字列
	 */
	public static String replace(String template, Map<String, String> replaceMap) {
		String replaced = replaceNewLineCharacter(template);
		for (String placeHolder : replaceMap.keySet()) {
			if (replaceMap.get(placeHolder) != null && !"".equals(replaceMap.get(placeHolder))) {
				replaced = replaced.replace(placeHolder, replaceMap.get(placeHolder));
			} else {
				// 空の時はプレースホルダー後の改行コードも削除
				replaced = replaced.replace(placeHolder + System.getProperty("line.separator"), "");
				replaced = replaced.replace(placeHolder + " ", "");
			}
		}
		return replaced;
	}
	
	/**
	 * 改行コードを環境にあわせて変更する
	 * ※変換元のファイルはCRLFとすること
	 * @param line
	 * @return
	 */
	public static String replaceNewLineCharacter(String line) {
		return line.replace("\r\n", System.getProperty("line.separator"));
	}
	
}
