/**
 * @license
 * Visual Blocks Editor
 *
 * Copyright 2018 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 methods for Scratch Blocks but not Blockly.
 * @author fenichel@google.com (Rachel Fenichel)
 */
'use strict';

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


/**
 * Measure some text using a canvas in-memory.
 * Does not exist in Blockly, but needed in scratch-blocks
 * @param {string} fontSize E.g., '10pt'
 * @param {string} fontFamily E.g., 'Arial'
 * @param {string} fontWeight E.g., '600'
 * @param {string} text The actual text to measure
 * @return {number} Width of the text in px.
 * @package
 */
Blockly.scratchBlocksUtils.measureText = function(fontSize, fontFamily,
    fontWeight, text) {
  var canvas = document.createElement('canvas');
  var context = canvas.getContext('2d');
  context.font = fontWeight + ' ' + fontSize + ' ' + fontFamily;
  return context.measureText(text).width;
};

/**
 * Encode a string's HTML entities.
 * E.g., <a> -> &lt;a&gt;
 * Does not exist in Blockly, but needed in scratch-blocks
 * @param {string} rawStr Unencoded raw string to encode.
 * @return {string} String with HTML entities encoded.
 * @package
 */
Blockly.scratchBlocksUtils.encodeEntities = function(rawStr) {
  // CC-BY-SA https://stackoverflow.com/questions/18749591/encode-html-entities-in-javascript
  return rawStr.replace(/[\u00A0-\u9999<>&]/gim, function(i) {
    return '&#' + i.charCodeAt(0) + ';';
  });
};

/**
 * Re-assign obscured shadow blocks new IDs to prevent collisions
 * Scratch specific to help the VM handle deleting obscured shadows.
 * @param {Blockly.Block} block the root block to be processed.
 * @package
 */
Blockly.scratchBlocksUtils.changeObscuredShadowIds = function(block) {
  var blocks = block.getDescendants(false);
  for (var i = blocks.length - 1; i >= 0; i--) {
    var descendant = blocks[i];
    for (var j = 0; j < descendant.inputList.length; j++) {
      var connection = descendant.inputList[j].connection;
      if (connection) {
        var shadowDom = connection.getShadowDom();
        if (shadowDom) {
          shadowDom.setAttribute('id', Blockly.utils.genUid());
          connection.setShadowDom(shadowDom);
        }
      }
    }
  }
};

/**
 * Whether a block is both a shadow block and an argument reporter.  These
 * blocks have special behaviour in scratch-blocks: they're duplicated when
 * dragged, and they are rendered slightly differently from normal shadow
 * blocks.
 * @param {!Blockly.BlockSvg} block The block that should be used to make this
 *     decision.
 * @return {boolean} True if the block should be duplicated on drag.
 * @package
 */
Blockly.scratchBlocksUtils.isShadowArgumentReporter = function(block) {
  return (block.isShadow() && (block.type == 'argument_reporter_boolean' ||
      block.type == 'argument_reporter_string_number'));
};

/**
 * Compare strings with natural number sorting.
 * @param {string} str1 First input.
 * @param {string} str2 Second input.
 * @return {number} -1, 0, or 1 to signify greater than, equality, or less than.
 */
Blockly.scratchBlocksUtils.compareStrings = function(str1, str2) {
  return str1.localeCompare(str2, [], {
    sensitivity: 'base',
    numeric: true
  });
};

/**
 * Determine if this block can be recycled in the flyout.  Blocks that have no
 * variablees and are not dynamic shadows can be recycled.
 * @param {Blockly.Block} block The block to check.
 * @return {boolean} True if the block can be recycled.
 * @package
 */
Blockly.scratchBlocksUtils.blockIsRecyclable = function(block) {
  // If the block needs to parse mutations, never recycle.
  if (block.mutationToDom && block.domToMutation) {
    return false;
  }

  for (var i = 0; i < block.inputList.length; i++) {
    var input = block.inputList[i];
    for (var j = 0; j < input.fieldRow.length; j++) {
      var field = input.fieldRow[j];
      // No variables.
      if (field instanceof Blockly.FieldVariable ||
          field instanceof Blockly.FieldVariableGetter) {
        return false;
      }
      if (field instanceof Blockly.FieldDropdown ||
          field instanceof Blockly.FieldNumberDropdown ||
          field instanceof Blockly.FieldTextDropdown) {
        if (field.isOptionListDynamic()) {
          return false;
        }
      }
    }
    // Check children.
    if (input.connection) {
      var child = input.connection.targetBlock();
      if (child && !Blockly.scratchBlocksUtils.blockIsRecyclable(child)) {
        return false;
      }
    }
  }
  return true;
};


/**
 * Creates a callback function for a click on the "duplicate" context menu
 * option in Scratch Blocks.  The block is duplicated and attached to the mouse,
 * which acts as though it were pressed and mid-drag.  Clicking the mouse
 * releases the new dragging block.
 * @param {!Blockly.BlockSvg} oldBlock The block that will be duplicated.
 * @param {!Event} event Event that caused the context menu to open.
 * @return {Function} A callback function that duplicates the block and starts a
 *     drag.
 * @package
 */
Blockly.scratchBlocksUtils.duplicateAndDragCallback = function(oldBlock, event) {
  var isMouseEvent = Blockly.Touch.getTouchIdentifierFromEvent(event) === 'mouse';
  return function(e) {
    // Give the context menu a chance to close.
    setTimeout(function() {
      var ws = oldBlock.workspace;
      var svgRootOld = oldBlock.getSvgRoot();
      if (!svgRootOld) {
        throw new Error('oldBlock is not rendered.');
      }

      // Create the new block by cloning the block in the flyout (via XML).
      var xml = Blockly.Xml.blockToDom(oldBlock);
      // The target workspace would normally resize during domToBlock, which
      // will lead to weird jumps.
      // Resizing will be enabled when the drag ends.
      ws.setResizesEnabled(false);

      // Disable events and manually emit events after the block has been
      // positioned and has had its shadow IDs fixed (Scratch-specific).
      Blockly.Events.disable();
      try {
        // Using domToBlock instead of domToWorkspace means that the new block
        // will be placed at position (0, 0) in main workspace units.
        var newBlock = Blockly.Xml.domToBlock(xml, ws);

        // Scratch-specific: Give shadow dom new IDs to prevent duplicating on paste
        Blockly.scratchBlocksUtils.changeObscuredShadowIds(newBlock);

        var svgRootNew = newBlock.getSvgRoot();
        if (!svgRootNew) {
          throw new Error('newBlock is not rendered.');
        }

        // The position of the old block in workspace coordinates.
        var oldBlockPosWs = oldBlock.getRelativeToSurfaceXY();

        // Place the new block as the same position as the old block.
        // TODO: Offset by the difference between the mouse position and the upper
        // left corner of the block.
        newBlock.moveBy(oldBlockPosWs.x, oldBlockPosWs.y);
        if (!isMouseEvent) {
          var offsetX = ws.RTL ? -100 : 100;
          var offsetY = 100;
          newBlock.moveBy(offsetX, offsetY); // Just offset the block for touch.
        }
      } finally {
        Blockly.Events.enable();
      }
      if (Blockly.Events.isEnabled()) {
        Blockly.Events.fire(new Blockly.Events.BlockCreate(newBlock));
      }

      if (isMouseEvent) {
        // e is not a real mouseEvent/touchEvent/pointerEvent.  It's an event
        // created by the context menu and has the coordinates of the mouse
        // click that opened the context menu.
        var fakeEvent = {
          clientX: event.clientX,
          clientY: event.clientY,
          type: 'mousedown',
          preventDefault: function() {
            e.preventDefault();
          },
          stopPropagation: function() {
            e.stopPropagation();
          },
          target: e.target
        };
        ws.startDragWithFakeEvent(fakeEvent, newBlock);
      }
    }, 0);
  };
};