text: /**
   * @license
   * Copyright 2012 Google LLC
   * SPDX-License-Identifier: Apache-2.0
   */
  
  /**
   * @fileoverview Utility functions for handling procedures.
   * @author fraser@google.com (Neil Fraser)
   */
  'use strict';
  
  /**
   * @name Blockly.Procedures
   * @namespace
   */
  goog.provide('Blockly.Procedures');
  
  goog.require('Blockly.Blocks');
  /** @suppress {extraRequire} */
  goog.require('Blockly.constants');
  goog.require('Blockly.Events');
  /** @suppress {extraRequire} */
  goog.require('Blockly.Events.BlockChange');
  goog.require('Blockly.Field');
  goog.require('Blockly.Msg');
  goog.require('Blockly.Names');
  goog.require('Blockly.utils.xml');
  goog.require('Blockly.Workspace');
  goog.require('Blockly.Xml');
  
  goog.requireType('Blockly.Block');
  goog.requireType('Blockly.Events.Abstract');
  goog.requireType('Blockly.WorkspaceSvg');
  
  
  /**
   * Constant to separate procedure names from variables and generated functions
   * when running generators.
   * @deprecated Use Blockly.PROCEDURE_CATEGORY_NAME
   */
  Blockly.Procedures.NAME_TYPE = Blockly.PROCEDURE_CATEGORY_NAME;
  
  /**
   * The default argument for a procedures_mutatorarg block.
   * @type {string}
   */
  Blockly.Procedures.DEFAULT_ARG = 'x';
  
  /**
   * Procedure block type.
   * @typedef {{
   *    getProcedureCall: function():string,
   *    renameProcedure: function(string,string),
   *    getProcedureDef: function():!Array
   * }}
   */
  Blockly.Procedures.ProcedureBlock;
  
  /**
   * Find all user-created procedure definitions in a workspace.
   * @param {!Blockly.Workspace} root Root workspace.
   * @return {!Array.<!Array.<!Array>>} Pair of arrays, the
   *     first contains procedures without return variables, the second with.
   *     Each procedure is defined by a three-element list of name, parameter
   *     list, and return value boolean.
   */
  Blockly.Procedures.allProcedures = function(root) {
    var proceduresNoReturn = root.getBlocksByType('procedures_defnoreturn', false)
        .map(function(block) {
          return /** @type {!Blockly.Procedures.ProcedureBlock} */ (block).getProcedureDef();
        });
    var proceduresReturn = root.getBlocksByType('procedures_defreturn', false).map(function(block) {
      return /** @type {!Blockly.Procedures.ProcedureBlock} */ (block).getProcedureDef();
    });
    proceduresNoReturn.sort(Blockly.Procedures.procTupleComparator_);
    proceduresReturn.sort(Blockly.Procedures.procTupleComparator_);
    return [proceduresNoReturn, proceduresReturn];
  };
  
  /**
   * Comparison function for case-insensitive sorting of the first element of
   * a tuple.
   * @param {!Array} ta First tuple.
   * @param {!Array} tb Second tuple.
   * @return {number} -1, 0, or 1 to signify greater than, equality, or less than.
   * @private
   */
  Blockly.Procedures.procTupleComparator_ = function(ta, tb) {
    return ta[0].toLowerCase().localeCompare(tb[0].toLowerCase());
  };
  
  /**
   * Ensure two identically-named procedures don't exist.
   * Take the proposed procedure name, and return a legal name i.e. one that
   * is not empty and doesn't collide with other procedures.
   * @param {string} name Proposed procedure name.
   * @param {!Blockly.Block} block Block to disambiguate.
   * @return {string} Non-colliding name.
   */
  Blockly.Procedures.findLegalName = function(name, block) {
    if (block.isInFlyout) {
      // Flyouts can have multiple procedures called 'do something'.
      return name;
    }
    name = name || Blockly.Msg['UNNAMED_KEY'] || 'unnamed';
    while (!Blockly.Procedures.isLegalName_(name, block.workspace, block)) {
      // Collision with another procedure.
      var r = name.match(/^(.*?)(\d+)$/);
      if (!r) {
        name += '2';
      } else {
        name = r[1] + (parseInt(r[2], 10) + 1);
      }
    }
    return name;
  };
  
  /**
   * Does this procedure have a legal name?  Illegal names include names of
   * procedures already defined.
   * @param {string} name The questionable name.
   * @param {!Blockly.Workspace} workspace The workspace to scan for collisions.
   * @param {Blockly.Block=} opt_exclude Optional block to exclude from
   *     comparisons (one doesn't want to collide with oneself).
   * @return {boolean} True if the name is legal.
   * @private
   */
  Blockly.Procedures.isLegalName_ = function(name, workspace, opt_exclude) {
    return !Blockly.Procedures.isNameUsed(name, workspace, opt_exclude);
  };
  
  /**
   * Return if the given name is already a procedure name.
   * @param {string} name The questionable name.
   * @param {!Blockly.Workspace} workspace The workspace to scan for collisions.
   * @param {Blockly.Block=} opt_exclude Optional block to exclude from
   *     comparisons (one doesn't want to collide with oneself).
   * @return {boolean} True if the name is used, otherwise return false.
   */
  Blockly.Procedures.isNameUsed = function(name, workspace, opt_exclude) {
    var blocks = workspace.getAllBlocks(false);
    // Iterate through every block and check the name.
    for (var i = 0; i < blocks.length; i++) {
      if (blocks[i] == opt_exclude) {
        continue;
      }
      if (blocks[i].getProcedureDef) {
        var procedureBlock = /** @type {!Blockly.Procedures.ProcedureBlock} */ (
          blocks[i]);
        var procName = procedureBlock.getProcedureDef();
        if (Blockly.Names.equals(procName[0], name)) {
          return true;
        }
      }
    }
    return false;
  };
  
  /**
   * Rename a procedure.  Called by the editable field.
   * @param {string} name The proposed new name.
   * @return {string} The accepted name.
   * @this {Blockly.Field}
   */
  Blockly.Procedures.rename = function(name) {
    // Strip leading and trailing whitespace.  Beyond this, all names are legal.
    name = name.trim();
  
    var legalName = Blockly.Procedures.findLegalName(name,
        /** @type {!Blockly.Block} */ (this.getSourceBlock()));
    var oldName = this.getValue();
    if (oldName != name && oldName != legalName) {
      // Rename any callers.
      var blocks = this.getSourceBlock().workspace.getAllBlocks(false);
      for (var i = 0; i < blocks.length; i++) {
        if (blocks[i].renameProcedure) {
          var procedureBlock = /** @type {!Blockly.Procedures.ProcedureBlock} */ (
            blocks[i]);
          procedureBlock.renameProcedure(
              /** @type {string} */ (oldName), legalName);
        }
      }
    }
    return legalName;
  };
  
  /**
   * Construct the blocks required by the flyout for the procedure category.
   * @param {!Blockly.Workspace} workspace The workspace containing procedures.
   * @return {!Array.<!Element>} Array of XML block elements.
   */
  Blockly.Procedures.flyoutCategory = function(workspace) {
    var xmlList = [];
    if (Blockly.Blocks['arduino_functions']) {
      // <block type="arduino_functions" gap="16"></block>
      var block = Blockly.utils.xml.createElement('block');
      block.setAttribute('type', 'arduino_functions');
      block.setAttribute('gap', 16);
      // If this parent block present already in the workspace show as disabled 
      var workspaceTopBlocks = workspace.getTopBlocks(false);
      for (var i = 0; i < workspaceTopBlocks.length; i++) {
        if (workspaceTopBlocks[i].getArduinoLoopsInstance &&
            workspaceTopBlocks[i].getArduinoLoopsInstance()) {
          block.setAttribute('disabled', true);
        }
      }
      xmlList.push(block);
    }
    if (Blockly.Blocks['arduino_declareupfront']) {
      var block = Blockly.utils.xml.createElement('block');
      block.setAttribute('type', 'arduino_declareupfront');
      block.setAttribute('gap', 16);
      // If this parent block present already in the workspace show as disabled 
      var workspaceTopBlocks = workspace.getTopBlocks(false);
      for (var i = 0; i < workspaceTopBlocks.length; i++) {
        if (workspaceTopBlocks[i].getArduinoDeclareUpfrontInstance &&
            workspaceTopBlocks[i].getArduinoDeclareUpfrontInstance()) {
          block.setAttribute('disabled', true);
        }
      }
      xmlList.push(block);
    }
    if (Blockly.Blocks['procedures_defnoreturn']) {
      // <block type="procedures_defnoreturn" gap="16">
      //     <field name="NAME">do something</field>
      // </block>
      var block = Blockly.utils.xml.createElement('block');
      block.setAttribute('type', 'procedures_defnoreturn');
      block.setAttribute('gap', 16);
      var nameField = Blockly.utils.xml.createElement('field');
      nameField.setAttribute('name', 'NAME');
      nameField.appendChild(Blockly.utils.xml.createTextNode(
          Blockly.Msg['PROCEDURES_DEFNORETURN_PROCEDURE']));
      block.appendChild(nameField);
      xmlList.push(block);
    }
    if (Blockly.Blocks['procedures_defreturn']) {
      // <block type="procedures_defreturn" gap="16">
      //     <field name="NAME">do something</field>
      // </block>
      var block = Blockly.utils.xml.createElement('block');
      block.setAttribute('type', 'procedures_defreturn');
      block.setAttribute('gap', 16);
      var nameField = Blockly.utils.xml.createElement('field');
      nameField.setAttribute('name', 'NAME');
      nameField.appendChild(Blockly.utils.xml.createTextNode(
          Blockly.Msg['PROCEDURES_DEFRETURN_PROCEDURE']));
      block.appendChild(nameField);
      xmlList.push(block);
    }
    if (Blockly.Blocks['procedures_ifreturn']) {
      // <block type="procedures_ifreturn" gap="16"></block>
      var block = Blockly.utils.xml.createElement('block');
      block.setAttribute('type', 'procedures_ifreturn');
      block.setAttribute('gap', 16);
      xmlList.push(block);
    }
    if (Blockly.Blocks['controls_effect']) {
      var block = Blockly.utils.xml.createElement('block');
      block.setAttribute('type', 'controls_effect');
      block.setAttribute('gap', 16);
      xmlList.push(block);
    }
    if (xmlList.length) {
      // Add slightly larger gap between system blocks and user calls.
      xmlList[xmlList.length - 1].setAttribute('gap', 24);
    }
  
    function populateProcedures(procedureList, templateName) {
      for (var i = 0; i < procedureList.length; i++) {
        var name = procedureList[i][0];
        var args = procedureList[i][1];
        // <block type="procedures_callnoreturn" gap="16">
        //   <mutation name="do something">
        //     <arg name="x"></arg>
        //   </mutation>
        // </block>
        var block = Blockly.utils.xml.createElement('block');
        block.setAttribute('type', templateName);
        block.setAttribute('gap', 16);
        var mutation = Blockly.utils.xml.createElement('mutation');
        mutation.setAttribute('name', name);
        block.appendChild(mutation);
        for (var j = 0; j < args.length; j++) {
          var arg = Blockly.utils.xml.createElement('arg');
          arg.setAttribute('name', args[j]);
          mutation.appendChild(arg);
        }
        xmlList.push(block);
      }
    }
  
    var tuple = Blockly.Procedures.allProcedures(workspace);
    populateProcedures(tuple[0], 'procedures_callnoreturn');
    populateProcedures(tuple[1], 'procedures_callreturn');
    return xmlList;
  };
  
  /**
   * Updates the procedure mutator's flyout so that the arg block is not a
   * duplicate of another arg.
   * @param {!Blockly.Workspace} workspace The procedure mutator's workspace. This
   *     workspace's flyout is what is being updated.
   * @private
   */
  Blockly.Procedures.updateMutatorFlyout_ = function(workspace) {
    var usedNames = [];
    var blocks = workspace.getBlocksByType('procedures_mutatorarg', false);
    for (var i = 0, block; (block = blocks[i]); i++) {
      usedNames.push(block.getFieldValue('NAME'));
    }
  
    var xml = Blockly.utils.xml.createElement('xml');
    var argBlock = Blockly.utils.xml.createElement('block');
    argBlock.setAttribute('type', 'procedures_mutatorarg');
    var nameField = Blockly.utils.xml.createElement('field');
    nameField.setAttribute('name', 'NAME');
    var argValue = Blockly.Variables.generateUniqueNameFromOptions(
        Blockly.Procedures.DEFAULT_ARG, usedNames);
    var fieldContent = Blockly.utils.xml.createTextNode(argValue);
  
    nameField.appendChild(fieldContent);
    argBlock.appendChild(nameField);
    xml.appendChild(argBlock);
  
    workspace.updateToolbox(xml);
  };
  
  /**
   * Listens for when a procedure mutator is opened. Then it triggers a flyout
   * update and adds a mutator change listener to the mutator workspace.
   * @param {!Blockly.Events.Abstract} e The event that triggered this listener.
   * @package
   */
  Blockly.Procedures.mutatorOpenListener = function(e) {
    if (!(e.type == Blockly.Events.BUBBLE_OPEN && e.bubbleType === 'mutator' &&
        e.isOpen)) {
      return;
    }
    var workspaceId = /** @type {string} */ (e.workspaceId);
    var block = Blockly.Workspace.getById(workspaceId)
        .getBlockById(e.blockId);
    var type = block.type;
    if (type != 'procedures_defnoreturn' && type != 'procedures_defreturn') {
      return;
    }
    var workspace = block.mutator.getWorkspace();
    Blockly.Procedures.updateMutatorFlyout_(workspace);
    workspace.addChangeListener(Blockly.Procedures.mutatorChangeListener_);
  };
  
  /**
   * Listens for changes in a procedure mutator and triggers flyout updates when
   * necessary.
   * @param {!Blockly.Events.Abstract} e The event that triggered this listener.
   * @private
   */
  Blockly.Procedures.mutatorChangeListener_ = function(e) {
    if (e.type != Blockly.Events.BLOCK_CREATE &&
        e.type != Blockly.Events.BLOCK_DELETE &&
        e.type != Blockly.Events.BLOCK_CHANGE) {
      return;
    }
    var workspaceId = /** @type {string} */ (e.workspaceId);
    var workspace = /** @type {!Blockly.WorkspaceSvg} */
        (Blockly.Workspace.getById(workspaceId));
    Blockly.Procedures.updateMutatorFlyout_(workspace);
  };
  
  /**
   * Find all the callers of a named procedure.
   * @param {string} name Name of procedure.
   * @param {!Blockly.Workspace} workspace The workspace to find callers in.
   * @return {!Array.<!Blockly.Block>} Array of caller blocks.
   */
  Blockly.Procedures.getCallers = function(name, workspace) {
    var callers = [];
    var blocks = workspace.getAllBlocks(false);
    // Iterate through every block and check the name.
    for (var i = 0; i < blocks.length; i++) {
      if (blocks[i].getProcedureCall) {
        var procedureBlock = /** @type {!Blockly.Procedures.ProcedureBlock} */ (
          blocks[i]);
        var procName = procedureBlock.getProcedureCall();
        // Procedure name may be null if the block is only half-built.
        if (procName && Blockly.Names.equals(procName, name)) {
          callers.push(blocks[i]);
        }
      }
    }
    return callers;
  };
  
  /**
   * When a procedure definition changes its parameters, find and edit all its
   * callers.
   * @param {!Blockly.Block} defBlock Procedure definition block.
   */
  Blockly.Procedures.mutateCallers = function(defBlock) {
    var oldRecordUndo = Blockly.Events.recordUndo;
    var procedureBlock = /** @type {!Blockly.Procedures.ProcedureBlock} */ (
      defBlock);
    var name = procedureBlock.getProcedureDef()[0];
    var xmlElement = defBlock.mutationToDom(true);
    var callers = Blockly.Procedures.getCallers(name, defBlock.workspace);
    for (var i = 0, caller; (caller = callers[i]); i++) {
      var oldMutationDom = caller.mutationToDom();
      var oldMutation = oldMutationDom && Blockly.Xml.domToText(oldMutationDom);
      caller.domToMutation(xmlElement);
      var newMutationDom = caller.mutationToDom();
      var newMutation = newMutationDom && Blockly.Xml.domToText(newMutationDom);
      if (oldMutation != newMutation) {
        // Fire a mutation on every caller block.  But don't record this as an
        // undo action since it is deterministically tied to the procedure's
        // definition mutation.
        Blockly.Events.recordUndo = false;
        Blockly.Events.fire(new (Blockly.Events.get(Blockly.Events.BLOCK_CHANGE))(
            caller, 'mutation', null, oldMutation, newMutation));
        Blockly.Events.recordUndo = oldRecordUndo;
      }
    }
  };
  
  /**
   * Find the definition block for the named procedure.
   * @param {string} name Name of procedure.
   * @param {!Blockly.Workspace} workspace The workspace to search.
   * @return {Blockly.Block} The procedure definition block, or null not found.
   */
  Blockly.Procedures.getDefinition = function(name, workspace) {
    // Assume that a procedure definition is a top block.
    var blocks = workspace.getTopBlocks(false);
    for (var i = 0; i < blocks.length; i++) {
      if (blocks[i].getProcedureDef) {
        var procedureBlock = /** @type {!Blockly.Procedures.ProcedureBlock} */ (
          blocks[i]);
        var tuple = procedureBlock.getProcedureDef();
        if (tuple && Blockly.Names.equals(tuple[0], name)) {
          return blocks[i];
        }
      }
    }
    return null;
  };
  