/**
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 *
 * @flow
 */

import escapeStringRegExp from 'escape-string-regexp';
import {meta} from '../../hydration';
import {formatDataForPreview} from '../../utils';
import isArray from 'react-devtools-shared/src/isArray';

import type {HooksTree} from 'react-debug-tools/src/ReactDebugHooks';

// $FlowFixMe[method-unbinding]
const hasOwnProperty = Object.prototype.hasOwnProperty;

export function alphaSortEntries(
  entryA: [string, mixed],
  entryB: [string, mixed],
): number {
  const a = entryA[0];
  const b = entryB[0];
  if (String(+a) === a) {
    if (String(+b) !== b) {
      return -1;
    }
    return +a < +b ? -1 : 1;
  }
  return a < b ? -1 : 1;
}

export function createRegExp(string: string): RegExp {
  // Allow /regex/ syntax with optional last /
  if (string[0] === '/') {
    // Cut off first slash
    string = string.slice(1);
    // Cut off last slash, but only if it's there
    if (string[string.length - 1] === '/') {
      string = string.slice(0, string.length - 1);
    }
    try {
      return new RegExp(string, 'i');
    } catch (err) {
      // Bad regex. Make it not match anything.
      // TODO: maybe warn in console?
      return new RegExp('.^');
    }
  }

  function isLetter(char: string) {
    return char.toLowerCase() !== char.toUpperCase();
  }

  function matchAnyCase(char: string) {
    if (!isLetter(char)) {
      // Don't mess with special characters like [.
      return char;
    }
    return '[' + char.toLowerCase() + char.toUpperCase() + ']';
  }

  // 'item' should match 'Item' and 'ListItem', but not 'InviteMom'.
  // To do this, we'll slice off 'tem' and check first letter separately.
  const escaped = escapeStringRegExp(string);
  const firstChar = escaped[0];
  let restRegex = '';
  // For 'item' input, restRegex becomes '[tT][eE][mM]'
  // We can't simply make it case-insensitive because first letter case matters.
  for (let i = 1; i < escaped.length; i++) {
    restRegex += matchAnyCase(escaped[i]);
  }

  if (!isLetter(firstChar)) {
    // We can't put a non-character like [ in a group
    // so we fall back to the simple case.
    return new RegExp(firstChar + restRegex);
  }

  // Construct a smarter regex.
  return new RegExp(
    // For example:
    // (^[iI]|I)[tT][eE][mM]
    // Matches:
    // 'Item'
    // 'ListItem'
    // but not 'InviteMom'
    '(^' +
      matchAnyCase(firstChar) +
      '|' +
      firstChar.toUpperCase() +
      ')' +
      restRegex,
  );
}

export function getMetaValueLabel(data: Object): string | null {
  if (hasOwnProperty.call(data, meta.preview_long)) {
    return data[meta.preview_long];
  } else {
    return formatDataForPreview(data, true);
  }
}

function sanitize(data: Object): void {
  for (const key in data) {
    const value = data[key];

    if (value && value[meta.type]) {
      data[key] = getMetaValueLabel(value);
    } else if (value != null) {
      if (isArray(value)) {
        sanitize(value);
      } else if (typeof value === 'object') {
        sanitize(value);
      }
    }
  }
}

export function serializeDataForCopy(props: Object): string {
  const cloned = isArray(props) ? props.slice(0) : Object.assign({}, props);

  sanitize(cloned);

  try {
    return JSON.stringify(cloned, null, 2);
  } catch (error) {
    return '';
  }
}

export function serializeHooksForCopy(hooks: HooksTree | null): string {
  // $FlowFixMe[not-an-object] "HooksTree is not an object"
  const cloned = Object.assign(([]: Array<any>), hooks);

  const queue = [...cloned];

  while (queue.length > 0) {
    const current = queue.pop();

    // These aren't meaningful
    // $FlowFixMe[incompatible-use]
    delete current.id;
    // $FlowFixMe[incompatible-use]
    delete current.isStateEditable;

    // $FlowFixMe[incompatible-use]
    if (current.subHooks.length > 0) {
      // $FlowFixMe[incompatible-use]
      queue.push(...current.subHooks);
    }
  }

  sanitize(cloned);

  try {
    return JSON.stringify(cloned, null, 2);
  } catch (error) {
    return '';
  }
}

// Keeping this in memory seems to be enough to enable the browser to download larger profiles.
// Without this, we would see a "Download failed: network error" failure.
let downloadUrl = null;

export function downloadFile(
  element: HTMLAnchorElement,
  filename: string,
  text: string,
): void {
  const blob = new Blob([text], {type: 'text/plain;charset=utf-8'});

  if (downloadUrl !== null) {
    URL.revokeObjectURL(downloadUrl);
  }

  downloadUrl = URL.createObjectURL(blob);

  element.setAttribute('href', downloadUrl);
  element.setAttribute('download', filename);

  element.click();
}

export function truncateText(text: string, maxLength: number): string {
  const {length} = text;
  if (length > maxLength) {
    return (
      text.slice(0, Math.floor(maxLength / 2)) +
      '…' +
      text.slice(length - Math.ceil(maxLength / 2) - 1)
    );
  } else {
    return text;
  }
}
