// Copyright (c) Jupyter Development Team.
// Distributed under the terms of the Modified BSD License.
/*-----------------------------------------------------------------------------
| Copyright (c) 2014-2017, PhosphorJS Contributors
|
| Distributed under the terms of the BSD 3-Clause License.
|
| The full license is in the file LICENSE, distributed with this software.
|----------------------------------------------------------------------------*/

/**
 * A type alias for a JSON primitive.
 */
export type JSONPrimitive = boolean | number | string | null;

/**
 * A type alias for a JSON value.
 */
export type JSONValue = JSONPrimitive | JSONObject | JSONArray;

/**
 * A type definition for a JSON object.
 */
export interface JSONObject {
  [key: string]: JSONValue;
}

/**
 * A type definition for a JSON array.
 */
export interface JSONArray extends Array<JSONValue> {}

/**
 * A type definition for a readonly JSON object.
 */
export interface ReadonlyJSONObject {
  readonly [key: string]: ReadonlyJSONValue;
}

/**
 * A type definition for a readonly JSON array.
 */
export interface ReadonlyJSONArray extends ReadonlyArray<ReadonlyJSONValue> {}

/**
 * A type alias for a readonly JSON value.
 */
export type ReadonlyJSONValue =
  | JSONPrimitive
  | ReadonlyJSONObject
  | ReadonlyJSONArray;

/**
 * A type alias for a partial JSON value.
 *
 * Note: Partial here means that JSON object attributes can be `undefined`.
 */
export type PartialJSONValue =
  | JSONPrimitive
  | PartialJSONObject
  | PartialJSONArray;

/**
 * A type definition for a partial JSON object.
 *
 * Note: Partial here means that the JSON object attributes can be `undefined`.
 */
export interface PartialJSONObject {
  [key: string]: PartialJSONValue | undefined;
}

/**
 * A type definition for a partial JSON array.
 *
 * Note: Partial here means that JSON object attributes can be `undefined`.
 */
export interface PartialJSONArray extends Array<PartialJSONValue> {}

/**
 * A type definition for a readonly partial JSON object.
 *
 * Note: Partial here means that JSON object attributes can be `undefined`.
 */
export interface ReadonlyPartialJSONObject {
  readonly [key: string]: ReadonlyPartialJSONValue | undefined;
}

/**
 * A type definition for a readonly partial JSON array.
 *
 * Note: Partial here means that JSON object attributes can be `undefined`.
 */
export interface ReadonlyPartialJSONArray
  extends ReadonlyArray<ReadonlyPartialJSONValue> {}

/**
 * A type alias for a readonly partial JSON value.
 *
 * Note: Partial here means that JSON object attributes can be `undefined`.
 */
export type ReadonlyPartialJSONValue =
  | JSONPrimitive
  | ReadonlyPartialJSONObject
  | ReadonlyPartialJSONArray;

/**
 * The namespace for JSON-specific functions.
 */
export namespace JSONExt {
  /**
   * A shared frozen empty JSONObject
   */
  export const emptyObject = Object.freeze({}) as ReadonlyJSONObject;

  /**
   * A shared frozen empty JSONArray
   */
  export const emptyArray = Object.freeze([]) as ReadonlyJSONArray;

  /**
   * Test whether a JSON value is a primitive.
   *
   * @param value - The JSON value of interest.
   *
   * @returns `true` if the value is a primitive,`false` otherwise.
   */
  export function isPrimitive(
    value: ReadonlyPartialJSONValue
  ): value is JSONPrimitive {
    return (
      value === null ||
      typeof value === 'boolean' ||
      typeof value === 'number' ||
      typeof value === 'string'
    );
  }

  /**
   * Test whether a JSON value is an array.
   *
   * @param value - The JSON value of interest.
   *
   * @returns `true` if the value is a an array, `false` otherwise.
   */
  export function isArray(value: JSONValue): value is JSONArray;
  export function isArray(value: ReadonlyJSONValue): value is ReadonlyJSONArray;
  export function isArray(value: PartialJSONValue): value is PartialJSONArray;
  export function isArray(
    value: ReadonlyPartialJSONValue
  ): value is ReadonlyPartialJSONArray;
  export function isArray(value: ReadonlyPartialJSONValue): boolean {
    return Array.isArray(value);
  }

  /**
   * Test whether a JSON value is an object.
   *
   * @param value - The JSON value of interest.
   *
   * @returns `true` if the value is a an object, `false` otherwise.
   */
  export function isObject(value: JSONValue): value is JSONObject;
  export function isObject(
    value: ReadonlyJSONValue
  ): value is ReadonlyJSONObject;
  export function isObject(value: PartialJSONValue): value is PartialJSONObject;
  export function isObject(
    value: ReadonlyPartialJSONValue
  ): value is ReadonlyPartialJSONObject;
  export function isObject(value: ReadonlyPartialJSONValue): boolean {
    return !isPrimitive(value) && !isArray(value);
  }

  /**
   * Compare two JSON values for deep equality.
   *
   * @param first - The first JSON value of interest.
   *
   * @param second - The second JSON value of interest.
   *
   * @returns `true` if the values are equivalent, `false` otherwise.
   */
  export function deepEqual(
    first: ReadonlyPartialJSONValue,
    second: ReadonlyPartialJSONValue
  ): boolean {
    // Check referential and primitive equality first.
    if (first === second) {
      return true;
    }

    // If one is a primitive, the `===` check ruled out the other.
    if (isPrimitive(first) || isPrimitive(second)) {
      return false;
    }

    // Test whether they are arrays.
    let a1 = isArray(first);
    let a2 = isArray(second);

    // Bail if the types are different.
    if (a1 !== a2) {
      return false;
    }

    // If they are both arrays, compare them.
    if (a1 && a2) {
      return deepArrayEqual(
        first as ReadonlyPartialJSONArray,
        second as ReadonlyPartialJSONArray
      );
    }

    // At this point, they must both be objects.
    return deepObjectEqual(
      first as ReadonlyPartialJSONObject,
      second as ReadonlyPartialJSONObject
    );
  }

  /**
   * Create a deep copy of a JSON value.
   *
   * @param value - The JSON value to copy.
   *
   * @returns A deep copy of the given JSON value.
   */
  export function deepCopy<T extends ReadonlyPartialJSONValue>(value: T): T {
    // Do nothing for primitive values.
    if (isPrimitive(value)) {
      return value;
    }

    // Deep copy an array.
    if (isArray(value)) {
      return deepArrayCopy(value);
    }

    // Deep copy an object.
    return deepObjectCopy(value);
  }

  /**
   * Compare two JSON arrays for deep equality.
   */
  function deepArrayEqual(
    first: ReadonlyPartialJSONArray,
    second: ReadonlyPartialJSONArray
  ): boolean {
    // Check referential equality first.
    if (first === second) {
      return true;
    }

    // Test the arrays for equal length.
    if (first.length !== second.length) {
      return false;
    }

    // Compare the values for equality.
    for (let i = 0, n = first.length; i < n; ++i) {
      if (!deepEqual(first[i], second[i])) {
        return false;
      }
    }

    // At this point, the arrays are equal.
    return true;
  }

  /**
   * Compare two JSON objects for deep equality.
   */
  function deepObjectEqual(
    first: ReadonlyPartialJSONObject,
    second: ReadonlyPartialJSONObject
  ): boolean {
    // Check referential equality first.
    if (first === second) {
      return true;
    }

    // Check for the first object's keys in the second object.
    for (let key in first) {
      if (first[key] !== undefined && !(key in second)) {
        return false;
      }
    }

    // Check for the second object's keys in the first object.
    for (let key in second) {
      if (second[key] !== undefined && !(key in first)) {
        return false;
      }
    }

    // Compare the values for equality.
    for (let key in first) {
      // Get the values.
      let firstValue = first[key];
      let secondValue = second[key];

      // If both are undefined, ignore the key.
      if (firstValue === undefined && secondValue === undefined) {
        continue;
      }

      // If only one value is undefined, the objects are not equal.
      if (firstValue === undefined || secondValue === undefined) {
        return false;
      }

      // Compare the values.
      if (!deepEqual(firstValue, secondValue)) {
        return false;
      }
    }

    // At this point, the objects are equal.
    return true;
  }

  /**
   * Create a deep copy of a JSON array.
   */
  function deepArrayCopy(value: any): any {
    let result = new Array<any>(value.length);
    for (let i = 0, n = value.length; i < n; ++i) {
      result[i] = deepCopy(value[i]);
    }
    return result;
  }

  /**
   * Create a deep copy of a JSON object.
   */
  function deepObjectCopy(value: any): any {
    let result: any = {};
    for (let key in value) {
      // Ignore undefined values.
      let subvalue = value[key];
      if (subvalue === undefined) {
        continue;
      }
      result[key] = deepCopy(subvalue);
    }
    return result;
  }
}
