/**
 * @fileoverview Proto internal runtime checks.
 *
 * Checks are grouped into different severity, see:
 * http://g3doc/third_party/protobuf/javascript/README.md#configurable-check-support-in-protocol-buffers
 *
 * Checks are also grouped into different sections:
 *   - CHECK_BOUNDS:
 *       Checks that ensure that indexed access is within bounds
 *       (e.g. an array being accessed past its size).
 *   - CHECK_STATE
 *       Checks related to the state of an object
 *       (e.g. a parser hitting an invalid case).
 *   - CHECK_TYPE:
 *       Checks that relate to type errors (e.g. code receives a number instead
 *       of a string).
 */
goog.module('protobuf.internal.checks');

const ByteString = goog.require('protobuf.ByteString');
const Int64 = goog.require('protobuf.Int64');
const WireType = goog.require('protobuf.binary.WireType');

//
// See
// http://g3doc/third_party/protobuf/javascript/README.md#configurable-check-support-in-protocol-buffers
//
/** @define{string} */
const CHECK_LEVEL_DEFINE = goog.define('protobuf.defines.CHECK_LEVEL', '');

/** @define{boolean} */
const POLYFILL_TEXT_ENCODING =
    goog.define('protobuf.defines.POLYFILL_TEXT_ENCODING', true);

/**
 * @const {number}
 */
const MAX_FIELD_NUMBER = Math.pow(2, 29) - 1;

/**
 * The largest finite float32 value.
 * @const {number}
 */
const FLOAT32_MAX = 3.4028234663852886e+38;

/** @enum {number} */
const CheckLevel = {
  DEBUG: 0,
  CRITICAL: 1,
  OFF: 2
};


/** @return {!CheckLevel} */
function calculateCheckLevel() {
  const definedLevel = CHECK_LEVEL_DEFINE.toUpperCase();
  if (definedLevel === '') {
    // user did not set a value, value now just depends on goog.DEBUG
    return goog.DEBUG ? CheckLevel.DEBUG : CheckLevel.CRITICAL;
  }

  if (definedLevel === 'CRITICAL') {
    return CheckLevel.CRITICAL;
  }

  if (definedLevel === 'OFF') {
    return CheckLevel.OFF;
  }

  if (definedLevel === 'DEBUG') {
    return CheckLevel.DEBUG;
  }

  throw new Error(`Unknown value for CHECK_LEVEL: ${CHECK_LEVEL_DEFINE}`);
}

const /** !CheckLevel */ CHECK_LEVEL = calculateCheckLevel();

const /** boolean */ CHECK_STATE = CHECK_LEVEL === CheckLevel.DEBUG;

const /** boolean */ CHECK_CRITICAL_STATE =
    CHECK_LEVEL === CheckLevel.CRITICAL || CHECK_LEVEL === CheckLevel.DEBUG;

const /** boolean */ CHECK_BOUNDS = CHECK_LEVEL === CheckLevel.DEBUG;

const /** boolean */ CHECK_CRITICAL_BOUNDS =
    CHECK_LEVEL === CheckLevel.CRITICAL || CHECK_LEVEL === CheckLevel.DEBUG;

const /** boolean */ CHECK_TYPE = CHECK_LEVEL === CheckLevel.DEBUG;

const /** boolean */ CHECK_CRITICAL_TYPE =
    CHECK_LEVEL === CheckLevel.CRITICAL || CHECK_LEVEL === CheckLevel.DEBUG;

/**
 * Ensures the truth of an expression involving the state of the calling
 * instance, but not involving any parameters to the calling method.
 *
 * For cases where failing fast is pretty important and not failing early could
 * cause bugs that are much harder to debug.
 * @param {boolean} state
 * @param {string=} message
 * @throws {!Error} If the state is false and the check state is critical.
 */
function checkCriticalState(state, message = '') {
  if (!CHECK_CRITICAL_STATE) {
    return;
  }
  if (!state) {
    throw new Error(message);
  }
}

/**
 * Ensures the truth of an expression involving the state of the calling
 * instance, but not involving any parameters to the calling method.
 *
 * @param {boolean} state
 * @param {string=} message
 * @throws {!Error} If the state is false and the check state is debug.
 */
function checkState(state, message = '') {
  if (!CHECK_STATE) {
    return;
  }
  checkCriticalState(state, message);
}

/**
 * Ensures that `index` specifies a valid position in an indexable object of
 * size `size`. A position index may range from zero to size, inclusive.
 * @param {number} index
 * @param {number} size
 * @throws {!Error} If the index is out of range and the check state is debug.
 */
function checkPositionIndex(index, size) {
  if (!CHECK_BOUNDS) {
    return;
  }
  checkCriticalPositionIndex(index, size);
}

/**
 * Ensures that `index` specifies a valid position in an indexable object of
 * size `size`. A position index may range from zero to size, inclusive.
 * @param {number} index
 * @param {number} size
 * @throws {!Error} If the index is out of range and the check state is
 * critical.
 */
function checkCriticalPositionIndex(index, size) {
  if (!CHECK_CRITICAL_BOUNDS) {
    return;
  }
  if (index < 0 || index > size) {
    throw new Error(`Index out of bounds: index: ${index} size: ${size}`);
  }
}

/**
 * Ensures that `index` specifies a valid element in an indexable object of
 * size `size`. A element index may range from zero to size, exclusive.
 * @param {number} index
 * @param {number} size
 * @throws {!Error} If the index is out of range and the check state is
 * debug.
 */
function checkElementIndex(index, size) {
  if (!CHECK_BOUNDS) {
    return;
  }
  checkCriticalElementIndex(index, size);
}

/**
 * Ensures that `index` specifies a valid element in an indexable object of
 * size `size`. A element index may range from zero to size, exclusive.
 * @param {number} index
 * @param {number} size
 * @throws {!Error} If the index is out of range and the check state is
 * critical.
 */
function checkCriticalElementIndex(index, size) {
  if (!CHECK_CRITICAL_BOUNDS) {
    return;
  }
  if (index < 0 || index >= size) {
    throw new Error(`Index out of bounds: index: ${index} size: ${size}`);
  }
}

/**
 * Ensures the range of [start, end) is with the range of [0, size).
 * @param {number} start
 * @param {number} end
 * @param {number} size
 * @throws {!Error} If start and end are out of range and the check state is
 * debug.
 */
function checkRange(start, end, size) {
  if (!CHECK_BOUNDS) {
    return;
  }
  checkCriticalRange(start, end, size);
}

/**
 * Ensures the range of [start, end) is with the range of [0, size).
 * @param {number} start
 * @param {number} end
 * @param {number} size
 * @throws {!Error} If start and end are out of range and the check state is
 * critical.
 */
function checkCriticalRange(start, end, size) {
  if (!CHECK_CRITICAL_BOUNDS) {
    return;
  }
  if (start < 0 || end < 0 || start > size || end > size) {
    throw new Error(`Range error: start: ${start} end: ${end} size: ${size}`);
  }
  if (start > end) {
    throw new Error(`Start > end: ${start} > ${end}`);
  }
}

/**
 * Ensures that field number is an integer and within the range of
 * [1, MAX_FIELD_NUMBER].
 * @param {number} fieldNumber
 * @throws {!Error} If the field number is out of range and the check state is
 * debug.
 */
function checkFieldNumber(fieldNumber) {
  if (!CHECK_TYPE) {
    return;
  }
  checkCriticalFieldNumber(fieldNumber);
}

/**
 * Ensures that the value is neither null nor undefined.
 *
 * @param {T} value
 * @return {R}
 *
 * @template T
 * @template R :=
 *     mapunion(T, (V) =>
 *         cond(eq(V, 'null'),
 *             none(),
 *             cond(eq(V, 'undefined'),
 *                 none(),
 *                 V)))
 *  =:
 */
function checkDefAndNotNull(value) {
  if (CHECK_TYPE) {
    // Note that undefined == null.
    if (value == null) {
      throw new Error(`Value can't be null`);
    }
  }
  return value;
}

/**
 * Ensures that the value exists and is a function.
 *
 * @param {function(?): ?} func
 */
function checkFunctionExists(func) {
  if (CHECK_TYPE) {
    if (typeof func !== 'function') {
      throw new Error(`${func} is not a function`);
    }
  }
}

/**
 * Ensures that field number is an integer and within the range of
 * [1, MAX_FIELD_NUMBER].
 * @param {number} fieldNumber
 * @throws {!Error} If the field number is out of range and the check state is
 * critical.
 */
function checkCriticalFieldNumber(fieldNumber) {
  if (!CHECK_CRITICAL_TYPE) {
    return;
  }
  if (fieldNumber <= 0 || fieldNumber > MAX_FIELD_NUMBER) {
    throw new Error(`Field number is out of range: ${fieldNumber}`);
  }
}

/**
 * Ensures that wire type is valid.
 * @param {!WireType} wireType
 * @throws {!Error} If the wire type is invalid and the check state is debug.
 */
function checkWireType(wireType) {
  if (!CHECK_TYPE) {
    return;
  }
  checkCriticalWireType(wireType);
}

/**
 * Ensures that wire type is valid.
 * @param {!WireType} wireType
 * @throws {!Error} If the wire type is invalid and the check state is critical.
 */
function checkCriticalWireType(wireType) {
  if (!CHECK_CRITICAL_TYPE) {
    return;
  }
  if (wireType < WireType.VARINT || wireType > WireType.FIXED32) {
    throw new Error(`Invalid wire type: ${wireType}`);
  }
}

/**
 * Ensures the given value has the correct type.
 * @param {boolean} expression
 * @param {string} errorMsg
 * @throws {!Error} If the value has the wrong type and the check state is
 * critical.
 */
function checkCriticalType(expression, errorMsg) {
  if (!CHECK_CRITICAL_TYPE) {
    return;
  }
  if (!expression) {
    throw new Error(errorMsg);
  }
}

/**
 * Checks whether a given object is an array.
 * @param {*} value
 * @return {!Array<*>}
 */
function checkCriticalTypeArray(value) {
  checkCriticalType(
      Array.isArray(value), `Must be an array, but got: ${value}`);
  return /** @type {!Array<*>} */ (value);
}

/**
 * Checks whether a given object is an iterable.
 * @param {*} value
 * @return {!Iterable<*>}
 */
function checkCriticalTypeIterable(value) {
  checkCriticalType(
      !!value[Symbol.iterator], `Must be an iterable, but got: ${value}`);
  return /** @type {!Iterable<*>} */ (value);
}

/**
 * Checks whether a given object is a boolean.
 * @param {*} value
 */
function checkCriticalTypeBool(value) {
  checkCriticalType(
      typeof value === 'boolean', `Must be a boolean, but got: ${value}`);
}

/**
 * Checks whether a given object is an array of boolean.
 * @param {*} values
 */
function checkCriticalTypeBoolArray(values) {
  // TODO(b/134765672)
  if (!CHECK_CRITICAL_TYPE) {
    return;
  }
  const array = checkCriticalTypeArray(values);
  for (const value of array) {
    checkCriticalTypeBool(value);
  }
}

/**
 * Checks whether a given object is a ByteString.
 * @param {*} value
 */
function checkCriticalTypeByteString(value) {
  checkCriticalType(
      value instanceof ByteString, `Must be a ByteString, but got: ${value}`);
}

/**
 * Checks whether a given object is an array of ByteString.
 * @param {*} values
 */
function checkCriticalTypeByteStringArray(values) {
  // TODO(b/134765672)
  if (!CHECK_CRITICAL_TYPE) {
    return;
  }
  const array = checkCriticalTypeArray(values);
  for (const value of array) {
    checkCriticalTypeByteString(value);
  }
}

/**
 * Checks whether a given object is a number.
 * @param {*} value
 * @throws {!Error} If the value is not float and the check state is debug.
 */
function checkTypeDouble(value) {
  if (!CHECK_TYPE) {
    return;
  }
  checkCriticalTypeDouble(value);
}

/**
 * Checks whether a given object is a number.
 * @param {*} value
 * @throws {!Error} If the value is not float and the check state is critical.
 */
function checkCriticalTypeDouble(value) {
  checkCriticalType(
      typeof value === 'number', `Must be a number, but got: ${value}`);
}

/**
 * Checks whether a given object is an array of double.
 * @param {*} values
 */
function checkCriticalTypeDoubleArray(values) {
  // TODO(b/134765672)
  if (!CHECK_CRITICAL_TYPE) {
    return;
  }
  const array = checkCriticalTypeArray(values);
  for (const value of array) {
    checkCriticalTypeDouble(value);
  }
}

/**
 * Checks whether a given object is a number.
 * @param {*} value
 * @throws {!Error} If the value is not signed int32 and the check state is
 *     debug.
 */
function checkTypeSignedInt32(value) {
  if (!CHECK_TYPE) {
    return;
  }
  checkCriticalTypeSignedInt32(value);
}

/**
 * Checks whether a given object is a number.
 * @param {*} value
 * @throws {!Error} If the value is not signed int32 and the check state is
 *     critical.
 */
function checkCriticalTypeSignedInt32(value) {
  checkCriticalTypeDouble(value);
  const valueAsNumber = /** @type {number} */ (value);
  if (CHECK_CRITICAL_TYPE) {
    if (valueAsNumber < -Math.pow(2, 31) || valueAsNumber > Math.pow(2, 31) ||
        !Number.isInteger(valueAsNumber)) {
      throw new Error(`Must be int32, but got: ${valueAsNumber}`);
    }
  }
}

/**
 * Checks whether a given object is an array of numbers.
 * @param {*} values
 */
function checkCriticalTypeSignedInt32Array(values) {
  // TODO(b/134765672)
  if (!CHECK_CRITICAL_TYPE) {
    return;
  }
  const array = checkCriticalTypeArray(values);
  for (const value of array) {
    checkCriticalTypeSignedInt32(value);
  }
}

/**
 * Ensures that value is a long instance.
 * @param {*} value
 * @throws {!Error} If the value is not a long instance and check state is
 *     debug.
 */
function checkTypeSignedInt64(value) {
  if (!CHECK_TYPE) {
    return;
  }
  checkCriticalTypeSignedInt64(value);
}

/**
 * Ensures that value is a long instance.
 * @param {*} value
 * @throws {!Error} If the value is not a long instance and check state is
 *     critical.
 */
function checkCriticalTypeSignedInt64(value) {
  if (!CHECK_CRITICAL_TYPE) {
    return;
  }
  if (!(value instanceof Int64)) {
    throw new Error(`Must be Int64 instance, but got: ${value}`);
  }
}

/**
 * Checks whether a given object is an array of long instances.
 * @param {*} values
 * @throws {!Error} If values is not an array of long instances.
 */
function checkCriticalTypeSignedInt64Array(values) {
  // TODO(b/134765672)
  if (!CHECK_CRITICAL_TYPE) {
    return;
  }
  const array = checkCriticalTypeArray(values);
  for (const value of array) {
    checkCriticalTypeSignedInt64(value);
  }
}

/**
 * Checks whether a given object is a number and within float32 precision.
 * @param {*} value
 * @throws {!Error} If the value is not float and the check state is debug.
 */
function checkTypeFloat(value) {
  if (!CHECK_TYPE) {
    return;
  }
  checkCriticalTypeFloat(value);
}

/**
 * Checks whether a given object is a number and within float32 precision.
 * @param {*} value
 * @throws {!Error} If the value is not float and the check state is critical.
 */
function checkCriticalTypeFloat(value) {
  checkCriticalTypeDouble(value);
  if (CHECK_CRITICAL_TYPE) {
    const valueAsNumber = /** @type {number} */ (value);
    if (Number.isFinite(valueAsNumber) &&
        (valueAsNumber > FLOAT32_MAX || valueAsNumber < -FLOAT32_MAX)) {
      throw new Error(
          `Given number does not fit into float precision: ${value}`);
    }
  }
}

/**
 * Checks whether a given object is an iterable of floats.
 * @param {*} values
 */
function checkCriticalTypeFloatIterable(values) {
  // TODO(b/134765672)
  if (!CHECK_CRITICAL_TYPE) {
    return;
  }
  const iterable = checkCriticalTypeIterable(values);
  for (const value of iterable) {
    checkCriticalTypeFloat(value);
  }
}

/**
 * Checks whether a given object is a string.
 * @param {*} value
 */
function checkCriticalTypeString(value) {
  checkCriticalType(
      typeof value === 'string', `Must be string, but got: ${value}`);
}

/**
 * Checks whether a given object is an array of string.
 * @param {*} values
 */
function checkCriticalTypeStringArray(values) {
  // TODO(b/134765672)
  if (!CHECK_CRITICAL_TYPE) {
    return;
  }
  const array = checkCriticalTypeArray(values);
  for (const value of array) {
    checkCriticalTypeString(value);
  }
}

/**
 * Ensures that value is a valid unsigned int32.
 * @param {*} value
 * @throws {!Error} If the value is out of range and the check state is debug.
 */
function checkTypeUnsignedInt32(value) {
  if (!CHECK_TYPE) {
    return;
  }
  checkCriticalTypeUnsignedInt32(value);
}

/**
 * Ensures that value is a valid unsigned int32.
 * @param {*} value
 * @throws {!Error} If the value is out of range and the check state
 * is critical.
 */
function checkCriticalTypeUnsignedInt32(value) {
  if (!CHECK_CRITICAL_TYPE) {
    return;
  }
  checkCriticalTypeDouble(value);
  const valueAsNumber = /** @type {number} */ (value);
  if (valueAsNumber < 0 || valueAsNumber > Math.pow(2, 32) - 1 ||
      !Number.isInteger(valueAsNumber)) {
    throw new Error(`Must be uint32, but got: ${value}`);
  }
}

/**
 * Checks whether a given object is an array of unsigned int32.
 * @param {*} values
 */
function checkCriticalTypeUnsignedInt32Array(values) {
  // TODO(b/134765672)
  if (!CHECK_CRITICAL_TYPE) {
    return;
  }
  const array = checkCriticalTypeArray(values);
  for (const value of array) {
    checkCriticalTypeUnsignedInt32(value);
  }
}

/**
 * Checks whether a given object is an array of message.
 * @param {*} values
 */
function checkCriticalTypeMessageArray(values) {
  // TODO(b/134765672)
  if (!CHECK_CRITICAL_TYPE) {
    return;
  }
  const array = checkCriticalTypeArray(values);
  for (const value of array) {
    checkCriticalType(
        value !== null, 'Given value is not a message instance: null');
  }
}

exports = {
  checkDefAndNotNull,
  checkCriticalElementIndex,
  checkCriticalFieldNumber,
  checkCriticalPositionIndex,
  checkCriticalRange,
  checkCriticalState,
  checkCriticalTypeBool,
  checkCriticalTypeBoolArray,
  checkCriticalTypeByteString,
  checkCriticalTypeByteStringArray,
  checkCriticalTypeDouble,
  checkTypeDouble,
  checkCriticalTypeDoubleArray,
  checkTypeFloat,
  checkCriticalTypeFloat,
  checkCriticalTypeFloatIterable,
  checkCriticalTypeMessageArray,
  checkCriticalTypeSignedInt32,
  checkCriticalTypeSignedInt32Array,
  checkCriticalTypeSignedInt64,
  checkTypeSignedInt64,
  checkCriticalTypeSignedInt64Array,
  checkCriticalTypeString,
  checkCriticalTypeStringArray,
  checkCriticalTypeUnsignedInt32,
  checkCriticalTypeUnsignedInt32Array,
  checkCriticalType,
  checkCriticalWireType,
  checkElementIndex,
  checkFieldNumber,
  checkFunctionExists,
  checkPositionIndex,
  checkRange,
  checkState,
  checkTypeUnsignedInt32,
  checkTypeSignedInt32,
  checkWireType,
  CHECK_BOUNDS,
  CHECK_CRITICAL_BOUNDS,
  CHECK_STATE,
  CHECK_CRITICAL_STATE,
  CHECK_TYPE,
  CHECK_CRITICAL_TYPE,
  MAX_FIELD_NUMBER,
  POLYFILL_TEXT_ENCODING,
};
