/* This file is a part of @mdn/browser-compat-data
 * See LICENSE file for more information. */

import { compareVersions } from 'compare-versions';

/** @import {CompatStatement, SimpleSupportStatement, StatusBlock, BrowserStatement, ReleaseStatement} from '../../types/types.js' */

const propOrder = {
  browsers: {
    browser: [
      'name',
      'type',
      'upstream',
      'preview_name',
      'pref_url',
      'accepts_flags',
      'accepts_webextensions',
      'releases',
    ],
    release: [
      'release_date',
      'release_notes',
      'status',
      'engine',
      'engine_version',
    ],
  },
  data: {
    __compat: [
      'description',
      'mdn_url',
      'spec_url',
      'tags',
      'support',
      'status',
    ],
    support: [
      'alternative_name',
      'prefix',
      'version_added',
      'version_removed',
      'flags',
      'impl_url',
      'partial_implementation',
      'notes',
    ],
    status: ['experimental', 'standard_track', 'deprecated'],
  },
};

/**
 * Perform property ordering
 * @template T
 * @param {T} value The object to order properties for
 * @param {string[]} order The order to follow
 * @returns {T} The ordered object
 */
const doOrder = (value, order) => {
  if (value && typeof value === 'object') {
    return /** @type {T} */ (
      order.reduce((result, key) => {
        if (key in value) {
          result[key] = value[key];
        }
        return result;
      }, /** @type {Record<string, *>} */ ({}))
    );
  }
  return value;
};

/**
 * Return a stringified version of the releases list in a browser file, with a
 * prefix and suffix added, which will be removed after performing JSON
 * stringification. This is important because JavaScript wants to move object
 * entries with a floating point as the key to the very end of the list.
 * @param {Record<string, ReleaseStatement>} releases The release data
 * @returns {string} The stringified releases
 */
export const stringifyReleases = (releases) => {
  const indentStep = '  '; // Constant with the indent step that sortStringify will use

  const sortedKeys = Object.keys(releases).sort(compareVersions);

  let result = '';
  for (let i = 0; i < sortedKeys.length; i++) {
    const k = sortedKeys[i];
    const v = JSON.stringify(releases[k], null, 2).replace(/\n/g, '\n  ');

    // Add it to the result
    result += `${indentStep}"${k}": ${v}`;

    // Check if this is the last entry or not: if not, add a comma
    if (i != sortedKeys.length - 1) {
      result += ',';
    }

    // We always need a carriage return
    result += '\n';
  }
  return `*#*#{\n${result}}#*#*`; // Close the brace and return the string
};

/**
 * Return a new feature object whose first-level properties have been
 * ordered according to doOrder, and so will be stringified in that
 * order as well. This relies on guaranteed "own" property ordering,
 * which is insertion order for non-integer keys (which is our case).
 * @param {string} key The key in the object
 * @param {*} value The value of the key
 * @returns {*} The new value
 */
export const orderProperties = (key, value) => {
  if (value instanceof Object) {
    // Order properties for data
    if ('__compat' in value) {
      value.__compat = doOrder(
        /** @type {CompatStatement} */ (value.__compat),
        propOrder.data.__compat,
      );

      for (const browser of Object.keys(value.__compat.support)) {
        /** @type {SimpleSupportStatement[]} */
        const result = [];
        let data = value.__compat.support[browser];
        if (!Array.isArray(data)) {
          data = [data];
        }

        for (const statement of data) {
          result.push(
            doOrder(
              /** @type {SimpleSupportStatement} */ (statement),
              propOrder.data.support,
            ),
          );
        }

        value.__compat.support[browser] =
          result.length === 1 ? result[0] : result;
      }

      if ('status' in value.__compat) {
        value.__compat.status = doOrder(
          /** @type {StatusBlock} */ (value.__compat.status),
          propOrder.data.status,
        );
      }
    }

    // Order properties for browsers
    if ('browsers' in value) {
      const browser = Object.keys(value.browsers)[0];

      value.browsers[browser] = doOrder(
        /** @type {BrowserStatement} */ (value.browsers[browser]),
        propOrder.browsers.browser,
      );

      for (const r of Object.keys(value.browsers[browser].releases)) {
        value.browsers[browser].releases[r] = doOrder(
          /** @type {ReleaseStatement} */ (value.browsers[browser].releases[r]),
          propOrder.browsers.release,
        );
      }

      value.browsers[browser].releases = stringifyReleases(
        value.browsers[browser].releases,
      );
    }
  }
  return value;
};

/**
 * Stringify an object in a specific order of properties
 * @param {*} rawdata The object to stringify
 * @returns {string} The stringified object
 */
const stringifyAndOrderProperties = (rawdata) => {
  if (rawdata instanceof Object) {
    rawdata = JSON.stringify(rawdata);
  }
  const data = JSON.parse(rawdata, orderProperties);

  if ('browsers' in data) {
    // Browser data needs to be stringified in a special way due to the release data
    return JSON.stringify(data, null, 2)
      .replace('"*#*#', '')
      .replace('#*#*"', '')
      .replace(/\\n/g, '\n      ')
      .replace(/\\"/g, '"');
  }

  return JSON.stringify(data, null, 2);
};

export default stringifyAndOrderProperties;
