/*
 * Copyright (c) 2008-2011 Hasso Plattner Institute
 *
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.

 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

module('lively.AST.Interpreter').requires('lively.AST.generated.Nodes').toRun(function() {

Object.subclass('lively.AST.Interpreter.Frame',
'initialization', {
	initialize: function(mapping, pc, func) {
		this.mapping = mapping || {};
		this.returnTriggered = false;
		this.breakTriggered = false;
		this.continueTriggered = false;
		this.findSetterMode = false;
		this.pc = pc; // "program counter", actually an index into a linearized AST
		this.func = func;
		this.thisObj = undefined;
	},
	newScope: function(mapping) {
		var newFrame = new this.constructor(mapping);
		newFrame.setContainingScope(this);
		return newFrame;
	},
	addCallingFrame: function(mapping, pc, func) {
		var newFrame = new this.constructor(mapping, pc, func);
		this.setContainingScope(newFrame);
		return newFrame;
	},
	copy: function() {
		var copy = new this.constructor(Object.extend({}, this.mapping), this.pc, this.func);
		copy.returnTriggered = this.returnTriggered;
		copy.breakTriggered = this.breakTriggered;
		copy.continueTriggered = this.continueTriggered;
		copy.newTriggered = this.newTriggered;
		copy.thisObj = this.thisObj; // FIXME copy?
		var parentFrame = this.getContainingScope();
		if (parentFrame) copy.setContainingScope(parentFrame.copy());
		return copy;
	},
},
'accessing', {
	setContainingScope: function(frame) {
		return this.containingScope = frame;
	},
	getContainingScope: function() {
		return this.containingScope
	},
	setThis: function(thisObj) {
		// return this.addToMapping('this', thisObj);
		return this.thisObj = thisObj;
	},
	getThis: function() {
		// return this.lookup('this')
		return this.thisObj !== undefined ?
			this.thisObj :
			(this.containingScope && this.containingScope.getThis());
	},
	setArguments: function(args) {
		return this.arguments = args;
	},
	getArguments: function(args) {
		return this.arguments;
	},
	getFunc: function() { return this.func },
	setFunc: function(func) { this.funcAst = null; this.func = func },
	getFuncAst: function() {
		if (!this.funcAst) {
			if (!this.func) throw new Error('Frame has no function to create AST!');
			if (!this.funcAst) this.funcAst = this.func.ast();
		}
		return this.funcAst;
	},
	getFuncName: function() {
		if (!this.getFunc()) { return 'frame has no function!' }
		return this.getFunc().name || 'anonymous'
	},


	lookup: function(name) {
		if (name === 'undefined') return undefined;
		if (name === 'null') return null;
		if (name === 'NaN') return NaN;
		if (name === 'arguments') return this.getArguments();
		var val = this.mapping[name];
		if (val !== undefined) return val;
		// lookup in my current function
		var func = this.getFunc();
		val = func && func.hasLivelyClosure && func.livelyClosure.lookup(name);
		if (val !== undefined) return val;
		var containingScope = this.getContainingScope();
		if (containingScope) return containingScope.lookup(name);
		return undefined;
	},
	addToMapping: function(name, value) {
		return this.mapping[name] = value;
	},
	addAllToMapping: function(otherMapping) {
		for (var name in otherMapping) {
			if (!otherMapping.hasOwnProperty(name)) continue;
			this.mapping[name] = otherMapping[name];
		}
	},


	triggerReturn: function() { this.returnTriggered = true },
	triggerBreak: function() { this.breakTriggered = true },
	stopBreak: function() { this.breakTriggered = false },
	triggerContinue: function() { this.continueTriggered = true },
	stopContinue: function() { this.continueTriggered = false },
	triggerNew: function() { this.newTriggered = true },
	stopNew: function() { this.newTriggered = false },

},
'accessing for UI', {
	listItemsForIntrospection: function() {
		var items = [];
		Properties.forEachOwn(this.mapping, function(name, value) {
			items.push({isListItem: true, string: name + ': ' + String(value).truncate(50), value: value})
		});
		return items;
	},
},
'program counter', {
	jumpToNextStatement: function() {
		var nextStmt = this.getFuncAst().nodeForAstIndex(this.pc).nextStatement();
		if (!nextStmt) throw new Error('Cannot jump to next statement, last statement already reached!');
		nextPc = nextStmt.astIndex();
		this.pc = nextPc;
	},
	hasNextStatement: function() {
		return this.getFuncAst().nodeForAstIndex(this.pc).nextStatement() != null;
	},

},
'resuming', {
	isResuming: function() { return this.pc != undefined },


	resumesNow: function() { this.pc = null },
	wantsInterpretation: function(node) {
		if (!this.isResuming()) return true;
		var nodeIdx = node.astIndex();
		if (nodeIdx < this.pc) return false;
		if (nodeIdx === this.pc) this.resumesNow();
		return true;
	},
	resume: function() {
		return this.getFuncAst().resume(this);
	},

},
'printing', {
	highlightedSourceText: function() {
		var source = String(this.getFunc()),
			text = new lively.Text.Text(source);
		if (this.pc) {
			var currentNode = this.getFuncAst().nodeForAstIndex(this.pc);
			text.emphasize({color: Color.red, style: 'bold'}, currentNode.pos[0], currentNode.pos[1]);
		}
		return text;
	},
},
'debugging', {
	toString: function() {
		var mappings = [];
		for (var name in this.mapping)
			if (this.mapping.hasOwnProperty(name))
				mappings.push(name + ': ' + this.mapping[name])
		var mappingString = '{' + mappings.join(',') + '}';
		return 'Frame(' + mappingString + ')';

	},
	printStack: function(str) {
		if (!str) str = '';
		str += this.toString() + '\n';
		if (this.getContainingScope())
			str = this.getContainingScope().printStack(str);
		return str;
	},
});

Object.extend(lively.AST.Interpreter.Frame, {
	create: function(mapping) {
		return new lively.AST.Interpreter.Frame(mapping || {});
	},
	global: function() {
		return this.create(Global.asMapping());
	},
});

lively.lang.Namespace.addMethods(
'lively.AST interpretation', {
	asMapping: function() {
		// create an object whose prototype points to the namespace
		// to lookup objects but doesn't modifies the real namespace
		var propName = 'lively.AST mapping';
		if (this[propName]) return this[propName];
		function MapptingForGlobal() {};
		MapptingForGlobal.prototype = this;
		return this[propName] = new MapptingForGlobal()
	},
})

Object.extend(Global, {
	asMapping: lively.lang.Namespace.prototype.asMapping,
});

Object.extend(Function.prototype, {
	forInterpretation: function(optMapping) {
		var func = this.ast().startInterpretation(optMapping);
		func.prototype = this.prototype;
		return func;
	},
});
lively.AST.Visitor.subclass('lively.AST.InterpreterVisitor',
'interface', {
	run: function(node, optMapping) {
		return this.runWithFrame(node, lively.AST.Interpreter.Frame.create(optMapping));
	},
	runWithFrame: function(node, frame) {
		this.setRootFrame(frame);
		return this.visit(node);
	},

},
'frame management', {
	setRootFrame: function(frame) {
		this.rootFrame = frame;
		this.currentFrame = frame;
	},
},
'additional visiting', {
	visitUntilSetter: function(node) {
		// for expr like "x.a++"
		this.currentFrame.findSetterMode = true;
		var result = this.visit(node);
		this.currentFrame.findSetterMode = false;
		return result;
	},
},
'visiting', {
	visitSequence: function(node) {
		var result, frame = this.currentFrame;
		for (var i = 0; i < node.children.length; i++) {
			result = this.visit(node.children[i]);
			if (frame.returnTriggered || frame.breakTriggered || frame.continueTriggered)
				return result;
		}
		return result;
	},
	visitNumber: function(node) { return node.value },
	visitString: function(node) { return node.value },
	visitCond: function(node) {
		var frame = this.currentFrame,
			condVal = this.visit(node.condExpr);
		return condVal ? this.visit(node.trueExpr) : this.visit(node.falseExpr);
	},
	visitIf: function(node) {
		return this.visitCond(node);
	},
	visitWhile: function(node) {
		var result, frame = this.currentFrame;
		while (this.visit(node.condExpr)) {
			result = this.visit(node.body);
			if (frame.continueTriggered) { frame.stopContinue() };
			if (frame.breakTriggered) { frame.stopBreak(); break };
			if (frame.returnTriggered) { return result };
		}
		return result;
	},
	visitDoWhile: function(node) {
		var frame = this.currentFrame, result;
		do {
			result = this.visit(node.body);
			if (frame.continueTriggered) { frame.stopContinue() };
			if (frame.breakTriggered) { frame.stopBreak(); break };
			if (frame.returnTriggered) { return result };
		} while (this.visit(node.condExpr));
		return result;
	},
	visitFor: function(node) {
		var frame = this.currentFrame, result;
		this.visit(node.init);
		while (this.visit(node.condExpr)) {
			result = this.visit(node.body);
			if (frame.continueTriggered) { frame.stopContinue() };
			if (frame.breakTriggered) { frame.stopBreak(); break };
			if (frame.returnTriggered) { return result };
			this.visit(node.upd);
		}
		return result;
	},
	visitForIn: function(node) {
		var frame = this.currentFrame,
			varDecl = node.name, obj = this.visit(node.obj), result;
		varDecl.val.name = varDecl.name;
		for (var name in obj) {
			varDecl.val.set(name, frame, this);
			result = this.visit(node.body);
			if (frame.continueTriggered) { frame.stopContinue() };
			if (frame.breakTriggered) { frame.stopBreak(); break };
			if (frame.returnTriggered) { return result };
		}
		return result;
	},
	visitSet: function(node) {
		var frame = this.currentFrame;
		return node.left.set(this.visit(node.right), frame, this);
	},
	visitModifyingSet: function(node) {
		var frame = this.currentFrame,
			op = node.name + '=', right = this.visit(node.right),
			oldValue = this.visit(node.left), newValue;
		switch (op) {
			case '+=': newValue = oldValue + right; break;
			case '-=': newValue = oldValue - right; break;
			case '*=': newValue = oldValue * right; break;
			case '/=': newValue = oldValue / right; break;
			case '>>=': newValue = oldValue >>= right; break;
			case '<<=': newValue = oldValue <<= right; break;
			case '>>>=': newValue = oldValue >>> right; break;
			case '&=': newValue = oldValue & right; break;
			case '|=': newValue = oldValue | right; break;
			case '&=': newValue = oldValue & right; break;
			case '^=': newValue = oldValue ^ right; break;
			case '||=': newValue = oldValue || right; break; // FIXME lazy evaluation
			case '&&=': newValue = oldValue && right; break; // FIXME lazy evaluation
			default: throw new Error('Modifying set has unknown operation ' + op);
		}
		return node.left.set(newValue, frame, this);
	},
	visitBinaryOp: function(node) {
		var frame = this.currentFrame;
		switch (node.name) {
			case '||': return this.visit(node.left) || this.visit(node.right);
			case '&&': return this.visit(node.left) && this.visit(node.right);
		}
		var leftVal = this.visit(node.left),
			rightVal = this.visit(node.right);
		switch (node.name) {
			case '+': return leftVal + rightVal;
			case '-': return leftVal - rightVal;
			case '*': return leftVal * rightVal;
			case '/': return leftVal / rightVal;
			case '%': return leftVal % rightVal;
			case '<': return leftVal < rightVal;
			case '<=': return leftVal <= rightVal;
			case '>': return leftVal > rightVal;
			case '>=': return leftVal >= rightVal;
			case '==': return leftVal == rightVal;
			case '===': return leftVal === rightVal;
			case '!=': return leftVal != rightVal;
			case '!==': return leftVal !== rightVal;
			case '&': return leftVal & rightVal;
			case '|': return leftVal | rightVal;
			case '^': return leftVal ^ rightVal;
			case '>>': return leftVal >> rightVal;
			case '<<': return leftVal << rightVal;
			case '>>>': return leftVal >>> rightVal;
			case 'instanceof': return leftVal instanceof (rightVal.isFunction ? rightVal.prototype.constructor : rightVal);
			default: throw new Error('No semantics for binary op ' + node.name)
		}
	},
	visitUnaryOp: function(node) {
		var frame = this.currentFrame,
			val = this.visit(node.expr);
		switch (node.name) {
			case '-': return -val;
			case '!': return !val;
			case '~': return ~val;
			default: throw new Error('No semantics for unary op ' + node.name)
		}
	},
	visitPreOp: function(node) {
		var frame = this.currentFrame,
			setExpr = this.visitUntilSetter(node.expr);
		if (!setExpr.isVariable && !setExpr.isGetSlot)
			throw new Error('Invalid expr in pre op ' + setExpr);
		var value = this.visit(setExpr), newValue;
		switch (node.name) {
			case '++': newValue = value + 1; break;
			case '--': newValue = value - 1; break;
			default: throw new Error('No semantics for pre op ' + node.name)
		}
		setExpr.set(newValue, frame, this);
		return newValue;
	},
	visitPostOp: function(node) {
		var frame = this.currentFrame,
			setExpr = this.visitUntilSetter(node.expr);
		if (!setExpr.isVariable && !setExpr.isGetSlot)
			throw dbgOn(new Error('Invalid expr in post op ' + setExpr));
		var value = this.visit(setExpr), newValue;
		switch (node.name) {
			case '++': newValue = value + 1; break;
			case '--': newValue = value - 1; break;
			default: throw new Error('No semantics for post op ' + node.name)
		}
		setExpr.set(newValue, frame, this);
		return value;
	},
	visitThis: function(node) {
		return this.currentFrame.getThis();
	},
	visitVariable: function(node) {
		return this.currentFrame.findSetterMode ? node : this.currentFrame.lookup(node.name);
	},
	visitGetSlot: function(node) {
		if (this.currentFrame.findSetterMode) return node;
		try {
		var obj = this.visit(node.obj),
			name = this.visit(node.slotName),
			value = obj[name];
		return value;
		} catch(e) { debugger }
	},
	visitBreak: function(node) {
		this.currentFrame.triggerBreak();
	},
	visitContinue: function(node) {
		this.currentFrame.triggerContinue();
	},
	visitArrayLiteral: function(node) {
		var result = new Array(node.elements.length);
		for (var i = 0; i < node.elements.length; i++)
			result[i] = this.visit(node.elements[i]);
		return result;
	},
	visitReturn: function(node) {
		var frame = this.currentFrame,
			val = this.visit(node.expr);
		frame.triggerReturn();
		return val;
	},
	visitWith: function(node) {
		throw new Error('with statement not yet supported');
	},
	visitSend: function(node) {
		var frame = this.currentFrame, newCall = frame.newTriggered;

		if (newCall) frame.stopNew()
		var argValues = node.args.collect(function(ea) { return this.visit(ea) }, this),
			recv = this.visit(node.recv),
			func = recv[node.name],
			caller = lively.AST.FunctionCaller.defaultInstance;

		return caller.activate(frame, newCall, func, node.name, recv, argValues);
	},
	visitCall: function(node) {
		var frame = this.currentFrame, newCall = frame.newTriggered;

		if (newCall) frame.stopNew()

		var argValues = node.args.collect(function(ea) { return this.visit(ea) }, this),
			func = this.visit(node.fn),
			recv = func.isFunction ? func.lexicalScope.getThis() : Global,
			caller = lively.AST.FunctionCaller.defaultInstance;
		
		return caller.activate(frame, newCall, func, func.name, recv, argValues);
	},
	visitNew: function(node) {
		var frame = this.currentFrame;
		frame.triggerNew();
		return this.visit(node.clsExpr);
	},
	visitVarDeclaration: function(node) {
		var frame = this.currentFrame, val = this.visit(node.val);
		frame.addToMapping(node.name, val);
		return val;
	},
	visitThrow: function(node) {
		var frame = this.currentFrame, exceptionObj = this.visit(node.expr);
		throw exceptionObj;
	},
	visitTryCatchFinally: function(node) {
		var frame = this.currentFrame, result;
		try {
			result = this.visit(node.trySeq);
		} catch(e) {
			frame.addToMapping(node.errName, e);
			result = this.visit(node.catchSeq);
		} finally {
			if (node.finallySeq.isVariable && node.finallySeq.name == 'undefined') {
				// do nothing, no finally block
			} else {
				result = this.visit(node.finallySeq);
			}
		}
		return result
	},
	visitFunction: function(node) {
		var frame = this.currentFrame;
		if (node.name) frame.addToMapping(node.name, node)
		if (!node.prototype) node.prototype = {};
		node.lexicalScope = frame;
		return node;
	},
	visitObjectLiteral: function(node) {
		var frame = this.currentFrame, obj = {};
		for (var i = 0; i < node.properties.length; i++) {
			var name = node.properties[i].name,
				prop = this.visit(node.properties[i].property);
			obj[name] = prop;
		}
		return obj;
	},
	visitObjProperty: function(node) {
		throw new Error('?????')
	},
	visitSwitch: function(node) {
		var frame = this.currentFrame,
			val = this.visit(node.expr), caseMatched = false; result;
		for (var i = 0; i < node.cases.length; i++) {
			node.cases[i].prevCaseMatched = caseMatched;
			node.cases[i].switchValue = val;
			result = this.visit(node.cases[i]);
			caseMatched = result !== undefined; // FIXME what when case returns undefined?
			if (frame.breakTriggered) { frame.stopBreak(); break };
		}
		return result;
	},
	visitCase: function(node) {
		return node.prevCaseMatched || node.switchValue == this.visit(node.condExpr) ?
			this.visit(node.thenExpr) : undefined;
	},
	visitDefault: function(node) {
		return node.prevCaseMatched ? undefined : this.visit(node.defaultExpr);
	},
	visitRegex: function(node) {
		return new RegExp(node.exprString, node.flags);
	},

});
lively.AST.InterpreterVisitor.subclass('lively.AST.ResumingInterpreterVisitor',
'visiting', {
	visit: function($super, node) {
		return this.currentFrame.wantsInterpretation(node) ? $super(node) : true;
	},
});
Object.extend(lively.AST, {
	getInterpreter: function() {
		return new lively.AST.ResumingInterpreterVisitor();
	},
});


Object.subclass('lively.AST.FunctionCaller',
'documentation', {
	documentation: 'strategy for invoking functions',
},
'initializiation', {
	initialize: function() {
		this.logEnabled = false;
		this.resetLog();
	},
},
'interpretation', {

	activate: function(interpreter, isNewCall, func, funcName, recv, argValues) {
		// if we send apply to a function (recv) we want to interpret it although apply is a native function
		if (recv && (Object.isFunction(recv) || recv.isFunction) && funcName == 'apply') {
			if (!recv.isFunction) recv = recv.forInterpretation(Global.asMapping())
			func = recv; // The function object is what we want to run
			recv = argValues.shift() // thisObj is first parameter
			argValues = argValues[0] // the second arg are the arguments (as an array)
		}

		var shouldInterpret = Config.deepInterpretation && !this.isNative(func);
		if (shouldInterpret && Object.isFunction(func) && !func.isWrapper)
			func = func.forInterpretation(Global.asMapping());

		// var isSpecial = !this.isSpecial(func, funcName);
		// if (!shouldInterpret && !Object.isFunction(func))
			// func = func.getRealFunction();		

		if (!func || !func.apply) this.lookupError(recv, funcName)

		try {
			if (!this.logEnabled)
				return isNewCall ? this.doNew(interpreter, func, argValues) : func.apply(recv, argValues);

			return this.doLog(function() {
				return isNewCall ? this.doNew(interpreter, func, argValues) : func.apply(recv, argValues);
			}, funcName, recv, argValues, interpreter)
		} catch(e) { debugger; throw e }
	},

	doNew: function(interpreter, func, args) {
		var proto = func.prototype, constructor = function() {};
		constructor.prototype = proto;
		var newObj = new constructor();
		func.apply(newObj, args); // call with new object
		return newObj;
	},

	lookupError: function(recv, slotName) {
		debugger
		var msg = Strings.format('Send error: recevier %s does not understand %s', recv, slotName)
		throw new Error(msg)
	},

	isNative: function(func) {
		if (func.isFunction) return false; // ast node
		if (!this._nativeFuncRegex)
			this._nativeFuncRegex = /\{\s+\[native\scode\]\s+\}$/
		return this._nativeFuncRegex.test(func.toString())
	},
	isSpecial: function(func, funcName) {
		var realName = func.isFunction ? func.getRealFunction().name : func.name;
		return realName == 'wrapped'
	},

},
'logging', {
	log: function(msg) {
		this.logMsgs.push(this.logIndent + msg);
	},
	doLog: function(callback, funcName, recv, args, interpreter) {
		function shorten(obj) { return String(obj).replace(/\n/g, '').truncate(20) }
		try {
			// this.log( shorten(recv) + '>>' + funcName + '(' + args.collect(function(ea) { return shorten(ea) }).join(',') + ')')
			this.log(funcName)
		} catch(e) {
			this.log('Cannot log ' + funcName + ' because ' + e)
		}
		this.increaseLogIndent()
		var result = callback.call(this)
		this.decreaseLogIndent()
		return result
	},
	loggingOnOrOff: function(state) { this.logEnabled = state },


	increaseLogIndent: function(msg) { this.logIndent += '  ' },
	decreaseLogIndent: function(msg) { this.logIndent = this.logIndent.slice(2) },
	resetLog: function() { this.logIndent = '', this.logMsgs = [] },
	getLog: function() { return this.logMsgs.join('\n') },
});
Object.extend(lively.AST.FunctionCaller, {
	defaultInstance: new lively.AST.FunctionCaller(),
});
lively.AST.Node.addMethods(
'interpretation', {
	startInterpretation: function(optMapping) {
		return lively.AST.getInterpreter().run(this, optMapping);
	},
});
lively.AST.Variable.addMethods(
'interpretation', {
	set: function(value, frame, interpreter) {
		return frame.addToMapping(this.name, value);
	},
});
lively.AST.GetSlot.addMethods(
'interpretation', {
	set: function(value, frame, interpreter) {
		var obj = interpreter.visit(this.obj),
			name = interpreter.visit(this.slotName);
		return obj[name] = value;
	},
});
lively.AST.Function.addMethods(
'accessing', {
	getRealFunction: function() {
		if (this.realFunction) return this.realFunction;
		return this.realFunction = this.eval();
	},
},
'interpretation', {
	basicApply: function(frame) {
		return lively.AST.getInterpreter().runWithFrame(this.body, frame);
	},
	apply: function(thisObj, argValues) {
		var mapping = {};
		for (var i = 0; i < this.args.length; i++)
			mapping[this.args[i]] = argValues[i];
		var newFrame = this.lexicalScope.newScope(mapping);
		newFrame.setThis(thisObj);
		newFrame.setArguments(argValues);
		return this.basicApply(newFrame);
	},
	call: function(/*args*/) {
		var args = $A(arguments), thisObj = args.shift();
		return this.apply(thisObj, args);
	},
},
'continued interpretation', {
	resume: function(frame) {
		return this.startInterpretation().basicApply(frame);
	},
	resumeAt: function(pc, optMapping) {
		var frame = lively.AST.Interpreter.Frame.create(optMapping);
		frame.pc = pc;
		return this.resume(frame);
	},
});

}) // end of module