/**
 * @license
 * Copyright 2012 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */

/**
 * @fileoverview Procedure blocks for Blockly.
 * @author fraser@google.com (Neil Fraser)
 */
 'use strict';

 goog.provide('Blockly.Blocks.procedures');
 
 goog.require('Blockly');
 goog.require('Blockly.Blocks');
 goog.require('Blockly.Comment');
 goog.require('Blockly.FieldCheckbox');
 goog.require('Blockly.FieldLabel');
 goog.require('Blockly.FieldTextInput');
 goog.require('Blockly.Mutator');
 goog.require('Blockly.Warning');
 
 
 Blockly.Blocks['procedures_defnoreturn'] = {
   /**
    * Block for defining a procedure with no return value.
    * @this {Blockly.Block}
    */
   init: function() {
     var initName = Blockly.Procedures.findLegalName('', this);
     var nameField = new Blockly.FieldTextInput(initName,
         Blockly.Procedures.rename);
     nameField.setSpellcheck(false);
     this.appendDummyInput()
         .appendField(Blockly.Msg['PROCEDURES_DEFNORETURN_TITLE'])
         .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.setStyle('procedure_blocks');
     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.
    * @private
    * @this {Blockly.Block}
    */
   updateParams_: function() {
 
     // Merge the arguments into a human-readable list.
     var paramString = '';
     if (this.arguments_.length) {
       paramString = Blockly.Msg['PROCEDURES_BEFORE_PARAMS'] +
           ' ' + this.arguments_.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) {
     var container = Blockly.utils.xml.createElement('mutation');
     if (opt_paramIds) {
       container.setAttribute('name', this.getFieldValue('NAME'));
     }
     for (var i = 0; i < this.argumentVarModels_.length; i++) {
       var parameter = Blockly.utils.xml.createElement('arg');
       var argModel = this.argumentVarModels_[i];
       parameter.setAttribute('name', argModel.name);
       parameter.setAttribute('varid', argModel.getId());
       if (opt_paramIds && this.paramIds_) {
         parameter.setAttribute('paramId', this.paramIds_[i]);
       }
       container.appendChild(parameter);
     }
 
     // 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 (var i = 0, childNode; (childNode = xmlElement.childNodes[i]); i++) {
       if (childNode.nodeName.toLowerCase() == 'arg') {
         var varName = childNode.getAttribute('name');
         var varId = childNode.getAttribute('varid') || childNode.getAttribute('varId');
         this.arguments_.push(varName);
         var variable = Blockly.Variables.getOrCreateVariablePackage(
             this.workspace, varId, varName, '');
         if (variable != null) {
           this.argumentVarModels_.push(variable);
         } else {
           console.log('Failed to create a variable with name ' + varName + ', ignoring.');
         }
       }
     }
     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) {
     /*
      * Creates the following XML:
      * <block type="procedures_mutatorcontainer">
      *   <statement name="STACK">
      *     <block type="procedures_mutatorarg">
      *       <field name="NAME">arg1_name</field>
      *       <next>etc...</next>
      *     </block>
      *   </statement>
      * </block>
      */
 
     var containerBlockNode = Blockly.utils.xml.createElement('block');
     containerBlockNode.setAttribute('type', 'procedures_mutatorcontainer');
     var statementNode = Blockly.utils.xml.createElement('statement');
     statementNode.setAttribute('name', 'STACK');
     containerBlockNode.appendChild(statementNode);
 
     var node = statementNode;
     for (var i = 0; i < this.arguments_.length; i++) {
       var argBlockNode = Blockly.utils.xml.createElement('block');
       argBlockNode.setAttribute('type', 'procedures_mutatorarg');
       var fieldNode = Blockly.utils.xml.createElement('field');
       fieldNode.setAttribute('name', 'NAME');
       var argumentName = Blockly.utils.xml.createTextNode(this.arguments_[i]);
       fieldNode.appendChild(argumentName);
       argBlockNode.appendChild(fieldNode);
       var nextNode = Blockly.utils.xml.createElement('next');
       argBlockNode.appendChild(nextNode);
 
       node.appendChild(argBlockNode);
       node = nextNode;
     }
 
     var containerBlock = Blockly.Xml.domToBlock(containerBlockNode, workspace);
 
     if (this.type == 'procedures_defreturn') {
       containerBlock.setFieldValue(this.hasStatements_, 'STATEMENTS');
     } else {
       containerBlock.removeInput('STATEMENT_INPUT');
     }
 
     // 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_ = [];
     var paramBlock = containerBlock.getInputTargetBlock('STACK');
     while (paramBlock && !paramBlock.isInsertionMarker()) {
       var varName = paramBlock.getFieldValue('NAME');
       this.arguments_.push(varName);
       var variable = this.workspace.getVariable(varName, '');
       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.
     var 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.
           var stackConnection = this.getInput('STACK').connection;
           this.statementConnection_ = stackConnection.targetConnection;
           if (this.statementConnection_) {
             var 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];
   },
   /**
    * 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) {
     var oldVariable = this.workspace.getVariableById(oldId);
     if (oldVariable.type != '') {
       // Procedure arguments always have the empty type.
       return;
     }
     var oldName = oldVariable.name;
     var newVar = this.workspace.getVariableById(newId);
 
     var change = false;
     for (var 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);
       Blockly.Procedures.mutateCallers(this);
     }
   },
   /**
    * 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) {
     var newName = variable.name;
     var change = false;
     for (var i = 0; i < this.argumentVarModels_.length; i++) {
       if (this.argumentVarModels_[i].getId() == variable.getId()) {
         var oldName = this.arguments_[i];
         this.arguments_[i] = newName;
         change = true;
       }
     }
     if (change) {
       this.displayRenamedVar_(oldName, newName);
       Blockly.Procedures.mutateCallers(this);
     }
   },
   /**
    * 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
    * @this {Blockly.Block}
    */
   displayRenamedVar_: function(oldName, newName) {
     this.updateParams_();
     // Update the mutator's variables if the mutator is open.
     if (this.mutator && this.mutator.isVisible()) {
       var blocks = this.mutator.workspace_.getAllBlocks(false);
       for (var 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.
     var option = {enabled: true};
     var name = this.getFieldValue('NAME');
     option.text = Blockly.Msg['PROCEDURES_CREATE_DO'].replace('%1', name);
     var xmlMutation = Blockly.utils.xml.createElement('mutation');
     xmlMutation.setAttribute('name', name);
     for (var i = 0; i < this.arguments_.length; i++) {
       var xmlArg = Blockly.utils.xml.createElement('arg');
       xmlArg.setAttribute('name', this.arguments_[i]);
       xmlMutation.appendChild(xmlArg);
     }
     var xmlBlock = Blockly.utils.xml.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 (var i = 0; i < this.argumentVarModels_.length; i++) {
         var argOption = {enabled: true};
         var argVar = this.argumentVarModels_[i];
         argOption.text = Blockly.Msg['VARIABLES_SET_CREATE_GET']
             .replace('%1', argVar.name);
 
         var argXmlField = Blockly.Variables.generateVariableFieldDom(argVar);
         var argXmlBlock = Blockly.utils.xml.createElement('block');
         argXmlBlock.setAttribute('type', 'variables_get');
         argXmlBlock.appendChild(argXmlField);
         argOption.callback =
             Blockly.ContextMenu.callbackFactory(this, argXmlBlock);
         options.push(argOption);
       }
     }
   },
   callType_: 'procedures_callnoreturn'
 };
 
 Blockly.Blocks['procedures_defreturn'] = {
   /**
    * Block for defining a procedure with a return value.
    * @this {Blockly.Block}
    */
   init: function() {
     var initName = Blockly.Procedures.findLegalName('', this);
     var nameField = new Blockly.FieldTextInput(initName,
         Blockly.Procedures.rename);
     nameField.setSpellcheck(false);
     this.appendDummyInput()
         .appendField(Blockly.Msg['PROCEDURES_DEFRETURN_TITLE'])
         .appendField(nameField, 'NAME')
         .appendField('', 'PARAMS');
     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.setStyle('procedure_blocks');
     this.setTooltip(Blockly.Msg['PROCEDURES_DEFRETURN_TOOLTIP']);
     this.setHelpUrl(Blockly.Msg['PROCEDURES_DEFRETURN_HELPURL']);
     this.arguments_ = [];
     this.argumentVarModels_ = [];
     this.setStatements_(true);
     this.statementConnection_ = null;
   },
   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];
   },
   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.setStyle('procedure_blocks');
     this.setTooltip(Blockly.Msg['PROCEDURES_MUTATORCONTAINER_TOOLTIP']);
     this.contextMenu = false;
   },
 };
 
 Blockly.Blocks['procedures_mutatorarg'] = {
   /**
    * Mutator block for procedure argument.
    * @this {Blockly.Block}
    */
   init: function() {
     var field = new Blockly.FieldTextInput(
         Blockly.Procedures.DEFAULT_ARG, this.validator_);
     // 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.
     field.oldShowEditorFn_ = field.showEditor_;
     var newShowEditorFn = function() {
       this.createdVariables_ = [];
       this.oldShowEditorFn_();
     };
     field.showEditor_ = newShowEditorFn;
 
     this.appendDummyInput()
         .appendField(Blockly.Msg.PROCEDURES_BEFORE_PARAMS)
         .appendField(field, 'NAME');
     this.setPreviousStatement(true);
     this.setNextStatement(true);
     this.setStyle('procedure_blocks');
     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.
     field.onFinishEditing_ = this.deleteIntermediateVars_;
     // Create an empty list so onFinishEditing_ has something to look at, even
     // though the editor was never opened.
     field.createdVariables_ = [];
     field.onFinishEditing_('x');
   },
   /**
    * 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
    * @this Blockly.FieldTextInput
    */
   validator_: function(varName) {
     var sourceBlock = this.getSourceBlock();
     var outerWs = Blockly.Mutator.findParentWs(sourceBlock.workspace);
     varName = varName.replace(/[\s\xa0]+/g, ' ').replace(/^ | $/g, '');
     if (!varName) {
       return null;
     }
 
     // Prevents duplicate parameter names in functions
     var workspace = sourceBlock.workspace.targetWorkspace ||
         sourceBlock.workspace;
     var blocks = workspace.getAllBlocks(false);
     var caselessName = varName.toLowerCase();
     for (var i = 0; i < blocks.length; i++) {
       if (blocks[i].id == this.getSourceBlock().id) {
         continue;
       }
       // Other blocks values may not be set yet when this is loaded.
       var otherVar = blocks[i].getFieldValue('NAME');
       if (otherVar && otherVar.toLowerCase() == caselessName) {
         return null;
       }
     }
 
     // Don't create variables for arg blocks that
     // only exist in the mutator's flyout.
     if (sourceBlock.isInFlyout) {
       return varName;
     }
 
     var model = outerWs.getVariable(varName, '');
     if (model && model.name != varName) {
       // Rename the variable (case change)
       outerWs.renameVariableById(model.getId(), varName);
     }
     if (!model) {
       model = outerWs.createVariable(varName, '');
       if (model && this.createdVariables_) {
         this.createdVariables_.push(model);
       }
     }
     return varName;
   },
 
   /**
    * Called when focusing away from the text field.
    * Deletes all variables that were created as the user typed their intended
    * variable name.
    * @param {string} newText The new variable name.
    * @private
    * @this Blockly.FieldTextInput
    */
   deleteIntermediateVars_: function(newText) {
     var outerWs = Blockly.Mutator.findParentWs(this.getSourceBlock().workspace);
     if (!outerWs) {
       return;
     }
     for (var i = 0; i < this.createdVariables_.length; i++) {
       var model = this.createdVariables_[i];
       if (model.name != newText) {
         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('', 'NAME');
     this.setPreviousStatement(true);
     this.setNextStatement(true);
     this.setStyle('procedure_blocks');
     // Tooltip is set in renameProcedure.
     this.setHelpUrl(Blockly.Msg['PROCEDURES_CALLNORETURN_HELPURL']);
     this.arguments_ = [];
     this.argumentVarModels_ = [];
     this.quarkConnections_ = {};
     this.quarkIds_ = null;
     this.previousEnabledState_ = true;
   },
 
   /**
    * 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');
       var 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'].
    * @private
    * @this {Blockly.Block}
    */
   setProcedureParameters_: function(paramNames, paramIds) {
     // 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.
     var defBlock = Blockly.Procedures.getDefinition(this.getProcedureCall(),
         this.workspace);
     var 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;
     }
     // 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')) {
       // 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.
     var savedRendered = this.rendered;
     this.rendered = false;
     // Update the quarkConnections_ with existing connections.
     for (var i = 0; i < this.arguments_.length; i++) {
       var input = this.getInput('ARG' + i);
       if (input) {
         var 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 (var i = 0; i < this.arguments_.length; i++) {
       var variable = Blockly.Variables.getOrCreateVariablePackage(
           this.workspace, null, this.arguments_[i], '');
       this.argumentVarModels_.push(variable);
     }
 
     this.updateShape_();
     this.quarkIds_ = paramIds;
     // Reconnect any child blocks.
     if (this.quarkIds_) {
       for (var i = 0; i < this.arguments_.length; i++) {
         var quarkId = this.quarkIds_[i];
         if (quarkId in this.quarkConnections_) {
           var connection = this.quarkConnections_[quarkId];
           if (!Blockly.Mutator.reconnect(connection, this, 'ARG' + i)) {
             // Block no longer exists or has been attached elsewhere.
             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() {
     for (var i = 0; i < this.arguments_.length; i++) {
       var field = this.getField('ARGNAME' + i);
       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(this.arguments_[i]);
         } finally {
           Blockly.Events.enable();
         }
       } else {
         // Add new input.
         field = new Blockly.FieldLabel(this.arguments_[i]);
         var input = this.appendValueInput('ARG' + i)
             .setAlign(Blockly.ALIGN_RIGHT)
             .appendField(field, 'ARGNAME' + i);
         input.init();
       }
     }
     // Remove deleted inputs.
     while (this.getInput('ARG' + i)) {
       this.removeInput('ARG' + i);
       i++;
     }
     // Add 'with:' if there are parameters, remove otherwise.
     var 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() {
     var container = Blockly.utils.xml.createElement('mutation');
     container.setAttribute('name', this.getProcedureCall());
     for (var i = 0; i < this.arguments_.length; i++) {
       var parameter = Blockly.utils.xml.createElement('arg');
       parameter.setAttribute('name', this.arguments_[i]);
       container.appendChild(parameter);
     }
     return container;
   },
   /**
    * Parse XML to restore the (non-editable) name and parameters.
    * @param {!Element} xmlElement XML storage element.
    * @this {Blockly.Block}
    */
   domToMutation: function(xmlElement) {
     var name = xmlElement.getAttribute('name');
     this.renameProcedure(this.getProcedureCall(), name);
     var args = [];
     var paramIds = [];
     for (var i = 0, childNode; (childNode = xmlElement.childNodes[i]); i++) {
       if (childNode.nodeName.toLowerCase() == 'arg') {
         args.push(childNode.getAttribute('name'));
         paramIds.push(childNode.getAttribute('paramId'));
       }
     }
     this.setProcedureParameters_(args, paramIds);
   },
   /**
    * 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_;
   },
   /**
    * 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) {
       // 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.
       var name = this.getProcedureCall();
       var def = Blockly.Procedures.getDefinition(name, this.workspace);
       if (def && (def.type != this.defType_ ||
           JSON.stringify(def.getVars()) != JSON.stringify(this.arguments_))) {
         // The signatures don't match.
         def = null;
       }
       if (!def) {
         Blockly.Events.setGroup(event.group);
         /**
          * Create matching definition block.
          * <xml xmlns="https://developers.google.com/blockly/xml">
          *   <block type="procedures_defreturn" x="10" y="20">
          *     <mutation name="test">
          *       <arg name="x"></arg>
          *     </mutation>
          *     <field name="NAME">test</field>
          *   </block>
          * </xml>
          */
         var xml = Blockly.utils.xml.createElement('xml');
         var block = Blockly.utils.xml.createElement('block');
         block.setAttribute('type', this.defType_);
         var xy = this.getRelativeToSurfaceXY();
         var x = xy.x + Blockly.SNAP_RADIUS * (this.RTL ? -1 : 1);
         var y = xy.y + Blockly.SNAP_RADIUS * 2;
         block.setAttribute('x', x);
         block.setAttribute('y', y);
         var mutation = this.mutationToDom();
         block.appendChild(mutation);
         var field = Blockly.utils.xml.createElement('field');
         field.setAttribute('name', 'NAME');
         var callName = this.getProcedureCall();
         if (!callName) {
           // Rename if name is empty string.
           callName = Blockly.Procedures.findLegalName('', this);
           this.renameProcedure('', callName);
         }
         field.appendChild(Blockly.utils.xml.createTextNode(callName));
         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.
       var name = this.getProcedureCall();
       var def = Blockly.Procedures.getDefinition(name, this.workspace);
       if (!def) {
         Blockly.Events.setGroup(event.group);
         this.dispose(true);
         Blockly.Events.setGroup(false);
       }
     } else if (event.type == Blockly.Events.CHANGE && event.element == 'disabled') {
       var name = this.getProcedureCall();
       var def = Blockly.Procedures.getDefinition(name, this.workspace);
       if (def && def.id == event.blockId) {
         // in most cases the old group should be ''
         var oldGroup = Blockly.Events.getGroup();
         if (oldGroup) {
           // This should only be possible programmatically 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.
           console.log('Saw an existing group while responding to a definition change');
         }
         Blockly.Events.setGroup(event.group);
         if (event.newValue) {
           this.previousEnabledState_ = this.isEnabled();
           this.setEnabled(false);
         } else {
           this.setEnabled(this.previousEnabledState_);
         }
         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) {
     if (!this.workspace.isMovable()) {
       // If we center on the block and the workspace isn't movable we could
       // loose blocks at the edges of the workspace.
       return;
     }
 
     var option = {enabled: true};
     option.text = Blockly.Msg['PROCEDURES_HIGHLIGHT_DEF'];
     var name = this.getProcedureCall();
     var workspace = this.workspace;
     option.callback = function() {
       var 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);
     this.setStyle('procedure_blocks');
     // Tooltip is set in domToMutation.
     this.setHelpUrl(Blockly.Msg['PROCEDURES_CALLRETURN_HELPURL']);
     this.arguments_ = [];
     this.argumentVarModels_ = [];
     this.quarkConnections_ = {};
     this.quarkIds_ = null;
     this.previousEnabledState_ = true;
   },
 
   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,
   getVars: Blockly.Blocks['procedures_callnoreturn'].getVars,
   getVarModels: Blockly.Blocks['procedures_callnoreturn'].getVarModels,
   onchange: Blockly.Blocks['procedures_callnoreturn'].onchange,
   customContextMenu:
       Blockly.Blocks['procedures_callnoreturn'].customContextMenu,
   defType_: 'procedures_defreturn'
 };
 
 Blockly.Blocks['procedures_ifreturn'] = {
   /**
    * Block for conditionally returning a value from a procedure.
    * @this {Blockly.Block}
    */
   init: function() {
     this.appendValueInput('CONDITION')
         .setCheck('Boolean')
         .appendField(Blockly.Msg['CONTROLS_IF_MSG_IF']);
     this.appendValueInput('VALUE')
         .appendField(Blockly.Msg['PROCEDURES_DEFRETURN_RETURN']);
     this.setInputsInline(true);
     this.setPreviousStatement(true);
     this.setNextStatement(true);
     this.setStyle('procedure_blocks');
     this.setTooltip(Blockly.Msg['PROCEDURES_IFRETURN_TOOLTIP']);
     this.setHelpUrl(Blockly.Msg['PROCEDURES_IFRETURN_HELPURL']);
     this.hasReturnValue_ = true;
   },
   /**
    * Create XML to represent whether this block has a return value.
    * @return {!Element} XML storage element.
    * @this {Blockly.Block}
    */
   mutationToDom: function() {
     var container = Blockly.utils.xml.createElement('mutation');
     container.setAttribute('value', Number(this.hasReturnValue_));
     return container;
   },
   /**
    * Parse XML to restore whether this block has a return value.
    * @param {!Element} xmlElement XML storage element.
    * @this {Blockly.Block}
    */
   domToMutation: function(xmlElement) {
     var value = xmlElement.getAttribute('value');
     this.hasReturnValue_ = (value == 1);
     if (!this.hasReturnValue_) {
       this.removeInput('VALUE');
       this.appendDummyInput('VALUE')
           .appendField(Blockly.Msg['PROCEDURES_DEFRETURN_RETURN']);
     }
   },
   /**
    * Called whenever anything on the workspace changes.
    * Add warning if this flow block is not nested inside a loop.
    * @param {!Blockly.Events.Abstract} _e Change event.
    * @this {Blockly.Block}
    */
   onchange: function(_e) {
     if (!this.workspace.isDragging || this.workspace.isDragging()) {
       return;  // Don't change state at the start of a drag.
     }
     var legal = false;
     // Is the block nested in a procedure?
     var block = this;
     do {
       if (this.FUNCTION_TYPES.indexOf(block.type) != -1) {
         legal = true;
         break;
       }
       block = block.getSurroundParent();
     } while (block);
     if (legal) {
       // If needed, toggle whether this block has a return value.
       if (block.type == 'procedures_defnoreturn' && this.hasReturnValue_) {
         this.removeInput('VALUE');
         this.appendDummyInput('VALUE')
             .appendField(Blockly.Msg['PROCEDURES_DEFRETURN_RETURN']);
         this.hasReturnValue_ = false;
       } else if (block.type == 'procedures_defreturn' &&
                  !this.hasReturnValue_) {
         this.removeInput('VALUE');
         this.appendValueInput('VALUE')
             .appendField(Blockly.Msg['PROCEDURES_DEFRETURN_RETURN']);
         this.hasReturnValue_ = true;
       }
       this.setWarningText(null);
       if (!this.isInFlyout) {
         this.setEnabled(true);
       }
     } else {
       this.setWarningText(Blockly.Msg['PROCEDURES_IFRETURN_WARNING']);
       if (!this.isInFlyout && !this.getInheritedDisabled()) {
         this.setEnabled(false);
       }
     }
   },
   /**
    * List of block types that are functions and thus do not need warnings.
    * To add a new function type add this to your code:
    * Blockly.Blocks['procedures_ifreturn'].FUNCTION_TYPES.push('custom_func');
    */
   FUNCTION_TYPES: ['procedures_defnoreturn', 'procedures_defreturn']
 };
 
 Blockly.Blocks['procedures_return'] = {
   /**
    * Block for conditionally returning a value from a procedure.
    * @this Blockly.Block
    */
   init: function() {
     this.setStyle('procedure_blocks');
     // this.appendValueInput('CONDITION')
     //     .setCheck(Boolean)
     //     .appendField(Blockly.Msg.CONTROLS_IF_MSG_IF);
     this.appendValueInput('VALUE')
         .appendField(Blockly.Msg.PROCEDURES_DEFRETURN_RETURN);
     this.setInputsInline(true);
     this.setPreviousStatement(true);
     this.setNextStatement(true);
     this.setTooltip(Blockly.Msg.PROCEDURES_IFRETURN_TOOLTIP);
     this.hasReturnValue_ = true;
   },
   /**
    * Create XML to represent whether this block has a return value.
    * @return {!Element} XML storage element.
    * @this Blockly.Block
    */
   mutationToDom: function() {
     var container = document.createElement('mutation');
     container.setAttribute('value', Number(this.hasReturnValue_));
     return container;
   },
   /**
    * Parse XML to restore whether this block has a return value.
    * @param {!Element} xmlElement XML storage element.
    * @this Blockly.Block
    */
   domToMutation: function(xmlElement) {
     var value = xmlElement.getAttribute('value');
     this.hasReturnValue_ = (value == 1);
     if (!this.hasReturnValue_) {
       this.removeInput('VALUE');
       this.appendDummyInput('VALUE')
         .appendField(Blockly.Msg.PROCEDURES_DEFRETURN_RETURN);
     }
   },
   /**
    * Called whenever anything on the workspace changes.
    * Add warning if this flow block is not nested inside a loop.
    * @param {!Blockly.Events.Abstract} e Change event.
    * @this Blockly.Block
    */
   onchange: function(e) {
     var legal = false;
     // Is the block nested in a procedure?
     var block = this;
     do {
       if (this.FUNCTION_TYPES.indexOf(block.type) != -1) {
         legal = true;
         break;
       }
       block = block.getSurroundParent();
     } while (block);
     if (legal) {
       // If needed, toggle whether this block has a return value.
       if (block.type == 'procedures_defnoreturn' && this.hasReturnValue_) {
         this.removeInput('VALUE');
         this.appendDummyInput('VALUE')
           .appendField(Blockly.Msg.PROCEDURES_DEFRETURN_RETURN);
         this.hasReturnValue_ = false;
       } else if (block.type == 'procedures_defreturn' &&
                  !this.hasReturnValue_) {
         this.removeInput('VALUE');
         this.appendValueInput('VALUE')
           .appendField(Blockly.Msg.PROCEDURES_DEFRETURN_RETURN);
         this.hasReturnValue_ = true;
       }
       this.setWarningText(null);
     } else {
       this.setWarningText(Blockly.Msg.PROCEDURES_IFRETURN_WARNING);
     }
   },
   /**
    * List of block types that are functions and thus do not need warnings.
    * To add a new function type add this to your code:
    * Blockly.Blocks['procedures_ifreturn'].FUNCTION_TYPES.push('custom_func');
    */
   FUNCTION_TYPES: ['procedures_defnoreturn', 'procedures_defreturn','method_procedures_defnoreturn', 'method_procedures_defreturn']
 };