/*
 * Copyright 2013 Yuichiro Moriguchi
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package net.morilib.awk.statement;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Stack;

import net.morilib.awk.AwkExitException;
import net.morilib.awk.AwkNextException;
import net.morilib.awk.expr.AwkExpression;
import net.morilib.awk.io.AwkFiles;
import net.morilib.awk.namespace.AwkNamespace;
import net.morilib.awk.value.AwkUndefined;
import net.morilib.awk.value.AwkValue;

public class AwkExpressionMachine extends AwkExpression {

	public static class Builder {

		private Map<Object, Integer> labels;
		private Map<Object, List<Code>> unresolved;
		private List<Code> codes;

		public Builder() {
			codes = new ArrayList<Code>();
			labels = new HashMap<Object, Integer>();
			unresolved = new HashMap<Object, List<Code>>();
		}

		public void add(AwkExpression e) {
			codes.add(new Code(Mnemonic.COMP, e, codes.size()));
		}

		Code add(Mnemonic m, Object label) {
			List<Code> c;
			Code d;

			if(label == null) {
				codes.add(d = new Code(m, null, JMP_END));
			} else if(labels.containsKey(label)) {
				codes.add(d = new Code(m, null, labels.get(label)));
			} else {
				codes.add(d = new Code(m, null, UNRESOLVED));
				if((c = unresolved.get(label)) == null) {
					c = new ArrayList<Code>();
					unresolved.put(label, c);
				}
				c.add(d);
			}
			return d;
		}

		public void addReturn() {
			codes.add(new Code(Mnemonic.RETURN, null, 0));
		}

		public void addNext() {
			codes.add(new Code(Mnemonic.NEXT, null, 0));
		}

		public void addExit() {
			codes.add(new Code(Mnemonic.EXIT, null, 0));
		}

		public void addJmp(Object l) {
			add(Mnemonic.JMP, l);
		}

		public void addJmpZ(Object l) {
			add(Mnemonic.JMP_Z, l);
		}

		public void addJmpNZ(Object l) {
			add(Mnemonic.JMP_NZ, l);
		}

		public void addPushin() {
			codes.add(new Code(Mnemonic.PUSHIN, null, -1));
		}

		public void addGetin(String s, Object l) {
			add(Mnemonic.GETIN, l).name = s;
		}

		public void addLabel(Object label) {
			List<Code> c;
			int addr = codes.size();

			if((c = unresolved.get(label)) != null) {
				for(Code d : c)  d.next = addr;
			}
			labels.put(label, addr);
		}

		public AwkExpression get() {
			return new AwkExpressionMachine(
					codes.toArray(new Code[0]));
		}

	}

	static enum Mnemonic {
		COMP, JMP, JMP_Z, JMP_NZ, PUSHIN, GETIN, RETURN, NEXT, EXIT
	}

	static final int UNRESOLVED = -1;
	static final int JMP_END = Integer.MAX_VALUE;

	static class Code {

		Mnemonic mnemonic;
		AwkExpression expr;
		String name;
		int next;

		Code(Mnemonic m, AwkExpression e, String s, int n) {
			mnemonic = m;
			expr = e;
			next = n;
			name = s;
		}

		Code(Mnemonic m, AwkExpression e, int n) {
			this(m, e, null, n);
		}

	}

	private Code[] codes;

	private AwkExpressionMachine(Code... codes) {
		this.codes = codes;
	}

	@Override
	public AwkValue eval(AwkNamespace ns, AwkFiles f) {
		Stack<Iterator<AwkValue>> st = new Stack<Iterator<AwkValue>>();
		AwkValue e = AwkUndefined.UNDEF;
		Code c;
		int pc = 0;

		while(pc < codes.length) {
			if(pc < 0)  throw new RuntimeException();
			c = codes[pc];
			switch(c.mnemonic) {
			case COMP:
				e = c.expr.eval(ns, f);  pc++;  break;
			case JMP:
				pc = c.next;  break;
			case JMP_Z:
				pc = e.isZeroValue() ? c.next : pc + 1;  break;
			case JMP_NZ:
				pc = e.isZeroValue() ? pc + 1 : c.next;  break;
			case PUSHIN:
				st.push(e.values().iterator());  pc++;  break;
			case GETIN:
				if(st.peek().hasNext()) {
					e = st.peek().next();
					ns.assign(c.name, e);
					pc++;
				} else {
					st.pop();  pc = c.next;
				}
				break;
			case RETURN:  return e;
			case NEXT:  throw new AwkNextException();
			case EXIT:  throw new AwkExitException(e);
			}
		}
		return e;
	}

}
