import shajs from "sha.js";
import { Hash } from "crypto";

/**
 * Returns a deterministic JSON string of the given object. The object is compacted as it goes to ensure that the
 * absence of a properties (for example) is the same as null or an empty array.
 */

const sortArrays = {
  field: comparisonFunctionFactory("tip"),
  i18n: comparisonFunctionFactory("lang"),
  parent: undefined, // Unicode sort
};

function comparisonFunctionFactory(key: string) {
  return function (a: any, b: any) {
    if (
      typeof a === "object" &&
      typeof b === "object" &&
      a[key] !== undefined &&
      b[key] !== undefined
    ) {
      return a[key] < b[key] ? -1 : a[key] > b[key] ? 1 : 0;
    }

    return 0;
  };
}

function RFCJSONEncode(obj: any): string {
  const json = JSON.stringify(obj);

  return json.replace(/[\u007F-\uFFFF]/g, function (chr) {
    return "\\u" + ("0000" + chr.charCodeAt(0).toString(16)).substr(-4);
  });
}

export function sidStringify(obj: any): string {
  if (obj === undefined || obj === null) {
    return "null";
  }

  if (obj instanceof Array) {
    return "[" + obj.map(sidStringify) + "]";
  }

  if (typeof obj === "object") {
    return (
      "{" +
      Object.keys(obj)
        .filter(function (key) {
          return !(
            obj[key] === undefined ||
            obj[key] === null ||
            (typeof obj[key] === "object" && Object.keys(obj[key]).length === 0)
          );
          // return true;
        })
        .sort()
        .map(function (key) {
          if (obj[key] instanceof Array && sortArrays.hasOwnProperty(key)) {
            return (
              RFCJSONEncode(key) +
              ":[" +
              obj[key]
                .slice()
                .sort((<any>sortArrays)[key])
                .map(sidStringify) +
              "]"
            );
          }

          return RFCJSONEncode(key) + ":" + sidStringify(obj[key]);
        }) +
      "}"
    );
  }

  return RFCJSONEncode(obj);
}

const hashChunkSize = 1024;
const hashUnicodeRegExp = /[\u007F-\uFFFF]/g;
const hashUnicodeReplacer = (chr: string): string => {
  return "\\u" + ("0000" + chr.charCodeAt(0).toString(16)).substr(-4);
};

function hashRFCJSONEncode(sha256: Hash, obj: any): Hash {
  if (typeof obj === "string") {
    const l = obj.length;
    sha256.update('"');
    for (let i = 0; i < l; i += hashChunkSize) {
      sha256.update(
        JSON.stringify(obj.substring(i, i + hashChunkSize))
          .slice(1, -1)
          .replace(hashUnicodeRegExp, hashUnicodeReplacer)
      );
    }
    sha256.update('"');
    return sha256;
  }
  return sha256.update(JSON.stringify(obj));
}

export function hashSidStringify(sha256: Hash, obj: any): Hash {
  if (obj === undefined || obj === null) {
    return sha256.update("null");
  }

  if (obj instanceof Array) {
    sha256.update("[");
    obj.forEach((elem, i) => {
      if (i > 0) {
        sha256.update(",");
      }
      hashSidStringify(sha256, elem);
    });
    return sha256.update("]");
  }

  if (typeof obj === "object") {
    sha256.update("{");
    Object.keys(obj)
      .filter((key: string) => {
        return !(
          obj[key] === undefined ||
          obj[key] === null ||
          (typeof obj[key] === "object" && Object.keys(obj[key]).length === 0)
        );
      })
      .sort()
      .forEach((key: string, i: number) => {
        if (i > 0) {
          sha256.update(",");
        }
        if (obj[key] instanceof Array && sortArrays.hasOwnProperty(key)) {
          hashRFCJSONEncode(sha256, key);
          sha256.update(":[");
          obj[key]
            .slice()
            .sort((<any>sortArrays)[key])
            .forEach((elem, i) => {
              if (i > 0) {
                sha256.update(",");
              }
              hashSidStringify(sha256, elem);
            });
          return sha256.update("]");
        }
        hashRFCJSONEncode(sha256, key);
        sha256.update(":");
        hashSidStringify(sha256, obj[key]);
      });
    return sha256.update("}");
  }

  return hashRFCJSONEncode(sha256, obj);
}
