import { JSONObject, JSONValue } from "./validate";
import * as log from "./log";

/**
 * patches _old to deep equal _new
 * needed so objects can be updated without breaking references to them
 *
 * @param oldObject object to be updated
 * @param newObject object with values to use for update
 * @param path path to the object to be updated
 */

export function patchInPlace(
  oldObject: JSONObject,
  newObject: JSONObject,
  path = ""
): void {
  if (typeof oldObject !== "object") {
    log.error("_old is not an object");
    return;
  }
  if (typeof newObject !== "object") {
    log.error("_new is not an object");
    return;
  }

  const oldKeys = Object.keys(oldObject).reverse();
  const newKeys = Object.keys(newObject);

  // remove oldObject keys that are not in newObject
  for (const key of oldKeys) {
    if (!Object.prototype.hasOwnProperty.call(newObject, key)) {
      // log.debug(`remove ${_keyPath}.${key}`);
      if (Array.isArray(oldObject)) {
        oldObject.splice(parseInt(key), 1);
      } else {
        // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
        delete oldObject[key];
      }
    }
  }

  // add newObject keys that are not in oldObject
  for (const key of newKeys) {
    if (!Object.prototype.hasOwnProperty.call(oldObject, key)) {
      // log.debug(`add ${_keyPath}.${key}`);
      oldObject[key] = newObject[key];
    }
  }

  // patch shared object and array keys
  for (const key of newKeys) {
    if (Object.prototype.hasOwnProperty.call(oldObject, key)) {
      const oldType = getMergeType(oldObject[key]);
      const newType = getMergeType(newObject[key]);

      // bail if type is unsupported
      if (newType === "unsupported") {
        log.error(
          `${path}.${key} is unsupported type: ${typeof newObject[key]}`
        );
        continue;
      }

      // merge objects
      if (oldType === "object" && newType === "object") {
        // casts are safe; objects in JSONObjects are always JSON Objects
        patchInPlace(
          oldObject[key] as JSONObject,
          newObject[key] as JSONObject,
          `${path}.${key}`
        );
        continue;
      }

      // merge arrays
      if (oldType === "array" && newType === "array") {
        // casts are safe; arrays in JSONObjects can be treated by
        // this function as JSONObjects
        patchInPlace(
          oldObject[key] as JSONObject,
          newObject[key] as JSONObject,
          `${path}.${key}`
        );
        continue;
      }

      // replace everything else
      if (oldObject[key] !== newObject[key]) {
        oldObject[key] = newObject[key];
      }
    }
  }
}

// module.exports = { patchInPlace };
function getMergeType(value: JSONValue): string {
  if (value === null) return "null";
  if (Array.isArray(value)) return "array";
  if (typeof value === "object") return "object";
  if (typeof value === "boolean") return "primitive";
  if (typeof value === "number" && Number.isFinite(value)) return "primitive";
  if (typeof value === "string") return "primitive";
  return "unsupported";
}
