package jp.igapyon.jcfa;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;

import jp.igapyon.jcfa.util.JcfaUtil;
import jp.igapyon.jcfa.vo.JcfaClass;
import jp.igapyon.jcfa.vo.JcfaCode;
import jp.igapyon.jcfa.vo.JcfaField;
import jp.igapyon.jcfa.vo.JcfaMethod;
import jp.igapyon.jcfa.vo.JcfaUnit;
import jp.igapyon.jcfa.vo.item.JcfaItemLocalVariable;
import jp.igapyon.jcfa.vo.item.JcfaItemReference;

import org.apache.bcel.Constants;
import org.apache.bcel.classfile.ClassFormatException;
import org.apache.bcel.classfile.ClassParser;
import org.apache.bcel.classfile.Code;
import org.apache.bcel.classfile.ConstantValue;
import org.apache.bcel.classfile.Field;
import org.apache.bcel.classfile.JavaClass;
import org.apache.bcel.classfile.Method;
import org.apache.bcel.generic.Type;

public class JcfaParser {
	protected JcfaUnit jcfaUnit = new JcfaUnit();

	public JcfaUnit parseUnit(final File inputFile, final File outputDir) {
		try {
			final JavaClass jc = new ClassParser(inputFile.getCanonicalPath())
					.parse();
			final JcfaClass jcfaClass = new JcfaClass();
			jcfaUnit.getClassList().add(jcfaClass);

			jcfaClass.setName(jc.getClassName());
			jcfaClass.setExtendsName(jc.getSuperclassName());

			jcfaClass.getComment().getCommentList()
					.add("TODO import func. is missing.");
			jcfaClass.getComment().setJavaDoc(true);

			final String[] split = jc.getClassName().split("\\.");
			File actualyTargetDir = outputDir;
			if (split.length > 1) {
				for (int index = 0; index < split.length - 1; index++) {
					actualyTargetDir = new File(actualyTargetDir, split[index]);
					actualyTargetDir.mkdirs();
				}
			}

			parseFields(jc, jcfaClass);
			parseMethods(jc, jcfaClass);

			jcfaUnit.setTargetFile(new File(actualyTargetDir,
					split[split.length - 1] + ".jcfa"));
		} catch (ClassFormatException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		}
		return jcfaUnit;
	}

	private void parseFields(final JavaClass jc, final JcfaClass jcfaClass) {
		final org.apache.bcel.classfile.Field[] fields = jc.getFields();
		for (int indexField = 0; indexField < fields.length; indexField++) {
			final Field field = fields[indexField];
			parseField(jc, field, jcfaClass);
		}
	}

	private void parseField(final JavaClass jc, final Field field,
			final JcfaClass jcfaClass) {
		final JcfaField jcfaField = new JcfaField();
		jcfaField.setName(field.getName());
		jcfaClass.getFieldList().add(jcfaField);

		jcfaField.getComment().setJavaDoc(true);

		// TODO type should be more collect.
		jcfaField.setType(field.getType().toString());

		String access = "";
		access += field.isPublic() ? "public " : "";
		access += field.isProtected() ? "protected " : "";
		access += field.isPrivate() ? "private " : "";
		access += field.isAbstract() ? "abstract " : "";
		access += field.isStatic() ? "static " : "";
		access += field.isVolatile() ? "volatile " : "";
		access += field.isFinal() ? "final " : "";
		jcfaField.setAccess(access);

		final ConstantValue cv = field.getConstantValue();
		if (cv != null) {
			jcfaField.setConstantValue("\""
					+ jc.getConstantPool().getConstantString(
							cv.getConstantValueIndex(),
							Constants.CONSTANT_String) + "\"");

			jcfaField
					.getComment()
					.getCommentList()
					.add("FIXME other type support is missing. <br />Now only String.");
		}
	}

	private void parseMethods(final JavaClass jc, final JcfaClass jcfaClass)
			throws IOException {
		final org.apache.bcel.classfile.Method[] methods = jc.getMethods();
		for (int indexMethod = 0; indexMethod < methods.length; indexMethod++) {
			final Method method = methods[indexMethod];
			parseMethod(jc, method, jcfaClass);

		}
	}

	/**
	 * Analyze method.
	 * 
	 * @param jc
	 * @param method
	 * @param jcfaClass
	 * @throws IOException
	 */
	private void parseMethod(final JavaClass jc, final Method method,
			final JcfaClass jcfaClass) throws IOException {
		final JcfaMethod jcfaMethod = new JcfaMethod();
		jcfaClass.getMethodList().add(jcfaMethod);

		jcfaMethod.setName(method.getName());
		jcfaMethod.getComment().setJavaDoc(true);

		{
			// push this to local variable.
			final JcfaItemLocalVariable jcfaLocalVariable = new JcfaItemLocalVariable();
			jcfaMethod.getFrame().setLocalVariable(0, jcfaLocalVariable);
			jcfaLocalVariable.setName("this");
			final JcfaItemReference itemRef = new JcfaItemReference();
			jcfaLocalVariable.setVal(itemRef);
			itemRef.setObject(jcfaClass.getName());
		}

		if (jcfaMethod.getName().equals("<init>")) {
			jcfaMethod.getComment().getCommentList().add("Constructor.");
		} else {
			jcfaMethod.getComment().getCommentList().add("Method.");
		}

		for (Type type : method.getArgumentTypes()) {
			jcfaMethod.getComment().getCommentList().add(type.toString());
			jcfaMethod.getArugumentTypeList().add(type.toString());
		}
		jcfaMethod.setType(method.getReturnType().toString());

		final Code code = method.getCode();
		if (code == null) {
			return;
		}

		final byte[] codes = code.getCode();
		for (int pc = 0; pc < codes.length; pc++) {
			final int operands = parseCodes(jc, method, jcfaClass, jcfaMethod,
					pc, codes);
			if (operands < 0) {
				break;
			}

			pc += operands;
		}
	}

	private int parseCodes(final JavaClass jc, final Method method,
			final JcfaClass jcfaClass, final JcfaMethod jcfaMethod,
			final int pc, final byte[] codes) throws IOException {
		final JcfaCode jcfaCode = new JcfaCode();
		jcfaMethod.getCodeList().add(jcfaCode);
		jcfaCode.setJavaClass(jc);

		jcfaCode.setOpcode(JcfaUtil.byte2UnsignedByte(codes[pc]));
		jcfaCode.getComment()
				.getCommentList()
				.add("" + pc + ": "
						+ Constants.OPCODE_NAMES[jcfaCode.getOpcode()]);

		short operands = Constants.NO_OF_OPERANDS[jcfaCode.getOpcode()];
		if (operands == Constants.UNPREDICTABLE) {
			switch (jcfaCode.getOpcode()) {
			case Constants.TABLESWITCH: {
				// pad
				// pad
				// pad
				// pad
				// defaultbyte1
				// defaultbyte2
				// defaultbyte3
				// defaultbyte4
				// lowbyte1
				// lowbyte2
				// lowbyte3
				// lowbyte4
				// highbyte1
				// highbyte2
				// highbyte3
				// highbyte4
				// jump offsets here...

				jcfaCode.getComment().getCommentList()
						.add("  TODO no support opecode and operands");
				int result = JcfaUtil.byte2Int(codes[pc + 1], codes[pc + 2],
						codes[pc + 3], codes[pc + 4]);
				jcfaCode.getComment().getCommentList()
						.add("  MEMO skipping operands: " + result);
				System.out.println("skipBytes: " + result);
				int lookupOp = pc + 5;
				short diff = JcfaUtil.byte2UnsignedByte(codes[lookupOp++]);
				System.out.println("diff: " + diff);

				int loopCount = JcfaUtil
						.byte2Int(codes[lookupOp++], codes[lookupOp++],
								codes[lookupOp++], codes[lookupOp++]);
				System.out.println("count: " + loopCount);

				return Constants.UNPREDICTABLE;
			}
			case Constants.LOOKUPSWITCH: {
				// pad
				// pad
				// pad
				// pad
				// defaultbyte1
				// defaultbyte2
				// defaultbyte3
				// defaultbyte4
				// npairs1
				// npairs2
				// npairs3
				// npairs4
				// match-offset pairs here...

				int result = JcfaUtil.byte2Int(codes[pc + 1], codes[pc + 2],
						codes[pc + 3], codes[pc + 4]);
				jcfaCode.getComment().getCommentList()
						.add("  TODO skipping operands: why?: " + result);
				System.out.println("skipBytes: " + result);

				int lookupOp = pc + 5;

				short diff = JcfaUtil.byte2UnsignedByte(codes[lookupOp++]);

				int loopCount = JcfaUtil
						.byte2Int(codes[lookupOp++], codes[lookupOp++],
								codes[lookupOp++], codes[lookupOp++]);
				System.out.println("count: " + loopCount);

				// switch table on loopCount.

				short diff2 = JcfaUtil.byte2UnsignedByte(codes[lookupOp++]);
				jcfaCode.getComment().getCommentList()
						.add("  TODO skipping bytes: " + (diff2));

				operands += (lookupOp - pc);
				return Constants.UNPREDICTABLE;
				// break;
			}
			case Constants.WIDE: {
				jcfaCode.getComment().getCommentList()
						.add("  TODO no support opecode and operands");
				return Constants.UNPREDICTABLE;
			}
			}
		}

		{
			final ByteArrayOutputStream outStream = new ByteArrayOutputStream();
			outStream.write(codes, pc, operands + 1);
			outStream.flush();
			jcfaCode.setCodes(outStream.toByteArray());
		}
		return operands;
	}
}
