package cmaj7.tree;

import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import cmaj7.vm.Instruction;
import cmaj7.vm.Instruction.Instr;

public class GenCode implements Visitor<Void> {
	static int lbl = 0;
	public static String label() {
		lbl++;
		return "label"+lbl;
	}
	public List<Instruction> result=new LinkedList<>();
	Map<String,Integer> env = new HashMap<>();
	int offset = 0;
	public List<String> constructors = new LinkedList<>();
	Map<String,Integer> constructorNumbers= new HashMap<>();
	
	
	@Override
	public Void visit(BinOpExp node) {
		node.lhs.welcome(this);
		node.rhs.welcome(this);
		result.add(node.op.getInstruction());
		offset--;
		return null;
	}

	@Override
	public Void visit(IntLiteral node) {
		result.add(new Instruction(Instruction.Instr.PUSHINT,node.n));
		offset++;
		return null;
	}

	@Override
	public Void visit(Variable node) {
		result.add(new Instruction(Instr.PUSH,env.get(node.name)+offset));
		offset++;
		return null;
	}

	@Override
	public Void visit(IfExpr ifExpr) {
		ifExpr.cond.welcome(this);
		int oldSize = result.size();
		String jumpLabel1 = label();
		Instruction theJump 
		 = new Instruction(Instr.JMPIFNONZERO, 0);
		theJump.label=jumpLabel1;
		offset--;
		int oldOffset = offset;
		
		result.add(theJump);
		ifExpr.alt2.welcome(this);
		theJump.arg1 = result.size()-oldSize+1;
		
		offset = oldOffset;
		String jumpLabel2 = label();
		Instruction theSecondJump = new Instruction(Instr.JUMP, 0);
		theSecondJump.label=jumpLabel2;
		result.add(theSecondJump);
		result.add(new Instruction(jumpLabel1));
		int secondOldSize = result.size();
		ifExpr.alt1.welcome(this);
		result.add(new Instruction(jumpLabel2));
		theSecondJump.arg1=result.size()-secondOldSize+1;
		
		return null;
	}

	@Override
	public Void visit(Program program) {
		{
			int i=0;
			for (ConstrDef constr:program.constrs){
				constr.welcome(this);
				constructorNumbers.put(constr.name, i);
				i++;
			}
		}		
		
		for (FunDef fun:program.funs){
			fun.welcome(this);
		}
		
		program.body.welcome(this);
		result.add(Instruction.HALTI);
		
		
		Map<String,Integer> funAdr= new HashMap<>();
		
		
		for (FunDef fun:program.funs){
			funAdr.put(fun.name, result.size());
			result.addAll(fun.code);
		}
		for (Instruction instr:result){
			if (instr.instr.equals(Instr.CALL)){
				instr.arg1 = funAdr.get(instr.label);
			}
		}
		
		return null;
	}

	@Override
	public Void visit(FunDef fun) {
		GenCode gc = new GenCode();
		gc.constructors=constructors;
		gc.constructorNumbers=constructorNumbers;
		int i = fun.params.size()-1;
		for(String param: fun.params){
			gc.env.put(param, i+1);
			i--;
		}
		gc.result.add(new Instruction(fun.name));
		fun.body.welcome(gc);
		gc.result.add(Instruction.SWAPI);
		gc.result.add(Instruction.RETURNI);
		fun.code = gc.result;
		
		return null;
	}

	@Override
	public Void visit(FunCall funCall) {
		if (this.constructors.contains(funCall.name)) {
			for (Node arg : funCall.args) {
				arg.welcome(this);
			}
			result.add(new Instruction(Instr.PUSHINT, constructorNumbers.get(funCall.name)));
			
			result.add(new Instruction(Instr.PACK, funCall.args.size()));
			offset++;

		} else {
			for (Node arg : funCall.args) {
				arg.welcome(this);
			}

			Instruction pushPC = new Instruction(Instr.PUSHPC);
			result.add(pushPC);
			offset++;
			int pc = result.size();

			Instruction gto = new Instruction(Instr.CALL);
			gto.label = funCall.name;
			result.add(gto);
			
			pushPC.arg1 = result.size() - pc;
			for (Node arg : funCall.args) {
				result.add(Instruction.SWAPI);
				result.add(Instruction.POPI);
			}

		}
		offset=offset-funCall.args.size();

		return null;
	}

	@Override
	public Void visit(ConstrDef constrDef) {
		constructors.add(constrDef.name);
		return null;
	}

	@Override
	public Void visit(CaseExpr caseExpr) {
		caseExpr.cond.welcome(this);
		result.add(Instruction.UNPACKI);
		
		List<Instruction> jumps=new LinkedList<>();
		int oldOffset = offset;
		for (SingleCase sing:caseExpr.cases){						
			//offset-neutrale Folge
			result.add(new Instruction(Instr.PUSH,0));
			result.add(new Instruction(Instr.PUSHINT,constructorNumbers.get(sing.name)));
			result.add(new Instruction(Instr.SUB));
			String jumpLabel1 = label();
			Instruction jump = new Instruction(Instr.JMPIFNONZERO,0);
			jump.label=jumpLabel1;
			int oldSize = result.size();

			result.add(jump);
			result.add(new Instruction(Instr.POP,0));
			offset--;
			Map<String,Integer> oldEnv = new HashMap<>();
			oldEnv.putAll(env);

			int i = sing.vars.size();
			//insgesamt jetzt also die Argumente des ausgepackten Objekts
			offset=offset+sing.vars.size();
			for (String var:sing.vars){
				i--;
				env.put(var, i-offset);
			}
			
			sing.welcome(this);
			offset=offset-sing.vars.size();
			
			Instruction jmp =new Instruction(Instr.JUMP,result.size()); 
			result.add(jmp);
			jumps.add(jmp);
						
			result.add(new Instruction(jumpLabel1));
			jump.arg1=result.size()-oldSize;
			env=oldEnv;
			offset = oldOffset;
		}
		String endLabel = label();
		result.add(new Instruction(endLabel));

		for (Instruction jmp:jumps){
			jmp.arg1=result.size()-jmp.arg1;
			jmp.label = endLabel;
		}
		offset++;
		return null;
	}	

	@Override
	public Void visit(SingleCase sing) {
		Set<String> freeVars = sing.welcome(new CollectFreeVars());
		for (Entry<String, Integer> p :env.entrySet()){
			if (!freeVars.contains(p.getKey())){
				final Instruction delInstr = new Instruction(Instruction.Instr.DEL, p.getValue()+offset);
				if (!sing.vars.contains(p.getKey())) result.add(delInstr);
				delInstr.label= p.getKey();
			}
		}
		sing.expr.welcome(this);
		for (String var:sing.vars){
			result.add(Instruction.SWAPI);
			result.add(Instruction.POPI);
		}

		return null;
	}

}
