/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

/*
 * Based on the Natural Sort algorithm for Javascript - Version 0.8.1 - adapted
 * for Firefox DevTools and released under the MIT license.
 *
 * Author: Jim Palmer (based on chunking idea from Dave Koelle)
 *
 * Repository:
 *   https://github.com/overset/javascript-natural-sort/
 */

"use strict";

const tokenizeNumbersRx =
  /(^([+\-]?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?(?=\D|\s|$))|^0x[\da-fA-F]+$|\d+)/g;
const hexRx = /^0x[0-9a-f]+$/i;
const startsWithNullRx = /^\0/;
const endsWithNullRx = /\0$/;
const whitespaceRx = /\s+/g;
const startsWithZeroRx = /^0/;
const versionRx = /^([\w-]+-)?\d+\.\d+\.\d+$/;
const numericDateRx = /^\d+[- /]\d+[- /]\d+$/;

// If a string contains any of these, we'll try to parse it as a Date
const dateKeywords = [
  "mon",
  "tues",
  "wed",
  "thur",
  "fri",
  "sat",
  "sun",

  "jan",
  "feb",
  "mar",
  "apr",
  "may",
  "jun",
  "jul",
  "aug",
  "sep",
  "oct",
  "nov",
  "dec",
];

/**
 * Figures whether a given string should be considered by naturalSort to be a
 * Date, and returns the Date's timestamp if so. Some Date formats, like
 * single numbers and MM.DD.YYYY, are not supported due to conflicts with things
 * like version numbers.
 */
function tryParseDate(str) {
  const lowerCaseStr = str.toLowerCase();
  return (
    !versionRx.test(str) &&
    (numericDateRx.test(str) ||
      dateKeywords.some(s => lowerCaseStr.includes(s))) &&
    Date.parse(str)
  );
}

/**
 * Sort numbers, strings, IP Addresses, Dates, Filenames, version numbers etc.
 * "the way humans do."
 *
 * @param  {object} a
 *         Passed in by Array.sort(a, b)
 * @param  {object} b
 *         Passed in by Array.sort(a, b)
 * @param  {string} sessionString
 *         Client-side value of storage-expires-session l10n string.
 *         Since this function can be called from both the client and the server,
 *         and given that client and server might have different locale, we can't compute
 *         the localized string directly from here.
 * @param  {boolean} insensitive
 *         Should the search be case insensitive?
 */
// eslint-disable-next-line complexity
function naturalSort(a = "", b = "", sessionString, insensitive = false) {
  // Ensure we are working with trimmed strings
  a = (a + "").trim();
  b = (b + "").trim();

  if (insensitive) {
    a = a.toLowerCase();
    b = b.toLowerCase();
    sessionString = sessionString.toLowerCase();
  }

  // Chunk/tokenize - Here we split the strings into arrays or strings and
  // numbers.
  const aChunks = a
    .replace(tokenizeNumbersRx, "\0$1\0")
    .replace(startsWithNullRx, "")
    .replace(endsWithNullRx, "")
    .split("\0");
  const bChunks = b
    .replace(tokenizeNumbersRx, "\0$1\0")
    .replace(startsWithNullRx, "")
    .replace(endsWithNullRx, "")
    .split("\0");

  // Hex or date detection.
  const aHexOrDate = parseInt(a.match(hexRx), 16) || tryParseDate(a);
  const bHexOrDate = parseInt(b.match(hexRx), 16) || tryParseDate(b);

  if (
    (aHexOrDate || bHexOrDate) &&
    (a === sessionString || b === sessionString)
  ) {
    // We have a date and a session string. Move "Session" above the date
    // (for session cookies)
    if (a === sessionString) {
      return -1;
    } else if (b === sessionString) {
      return 1;
    }
  }

  // Try and sort Hex codes or Dates.
  if (aHexOrDate && bHexOrDate) {
    if (aHexOrDate < bHexOrDate) {
      return -1;
    } else if (aHexOrDate > bHexOrDate) {
      return 1;
    }
    return 0;
  }

  // Natural sorting through split numeric strings and default strings
  const aChunksLength = aChunks.length;
  const bChunksLength = bChunks.length;
  const maxLen = Math.max(aChunksLength, bChunksLength);

  for (let i = 0; i < maxLen; i++) {
    const aChunk = normalizeChunk(aChunks[i] || "", aChunksLength);
    const bChunk = normalizeChunk(bChunks[i] || "", bChunksLength);

    // Handle numeric vs string comparison - number < string
    if (isNaN(aChunk) !== isNaN(bChunk)) {
      return isNaN(aChunk) ? 1 : -1;
    }

    // If unicode use locale comparison
    // eslint-disable-next-line no-control-regex
    if (/[^\x00-\x80]/.test(aChunk + bChunk) && aChunk.localeCompare) {
      const comp = aChunk.localeCompare(bChunk);
      return comp / Math.abs(comp);
    }
    if (aChunk < bChunk) {
      return -1;
    } else if (aChunk > bChunk) {
      return 1;
    }
  }
  return null;
}

// Normalize spaces; find floats not starting with '0', string or 0 if not
// defined
const normalizeChunk = function (str, length) {
  return (
    ((!str.match(startsWithZeroRx) || length == 1) && parseFloat(str)) ||
    str.replace(whitespaceRx, " ").trim() ||
    0
  );
};

exports.naturalSortCaseSensitive = function naturalSortCaseSensitive(
  a,
  b,
  sessionString
) {
  return naturalSort(a, b, sessionString, false);
};

exports.naturalSortCaseInsensitive = function naturalSortCaseInsensitive(
  a,
  b,
  sessionString
) {
  return naturalSort(a, b, sessionString, true);
};
