/* @flow */

import {sortAlpha} from '../util/misc.js';
import {LOCKFILE_VERSION} from '../constants.js';

import {version as YARN_VERSION} from '../../package.json';
const NODE_VERSION = process.version;

function shouldWrapKey(str: string): boolean {
  return (
    str.indexOf('true') === 0 ||
    str.indexOf('false') === 0 ||
    /[:\s\n\\",\[\]]/g.test(str) ||
    /^[0-9]/g.test(str) ||
    !/^[a-zA-Z]/g.test(str)
  );
}

function maybeWrap(str: string | boolean | number): string {
  if (typeof str === 'boolean' || typeof str === 'number' || shouldWrapKey(str)) {
    return JSON.stringify(str);
  } else {
    return str;
  }
}

const priorities: {[key: string]: number} = {
  name: 1,
  version: 2,
  uid: 3,
  resolved: 4,
  integrity: 5,
  registry: 6,
  dependencies: 7,
};

function priorityThenAlphaSort(a: string, b: string): number {
  if (priorities[a] || priorities[b]) {
    return (priorities[a] || 100) > (priorities[b] || 100) ? 1 : -1;
  } else {
    return sortAlpha(a, b);
  }
}

type Options = {
  indent: string,
  topLevel?: boolean,
};

function _stringify(obj: {[key: string]: mixed}, options: Options): string {
  if (typeof obj !== 'object') {
    throw new TypeError();
  }

  const indent = options.indent;
  const lines = [];

  // Sorting order needs to be consistent between runs, we run native sort by name because there are no
  // problems with it being unstable because there are no to keys the same
  // However priorities can be duplicated and native sort can shuffle things from run to run
  const keys = Object.keys(obj).sort(priorityThenAlphaSort);

  let addedKeys = [];

  for (let i = 0; i < keys.length; i++) {
    const key = keys[i];
    const val = obj[key];
    if (val == null || addedKeys.indexOf(key) >= 0) {
      continue;
    }

    const valKeys = [key];

    // get all keys that have the same value equality, we only want this for objects
    if (typeof val === 'object') {
      for (let j = i + 1; j < keys.length; j++) {
        const key = keys[j];
        if (val === obj[key]) {
          valKeys.push(key);
        }
      }
    }

    const keyLine = valKeys.sort(sortAlpha).map(maybeWrap).join(', ');

    if (typeof val === 'string' || typeof val === 'boolean' || typeof val === 'number') {
      lines.push(`${keyLine} ${maybeWrap(val)}`);
    } else if (typeof val === 'object') {
      lines.push(`${keyLine}:\n${_stringify(val, {indent: indent + '  '})}` + (options.topLevel ? '\n' : ''));
    } else {
      throw new TypeError();
    }

    addedKeys = addedKeys.concat(valKeys);
  }

  return indent + lines.join(`\n${indent}`);
}

export default function stringify(obj: Object, noHeader?: boolean, enableVersions?: boolean): string {
  const val = _stringify(obj, {
    indent: '',
    topLevel: true,
  });
  if (noHeader) {
    return val;
  }

  const lines = [];
  lines.push('# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.');
  lines.push(`# yarn lockfile v${LOCKFILE_VERSION}`);
  if (enableVersions) {
    lines.push(`# yarn v${YARN_VERSION}`);
    lines.push(`# node ${NODE_VERSION}`);
  }
  lines.push('\n');
  lines.push(val);

  return lines.join('\n');
}
