import orderedJson from 'json-order';
import beautify_js from 'js-beautify';

interface JSResponse<T> {
  /** results wanted, it will always be of the desired type */
  response: T | undefined;
  /** if true, response if valid, if false: response was forced
   * casted clean and it will not be correct, expect error */
  valid: boolean;
  /**
   * it will contain the error message if valid is false
   */
  error: string | undefined;
  /**
   * original value being operated on
   */
  value: any;
}

export default class JS {
  /**
   * @param srcObj an object with the properties in any order
   * @param map [optional]: the property map generated by parse above.
   * @param separator [optional]: a non-empty string that controls what the key separator is in the generated map. Defaults to ~.
   * If the map is unset, the response is a standard JSON.stringify.
   * @param space [optional]: a number used to insert white space into the output JSON string
   * for readability purposes, as per the JSON.stringify
   * @returns
   */
  static jsonStringify = (srcObj: object, map?: any, separator?: string) => {
    return orderedJson.stringify(srcObj, map, separator);
  };
  static jsonParse = (src: string) => {
    return orderedJson.parse(src);
  };
  static stringify(
    value: object,
    pretty?: boolean,
    jsonNotation?: boolean
  ): JSResponse<string> {
    if (typeof value === 'object') {
      try {
        const objectString = JS.objectToString(value, jsonNotation);
        // remove last dangling comma
        const str = objectString.replace(/,\s*$/, '');
        const prettyStr = JS.prettify(str);
        return {
          response: pretty ? prettyStr : str.replaceAll('\\', ' '),
          valid: true,
          error: undefined,
          value
        };
      } catch (e) {
        return {
          response: undefined,
          valid: false,
          error: JSON.stringify(e),
          value
        };
      }
    }
    return {
      response: undefined,
      valid: false,
      error: 'value is not an object',
      value
    };
  }

  static jsSingleQuotesToDouble(value: string): string {
    return value.replace(/'/g, '"');
  }

  static jsStringObjectToJSONObject(value: string) {
    const jsonStr = value.replace(/(\w+:)|(\w+ :)/g, function (matchedStr) {
      return '"' + matchedStr.substring(0, matchedStr.length - 1) + '":';
    });
    if (!jsonStr) {
      return '';
    }
    if (jsonStr.startsWith('{')) {
      return jsonStr;
    } else {
      return `{${jsonStr}}`;
    }
  }

  /**
   * Check if a string has valid JSON object syntax
   * @param text string to check if valid json
   * @returns false if not valid json, true if valid json
   */
  static testJSON = (text: string): boolean => {
    if (typeof text !== 'string') {
      return false;
    }
    try {
      JSON.parse(text);
      return true;
    } catch (error) {
      return false;
    }
  };

  static jsStringToObject(value: string, retry?: boolean): JSResponse<object> {
    if (typeof value === 'string') {
      const jsonStr = value.replace(/(\w+:)|(\w+ :)/g, function (matchedStr) {
        return '"' + matchedStr.substring(0, matchedStr.length - 1) + '":';
      });
      try {
        //converts to a regular object
        const response = {
          response: JSON.parse(jsonStr),
          valid: true,
          error: undefined,
          value
        };
        return response;
      } catch (error: any) {
        if (retry) {
          // has already tried to fix the string
          return {
            response: undefined,
            valid: false,
            error: String(error),
            value
          };
        } else {
          // retrying
          return JS.jsStringToObject(JS.escapeJSON(value), true);
        }
      }
    }

    return {
      response: undefined,
      valid: false,
      error: 'value is not a string',
      value
    };
  }

  static prettify(value: string): string {
    if (!value) {
      return '';
    }

    if (value.startsWith('{')) {
      return beautify_js(value, {
        indent_size: 2,
        js: {
          preserve_newlines: true,
          escape_quotes: true
        }
      });
    }
    return beautify_js(`{${value}}`, {
      indent_size: 2,
      quote_keys: true,
      js: {
        preserve_newlines: true,
        escape_quotes: true,
        quote_keys: true
      }
    });
  }

  static objectToString(obj: any, jsonNotation?: boolean) {
    var str = '';
    var i = 0;
    for (var key in obj) {
      if (obj.hasOwnProperty(key)) {
        if (typeof obj[key] == 'object') {
          if (obj[key] instanceof Array) {
            if (jsonNotation) {
              str += `"${key}":[`;
            } else {
              //let's check if the key has special characters
              //so we can keep the quotes
              if (key.match(/[^a-zA-Z0-9]/)) {
                str += `"${key}": [`;
              } else {
                str += `${key}:[`;
              }
            }

            for (var j = 0; j < obj[key].length; j++) {
              const item = obj[key][j];
              if (typeof item == 'object') {
                const strObj =
                  JS.objectToString(item, jsonNotation) +
                  (j <= item.length - 1 ? ',' : '');
                const newStr = strObj.replace(/,\s*$/, '');
                // substitute string with cleaned up string

                str += '{' + newStr + '},';
              } else if (typeof item == 'string') {
                // let's double quote it
                str += `"${item}",`; //non objects would be represented as strings
              } else {
                str += `${item},`;
              }
            }
            // remove last comma
            const newStr = str.replace(/,\s*$/, '');
            // substitute string with cleaned up string
            str = newStr;
            str += '],';
          } else {
            const objStr = JS.objectToString(obj[key], jsonNotation);
            // remove last comma
            const newStr = objStr.replace(/,\s*$/, '');
            // substitute string with cleaned up string
            str += `"${key}"` + ':{' + newStr + '},';
          }
        } else {
          const item = obj[key];
          if (typeof item == 'string') {
            // let's double quote it
            if (jsonNotation) {
              str += `"${key}":"${obj[key]}",`;
            } else {
              //let's check if the key has special characters
              //so we can keep the quotes
              if (key.match(/[^a-zA-Z0-9]/)) {
                str += `"${key}":"${obj[key]}",`;
              } else {
                str += `${key}:"${obj[key]}",`;
              }
            }
          } else {
            if (jsonNotation) {
              str += `"${key}":${obj[key]},`;
            } else {
              //let's check if the key has special characters
              //so we can keep the quotes
              if (key.match(/[^a-zA-Z0-9]/)) {
                str += `"${key}":${obj[key]},`;
              } else {
                str += `${key}:${obj[key]},`;
              }
            }
          }
        }
        i++;
      }
    }
    return str;
  }

  static escapeJSON(string: string) {
    return string.replace(/[\n"\&\r\t\b\f\\\/]/g, '\\$&');
  }

  static stringToObject(str: string): { [id: string]: any } {
    const obj: { [id: string]: any } = {};
    const markers: number[] = [];
    const chars = str.split('');
    chars.forEach((char, index) => {
      if (char === ':') {
        markers.push(index);
      }
    });

    markers.forEach((marker) => {
      // detect if : is indeed a marker
      if (chars[marker + 1] !== '\\') {
        // it is a marker
        const key = str.substring(0, marker);
        const value = str.substring(marker + 1);
        obj[key] = value;
      }
    });

    return obj;
  }

  /**
   * Order the CSSProperties by applying a map to it
   * @param obj CSS Properties object
   * @param map map with the css properties in sequence to render
   * @returns obj, if no Map is provided, it will return the same object
   */
  static applyMapToObj(obj: any, map?: any) {
    if (!map) {
      return obj;
    }
    const newObj: any = {};
    for (const key in obj) {
      if (obj.hasOwnProperty(key)) {
        const element = obj[key];
        if (map[key]) {
          newObj[map[key]] = element;
        } else {
          newObj[key] = element;
        }
      }
    }
    return newObj;
  }
}
