package uk.co.zonetora.fj.passes;

import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;

import uk.co.zonetora.fj.ast.analysis.DepthFirstAdapter;
import uk.co.zonetora.fj.ast.node.AClassDecl;
import uk.co.zonetora.fj.ast.node.ACommaField;
import uk.co.zonetora.fj.ast.node.ACommaParamArgList;
import uk.co.zonetora.fj.ast.node.AConstructorDecl;
import uk.co.zonetora.fj.ast.node.AEmptyFieldList;
import uk.co.zonetora.fj.ast.node.AEmptyParamDeclList;
import uk.co.zonetora.fj.ast.node.AFieldDecl;
import uk.co.zonetora.fj.ast.node.AFieldListFieldList;
import uk.co.zonetora.fj.ast.node.AFieldName;
import uk.co.zonetora.fj.ast.node.AIdentParamArg;
import uk.co.zonetora.fj.ast.node.AMethodDecl;
import uk.co.zonetora.fj.ast.node.AParamArgsParamDeclList;
import uk.co.zonetora.fj.ast.node.AThisFieldAssig;
import uk.co.zonetora.fj.ast.node.AThisParamArg;
import uk.co.zonetora.fj.ast.node.PFieldList;
import uk.co.zonetora.fj.ast.node.PParamArg;
import uk.co.zonetora.fj.ast.node.PParamDeclList;
import uk.co.zonetora.fj.ast.node.PTerm;
import uk.co.zonetora.fj.model.ArgumentName;
import uk.co.zonetora.fj.model.ClassDecl;
import uk.co.zonetora.fj.model.ClassName;
import uk.co.zonetora.fj.model.Constructor;
import uk.co.zonetora.fj.model.FieldName;
import uk.co.zonetora.fj.model.Method;
import uk.co.zonetora.fj.model.MethodName;
import uk.co.zonetora.fj.model.Term;
import uk.co.zonetora.fj.util.Tuple;

public class BuildModel extends DepthFirstAdapter {

	private final List<ClassDecl> classes;
	
	private final List<FJException> exceptions;
	
	public BuildModel() {
		this.classes = new ArrayList<ClassDecl>();
		this.exceptions = new ArrayList<FJException>();
	}
	
	@Override
	public void caseAClassDecl(AClassDecl node) {
		ClassName name = new ClassName(node.getClassname().getText().trim());
		ClassName extnds = new ClassName(node.getExtendsname().getText().trim());
	
		Constructor ctor = buildConstructor((AConstructorDecl)node.getConstructorDecl());
		ClassDecl classDecl = new ClassDecl(name, extnds, ctor);
	
		for( Object o : node.getFieldDecl() ) {
			AFieldDecl afd = (AFieldDecl) o;
			Tuple<ClassName, FieldName> cnfn = getFieldDecl(afd);
			classDecl.addField(cnfn.getX() , cnfn.getY());
		}

		for(Object o : node.getMethodDecl()) {
			AMethodDecl amd = (AMethodDecl) o;
			Method m = getMethodDecl(amd);
			classDecl.addMethod(m);
		}
		
		this.classes.add(classDecl);
		
	}
	
	
	private Method getMethodDecl(AMethodDecl amd) {
		ClassName returnType = new ClassName(amd.getClassname().getText().trim());
		MethodName methodName = new MethodName(amd.getMethodname().getText().trim());
		List<Tuple<ClassName, ArgumentName>> parameters = getFormalParameters(amd.getParamDeclList());
		
		Term code = getTerm(amd.getTerm());
		
		Method m = new Method(returnType, methodName, code);
		
		for(Tuple<ClassName, ArgumentName> argDecl : parameters) {
			m.addArgument(argDecl.getX(), argDecl.getY());
		}
		
		return m;
	}

	private Term getTerm(PTerm term) {
		return new TermBuilder().buildTerm(term);
	}

	private List<Tuple<ClassName, ArgumentName>> getFormalParameters(
			PParamDeclList paramDeclList) {
		
		if(paramDeclList instanceof AEmptyParamDeclList) {
			return new ArrayList<Tuple<ClassName,ArgumentName>>();
		}
		AParamArgsParamDeclList paramArgs = (AParamArgsParamDeclList) paramDeclList;
		
		Tuple<ClassName, ArgumentName> firstArg = getFormalParameter(paramArgs.getParamArg());
		
		List<Tuple<ClassName, ArgumentName>> otherArgs = getCommaFormalParameters(paramArgs.getCommaParamArgList());
		
		List<Tuple<ClassName, ArgumentName>> ret = new ArrayList<Tuple<ClassName,ArgumentName>>();
		ret.add(firstArg);
		ret.addAll(otherArgs);
		return ret;
	} 

	@SuppressWarnings("unchecked")
	private List<Tuple<ClassName, ArgumentName>> getCommaFormalParameters(
			LinkedList commaParamArgList) {
		List<Tuple<ClassName, ArgumentName>> ret = new ArrayList<Tuple<ClassName,ArgumentName>>();
		
		for(Object o : commaParamArgList) {
			ACommaParamArgList paramArg = (ACommaParamArgList) o;
			ret.add(getFormalParameter(paramArg.getParamArg()));
		}
		
		return ret;
	}

	private Tuple<ClassName, ArgumentName> getFormalParameter(PParamArg paramArg) {
		if(paramArg instanceof AIdentParamArg) {
			AIdentParamArg arg = (AIdentParamArg) paramArg;
			ClassName tipe = new ClassName(arg.getClassname().getText().trim());
			ArgumentName argName = new ArgumentName(arg.getParam().getText().trim());
			return new Tuple<ClassName, ArgumentName>(tipe, argName);
		} else if (paramArg instanceof AThisParamArg) {
			AThisParamArg arg = (AThisParamArg) paramArg;
			ClassName tipe = new ClassName(arg.getIdentifier().getText().trim());
			ArgumentName argName = Method.getThisArgName();
			return new Tuple<ClassName, ArgumentName>(tipe, argName);
		} else {
			throw new RuntimeException("Precondition failure!");
		}

	}

	private Tuple<ClassName, FieldName> getFieldDecl(AFieldDecl afd) {
		ClassName tipe = new ClassName(afd.getClassname().getText().trim());
		FieldName fieldName = new FieldName(afd.getFieldname().getText().trim());
		return new Tuple<ClassName, FieldName>(tipe, fieldName);
	}

	private Constructor buildConstructor(AConstructorDecl constructorDecl) {
		ClassName returnType = new ClassName(constructorDecl.getIdentifier().getText().trim());
		Constructor constructor = new Constructor(returnType);

		List<Tuple<ClassName,ArgumentName>> parameters = getFormalParameters(constructorDecl.getParamDeclList());
		for(Tuple<ClassName, ArgumentName> param : parameters) {
			constructor.addArgument(param.getX(), new FieldName(param.getY().getArgName()));			
		}

		
		List<FieldName> superFields = getSuperFieldNames(constructorDecl.getFieldList());
		for(FieldName f : superFields) {
			constructor.addSuperField(f);			
		}

		for(Object o : constructorDecl.getThisFieldAssig()) {
			AThisFieldAssig atfa = (AThisFieldAssig) o;
			FieldName f;
			try {
				f = getThisFieldAssig(atfa);
			} catch (FJException e) {
				System.err.println(e.getMessage());
				addException(e);
				continue;
			}
			constructor.addLocalField(f);
		}
		return constructor;
		
	}

	private void addException(FJException e) {
		this.exceptions.add(e);
	}

	private FieldName getThisFieldAssig(AThisFieldAssig atfa) throws FJException {
		FieldName left = new FieldName(atfa.getLeftField().getText().trim());
		FieldName right = new FieldName(atfa.getRightField().getText().trim());
		
		if(!left.equals(right)) {
			throw new FJException("Constructor with field assignment that is not a mirror in: this." + (left.getFieldName()) + " = " + (right.getFieldName()));
		}
		
		return left;
	}

	private List<FieldName> getSuperFieldNames(PFieldList fieldList) {
		if (fieldList instanceof AEmptyFieldList ) {
			return new ArrayList<FieldName>();
		}
		
		AFieldListFieldList aFieldList = (AFieldListFieldList) fieldList;
		
		FieldName first = getFieldName((AFieldName) aFieldList.getFieldName());
		
		List<FieldName> ret = new ArrayList<FieldName>();
		ret.add(first);
		
		for(Object o : aFieldList.getCommaField()) {
			ACommaField acf = (ACommaField) o;
			ret.add(getFieldName((AFieldName) acf.getFieldName())); 
		}
		
		return ret;
	}

	private FieldName getFieldName(AFieldName fieldName) {
		return new FieldName(fieldName.getIdentifier().getText().trim());
	}

	public List<ClassDecl> getClasses() {
		return this.classes;
	}
}
