/**
 * @license
 * Visual Blocks Editor
 *
 * Copyright 2012 Google Inc.
 * https://developers.google.com/blockly/
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

/**
 * @fileoverview Utility functions for handling variables.
 * @author fraser@google.com (Neil Fraser)
 */
'use strict';

/**
 * @name Blockly.Variables
 * @namespace
 **/
goog.provide('Blockly.Variables');

goog.require('Blockly.Blocks');
goog.require('Blockly.constants');
goog.require('Blockly.VariableModel');
goog.require('Blockly.Workspace');
goog.require('goog.string');


/**
 * Constant to separate variable names from procedures and generated functions
 * when running generators.
 * @deprecated Use Blockly.VARIABLE_CATEGORY_NAME
 */
Blockly.Variables.NAME_TYPE = Blockly.VARIABLE_CATEGORY_NAME;

/**
 * Find all user-created variables that are in use in the workspace.
 * For use by generators.
 * @param {!Blockly.Block|!Blockly.Workspace} root Root block or workspace.
 * @return {!Array.<string>} Array of variable names.
 */
Blockly.Variables.allUsedVariables = function(root) {
  var blocks;
  if (root instanceof Blockly.Block) {
    // Root is Block.
    blocks = root.getDescendants();
  } else if (root instanceof Blockly.Workspace ||
      root instanceof Blockly.WorkspaceSvg) {
    // Root is Workspace.
    blocks = root.getAllBlocks();
  } else {
    throw 'Not Block or Workspace: ' + root;
  }

  var ignorableName = Blockly.Variables.noVariableText();

  var variableHash = Object.create(null);
  // Iterate through every block and add each variable to the hash.
  for (var x = 0; x < blocks.length; x++) {
    var blockVariables = blocks[x].getVars();
    if (blockVariables) {
      for (var y = 0; y < blockVariables.length; y++) {
        var varName = blockVariables[y];
        // Variable name may be null if the block is only half-built.
        if (varName && varName.toLowerCase() != ignorableName) {
          variableHash[varName.toLowerCase()] = varName;
        }
      }
    }
  }
  // Flatten the hash into a list.
  var variableList = [];
  for (var name in variableHash) {
    variableList.push(variableHash[name]);
  }
  return variableList;
};

/**
 * Find all variables that the user has created through the workspace or
 * toolbox.  For use by generators.
 * @param {!Blockly.Workspace} root The workspace to inspect.
 * @return {!Array.<Blockly.VariableModel>} Array of variable models.
 */
Blockly.Variables.allVariables = function(root) {
  if (root instanceof Blockly.Block) {
    // Root is Block.
    console.warn('Deprecated call to Blockly.Variables.allVariables ' +
                 'with a block instead of a workspace.  You may want ' +
                 'Blockly.Variables.allUsedVariables');
    return {};
  }
  return root.getAllVariables();
};

/**
 * Construct the blocks required by the flyout for the variable category.
 * @param {!Blockly.Workspace} workspace The workspace contianing variables.
 * @return {!Array.<!Element>} Array of XML block elements.
 */
Blockly.Variables.flyoutCategory = function(workspace) {
  var variableModelList = workspace.getVariablesOfType('');
  variableModelList.sort(Blockly.VariableModel.compareByName);

  var xmlList = [];
  var button = goog.dom.createDom('button');
  button.setAttribute('text', Blockly.Msg.NEW_VARIABLE);
  button.setAttribute('callbackKey', 'CREATE_VARIABLE');

  workspace.registerButtonCallback('CREATE_VARIABLE', function(button) {
    Blockly.Variables.createVariable(button.getTargetWorkspace());
  });

  xmlList.push(button);

  for (var i = 0; i < variableModelList.length; i++) {
    if (Blockly.Blocks['data_variable']) {
      // <block type="data_variable">
      //    <field name="VARIABLE" variableType="" id="">variablename</field>
      // </block>
      var block = goog.dom.createDom('block');
      block.setAttribute('type', 'data_variable');
      block.setAttribute('gap', 8);
      block.setAttribute('id', 'VAR_' + variableModelList[i].name);

      var field = goog.dom.createDom('field', null, variableModelList[i].name);
      field.setAttribute('name', 'VARIABLE');
      field.setAttribute('variableType', variableModelList[i].type);
      field.setAttribute('id', variableModelList[i].getId());
      block.appendChild(field);

      xmlList.push(block);
    }
  }

  if (xmlList.length > 1) { // The button is always there.
    xmlList[xmlList.length - 1].setAttribute('gap', 24);

    if (Blockly.Blocks['data_setvariableto']) {
      // <block type="data_setvariableto" gap="20">
      //   <value name="VARIABLE">
      //    <shadow type="data_variablemenu"></shadow>
      //   </value>
      //   <value name="VALUE">
      //     <shadow type="text">
      //       <field name="TEXT">0</field>
      //     </shadow>
      //   </value>
      // </block>
      var block = goog.dom.createDom('block');
      block.setAttribute('type', 'data_setvariableto');
      block.setAttribute('gap', 8);
      block.appendChild(Blockly.Variables.createVariableDom_(variableModelList[0]));
      block.appendChild(Blockly.Variables.createTextDom_());
      xmlList.push(block);
    }
    if (Blockly.Blocks['data_changevariableby']) {
      // <block type="data_changevariableby">
      //   <value name="VARIABLE">
      //    <shadow type="data_variablemenu"></shadow>
      //   </value>
      //   <value name="VALUE">
      //     <shadow type="math_number">
      //       <field name="NUM">0</field>
      //     </shadow>
      //   </value>
      // </block>
      var block = goog.dom.createDom('block');
      block.setAttribute('type', 'data_changevariableby');
      block.setAttribute('gap', 8);
      block.appendChild(Blockly.Variables.createVariableDom_(variableModelList[0]));
      block.appendChild(Blockly.Variables.createMathNumberDom_());
      xmlList.push(block);
    }
    if (Blockly.Blocks['data_showvariable']) {
      // <block type="data_showvariable">
      //   <value name="VARIABLE">
      //     <shadow type="data_variablemenu"></shadow>
      //   </value>
      // </block>
      var block = goog.dom.createDom('block');
      block.setAttribute('type', 'data_showvariable');
      block.setAttribute('gap', 8);
      block.appendChild(Blockly.Variables.createVariableDom_(variableModelList[0]));
      xmlList.push(block);
    }
    if (Blockly.Blocks['data_hidevariable']) {
      // <block type="data_showvariable">
      //   <value name="VARIABLE">
      //     <shadow type="data_variablemenu"></shadow>
      //   </value>
      // </block>
      var block = goog.dom.createDom('block');
      block.setAttribute('type', 'data_hidevariable');
      block.appendChild(Blockly.Variables.createVariableDom_(variableModelList[0]));
      xmlList.push(block);
    }
  }
  return xmlList;
};

/**
 * Create a dom element for a value tag with the given name attribute.
 * @param {string} name The value to use for the name attribute.
 * @return {!Element} An XML element: <value name="name"></value>
 */
Blockly.Variables.createValueDom_ = function(name) {
  var value = goog.dom.createDom('value');
  value.setAttribute('name', name);
  return value;
};

/**
 * Create a dom element for a shadow tag with the given tupe attribute.
 * @param {string} type The value to use for the type attribute.
 * @param {string} value The value to have inside the tag.
 * @return {!Element} An XML element: <shadow type="type">value</shadow>
 */
Blockly.Variables.createShadowDom_ = function(type) {
  var shadow = goog.dom.createDom('shadow');
  shadow.setAttribute('type', type);
  return shadow;
};

/**
 * Create a dom element for value tag with a shadow variable inside.
 * @param {Blockly.VariableModel} variableModel The variable to use.
 * @return {!Element} An XML element.
 */
Blockly.Variables.createVariableDom_ = function(variableModel) {
  //   <value name="VARIABLE">
  //     <shadow type="data_variablemenu">
  //       <field name="VARIABLE">variablename
  //       </field>
  //     </shadow>
  //   </value>
  var value = Blockly.Variables.createValueDom_('VARIABLE');
  var shadow = Blockly.Variables.createShadowDom_('data_variablemenu');
  var field = goog.dom.createDom('field', null, variableModel.name);
  field.setAttribute('name', 'VARIABLE');
  field.setAttribute('variableType', variableModel.type);
  field.setAttribute('id', variableModel.getId());
  shadow.appendChild(field);
  value.appendChild(shadow);
  return value;
};

/**
 * Create a dom element for value tag with a shadow text inside.
 * @return {!Element} An XML element.
 */
Blockly.Variables.createTextDom_ = function() {
  //   <value name="VALUE">
  //     <shadow type="text">
  //       <field name="TEXT">0</field>
  //     </shadow>
  //   </value>
  var value = Blockly.Variables.createValueDom_('VALUE');
  var shadow = Blockly.Variables.createShadowDom_('text');
  var field = goog.dom.createDom('field', null, '0');
  field.setAttribute('name', 'TEXT');
  shadow.appendChild(field);
  value.appendChild(shadow);
  return value;
};

/**
 * Create a dom element for value tag with a shadow number inside.
 * @return {!Element} An XML element.
 */
Blockly.Variables.createMathNumberDom_ = function() {
  //   <value name="VALUE">
  //     <shadow type="math_number">
  //       <field name="NUM">0</field>
  //     </shadow>
  //   </value>
  var value = Blockly.Variables.createValueDom_('VALUE');
  var shadow = Blockly.Variables.createShadowDom_('math_number');
  var field = goog.dom.createDom('field', null, '1');
  field.setAttribute('name', 'NUM');
  shadow.appendChild(field);
  value.appendChild(shadow);
  return value;
};

/**
 * Return the text that should be used in a field_variable or
 * field_variable_getter when no variable exists.
 * TODO: #572
 * @return {string} The text to display.
 */
Blockly.Variables.noVariableText = function() {
  return "No variable selected";
};

/**
* Return a new variable name that is not yet being used. This will try to
* generate single letter variable names in the range 'i' to 'z' to start with.
* If no unique name is located it will try 'i' to 'z', 'a' to 'h',
* then 'i2' to 'z2' etc.  Skip 'l'.
 * @param {!Blockly.Workspace} workspace The workspace to be unique in.
* @return {string} New variable name.
*/
Blockly.Variables.generateUniqueName = function(workspace) {
  var variableList = workspace.getAllVariables();
  var newName = '';
  if (variableList.length) {
    var nameSuffix = 1;
    var letters = 'ijkmnopqrstuvwxyzabcdefgh';  // No 'l'.
    var letterIndex = 0;
    var potName = letters.charAt(letterIndex);
    while (!newName) {
      var inUse = false;
      for (var i = 0; i < variableList.length; i++) {
        if (variableList[i].name.toLowerCase() == potName) {
          // This potential name is already used.
          inUse = true;
          break;
        }
      }
      if (inUse) {
        // Try the next potential name.
        letterIndex++;
        if (letterIndex == letters.length) {
          // Reached the end of the character sequence so back to 'i'.
          // a new suffix.
          letterIndex = 0;
          nameSuffix++;
        }
        potName = letters.charAt(letterIndex);
        if (nameSuffix > 1) {
          potName += nameSuffix;
        }
      } else {
        // We can use the current potential name.
        newName = potName;
      }
    }
  } else {
    newName = 'i';
  }
  return newName;
};

/**
 * Create a new variable on the given workspace.
 * @param {!Blockly.Workspace} workspace The workspace on which to create the
 *     variable.
 * @param {function(?string=)=} opt_callback A callback. It will
 *     be passed an acceptable new variable name, or null if change is to be
 *     aborted (cancel button), or undefined if an existing variable was chosen.
 */
Blockly.Variables.createVariable = function(workspace, opt_callback) {
  var promptAndCheckWithAlert = function(defaultName) {
    Blockly.Variables.promptName(Blockly.Msg.NEW_VARIABLE_TITLE, defaultName,
      function(text) {
        if (text) {
          if (workspace.getVariable(text)) {
            Blockly.alert(Blockly.Msg.VARIABLE_ALREADY_EXISTS.replace('%1',
                text.toLowerCase()),
                function() {
                  promptAndCheckWithAlert(text);  // Recurse
                });
          }
          else if (!Blockly.Procedures.isLegalName_(text, workspace)) {
            Blockly.alert(Blockly.Msg.PROCEDURE_ALREADY_EXISTS.replace('%1',
                text.toLowerCase()),
                function() {
                  promptAndCheckWithAlert(text);  // Recurse
                });
          }
          else {
            workspace.createVariable(text);
            if (opt_callback) {
              opt_callback(text);
            }
          }
        } else {
          // User canceled prompt without a value.
          if (opt_callback) {
            opt_callback(null);
          }
        }
      });
  };
  promptAndCheckWithAlert('');
};

/**
 * Prompt the user for a new variable name.
 * @param {string} promptText The string of the prompt.
 * @param {string} defaultText The default value to show in the prompt's field.
 * @param {function(?string)} callback A callback. It will be passed the new
 *     variable name, or null if the user picked something illegal.
 */
Blockly.Variables.promptName = function(promptText, defaultText, callback) {
  Blockly.prompt(promptText, defaultText, function(newVar) {
    // Merge runs of whitespace.  Strip leading and trailing whitespace.
    // Beyond this, all names are legal.
    if (newVar) {
      newVar = newVar.replace(/[\s\xa0]+/g, ' ').replace(/^ | $/g, '');
      if (newVar == Blockly.Msg.RENAME_VARIABLE ||
          newVar == Blockly.Msg.NEW_VARIABLE) {
        // Ok, not ALL names are legal...
        newVar = null;
      }
    }
    callback(newVar);
  });
};