/**
 * Copyright (c) 2013-present, Facebook, Inc.
 * All rights reserved.
 *
 * This source code is licensed under the BSD-style license found in the
 * LICENSE file in the root directory of this source tree. An additional grant
 * of patent rights can be found in the PATENTS file in the same directory.
 *
 * @providesModule dumpReactTree
 * @flow
 */
'use strict';

const ReactNativeMount = require('react/lib/ReactNativeMount');
const getReactData = require('getReactData');

const INDENTATION_SIZE = 2;
const MAX_DEPTH = 2;
const MAX_STRING_LENGTH = 50;

/**
 * Dump all React Native root views and their content. This function tries
 * it best to get the content but ultimately relies on implementation details
 * of React and will fail in future versions.
 */
function dumpReactTree() {
  try {
    return getReactTree();
  } catch (e) {
    return 'Failed to dump react tree: ' + e;
  }
}

function getReactTree() {
  let output = '';
  const rootIds = Object.getOwnPropertyNames(ReactNativeMount._instancesByContainerID);
  for (const rootId of rootIds) {
    const instance = ReactNativeMount._instancesByContainerID[rootId];
    output += `============ Root ID: ${rootId} ============\n`;
    output += dumpNode(instance, 0);
    output += `============ End root ID: ${rootId} ============\n`;
  }
  return output;
}

function dumpNode(node: Object, identation: number) {
  const data = getReactData(node);
  if (data.nodeType === 'Text') {
    return indent(identation) + data.text + '\n';
  } else if (data.nodeType === 'Empty') {
    return '';
  }
  let output = indent(identation) + `<${data.name}`;
  if (data.nodeType === 'Composite') {
    for (const propName of Object.getOwnPropertyNames(data.props || {})) {
      if (isNormalProp(propName)) {
        try {
          const value = convertValue(data.props[propName]);
          if (value) {
            output += ` ${propName}=${value}`;
          }
        } catch (e) {
          const message = `[Failed to get property: ${e}]`;
          output += ` ${propName}=${message}`;
        }
      }
    }
  }
  let childOutput = '';
  for (const child of data.children || []) {
    childOutput += dumpNode(child, identation + 1);
  }

  if (childOutput) {
    output += '>\n' + childOutput + indent(identation) + `</${data.name}>\n`;
  } else {
    output += ' />\n';
  }

  return output;
}

function isNormalProp(name: string): boolean {
  switch (name) {
    case 'children':
    case 'key':
    case 'ref':
      return false;
    default:
      return true;
  }
}

function convertObject(object: Object, depth: number) {
  if (depth >= MAX_DEPTH) {
    return '[...omitted]';
  }
  let output = '{';
  let first = true;
  for (const key of Object.getOwnPropertyNames(object)) {
    if (!first) {
      output += ', ';
    }
    // $FlowFixMe(>=0.28.0)
    output += `${key}: ${convertValue(object[key], depth + 1)}`;
    first = false;
  }
  return output + '}';
}

function convertValue(value, depth = 0): ?string {
  if (!value) {
    return null;
  }

  switch (typeof value) {
    case 'string':
      return JSON.stringify(possiblyEllipsis(value).replace('\n', '\\n'));
    case 'boolean':
    case 'number':
      return JSON.stringify(value);
    case 'function':
      return '[function]';
    case 'object':
      return convertObject(value, depth);
    default:
      return null;
  }
}

function possiblyEllipsis(value: string) {
  if (value.length > MAX_STRING_LENGTH) {
    return value.slice(0, MAX_STRING_LENGTH) + '...';
  } else {
    return value;
  }
}

function indent(size: number) {
  return ' '.repeat(size * INDENTATION_SIZE);
}

module.exports = dumpReactTree;
