'use strict';

const {
  NODE_TYPE_IDS_MAP,
  NODE_TYPES_COUNT,
  LEAF_NODE_TYPES_COUNT,
} = require('../generated/lazy/types.js');

// Getter for private `#visitorsArr` property of `Visitor` class. Initialized in class body below.
let getVisitorsArr;

/**
 * Visitor class, used to visit an AST.
 */
class Visitor {
  #visitorsArr;

  /**
   * Create `Visitor`.
   *
   * Provide an object where keys are names of AST nodes you want to visit,
   * and values are visitor functions which receive AST node objects of that type.
   *
   * Keys can also be postfixed with `:exit` to visit when exiting the node, rather than entering.
   *
   * ```js
   * const visitor = new Visitor({
   *     BinaryExpression(binExpr) {
   *         // Do stuff when entering a `BinaryExpression`
   *     },
   *     'BinaryExpression:exit'(binExpr) {
   *         // Do stuff when exiting a `BinaryExpression`
   *     },
   * });
   * ```
   *
   * @constructor
   * @param {Object} visitor - Object defining visit functions for AST nodes
   * @returns {Visitor}
   */
  constructor(visitor) {
    this.#visitorsArr = createVisitorsArr(visitor);
  }

  static {
    getVisitorsArr = visitor => visitor.#visitorsArr;
  }
}

module.exports = { Visitor, getVisitorsArr };

/**
 * Create array of visitors, keyed by node type ID.
 *
 * Each element of array is one of:
 *
 * * No visitor for this type = `null`.
 * * Visitor for leaf node = visit function.
 * * Visitor for non-leaf node = object of form `{ enter, exit }`,
 *   where each property is either a visitor function or `null`.
 *
 * @param {Object} visitor - Visitors object from user
 * @returns {Array<Object|function|null>} - Array of visitors
 */
function createVisitorsArr(visitor) {
  if (visitor === null || typeof visitor !== 'object') {
    throw new Error('`visitor` must be an object');
  }

  // Create empty visitors array
  const visitorsArr = [];
  for (let i = NODE_TYPES_COUNT; i !== 0; i--) {
    visitorsArr.push(null);
  }

  // Populate visitors array from provided object
  for (let name of Object.keys(visitor)) {
    const visitFn = visitor[name];
    if (typeof visitFn !== 'function') {
      throw new Error(`'${name}' property of \`visitor\` object is not a function`);
    }

    const isExit = name.endsWith(':exit');
    if (isExit) name = name.slice(0, -5);

    const typeId = NODE_TYPE_IDS_MAP.get(name);
    if (typeId === void 0) throw new Error(`Unknown node type '${name}' in \`visitor\` object`);

    if (typeId < LEAF_NODE_TYPES_COUNT) {
      // Leaf node. Store just 1 function.
      const existingVisitFn = visitorsArr[typeId];
      if (existingVisitFn === null) {
        visitorsArr[typeId] = visitFn;
      } else if (isExit) {
        visitorsArr[typeId] = combineVisitFunctions(existingVisitFn, visitFn);
      } else {
        visitorsArr[typeId] = combineVisitFunctions(visitFn, existingVisitFn);
      }
      continue;
    }

    let enterExit = visitorsArr[typeId];
    if (enterExit === null) {
      enterExit = visitorsArr[typeId] = { enter: null, exit: null };
    }

    if (isExit) {
      enterExit.exit = visitFn;
    } else {
      enterExit.enter = visitFn;
    }
  }

  return visitorsArr;
}

/**
 * Combine 2 visitor functions into 1.
 *
 * @param {function} visit1 - 1st visitor function
 * @param {function} visit2 - 2nd visitor function
 * @returns {function} - Combined visitor function
 */
function combineVisitFunctions(visit1, visit2) {
  return function(node) {
    visit1(node);
    visit2(node);
  };
}
