var Thread = require('./thread');
var YieldTimers = require('../util/yieldtimers.js');

var execute = function (sequencer, thread, blockId, isInput) {
    var runtime = sequencer.runtime;

    // Save the yield timer ID, in case a primitive makes a new one
    // @todo hack - perhaps patch this to allow more than one timer per
    // primitive, for example...
    var oldYieldTimerId = YieldTimers.timerId;

    var opcode = runtime.blocks.getOpcode(blockId);

    // Push the current block to the stack
    thread.stack.push(blockId);
    // Push an empty stack frame, if we need one.
    // Might not, if we just popped the stack.
    if (thread.stack.length > thread.stackFrames.length) {
        thread.stackFrames.push({});
    }
    var currentStackFrame = thread.stackFrames[thread.stackFrames.length - 1];

    /**
     * A callback for the primitive to indicate its thread should yield.
     * @type {Function}
     */
    var threadYieldCallback = function () {
        thread.status = Thread.STATUS_YIELD;
    };

    /**
     * A callback for the primitive to indicate its thread is finished
     * @type {Function}
     */
    var threadDoneCallback = function () {
        // Pop the stack and stack frame
        thread.stack.pop();
        thread.stackFrames.pop();
        // If we're not executing an input sub-block,
        // mark the thread as done and proceed to the next block.
        if (!isInput) {
            thread.status = Thread.STATUS_DONE;
            // Refresh nextBlock in case it has changed during a yield.
            thread.nextBlock = runtime.blocks.getNextBlock(blockId);
        }
        // Stop showing run feedback in the editor.
        runtime.glowBlock(blockId, false);
    };

    /**
     * A callback for the primitive to start hats.
     * @todo very hacked...
     * Provide a callback that is passed in a block and returns true
     * if it is a hat that should be triggered.
     * @param {Function} callback Provided callback.
     */
    var startHats = function(callback) {
        var stacks = runtime.blocks.getStacks();
        for (var i = 0; i < stacks.length; i++) {
            var stack = stacks[i];
            var stackBlock = runtime.blocks.getBlock(stack);
            var result = callback(stackBlock);
            if (result) {
                // Check if the stack is already running
                var stackRunning = false;

                for (var j = 0; j < runtime.threads.length; j++) {
                    if (runtime.threads[j].topBlock == stack) {
                        stackRunning = true;
                        break;
                    }
                }
                if (!stackRunning) {
                    runtime._pushThread(stack);
                }
            }
        }
    };

    /**
     * Record whether we have switched stack,
     * to avoid proceeding the thread automatically.
     * @type {boolean}
     */
    var switchedStack = false;
    /**
     * A callback for a primitive to start a substack.
     * @type {Function}
     */
    var threadStartSubstack = function () {
        // Set nextBlock to the start of the substack
        var substack = runtime.blocks.getSubstack(blockId);
        if (substack && substack.value) {
            thread.nextBlock = substack.value;
        } else {
            thread.nextBlock = null;
        }
        switchedStack = true;
    };

    // Generate values for arguments (inputs).
    var argValues = {};

    // Add all fields on this block to the argValues.
    var fields = runtime.blocks.getFields(blockId);
    for (var fieldName in fields) {
        argValues[fieldName] = fields[fieldName];
    }

    // Recursively evaluate input blocks.
    var inputs = runtime.blocks.getInputs(blockId);
    for (var inputName in inputs) {
        var input = inputs[inputName];
        var inputBlockId = input.block;
        var result = execute(sequencer, thread, inputBlockId, true);
        argValues[input.name] = result;
    }

    // Start showing run feedback in the editor.
    runtime.glowBlock(blockId, true);

    if (!opcode) {
        console.warn('Could not get opcode for block: ' + blockId);
        console.groupEnd();
        return;
    }

    var blockFunction = runtime.getOpcodeFunction(opcode);
    if (!blockFunction) {
        console.warn('Could not get implementation for opcode: ' + opcode);
        console.groupEnd();
        return;
    }

    if (sequencer.DEBUG_BLOCK_CALLS) {
        console.groupCollapsed('Executing: ' + opcode);
        console.log('with arguments: ', argValues);
        console.log('and stack frame: ', currentStackFrame);
    }
    var blockFunctionReturnValue = null;
    try {
        // @todo deal with the return value
        blockFunctionReturnValue = blockFunction(argValues, {
            yield: threadYieldCallback,
            done: threadDoneCallback,
            timeout: YieldTimers.timeout,
            stackFrame: currentStackFrame,
            startSubstack: threadStartSubstack,
            startHats: startHats
        });
    }
    catch(e) {
        console.error(
            'Exception calling block function for opcode: ' +
            opcode + '\n' + e);
    } finally {
        // Update if the thread has set a yield timer ID
        // @todo hack
        if (YieldTimers.timerId > oldYieldTimerId) {
            thread.yieldTimerId = YieldTimers.timerId;
        }
        if (thread.status === Thread.STATUS_RUNNING && !switchedStack) {
            // Thread executed without yielding - move to done
            threadDoneCallback();
        }
        if (sequencer.DEBUG_BLOCK_CALLS) {
            console.log('ending stack frame: ', currentStackFrame);
            console.log('returned: ', blockFunctionReturnValue);
            console.groupEnd();
        }
    }
};

module.exports = execute;