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

/**
 * @fileoverview The class representing a basic cursor.
 * Used to demo switching between different cursors.
 * @author aschmiedt@google.com (Abby Schmiedt)
 */
'use strict';

goog.provide('Blockly.BasicCursor');

goog.require('Blockly.ASTNode');
goog.require('Blockly.Cursor');
goog.require('Blockly.registry');


/**
 * Class for a basic cursor.
 * This will allow the user to get to all nodes in the AST by hitting next or
 * previous.
 * @constructor
 * @extends {Blockly.Cursor}
 */
Blockly.BasicCursor = function() {
  Blockly.BasicCursor.superClass_.constructor.call(this);
};
Blockly.utils.object.inherits(Blockly.BasicCursor, Blockly.Cursor);

/**
 * Name used for registering a basic cursor.
 * @const {string}
 */
Blockly.BasicCursor.registrationName = 'basicCursor';

/**
 * Find the next node in the pre order traversal.
 * @return {Blockly.ASTNode} The next node, or null if the current node is
 *     not set or there is no next value.
 * @override
 */
Blockly.BasicCursor.prototype.next = function() {
  var curNode = this.getCurNode();
  if (!curNode) {
    return null;
  }
  var newNode = this.getNextNode_(curNode, this.validNode_);

  if (newNode) {
    this.setCurNode(newNode);
  }
  return newNode;
};

/**
 * For a basic cursor we only have the ability to go next and previous, so
 * in will also allow the user to get to the next node in the pre order
 * traversal.
 * @return {Blockly.ASTNode} The next node, or null if the current node is
 *     not set or there is no next value.
 * @override
 */
Blockly.BasicCursor.prototype.in = function() {
  return this.next();
};

/**
 * Find the previous node in the pre order traversal.
 * @return {Blockly.ASTNode} The previous node, or null if the current node
 *     is not set or there is no previous value.
 * @override
 */
Blockly.BasicCursor.prototype.prev = function() {
  var curNode = this.getCurNode();
  if (!curNode) {
    return null;
  }
  var newNode = this.getPreviousNode_(curNode, this.validNode_);

  if (newNode) {
    this.setCurNode(newNode);
  }
  return newNode;
};

/**
 * For a basic cursor we only have the ability to go next and previous, so
 * out will allow the user to get to the previous node in the pre order
 * traversal.
 * @return {Blockly.ASTNode} The previous node, or null if the current node is
 *     not set or there is no previous value.
 * @override
 */
Blockly.BasicCursor.prototype.out = function() {
  return this.prev();
};

/**
 * Uses pre order traversal to navigate the Blockly AST. This will allow
 * a user to easily navigate the entire Blockly AST without having to go in
 * and out levels on the tree.
 * @param {Blockly.ASTNode} node The current position in the AST.
 * @param {!function(Blockly.ASTNode) : boolean} isValid A function true/false
 *     depending on whether the given node should be traversed.
 * @return {Blockly.ASTNode} The next node in the traversal.
 * @protected
 */
Blockly.BasicCursor.prototype.getNextNode_ = function(node, isValid) {
  if (!node) {
    return null;
  }
  var newNode = node.in() || node.next();
  if (isValid(newNode)) {
    return newNode;
  } else if (newNode) {
    return this.getNextNode_(newNode, isValid);
  }
  var siblingOrParent = this.findSiblingOrParent_(node.out());
  if (isValid(siblingOrParent)) {
    return siblingOrParent;
  } else if (siblingOrParent) {
    return this.getNextNode_(siblingOrParent, isValid);
  }
  return null;
};

/**
 * Reverses the pre order traversal in order to find the previous node. This
 * will allow a user to easily navigate the entire Blockly AST without having to
 * go in and out levels on the tree.
 * @param {Blockly.ASTNode} node The current position in the AST.
 * @param {!function(Blockly.ASTNode) : boolean} isValid A function true/false
 *     depending on whether the given node should be traversed.
 * @return {Blockly.ASTNode} The previous node in the traversal or null if no
 *     previous node exists.
 * @protected
 */
Blockly.BasicCursor.prototype.getPreviousNode_ = function(node, isValid) {
  if (!node) {
    return null;
  }
  var newNode = node.prev();

  if (newNode) {
    newNode = this.getRightMostChild_(newNode);
  } else {
    newNode = node.out();
  }
  if (isValid(newNode)) {
    return newNode;
  } else if (newNode) {
    return this.getPreviousNode_(newNode, isValid);
  }
  return null;
};

/**
 * Decides what nodes to traverse and which ones to skip. Currently, it
 * skips output, stack and workspace nodes.
 * @param {Blockly.ASTNode} node The AST node to check whether it is valid.
 * @return {boolean} True if the node should be visited, false otherwise.
 * @protected
 */
Blockly.BasicCursor.prototype.validNode_ = function(node) {
  var isValid = false;
  var type = node && node.getType();
  if (type == Blockly.ASTNode.types.OUTPUT ||
      type == Blockly.ASTNode.types.INPUT ||
      type == Blockly.ASTNode.types.FIELD ||
      type == Blockly.ASTNode.types.NEXT ||
      type == Blockly.ASTNode.types.PREVIOUS ||
      type == Blockly.ASTNode.types.WORKSPACE) {
    isValid = true;
  }
  return isValid;
};

/**
 * From the given node find either the next valid sibling or parent.
 * @param {Blockly.ASTNode} node The current position in the AST.
 * @return {Blockly.ASTNode} The parent AST node or null if there are no
 *     valid parents.
 * @private
 */
Blockly.BasicCursor.prototype.findSiblingOrParent_ = function(node) {
  if (!node) {
    return null;
  }
  var nextNode = node.next();
  if (nextNode) {
    return nextNode;
  }
  return this.findSiblingOrParent_(node.out());
};


/**
 * Get the right most child of a node.
 * @param {Blockly.ASTNode} node The node to find the right most child of.
 * @return {Blockly.ASTNode} The right most child of the given node, or the node
 *     if no child exists.
 * @private
 */
Blockly.BasicCursor.prototype.getRightMostChild_ = function(node) {
  if (!node.in()) {
    return node;
  }
  var newNode = node.in();
  while (newNode.next()) {
    newNode = newNode.next();
  }
  return this.getRightMostChild_(newNode);
};

Blockly.registry.register(
    Blockly.registry.Type.CURSOR, Blockly.BasicCursor.registrationName,
    Blockly.BasicCursor);
