import visitorKeys from '../types/visitor-keys';
import {
  cannotRemoveNode,
  cannotReplaceNode,
  cannotReplaceOrRemoveInKeyHandlerYet
} from './errors';
import * as nodes from '../types/nodes';

export interface NodeVisitor {
  All?: NodeHandler<nodes.BaseNode>;
  Program?: NodeHandler<nodes.Program>;
  MustacheStatement?: NodeHandler<nodes.MustacheStatement>;
  BlockStatement?: NodeHandler<nodes.BlockStatement>;
  ElementModifierStatement?: NodeHandler<nodes.ElementModifierStatement>;
  PartialStatement?: NodeHandler<nodes.PartialStatement>;
  CommentStatement?: NodeHandler<nodes.CommentStatement>;
  MustacheCommentStatement?: NodeHandler<nodes.MustacheCommentStatement>;
  ElementNode?: NodeHandler<nodes.ElementNode>;
  AttrNode?: NodeHandler<nodes.AttrNode>;
  TextNode?: NodeHandler<nodes.TextNode>;
  ConcatStatement?: NodeHandler<nodes.ConcatStatement>;
  SubExpression?: NodeHandler<nodes.SubExpression>;
  PathExpression?: NodeHandler<nodes.PathExpression>;
  StringLiteral?: NodeHandler<nodes.StringLiteral>;
  BooleanLiteral?: NodeHandler<nodes.BooleanLiteral>;
  NumberLiteral?: NodeHandler<nodes.NumberLiteral>;
  UndefinedLiteral?: NodeHandler<nodes.UndefinedLiteral>;
  NullLiteral?: NodeHandler<nodes.NullLiteral>;
  Hash?: NodeHandler<nodes.Hash>;
  HashPair?: NodeHandler<nodes.HashPair>;
}

export type NodeHandler<T> = NodeHandlerFunction<T> | EnterExitNodeHandler<T>;

export interface NodeHandlerFunction<T> {
  (node: T): any;
}

export interface EnterExitNodeHandler<T> {
  enter: NodeHandlerFunction<T>;
  exit: NodeHandlerFunction<T>;
  keys: any;
}

function visitNode(visitor, node: nodes.BaseNode) {
  let handler = visitor[node.type] || visitor.All;
  let result;

  if (handler && handler.enter) {
    result = handler.enter.call(null, node);
  }

  if (result !== undefined && result !== null) {
    if (JSON.stringify(node) === JSON.stringify(result)) {
      result = undefined;
    } else if (Array.isArray(result)) {
      return visitArray(visitor, result) || result;
    } else {
      return visitNode(visitor, result) || result;
    }
  }

  if (result === undefined) {
    let keys = visitorKeys[node.type];

    for (let i = 0; i < keys.length; i++) {
      visitKey(visitor, handler, node, keys[i]);
    }

    if (handler && handler.exit) {
      result = handler.exit.call(null, node);
    }
  }

  return result;
}

function visitKey(visitor, handler, node: nodes.BaseNode, key) {
  let value = node[key];
  if (!value) { return; }

  let keyHandler = handler && (handler.keys[key] || handler.keys.All);
  let result;

  if (keyHandler && keyHandler.enter) {
    result = keyHandler.enter.call(null, node, key);
    if (result !== undefined) {
      throw cannotReplaceOrRemoveInKeyHandlerYet(node, key);
    }
  }

  if (Array.isArray(value)) {
    visitArray(visitor, value);
  } else {
    let result = visitNode(visitor, value);
    if (result !== undefined) {
      assignKey(node, key, result);
    }
  }

  if (keyHandler && keyHandler.exit) {
    result = keyHandler.exit.call(null, node, key);
    if (result !== undefined) {
      throw cannotReplaceOrRemoveInKeyHandlerYet(node, key);
    }
  }
}

function visitArray(visitor, array) {
  for (let i = 0; i < array.length; i++) {
    let result = visitNode(visitor, array[i]);
    if (result !== undefined) {
      i += spliceArray(array, i, result) - 1;
    }
  }
}

function assignKey(node: nodes.BaseNode, key, result) {
  if (result === null) {
    throw cannotRemoveNode(node[key], node, key);
  } else if (Array.isArray(result)) {
    if (result.length === 1) {
      node[key] = result[0];
    } else {
      if (result.length === 0) {
        throw cannotRemoveNode(node[key], node, key);
      } else {
        throw cannotReplaceNode(node[key], node, key);
      }
    }
  } else {
    node[key] = result;
  }
}

function spliceArray(array, index, result) {
  if (result === null) {
    array.splice(index, 1);
    return 0;
  } else if (Array.isArray(result)) {
    array.splice(index, 1, ...result);
    return result.length;
  } else {
    array.splice(index, 1, result);
    return 1;
  }
}

export default function traverse(node: nodes.BaseNode, visitor: NodeVisitor) {
  visitNode(normalizeVisitor(visitor), node);
}

export function normalizeVisitor(visitor: NodeVisitor) {
  let normalizedVisitor = {};

  for (let type in visitor) {
    let handler = visitor[type] || visitor.All;
    let normalizedKeys = {};

    if (typeof handler === 'object') {
      let keys = handler.keys;
      if (keys) {
        for (let key in keys) {
          let keyHandler = keys[key];
          if (typeof keyHandler === 'object') {
            normalizedKeys[key] = {
              enter: (typeof keyHandler.enter === 'function') ? keyHandler.enter : null,
              exit: (typeof keyHandler.exit === 'function') ? keyHandler.exit : null
            };
          } else if (typeof keyHandler === 'function') {
            normalizedKeys[key] = {
              enter: keyHandler,
              exit: null
            };
          }
        }
      }

      normalizedVisitor[type] = {
        enter: (typeof handler.enter === 'function') ? handler.enter : null,
        exit: (typeof handler.exit === 'function') ? handler.exit : null,
        keys: normalizedKeys
      };
    } else if (typeof handler === 'function') {
      normalizedVisitor[type] = {
        enter: handler,
        exit: null,
        keys: normalizedKeys
      };
    }
  }

  return normalizedVisitor;
}
