/* 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/. */

"use strict";

loader.lazyRequireGetter(
  this,
  "validator",
  "devtools/shared/storage/vendor/stringvalidator/validator"
);
loader.lazyRequireGetter(this, "JSON5", "devtools/shared/storage/vendor/json5");

const MATH_REGEX = /(?:(?:^|[-+_*/])(?:\s*-?\d+(\.\d+)?(?:[eE][+-]?\d+)?\s*))+$/;

/**
 * Tries to parse a string into an object on the basis of key-value pairs,
 * separated by various separators. If failed, tries to parse for single
 * separator separated values to form an array.
 *
 * @param {string} value
 *        The string to be parsed into an object or array
 */
function _extractKeyValPairs(value) {
  const makeObject = (keySep, pairSep) => {
    const object = {};
    for (const pair of value.split(pairSep)) {
      const [key, val] = pair.split(keySep);
      object[key] = val;
    }
    return object;
  };

  // Possible separators.
  const separators = ["=", ":", "~", "#", "&", "\\*", ",", "\\."];
  // Testing for object
  for (let i = 0; i < separators.length; i++) {
    const kv = separators[i];
    for (let j = 0; j < separators.length; j++) {
      if (i == j) {
        continue;
      }
      const p = separators[j];
      const word = `[^${kv}${p}]*`;
      const keyValue = `${word}${kv}${word}`;
      const keyValueList = `${keyValue}(${p}${keyValue})*`;
      const regex = new RegExp(`^${keyValueList}$`);
      if (
        value.match &&
        value.match(regex) &&
        value.includes(kv) &&
        (value.includes(p) || value.split(kv).length == 2)
      ) {
        return makeObject(kv, p);
      }
    }
  }
  // Testing for array
  for (const p of separators) {
    const word = `[^${p}]*`;
    const wordList = `(${word}${p})+${word}`;
    const regex = new RegExp(`^${wordList}$`);

    if (regex.test(value)) {
      const pNoBackslash = p.replace(/\\*/g, "");
      return value.split(pNoBackslash);
    }
  }
  return null;
}

/**
 * Check whether the value string represents something that should be
 * displayed as text. If so then it shouldn't be parsed into a tree.
 *
 * @param  {String} value
 *         The value to be parsed.
 */
function _shouldParse(value) {
  const validators = [
    "isBase64",
    "isBoolean",
    "isCurrency",
    "isDataURI",
    "isEmail",
    "isFQDN",
    "isHexColor",
    "isIP",
    "isISO8601",
    "isMACAddress",
    "isSemVer",
    "isURL",
  ];

  // Check for minus calculations e.g. 8-3 because otherwise 5 will be displayed.
  if (MATH_REGEX.test(value)) {
    return false;
  }

  // Check for any other types that shouldn't be parsed.
  for (const test of validators) {
    if (validator[test](value)) {
      return false;
    }
  }

  // Seems like this is data that should be parsed.
  return true;
}

/**
 * Tries to parse a string value into either a json or a key-value separated
 * object. The value can also be a key separated array.
 *
 * @param {string} originalValue
 *        The string to be parsed into an object
 */
function parseItemValue(originalValue) {
  // Find if value is URLEncoded ie
  let decodedValue = "";
  try {
    decodedValue = decodeURIComponent(originalValue);
  } catch (e) {
    // Unable to decode, nothing to do
  }
  const value =
    decodedValue && decodedValue !== originalValue
      ? decodedValue
      : originalValue;

  if (!_shouldParse(value)) {
    return value;
  }

  let obj = null;
  try {
    obj = JSON5.parse(value);
  } catch (ex) {
    obj = null;
  }

  if (!obj && value) {
    obj = _extractKeyValPairs(value);
  }

  // return if obj is null, or same as value, or just a string.
  if (!obj || obj === value || typeof obj === "string") {
    return value;
  }

  // If we got this far, originalValue is an object literal or array,
  // and we have successfully parsed it
  return obj;
}

exports.parseItemValue = parseItemValue;
