import isArray from 'lodash/isArray';
import isPlainObject from 'lodash/isPlainObject';
import mapValues from 'lodash/mapValues';

export interface Node {
  name: string;
  children?: this[];
  object?: unknown;
  value?: unknown;
}

function visit(
  parent: Node,
  visitFn: (parent: Node) => void,
  childrenFn: (parent: Node) => Node[] | undefined | null,
) {
  if (!parent) return;

  visitFn(parent);

  const children = childrenFn(parent);
  if (children) {
    const count = children.length;
    for (let i = 0; i < count; i++) {
      visit(children[i], visitFn, childrenFn);
    }
  }
}

function getNode(tree: Node, key: string): Node | null {
  let node = null;

  visit(
    tree,
    (d) => {
      if (d.name === key) {
        node = d;
      }
    },
    (d) => d.children,
  );

  return node;
}

export function map2tree(
  root: unknown,
  options: { key?: string; pushMethod?: 'push' | 'unshift' } = {},
  tree: Node = { name: options.key || 'state', children: [] },
  // eslint-disable-next-line @typescript-eslint/ban-types
): Node | {} {
  // eslint-disable-next-line @typescript-eslint/ban-types
  if (!isPlainObject(root) && root && !(root as { toJS: () => {} }).toJS) {
    return {};
  }

  const { key: rootNodeKey = 'state', pushMethod = 'push' } = options;
  const currentNode = getNode(tree, rootNodeKey);

  if (currentNode === null) {
    return {};
  }

  mapValues(
    // eslint-disable-next-line @typescript-eslint/ban-types
    root && (root as { toJS: () => {} }).toJS
      ? // eslint-disable-next-line @typescript-eslint/ban-types
        (root as { toJS: () => {} }).toJS()
      : // eslint-disable-next-line @typescript-eslint/ban-types
        (root as {}),
    // eslint-disable-next-line @typescript-eslint/ban-types
    (maybeImmutable: { toJS?: () => {} }, key) => {
      const value =
        maybeImmutable && maybeImmutable.toJS
          ? maybeImmutable.toJS()
          : maybeImmutable;
      const newNode: Node = { name: key };

      if (isArray(value)) {
        newNode.children = [];

        for (let i = 0; i < value.length; i++) {
          newNode.children[pushMethod]({
            name: `${key}[${i}]`,
            [isPlainObject(value[i]) ? 'object' : 'value']: value[i],
          });
        }
      } else if (isPlainObject(value)) {
        newNode.children = [];
      } else {
        newNode.value = value;
      }

      currentNode.children![pushMethod](newNode);

      map2tree(value, { key, pushMethod }, tree);
    },
  );

  return tree;
}
