/**
 * @license Licensed under the Apache License, Version 2.0 (the "License"):
 *          http://www.apache.org/licenses/LICENSE-2.0
 */

/**
 * @fileoverview Block for the Arduino functions.
 *     The Arduino built in functions syntax can be found at:
 *     https://arduino.cc/en/Reference/HomePage
 */

import * as Blockly from 'blockly/core';
import { getColour } from '../helpers/colour';
import * as Types from '../helpers/types';

Blockly.Blocks['arduino_functions'] = {
    /**
     * Block for defining the Arduino setup() and loop() functions.
     * @this Blockly.Block
     */
    init: function () {
        this.appendDummyInput()
            .appendField(Blockly.Msg.ARD_FUN_RUN_SETUP);
        this.appendStatementInput('SETUP_FUNC');
        this.appendDummyInput()
            .appendField(Blockly.Msg.ARD_FUN_RUN_LOOP);
        this.appendStatementInput('LOOP_FUNC');
        this.setInputsInline(false);
        this.setColour(getColour().procedures);
        this.setTooltip(Blockly.Msg.ARD_FUN_RUN_TIP);
        this.setHelpUrl('https://arduino.cc/en/Reference/Loop');
        this.contextMenu = false;
    },
    /** @return {!boolean} True if the block instance is in the workspace. */
    getArduinoLoopsInstance: function () {
        return true;
    }
};



Blockly.Blocks['procedures_defnoreturn'] = {
    /**
     * Block for defining a procedure with no return value.
     * @this Blockly.Block
     */
    init: function () {
        const nameField = new Blockly.FieldTextInput('', Blockly.Procedures.rename);
        nameField.setSpellcheck(false);
        this.appendDummyInput()
            .appendField(Blockly.Msg.PROCEDURES_DEFNORETURN)
            .appendField(nameField, 'NAME')
            .appendField('', 'PARAMS');
        this.setMutator(new Blockly.Mutator(['procedures_mutatorarg']));
        if (
            (this.workspace.options.comments ||
                (this.workspace.options.parentWorkspace &&
                    this.workspace.options.parentWorkspace.options.comments)) &&
            Blockly.Msg['PROCEDURES_DEFNORETURN_COMMENT']
        ) {
            this.setCommentText(Blockly.Msg['PROCEDURES_DEFNORETURN_COMMENT']);
        }
        this.setColour(getColour().procedures);
        this.setTooltip(Blockly.Msg['PROCEDURES_DEFNORETURN_TOOLTIP']);
        this.setHelpUrl(Blockly.Msg['PROCEDURES_DEFNORETURN_HELPURL']);
        this.arguments_ = [];
        this.argumentVarModels_ = [];
        this.setStatements_(true);
        this.statementConnection_ = null;
    },
    /**
     * Add or remove the statement block from this function definition.
     * @param {boolean} hasStatements True if a statement block is needed.
     * @this Blockly.Block
     */
    setStatements_: function (hasStatements) {
        if (this.hasStatements_ === hasStatements) {
            return;
        }
        if (hasStatements) {
            this.appendStatementInput('STACK').appendField(
                Blockly.Msg['PROCEDURES_DEFNORETURN_DO']
            );
            if (this.getInput('RETURN')) {
                this.moveInputBefore('STACK', 'RETURN');
            }
        } else {
            this.removeInput('STACK', true);
        }
        this.hasStatements_ = hasStatements;
    },
    /**
     * Update the display of parameters for this procedure definition block.
     * Display a warning if there are duplicate named parameters.
     * @private
     * @this Blockly.Block
     */
    updateParams_: function () {
        // Check for duplicated arguments.
        let badArg = false;
        const hash = {};
        for (let i = 0; i < this.arguments_.length; i++) {
            if (hash['arg_' + this.arguments_[i].toLowerCase()]) {
                badArg = true;
                break;
            }
            hash['arg_' + this.arguments_[i].toLowerCase()] = true;
        }
        if (badArg) {
            this.setWarningText(Blockly.Msg['PROCEDURES_DEF_DUPLICATE_WARNING']);
        } else {
            this.setWarningText(null);
        }
        // Merge the arguments into a human-readable list.
        let paramString = '';
        const paramStringArgs = [];
        if (this.argumentVarModels_.length) {
            for (let ii = 0; ii < this.argumentVarModels_.length; ii += 1) {
                paramStringArgs.push(
                    '  ' +
                    this.argumentVarModels_[ii].name +
                    ' (' +
                    this.argumentVarModels_[ii].type +
                    ')'
                );
            }
            paramString =
                Blockly.Msg['PROCEDURES_BEFORE_PARAMS'] + paramStringArgs.join(', ');
        }
        // The params field is deterministic based on the mutation,
        // no need to fire a change event.
        Blockly.Events.disable();
        try {
            this.setFieldValue(paramString, 'PARAMS');
        } finally {
            Blockly.Events.enable();
        }
    },
    /**
     * Create XML to represent the argument inputs.
     * @param {boolean=} opt_paramIds If true include the IDs of the parameter
     *     quarks.  Used by Blockly.Procedures.mutateCallers for reconnection.
     * @return {!Element} XML storage element.
     * @this Blockly.Block
     */
    mutationToDom: function (opt_paramIds) {
        const container = document.createElement('mutation');
        if (opt_paramIds) {
            container.setAttribute('name', this.getFieldValue('NAME'));
        }
        for (let i = 0; i < this.argumentVarModels_.length; i++) {
            const parameter = document.createElement('arg');
            const argModel = this.argumentVarModels_[i];
            parameter.setAttribute('name', argModel.name);
            parameter.setAttribute('type', argModel.type); // NOTES Setting the type of variable here
            parameter.setAttribute('varid', argModel.getId());
            if (opt_paramIds && this.paramIds_) {
                parameter.setAttribute('paramId', this.paramIds_[i]);
            }
            container.appendChild(parameter);
        }

        if (this.getFieldValue('RETURN TYPE')) {
            container.setAttribute('return_type', this.getFieldValue('RETURN TYPE'));
        }

        // Save whether the statement input is visible.
        if (!this.hasStatements_) {
            container.setAttribute('statements', 'false');
        }
        return container;
    },
    /**
     * Parse XML to restore the argument inputs.
     * @param {!Element} xmlElement XML storage element.
     * @this Blockly.Block
     */
    domToMutation: function (xmlElement) {
        this.arguments_ = [];
        this.argumentVarModels_ = [];
        for (let i = 0, childNode; (childNode = xmlElement.childNodes[i]); i++) {
            if (childNode.nodeName.toLowerCase() === 'arg') {
                const varName = childNode.getAttribute('name');
                const type = childNode.getAttribute('type');
                //  NOTES GETTING THE TYPE OF VARIABLE
                const varId = childNode.getAttribute('varid');
                this.arguments_.push(varName);
                const variable = Blockly.Variables.getOrCreateVariablePackage(
                    this.workspace,
                    varId,
                    varName,
                    type
                );
                this.argumentVarModels_.push(variable);
            }
        }
        this.updateParams_();
        Blockly.Procedures.mutateCallers(this);

        // Show or hide the statement input.
        this.setStatements_(xmlElement.getAttribute('statements') !== 'false');
    },
    /**
     * Populate the mutator's dialog with this block's components.
     * @param {!Blockly.Workspace} workspace Mutator's workspace.
     * @return {!Blockly.Block} Root block in mutator.
     * @this Blockly.Block
     */
    decompose: function (workspace) {
        const containerBlock = workspace.newBlock('procedures_mutatorcontainer');
        containerBlock.initSvg();

        // Check/uncheck the allow statement box.
        if (this.getInput('RETURN')) {
            containerBlock.setFieldValue(
                this.hasStatements_ ? 'TRUE' : 'FALSE',
                'STATEMENTS'
            );
        } else {
            containerBlock.getInput('STATEMENT_INPUT').setVisible(false);
        }

        // Parameter list.
        let connection = containerBlock.getInput('STACK').connection;
        for (let i = 0; i < this.arguments_.length; i++) {
            const paramBlock = workspace.newBlock('procedures_mutatorarg');
            paramBlock.initSvg();
            paramBlock.setFieldValue(this.arguments_[i], 'NAME');
            paramBlock.setFieldValue(this.argumentVarModels_[i].type, 'TYPE');
            // Trying to add type back
            // Store the old location.
            paramBlock.oldLocation = i;
            connection.connect(paramBlock.previousConnection);
            connection = paramBlock.nextConnection;
        }
        // Initialize procedure's callers with blank IDs.
        Blockly.Procedures.mutateCallers(this);
        return containerBlock;
    },
    /**
     * Reconfigure this block based on the mutator dialog's components.
     * @param {!Blockly.Block} containerBlock Root block in mutator.
     * @this Blockly.Block
     */
    compose: function (containerBlock) {
        // Parameter list.
        this.arguments_ = [];
        this.paramIds_ = [];
        this.argumentVarModels_ = [];
        let paramBlock = containerBlock.getInputTargetBlock('STACK');
        while (paramBlock) {
            const varName = paramBlock.getFieldValue('NAME');
            this.arguments_.push(varName);
            const variable = this.workspace.getVariable(
                varName,
                paramBlock.getFieldValue('TYPE') // NOTES SETTING TYPE OF VARIABLE
            );

            this.argumentVarModels_.push(variable);
            this.paramIds_.push(paramBlock.id);
            paramBlock =
                paramBlock.nextConnection && paramBlock.nextConnection.targetBlock();
        }
        this.updateParams_();
        Blockly.Procedures.mutateCallers(this);

        // Show/hide the statement input.
        let hasStatements = containerBlock.getFieldValue('STATEMENTS');
        if (hasStatements !== null) {
            hasStatements = hasStatements === 'TRUE';
            if (this.hasStatements_ !== hasStatements) {
                if (hasStatements) {
                    this.setStatements_(true);
                    // Restore the stack, if one was saved.
                    Blockly.Mutator.reconnect(this.statementConnection_, this, 'STACK');
                    this.statementConnection_ = null;
                } else {
                    // Save the stack, then disconnect it.
                    const stackConnection = this.getInput('STACK').connection;
                    this.statementConnection_ = stackConnection.targetConnection;
                    if (this.statementConnection_) {
                        const stackBlock = stackConnection.targetBlock();
                        stackBlock.unplug();
                        stackBlock.bumpNeighbours_();
                    }
                    this.setStatements_(false);
                }
            }
        }
    },

    /**
     * Return the signature of this procedure definition.
     * @return {!Array} Tuple containing three elements:
     *     - the name of the defined procedure,
     *     - a list of all its arguments,
     *     - that it DOES NOT have a return value.
     * @this Blockly.Block
     */
    getProcedureDef: function () {
        return [
            this.getFieldValue('NAME'),
            this.arguments_,
            false,
            this.argumentVarModels_
        ];
    },
    /**
     * Return all variables referenced by this block.
     * @return {!Array.<string>} List of variable names.
     * @this Blockly.Block
     */
    getVars: function () {
        return this.arguments_;
    },
    /**
     * Return all variables referenced by this block.
     * @return {!Array.<!Blockly.VariableModel>} List of variable models.
     * @this Blockly.Block
     */
    getVarModels: function () {
        return this.argumentVarModels_;
    },
    /**
     * Notification that a variable is renaming.
     * If the ID matches one of this block's variables, rename it.
     * @param {string} oldId ID of variable to rename.
     * @param {string} newId ID of new variable.  May be the same as oldId, but
     *     with an updated name.  Guaranteed to be the same type as the old
     *     variable.
     * @override
     * @this Blockly.Block
     */
    renameVarById: function (oldId, newId) {
        const oldVariable = this.workspace.getVariableById(oldId);
        if (oldVariable.type !== '') {
            // Procedure arguments always have the empty type.
            return;
        }
        const oldName = oldVariable.name;
        const newVar = this.workspace.getVariableById(newId);

        let change = false;
        for (let i = 0; i < this.argumentVarModels_.length; i++) {
            if (this.argumentVarModels_[i].getId() === oldId) {
                this.arguments_[i] = newVar.name;
                this.argumentVarModels_[i] = newVar;
                change = true;
            }
        }
        if (change) {
            this.displayRenamedVar_(oldName, newVar.name);
        }
    },
    /**
     * Notification that a variable is renaming but keeping the same ID.  If the
     * variable is in use on this block, rerender to show the new name.
     * @param {!Blockly.VariableModel} variable The variable being renamed.
     * @package
     * @override
     * @this Blockly.Block
     */
    updateVarName: function (variable) {
        let oldName;
        const newName = variable.name;
        let change = false;
        for (let i = 0; i < this.argumentVarModels_.length; i++) {
            if (this.argumentVarModels_[i].getId() === variable.getId()) {
                oldName = this.arguments_[i];
                this.arguments_[i] = newName;
                change = true;
            }
        }
        if (change) {
            this.displayRenamedVar_(oldName, newName);
        }
    },
    /**
     * Update the display to reflect a newly renamed argument.
     * @param {string} oldName The old display name of the argument.
     * @param {string} newName The new display name of the argument.
     * @private
     */
    displayRenamedVar_: function (oldName, newName) {
        this.updateParams_();
        // Update the mutator's variables if the mutator is open.
        if (this.mutator.isVisible()) {
            const blocks = this.mutator.workspace_.getAllBlocks(false);
            for (let i = 0, block; (block = blocks[i]); i++) {
                if (
                    block.type === 'procedures_mutatorarg' &&
                    Blockly.Names.equals(oldName, block.getFieldValue('NAME'))
                ) {
                    block.setFieldValue(newName, 'NAME');
                }
            }
        }
    },
    /**
     * Add custom menu options to this block's context menu.
     * @param {!Array} options List of menu options to add to.
     * @this Blockly.Block
     */
    customContextMenu: function (options) {
        if (this.isInFlyout) {
            return;
        }
        // Add option to create caller.
        const option = { enabled: true };
        const name = this.getFieldValue('NAME');
        option.text = Blockly.Msg['PROCEDURES_CREATE_DO'].replace('%1', name);
        const xmlMutation = document.createElement('mutation');
        xmlMutation.setAttribute('name', name);
        for (let i = 0; i < this.arguments_.length; i++) {
            const xmlArg = document.createElement('arg');
            xmlArg.setAttribute('name', this.arguments_[i]);
            xmlArg.setAttribute('type', this.argumentVarModels_[i].type); // TYPE FIGURED OUT
            xmlMutation.appendChild(xmlArg);
        }
        const xmlBlock = document.createElement('block');
        xmlBlock.setAttribute('type', this.callType_);
        xmlBlock.appendChild(xmlMutation);
        option.callback = Blockly.ContextMenu.callbackFactory(this, xmlBlock);
        options.push(option);

        // Add options to create getters for each parameter.
        if (!this.isCollapsed()) {
            for (let i = 0; i < this.argumentVarModels_.length; i++) {
                const option = { enabled: true };
                const argVar = this.argumentVarModels_[i];
                const name = argVar.name;
                option.text = Blockly.Msg['VARIABLES_SET_CREATE_GET'].replace(
                    '%1',
                    name
                );

                const xmlField = Blockly.Variables.generateVariableFieldDom(argVar);
                const xmlBlock = document.createElement('block');
                xmlBlock.setAttribute('type', 'variables_get');
                xmlBlock.appendChild(xmlField);
                option.callback = Blockly.ContextMenu.callbackFactory(this, xmlBlock);
                options.push(option);
            }
        }
    },
    callType_: 'procedures_callnoreturn'
};

Blockly.Blocks['procedures_defreturn'] = {
    /**
     * Block for defining a procedure with a return value.
     * @this Blockly.Block
     */
    init: function () {
        const returnTypeField = new Blockly.FieldDropdown(
            [['NUMBER', 'int'], ['DECIMAL', 'float'], ['TEXT', 'String'], ['CHARACTER', 'char'], ['BOOLEAN', 'boolean']],
            this.updateReturnType.bind(this)
        );

        const nameField = new Blockly.FieldTextInput('', Blockly.Procedures.rename);
        nameField.setSpellcheck(false);
        this.appendDummyInput()
            .appendField(Blockly.Msg.PROCEDURES_DEFNORETURN)
            .appendField(nameField, 'NAME')
            .appendField('', 'PARAMS')
            .appendField(Blockly.Msg.PROCEDURES_DEFRETURN_RETURN_TYPE)
            .appendField(returnTypeField, 'RETURN TYPE');

        this.appendValueInput('RETURN')
            .setAlign(Blockly.ALIGN_RIGHT)
            .appendField(Blockly.Msg['PROCEDURES_DEFRETURN_RETURN']);
        this.setMutator(new Blockly.Mutator(['procedures_mutatorarg']));
        if (
            (this.workspace.options.comments ||
                (this.workspace.options.parentWorkspace &&
                    this.workspace.options.parentWorkspace.options.comments)) &&
            Blockly.Msg['PROCEDURES_DEFRETURN_COMMENT']
        ) {
            this.setCommentText(Blockly.Msg['PROCEDURES_DEFRETURN_COMMENT']);
        }
        this.setColour(getColour().procedures);
        this.setTooltip(Blockly.Msg['PROCEDURES_DEFRETURN_TOOLTIP']);
        this.setHelpUrl(Blockly.Msg['PROCEDURES_DEFRETURN_HELPURL']);
        this.arguments_ = [];
        this.argumentVarModels_ = [];
        this.setStatements_(true);
        this.statementConnection_ = null;
        // Start the return the type off as a number.
        this.updateReturnType('Number');
    },
    setStatements_: Blockly.Blocks['procedures_defnoreturn'].setStatements_,
    updateParams_: Blockly.Blocks['procedures_defnoreturn'].updateParams_,
    mutationToDom: Blockly.Blocks['procedures_defnoreturn'].mutationToDom,
    domToMutation: Blockly.Blocks['procedures_defnoreturn'].domToMutation,
    decompose: Blockly.Blocks['procedures_defnoreturn'].decompose,
    compose: Blockly.Blocks['procedures_defnoreturn'].compose,
    /**
     * Return the signature of this procedure definition.
     * @return {!Array} Tuple containing three elements:
     *     - the name of the defined procedure,
     *     - a list of all its arguments,
     *     - that it DOES have a return value.
     * @this Blockly.Block
     */
    getProcedureDef: function () {
        return [
            this.getFieldValue('NAME'),
            this.arguments_,
            true,
            this.argumentVarModels_
        ];
    },

    /**
     * This will update all update the return type on the block and will update all the callers for the i
     *
     * @param returnType The type that is being checked
     */
    updateReturnType: function (returnType) {
        const returnInput = this.getInput('RETURN');

        if (!returnInput) {
            return;
        }

        returnInput.setCheck([returnType]);

        if (
            returnInput.connection &&
            returnInput.connection.getCheck().indexOf(returnType) === -1
        ) {
            returnInput.connection.disconnect();
            returnInput.connection.bumpNeighbours_();
        }

        const callers = Blockly.Procedures.getCallers(
            this.getFieldValue('NAME'),
            this.workspace
        );

        for (let i = 0, caller; (caller = callers[i]); i++) {
            caller.setOutput(true, returnType);
            const parent = caller.getParent();
            if (parent) {
                const input = parent.getInputWithBlock(caller);
                if (
                    input.connection.getCheck() !== null &&
                    input.connection.getCheck().indexOf(returnType) === -1
                ) {
                    input.connection.disconnect();
                    input.connection.bumpNeighbours_();
                }
            }
        }
    },
    getVars: Blockly.Blocks['procedures_defnoreturn'].getVars,
    getVarModels: Blockly.Blocks['procedures_defnoreturn'].getVarModels,
    renameVarById: Blockly.Blocks['procedures_defnoreturn'].renameVarById,
    updateVarName: Blockly.Blocks['procedures_defnoreturn'].updateVarName,
    displayRenamedVar_:
        Blockly.Blocks['procedures_defnoreturn'].displayRenamedVar_,
    customContextMenu: Blockly.Blocks['procedures_defnoreturn'].customContextMenu,
    callType_: 'procedures_callreturn'
};

Blockly.Blocks['procedures_mutatorcontainer'] = {
    /**
     * Mutator block for procedure container.
     * @this Blockly.Block
     */
    init: function () {
        this.appendDummyInput().appendField(
            Blockly.Msg['PROCEDURES_MUTATORCONTAINER_TITLE']
        );
        this.appendStatementInput('STACK');
        this.appendDummyInput('STATEMENT_INPUT')
            .appendField(Blockly.Msg['PROCEDURES_ALLOW_STATEMENTS'])
            .appendField(new Blockly.FieldCheckbox('TRUE'), 'STATEMENTS');
        this.setColour(getColour().procedures);
        this.setTooltip(Blockly.Msg['PROCEDURES_MUTATORCONTAINER_TOOLTIP']);
        this.contextMenu = false;
    }
};



Blockly.Blocks['procedures_mutatorarg'] = {

    /**
     * Mutator block for procedure argument.
     * @this Blockly.Block
     */
    init: function () {
        // let parameterCounter = 1;
        const paramName = 'x';//'param' + parameterCounter.toString();
        // This is for dialog box that get's opened
        // It has a flyout menu with the default variable
        // This will set the name of that default variable
        if (this.workspace.flyout_) {
            this.workspace.flyout_.workspace_
                .getAllBlocks()[0]
                .setFieldValue(paramName, 'NAME');
        }

        const typeField = new Blockly.FieldDropdown(
            [['NUMBER', 'int'], ['DECIMAL', 'float'], ['TEXT', 'String'], ['CHARACTER', 'char'], ['BOOLEAN', 'boolean']],
            this.validatorType_.bind(this)
        );
        const nameField = new Blockly.FieldTextInput(
            paramName,
            this.validateName.bind(this)
        );
        // Hack: override showEditor to do just a little bit more work.
        // We don't have a good place to hook into the start of a text edit.
        nameField.oldShowEditorFn_ = nameField.showEditor_;
        const newShowEditorFn = function () {
            this.createdVariables_ = [];
            this.oldShowEditorFn_();
        };

        nameField.showEditor_ = newShowEditorFn;

        this.appendDummyInput()
            .appendField(Blockly.Msg['PROCEDURES_MUTATORARG_TITLE'])
            .appendField(nameField, 'NAME')
            .appendField(typeField, 'TYPE');
        this.setPreviousStatement(true);
        this.setNextStatement(true);
        this.setColour(getColour().procedures);
        this.setTooltip(Blockly.Msg['PROCEDURES_MUTATORARG_TOOLTIP']);
        this.contextMenu = false;

        // Create the default variable when we drag the block in from the flyout.
        // Have to do this after installing the field on the block.
        nameField.onFinishEditing_ = this.deleteIntermediateVars_.bind(this);
        // Create an empty list so onFinishEditing_ has something to look at, even
        // though the editor was never opened.
        this.createdVariables_ = [];
        nameField.onFinishEditing_(paramName);

        // parameterCounter = 1;
    },
    /**
     * Obtain a valid name for the procedure argument. Create a variable if
     * necessary.
     * Merge runs of whitespace.  Strip leading and trailing whitespace.
     * Beyond this, all names are legal.
     * @param {string} varName User-supplied name.
     * @return {?string} Valid name, or null if a name was not specified.
     * @private
     */
    validateName: function (varName) {
        this.validateVariable(varName, this.getFieldValue('TYPE'));

        varName = varName.replace(/[\s\xa0]+/g, ' ').replace(/^ | $/g, '');

        if (!varName) {
            return null;
        }

        const blocks = this.workspace.getAllBlocks();

        for (let i = 0; i < blocks.length; i += 1) {
            if (blocks[i].id === this.id) {
                continue;
            }
            if (blocks[i].getFieldValue('NAME') === varName) {
                return null;
            }
        }

        return varName;
    },

    /**
     * This will add a variable to the list variables in the main workspace
     * @param varType
     * @private
     */
    validatorType_: function (varType) {
        this.validateVariable(this.getFieldValue('NAME'), varType);

        return varType;
    },

    /**
     * This will validate the variable being used
     *
     * @param varName
     * @param varType
     * @return {*}
     */
    validateVariable: function (varName, varType) {
        varName = varName.replace(/[\s\xa0]+/g, ' ').replace(/^ | $/g, '');

        if (!varName) {
            return null;
        }

        const outerWs = Blockly.mainWorkspace;
        let model = outerWs.getVariable(varName, varType);
        // CHANGING VARIABLE NAME TO REFLECT TYPE
        if (model && model.name !== varName) {
            // Rename the variable (case change)
            outerWs.renameVarById(model.getId(), varName);
        }

        if (!model) {
            model = outerWs.createVariable(varName, varType);
            // CHANGING VARIABLE NAME TO REFLECT TYPE
            if (model && this.createdVariables_) {
                this.createdVariables_.push(model);
            }
        }




        // for (let i = 0; i < this.createdVariables_.length; i++) {
        //     const model = this.createdVariables_[i];
        //     if (model.name !== varName || model.type !== varType) {
        //         //  DELETE CRITERIA NEEDS TO CHANGE

        //     }
        // }

    },

    /**
     * Called when focusing away from the text field.
     * Deletes all variables that were created as the user typed their intended
     * variable name.
     * @private
     */
    deleteIntermediateVars_: function (varName, varType) {
        varName = varName || this.getFieldValue('NAME');
        varType = varType || this.getFieldValue('TYPE');
        const outerWs = Blockly.mainWorkspace;
        if (!outerWs) {
            return;
        }

        for (let i = 0; i < this.createdVariables_.length; i++) {
            const model = this.createdVariables_[i];
            if (model.name !== varName || model.type !== varType) {
                //  DELETE CRITERIA NEEDS TO CHANGE
                outerWs.deleteVariableById(model.getId());
            }
        }
    }
};

Blockly.Blocks['procedures_callnoreturn'] = {
    /**
     * Block for calling a procedure with no return value.
     * @this Blockly.Block
     */
    init: function () {
        this.appendDummyInput('TOPROW')
            .appendField(Blockly.Msg.PROCEDURES_CALL)
            .appendField(this.id, 'NAME')
            .appendField(Blockly.Msg.PROCEDURES_CALL_END);
        this.setPreviousStatement(true);
        this.setNextStatement(true);
        this.setColour(getColour().procedures);
        // Tooltip is set in renameProcedure.
        this.setHelpUrl(Blockly.Msg['PROCEDURES_CALLNORETURN_HELPURL']);
        this.arguments_ = [];
        this.argumentVarModels_ = [];
        this.quarkConnections_ = {};
        this.quarkIds_ = null;
        this.previousDisabledState_ = false;
        this.returnType = null;
    },

    /**
     * Returns the name of the procedure this block calls.
     * @return {string} Procedure name.
     * @this Blockly.Block
     */
    getProcedureCall: function () {
        // The NAME field is guaranteed to exist, null will never be returned.
        return /** @type {string} */ this.getFieldValue('NAME');
    },
    /**
     * Notification that a procedure is renaming.
     * If the name matches this block's procedure, rename it.
     * @param {string} oldName Previous name of procedure.
     * @param {string} newName Renamed procedure.
     * @this Blockly.Block
     */
    renameProcedure: function (oldName, newName) {
        if (Blockly.Names.equals(oldName, this.getProcedureCall())) {
            this.setFieldValue(newName, 'NAME');
            const baseMsg = this.outputConnection
                ? Blockly.Msg['PROCEDURES_CALLRETURN_TOOLTIP']
                : Blockly.Msg['PROCEDURES_CALLNORETURN_TOOLTIP'];
            this.setTooltip(baseMsg.replace('%1', newName));
        }
    },
    /**
     * Notification that the procedure's parameters have changed.
     * @param {!Array.<string>} paramNames New param names, e.g. ['x', 'y', 'z'].
     * @param {!Array.<string>} paramIds IDs of params (consistent for each
     *     parameter through the life of a mutator, regardless of param renaming),
     *     e.g. ['piua', 'f8b_', 'oi.o'].
     * @param {!Array.<string>} types like Number, String, etc
     * @private
     * @this Blockly.Block
     */
    setProcedureParameters_: function (paramNames, paramIds, types) {
        // Data structures:
        // this.arguments = ['x', 'y']
        //     Existing param names.
        // this.quarkConnections_ {piua: null, f8b_: Blockly.Connection}
        //     Look-up of paramIds to connections plugged into the call block.
        // this.quarkIds_ = ['piua', 'f8b_']
        //     Existing param IDs.
        // Note that quarkConnections_ may include IDs that no longer exist, but
        // which might reappear if a param is reattached in the mutator.
        const defBlock = Blockly.Procedures.getDefinition(
            this.getProcedureCall(),
            this.workspace
        );
        const mutatorOpen =
            defBlock && defBlock.mutator && defBlock.mutator.isVisible();
        if (!mutatorOpen) {
            this.quarkConnections_ = {};
            this.quarkIds_ = null;
        }
        if (!paramIds) {
            // Reset the quarks (a mutator is about to open).
            return;
        }

        const oldTypes = this.argumentVarModels_
            ? this.argumentVarModels_.map(function (argModel) {
                return argModel.type;
            })
            : [];

        // Test arguments (arrays of strings) for changes. '\n' is not a valid
        // argument name character, so it is a valid delimiter here.
        if (
            paramNames.join('\n') === this.arguments_.join('\n') &&
            oldTypes.join(',') === types.join(',')
        ) {
            // No change.
            this.quarkIds_ = paramIds;
            return;
        }
        if (paramIds.length !== paramNames.length) {
            throw RangeError('paramNames and paramIds must be the same length.');
        }
        this.setCollapsed(false);
        if (!this.quarkIds_) {
            // Initialize tracking for this block.
            this.quarkConnections_ = {};
            this.quarkIds_ = [];
        }
        // Switch off rendering while the block is rebuilt.
        const savedRendered = this.rendered;
        this.rendered = false;
        // Update the quarkConnections_ with existing connections.
        for (let i = 0; i < this.arguments_.length; i++) {
            const input = this.getInput('ARG' + i);
            if (input) {
                const connection = input.connection.targetConnection;
                this.quarkConnections_[this.quarkIds_[i]] = connection;
                if (
                    mutatorOpen &&
                    connection &&
                    paramIds.indexOf(this.quarkIds_[i]) === -1
                ) {
                    // This connection should no longer be attached to this block.
                    connection.disconnect();
                    connection.getSourceBlock().bumpNeighbours_();
                }
            }
        }
        // Rebuild the block's arguments.
        this.arguments_ = [].concat(paramNames);
        // And rebuild the argument model list.
        this.argumentVarModels_ = [];
        for (let i = 0; i < this.arguments_.length; i++) {
            const variable = Blockly.Variables.getOrCreateVariablePackage(
                this.workspace,
                null,
                this.arguments_[i],
                types[i]
            );
            this.argumentVarModels_.push(variable);
        }

        this.updateShape_();
        this.quarkIds_ = paramIds;
        // Reconnect any child blocks.
        if (this.quarkIds_) {
            for (let i = 0; i < this.arguments_.length; i++) {
                const quarkId = this.quarkIds_[i];
                if (quarkId in this.quarkConnections_) {
                    const connection = this.quarkConnections_[quarkId];
                    try {
                        if (!Blockly.Mutator.reconnect(connection, this, 'ARG' + i)) {
                            // Block no longer exists or has been attached elsewhere.
                            delete this.quarkConnections_[quarkId];
                        }
                    } catch (e) {
                        connection.getSourceBlock().bumpNeighbours_();

                        console.error('DELETED DETACHED ' + e.message);
                        delete this.quarkConnections_[quarkId];
                    }
                }
            }
        }
        // Restore rendering and show the changes.
        this.rendered = savedRendered;
        if (this.rendered) {
            this.render();
        }
    },
    /**
     * Modify this block to have the correct number of arguments.
     * @private
     * @this Blockly.Block
     */
    updateShape_: function () {
        let i = 0;
        for (i = 0; i < this.arguments_.length; i++) {
            let field = this.getField('ARGNAME' + i);
            const labelString =
                this.argumentVarModels_[i].name +
                ' (' +
                this.argumentVarModels_[i].type +
                ') ';
            if (field) {
                // Ensure argument name is up to date.
                // The argument name field is deterministic based on the mutation,
                // no need to fire a change event.
                Blockly.Events.disable();
                try {
                    field.setValue(labelString);
                    this.getInput('ARG' + i).setCheck(Types.getCompatibleTypes([this.argumentVarModels_[i].type]));
                } finally {
                    Blockly.Events.enable();
                }
            } else {
                // Add new input.
                field = new Blockly.FieldLabel(labelString);
                const input = this.appendValueInput('ARG' + i)
                    .setAlign(Blockly.ALIGN_RIGHT)
                    .appendField(field, 'ARGNAME' + i)
                    .setCheck(Types.getCompatibleTypes([this.argumentVarModels_[i].type])); //  TESTING CHECK TYPES GOES HERE
                input.init();
            }
        }
        // Remove deleted inputs.
        while (this.getInput('ARG' + i)) {
            this.removeInput('ARG' + i);
            i++;
        }
        // Add 'with:' if there are parameters, remove otherwise.
        const topRow = this.getInput('TOPROW');
        if (topRow) {
            if (this.arguments_.length) {
                if (!this.getField('WITH')) {
                    topRow.appendField(
                        Blockly.Msg['PROCEDURES_CALL_BEFORE_PARAMS'],
                        'WITH'
                    );
                    topRow.init();
                }
            } else {
                if (this.getField('WITH')) {
                    topRow.removeField('WITH');
                }
            }
        }
    },
    /**
     * Create XML to represent the (non-editable) name and arguments.
     * @return {!Element} XML storage element.
     * @this Blockly.Block
     */
    mutationToDom: function () {
        const container = document.createElement('mutation');
        container.setAttribute('name', this.getProcedureCall());
        for (let i = 0; i < this.arguments_.length; i++) {
            const parameter = document.createElement('arg');
            parameter.setAttribute('name', this.arguments_[i]);
            parameter.setAttribute('type', this.argumentVarModels_[i].type);
            // TESTING ARGUMENTS NEED TO SET IN XML
            container.appendChild(parameter);
        }

        if (this.returnType) {
            container.setAttribute('return_type', this.returnType);
        }
        return container;
    },
    /**
     * Parse XML to restore the (non-editable) name and parameters.
     * @param {!Element} xmlElement XML storage element.
     * @this Blockly.Block
     */
    domToMutation: function (xmlElement) {
        const name = xmlElement.getAttribute('name');
        this.renameProcedure(this.getProcedureCall(), name);
        const args = [];
        const paramIds = [];
        const types = [];

        for (let i = 0, childNode; (childNode = xmlElement.childNodes[i]); i++) {
            if (childNode.nodeName.toLowerCase() === 'arg') {
                var variables = Blockly.mainWorkspace.getAllVariables();

                var varName = childNode.getAttribute('name');
                for (let y = 0; variables.length; y++) {
                    if (variables[y].name === varName) {
                        args.push(variables[y].name);
                        types.push(variables[y].type);
                        paramIds.push(variables[y].id_);
                        break;
                    }

                }
                // args.push(childNode.getAttribute('name'));
                // types.push(childNode.getAttribute('type'));
                // paramIds.push(childNode.getAttribute('paramId'));
            }
        }
        const returnType = xmlElement.getAttribute('return_type');

        // Sets the output of the block
        if (returnType) {
            this.setOutput(true, returnType);
            this.returnType = returnType;
        }
        this.setProcedureParameters_(args, paramIds, types);
    },
    /**
     * Return all variables referenced by this block.
     * @return {!Array.<!Blockly.VariableModel>} List of variable models.
     * @this Blockly.Block
     */
    getVarModels: function () {
        return this.argumentVarModels_;
    },
    /**
     * Procedure calls cannot exist without the corresponding procedure
     * definition.  Enforce this link whenever an event is fired.
     * @param {!Blockly.Events.Abstract} event Change event.
     * @this Blockly.Block
     */
    onchange: function (event) {
        if (!this.workspace || this.workspace.isFlyout) {
            // Block is deleted or is in a flyout.
            return;
        }
        if (!event.recordUndo) {
            // Events not generated by user. Skip handling.
            return;
        }

        if (
            event.type === Blockly.Events.BLOCK_CREATE &&
            event.ids.indexOf(this.id) !== -1
        ) {
            const typesAndNames = this.argumentVarModels_
                ? this.argumentVarModels_.map(function (arg) {
                    return { name: arg.name, type: arg.type };
                })
                : [];

            // Look for the case where a procedure call was created (usually through
            // paste) and there is no matching definition.  In this case, create
            // an empty definition block with the correct signature.
            const name = this.getProcedureCall();
            let def = Blockly.Procedures.getDefinition(name, this.workspace);

            const defTypesAndNames = def.argumentVarModels_
                ? def.argumentVarModels_.map(function (arg) {
                    return { name: arg.name, type: arg.type };
                })
                : [];

            if (
                def &&
                (def.type !== this.defType_ ||
                    JSON.stringify(typesAndNames) !== JSON.stringify(defTypesAndNames))
            ) {
                // The signatures don't match.
                def = null;
            }
            if (!def) {
                Blockly.Events.setGroup(event.group);
                /**
                 * Create matching definition block.
                 * <xml>
                 *   <block type="procedures_defreturn" x="10" y="20">
                 *     <mutation name="test">
                 *       <arg name="x"></arg>
                 *     </mutation>
                 *     <field name="NAME">test</field>
                 *   </block>
                 * </xml>
                 */
                const xml = document.createElement('xml');
                const block = document.createElement('block');
                block.setAttribute('type', this.defType_);
                const xy = this.getRelativeToSurfaceXY();
                const x = xy.x + Blockly.SNAP_RADIUS * (this.RTL ? -1 : 1);
                const y = xy.y + Blockly.SNAP_RADIUS * 2;
                block.setAttribute('x', x);
                block.setAttribute('y', y);
                const mutation = this.mutationToDom();
                block.appendChild(mutation);
                const field = document.createElement('field');
                field.setAttribute('name', 'NAME');
                field.appendChild(document.createTextNode(this.getProcedureCall()));
                block.appendChild(field);
                xml.appendChild(block);
                Blockly.Xml.domToWorkspace(xml, this.workspace);
                Blockly.Events.setGroup(false);
            }
        } else if (event.type === Blockly.Events.BLOCK_DELETE) {
            // Look for the case where a procedure definition has been deleted,
            // leaving this block (a procedure call) orphaned.  In this case, delete
            // the orphan.
            const name = this.getProcedureCall();
            const def = Blockly.Procedures.getDefinition(name, this.workspace);
            if (!def) {
                Blockly.Events.setGroup(event.group);
                this.dispose(true, false);
                Blockly.Events.setGroup(false);
            }
        } else if (
            event.type === Blockly.Events.CHANGE &&
            event.element === 'disabled'
        ) {
            const name = this.getProcedureCall();
            const def = Blockly.Procedures.getDefinition(name, this.workspace);
            if (def && def.id === event.blockId) {
                // in most cases the old group should be ''
                const oldGroup = Blockly.Events.getGroup();
                if (oldGroup) {
                    // This should only be possible programatically and may indicate a problem
                    // with event grouping. If you see this message please investigate. If the
                    // use ends up being valid we may need to reorder events in the undo stack.
                   
                }
                Blockly.Events.setGroup(event.group);
                if (event.newValue) {
                    this.previousDisabledState_ = this.disabled;
                    this.setDisabled(true);
                } else {
                    this.setDisabled(this.previousDisabledState_);
                }
                Blockly.Events.setGroup(oldGroup);
            }
        }
    },
    /**
     * Add menu option to find the definition block for this call.
     * @param {!Array} options List of menu options to add to.
     * @this Blockly.Block
     */
    customContextMenu: function (options) {
        const option = { enabled: true };
        option.text = Blockly.Msg['PROCEDURES_HIGHLIGHT_DEF'];
        const name = this.getProcedureCall();
        const workspace = this.workspace;
        option.callback = function () {
            const def = Blockly.Procedures.getDefinition(name, workspace);
            if (def) {
                workspace.centerOnBlock(def.id);
                def.select();
            }
        };
        options.push(option);
    },
    defType_: 'procedures_defnoreturn'
};

Blockly.Blocks['procedures_callreturn'] = {
    /**
     * Block for calling a procedure with a return value.
     * @this Blockly.Block
     */
    init: function () {
        this.appendDummyInput('TOPROW').appendField('', 'NAME');
        this.setOutput(true, 'Number'); // Start off as number
        this.setColour(getColour().procedures);
        // Tooltip is set in domToMutation.
        this.setHelpUrl(Blockly.Msg['PROCEDURES_CALLRETURN_HELPURL']);
        this.arguments_ = [];
        this.quarkConnections_ = {};
        this.quarkIds_ = null;
        this.previousDisabledState_ = false;
    },

    getProcedureCall: Blockly.Blocks['procedures_callnoreturn'].getProcedureCall,
    renameProcedure: Blockly.Blocks['procedures_callnoreturn'].renameProcedure,
    setProcedureParameters_:
        Blockly.Blocks['procedures_callnoreturn'].setProcedureParameters_,
    updateShape_: Blockly.Blocks['procedures_callnoreturn'].updateShape_,
    mutationToDom: Blockly.Blocks['procedures_callnoreturn'].mutationToDom,
    domToMutation: Blockly.Blocks['procedures_callnoreturn'].domToMutation,
    getVarModels: Blockly.Blocks['procedures_callnoreturn'].getVarModels,
    onchange: Blockly.Blocks['procedures_callnoreturn'].onchange,
    customContextMenu:
        Blockly.Blocks['procedures_callnoreturn'].customContextMenu,
    defType_: 'procedures_defreturn'
}