// Copyright (C) 2013 Massachusetts Institute of Technology
//
// This program is free software; you can redistribute it and/or
// modify it under the terms of the GNU General Public License version 2,
// as published by the Free Software Foundation.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program; if not, write to the Free Software
// Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.

// Scratch HTML5 Player
// Interpreter.js
// Tim Mickel, July 2011
// Based on the original by John Maloney

'use strict';

var Block = function(opAndArgs, optionalSubstack) {
    this.op = opAndArgs[0];
    this.primFcn = interp.lookupPrim(this.op); 
    this.args = opAndArgs.slice(1); // arguments can be either or constants (numbers, boolean strings, etc.) or expressions (Blocks)
    this.isLoop = false; // set to true for loop blocks the first time they run
    this.substack = optionalSubstack;
    this.subStack2 = null;
    this.nextBlock = null;
    this.tmp = -1;
    interp.fixArgs(this);
}

var Thread = function(block, target) {
    this.nextBlock = block; // next block to run; null when thread is finished
    this.firstBlock = block;
    this.stack = []; // stack of enclosing control structure blocks
    this.target = target; // target object running the thread
    this.tmp = null; // used for thread operations like Timer
    this.tmpObj = []; // used for Sprite operations like glide
    this.firstTime = true;
}

var Interpreter = function() {
    // Interpreter state
    this.primitiveTable = {}
    this.variables = {};
    this.threads = [];
    this.activeThread = new Thread(null);
    this.WorkTime = 30;
    this.currentMSecs = null;
    this.timer = new Timer();
    this.yield = false;
    this.doRedraw = false;
    this.opCount = 0; // used to benchmark the interpreter
}

// Utilities for building blocks and sequences of blocks
Interpreter.prototype.fixArgs = function(b) { 
    // Convert the arguments of the given block into blocks or substacks if necessary.
    // A block argument can be a constant (numbers, boolean strings, etc.), an expression (Blocks), or a substack (an array of blocks).
    var newArgs = [];
    for (var i = 0; i < b.args.length; i++) {
        var arg = b.args[i];
        if (arg && arg.constructor == Array) {
            if ((arg.length > 0) && (arg[0].constructor == Array)) {
                // if first element arg is itself an array, then arg is a substack
                if(!b.substack) {
                    b.substack = this.makeBlockList(arg);
                } else {
                    b.substack2 = this.makeBlockList(arg);
                }
            } else {
                // arg is a block
                newArgs.push(new Block(arg));
            }
        } else {
            newArgs.push(arg); // arg is a constant
        }
    }
    b.args = newArgs;
}

Interpreter.prototype.makeBlockList = function(blockList) {
    var firstBlock = null, lastBlock = null;
    for (var i = 0; i < blockList.length; i++) {
        var b = new Block(blockList[i]);
        if (firstBlock == null) firstBlock = b;
        if (lastBlock) lastBlock.nextBlock = b;
        lastBlock = b;
    }
    return firstBlock;
}

// The Interpreter proper
Interpreter.prototype.stepThreads = function() {
    var startTime;
    startTime = this.currentMSecs = this.timer.time();
    this.doRedraw = false;
    if (this.threads.length == 0) return;

    while ((this.currentMSecs - startTime) < this.WorkTime && !this.doRedraw) {
        var threadStopped = false;
        for (var a = this.threads.length-1; a >= 0; --a) {
            this.activeThread = this.threads[a];
            this.stepActiveThread();
            if (!this.activeThread || this.activeThread.nextBlock == null) {
                threadStopped = true;
            }
        }
        if (threadStopped) {
            var newThreads = [];
            for (var a = this.threads.length-1; a >= 0; --a) {
                if (this.threads[a].nextBlock != null) {
                    newThreads.push(this.threads[a]);
                }
            } 
            this.threads = newThreads;
            if (this.threads.length == 0) return;
        }
        this.currentMSecs = this.timer.time();
    }
}

Interpreter.prototype.stepActiveThread = function() {
    // Run the active thread until it yields.
    if(typeof(this.activeThread) == 'undefined') {
        return;
    }
    var b = this.activeThread.nextBlock;
    if (b == null) return;
    this.yield = false;
    while (true) {
        this.opCount++;
        // Advance the "program counter" to the next block before running the primitive.
        // Control flow primitives (e.g. if) may change activeThread.nextBlock.
        this.activeThread.nextBlock = b.nextBlock;
        b.primFcn(b);
        if (this.yield) { this.activeThread.nextBlock = b; return; }
        b = this.activeThread.nextBlock; // refresh local variable b in case primitive did some control flow
        while (!b) {
            // end of a substack; pop the owning control flow block from stack
            // Note: This is a loop to handle nested control flow blocks. 

            // yield at the end of a loop or when stack is empty
            if (this.activeThread.stack.length === 0) {
                this.activeThread.nextBlock = null;
                return;
            } else {
                b = this.activeThread.stack.pop();
                if (b.isLoop) {
                    this.activeThread.nextBlock = b; // preserve where it left off
                    return;
                } else {
                    b = b.nextBlock; // skip and continue for non looping blocks
                }
            }
        }
    }
}
  
Interpreter.prototype.toggleThread = function(b, targetObj) {
    var newThreads = [], wasRunning = false;
    for (var i = 0; i < this.threads.length; i++) {
        if (this.threads[i].stack[0] == b) {
            wasRunning = true;
        } else {
            newThreads.push(this.threads[i]);
        }
    }
    this.threads = newThreads;
    if(!wasRunning) {
        this.startThread(b, targetObj);
    }
  }
  
Interpreter.prototype.startThread = function(b, targetObj) {
    this.activeThread = new Thread(b, targetObj);
    this.threads.push(this.activeThread);
}
  
Interpreter.prototype.restartThread = function(b, targetObj) {
    // used by broadcast; stop any thread running on b, then start a new thread on b
    var newThread = new Thread(b, targetObj);
    var wasRunning = false;
    for (var i = 0; i < this.threads.length; i++) {
        if (this.threads[i].stack[0] == b) {
            this.threads[i] = newThread;
            wasRunning = true;
        }
    }
    if (!wasRunning) {
        this.threads.push(newThread);
    }
}

Interpreter.prototype.arg = function(block, index) {
    var arg = block.args[index];
    if ((typeof(arg) == 'object') && (arg.constructor == Block)) {
        this.opCount++;
        return arg.primFcn(arg); // expression
    }
    return arg;
}

Interpreter.prototype.numarg = function(block, index) {
    var arg = parseFloat(this.arg(block, index));
    if (arg !== arg) {
        return 0;
    }
    return arg;
}

Interpreter.prototype.boolarg = function(block, index) {
    var arg = this.arg(block, index);
    if (typeof arg === 'boolean') {
        return arg;
    } else if (typeof arg === 'string') {
        return !(arg === '' || arg === '0' || arg.toLowerCase() === 'false');
    }
    return arg;
}


Interpreter.prototype.targetSprite = function() {
    return this.activeThread.target;
}

// Timer
Interpreter.prototype.startTimer = function(secs) {
    var waitMSecs = 1000 * secs;
    if (waitMSecs < 0) waitMSecs = 0;
    this.activeThread.tmp = this.currentMSecs + waitMSecs; // end time in milliseconds
    this.activeThread.firstTime = false;
    this.yield = true;
}

Interpreter.prototype.checkTimer = function() {
    // check for timer expiration and clean up if expired. return true when expired
    if (this.currentMSecs >= this.activeThread.tmp) {
        // time expired
        this.activeThread.tmp = 0;
        this.activeThread.firstTime = true;
        return true;
    } else {
        this.yield = true;
        return false;
    }
}
  
Interpreter.prototype.redraw = function() {
    this.doRedraw = true;
}

// Primitive operations
Interpreter.prototype.initPrims = function() {
    this.primitiveTable = {};
    this.primitiveTable['whenGreenFlag']       = this.primNoop;
    this.primitiveTable['whenKeyPressed']      = this.primNoop;
    this.primitiveTable['whenClicked']         = this.primNoop;
    this.primitiveTable['if']                  = function(b) { if (interp.boolarg(b, 0)) interp.startSubstack(b) };
    this.primitiveTable['doForever']           = function(b) { interp.startSubstack(b, true) };
    this.primitiveTable['doForeverIf']         = function(b) { if (interp.boolarg(b, 0)) interp.startSubstack(b, true); else interp.yield = true; };
    this.primitiveTable['doIf']                = function(b) { if (interp.boolarg(b, 0)) interp.startSubstack(b); };
    this.primitiveTable['doRepeat']            = this.primRepeat;
    this.primitiveTable['doIfElse']            = function(b) { if (interp.boolarg(b, 0)) interp.startSubstack(b); else interp.startSubstack(b, false, true); };
    this.primitiveTable['doWaitUntil']         = function(b) { if (!interp.boolarg(b, 0)) interp.yield = true };
    this.primitiveTable['doUntil']             = function(b) { if (!interp.boolarg(b, 0)) interp.startSubstack(b, true) };
    this.primitiveTable['doReturn']            = function(b) { interp.activeThread = new Thread(null); };
    this.primitiveTable['stopAll']             = function(b) { interp.activeThread = new Thread(null); interp.threads = []; }
    this.primitiveTable['whenIReceive']        = this.primNoop;
    this.primitiveTable['broadcast:']          = function(b) { interp.broadcast(b, false); };
    this.primitiveTable['doBroadcastAndWait']  = function(b) { interp.broadcast(b, true); };
    this.primitiveTable['wait:elapsed:from:']  = this.primWait;

    // added by John:
    this.primitiveTable['showBubble'] = function(b) { console.log(interp.arg(b, 1)) }
    this.primitiveTable['timerReset'] = function(b) {interp.timerBase = (new Date()).getTime() }
    this.primitiveTable['timer'] = function(b) {return ((new Date()).getTime() - interp.timerBase) / 1000 }

    new Primitives().addPrimsTo(this.primitiveTable);
}

Interpreter.prototype.timerBase = (new Date()).getTime();
Interpreter.prototype.lookupPrim = function(op) {
    var fcn = interp.primitiveTable[op];
    if (fcn == null) fcn = function(b) { console.log('not implemented: ' + b.op) }
    return fcn;
}

Interpreter.prototype.primNoop = function(b) { console.log(b.op); }
	
Interpreter.prototype.primWait = function(b) {
    if (interp.activeThread.firstTime) interp.startTimer(interp.numarg(b, 0));
    else interp.checkTimer();
}
  
Interpreter.prototype.primRepeat = function(b) {
    if (b.tmp == -1) {
        b.tmp = Math.max(interp.numarg(b, 0), 0); // Initialize repeat count on this block
    }
    if (b.tmp > 0) {
        b.tmp -= 1; // decrement count
        interp.startSubstack(b, true);
    } else {
        // Done executing this repeat block for this round
        b.tmp = -1;
        b = null;
    }
}
  
Interpreter.prototype.broadcast = function(b, waitFlag) {
    var pair;
    if (interp.activeThread.firstTime) {
        var receivers = [];
        var msg = String(interp.arg(b, 0)).toLowerCase();
        var findReceivers = function (stack, target) {
            if ((stack.op == "whenIReceive") && (stack.args[0].toLowerCase() == msg)) {
                receivers.push([stack, target]);
            }
        }
        runtime.allStacksDo(findReceivers);
        for (pair in receivers) interp.restartThread(receivers[pair][0], receivers[pair][1]);
        if (!waitFlag) return;
        interp.activeThread.tmpObj = receivers;
        interp.activeThread.firstTime = false;
    }
    var done = true;
    for (pair in interp.activeThread.tmpObj) {
        if (interp.isRunning(interp.activeThread.tmpObj[pair][0])) {
            done = false;
        }
    }  
    if (done) {
        interp.activeThread.tmpObj = null;
        interp.activeThread.firstTime = true;
    } else {
        interp.yield = true;
    }
}

Interpreter.prototype.isRunning = function(b) {
    for (t in interp.threads) {
        if (interp.threads[t].firstBlock == b) {
            return true;
        }
    }
    return false;
}

Interpreter.prototype.startSubstack = function(b, isLoop, secondSubstack) {
    // Start the substack of a control structure command such as if or forever.
    b.isLoop = !!isLoop;
    this.activeThread.stack.push(b); // remember the block that started the substack
    if(!secondSubstack) {
        this.activeThread.nextBlock = b.substack;
    } else {
        this.activeThread.nextBlock = b.substack2;
    }
}
