import { ADDRESS_HEX_LENGTH, BATCH_ID_HEX_LENGTH, ENCRYPTED_REFERENCE_HEX_LENGTH, PUBKEY_HEX_LENGTH, REFERENCE_HEX_LENGTH, TAGS_LIMIT_MAX, TAGS_LIMIT_MIN, PSS_TARGET_HEX_LENGTH_MAX } from "../types/index.js";
import { BeeArgumentError, BeeError } from "./error.js";
import { isFile } from "./file.js";
import { assertHexString, assertPrefixedHexString, isHexString } from "./hex.js";
import { isReadable } from "./stream.js";
import { decodeCid, encodeReference } from '@ethersphere/swarm-cid';
export function isUint8Array(obj) {
  return obj instanceof Uint8Array;
}
export function isInteger(value) {
  return typeof value === 'string' && /^-?(0|[1-9][0-9]*)$/g.test(value) || typeof value === 'number' && value > Number.MIN_SAFE_INTEGER && value < Number.MAX_SAFE_INTEGER && Number.isInteger(value);
}
export function isObject(value) {
  return value !== null && typeof value === 'object';
}
/**
 * Generally it is discouraged to use `object` type, but in this case I think
 * it is best to do so as it is possible to easily convert from `object`to other
 * types, which will be usually the case after asserting that the object is
 * strictly object. With for example Record<string, unknown> you have to first
 * cast it to `unknown` which I think bit defeat the purpose.
 *
 * @param value
 */
// eslint-disable-next-line @typescript-eslint/ban-types

export function isStrictlyObject(value) {
  return isObject(value) && !Array.isArray(value);
}
/**
 * Asserts if object is Error
 *
 * @param e
 */

export function isError(e) {
  return e instanceof Error;
} // eslint-disable-next-line @typescript-eslint/ban-types

export function assertStrictlyObject(value, name = 'value') {
  if (!isStrictlyObject(value)) {
    throw new TypeError(`${name} has to be an object that is not null nor array!`);
  }
}
export function assertBoolean(value, name = 'value') {
  if (value !== true && value !== false) throw new TypeError(`${name} is not boolean`);
}
export function assertInteger(value, name = 'value') {
  if (!isInteger(value)) throw new TypeError(`${name} is not integer`);
}
export function assertNonNegativeInteger(value, name = 'Value') {
  assertInteger(value, name);
  if (Number(value) < 0) throw new BeeArgumentError(`${name} has to be bigger or equal to zero`, value);
}
export function assertPositiveInteger(value, name = 'Value') {
  assertInteger(value, name);
  if (Number(value) <= 0) throw new BeeArgumentError(`${name} has to be bigger then zero`, value);
}
export function assertReference(value) {
  try {
    assertHexString(value, REFERENCE_HEX_LENGTH);
  } catch (e) {
    assertHexString(value, ENCRYPTED_REFERENCE_HEX_LENGTH);
  }
}
export function assertReferenceOrEns(value) {
  if (typeof value !== 'string') {
    throw new TypeError('ReferenceOrEns has to be a string!');
  }

  if (isHexString(value)) {
    assertReference(value);
    return;
  }
  /**
   * a.asdf - VALID
   * test.eth - VALID
   * ADAM.ETH - VALID
   * ADAM UHLIR.ETH - INVALID
   * test.whatever.eth - VALID
   * -adg.ets - INVALID
   * adg-.ets - INVALID
   * as-a.com - VALID
   * ethswarm.org - VALID
   * http://asdf.asf - INVALID
   * řš+ýí.šě+ř.čě - VALID
   * tsg.asg?asg - INVALID
   * tsg.asg:1599 - INVALID
   * ethswarm.something- - INVALID
   * ethswarm.-something - INVALID
   * ethswarm.some-thing - VALID
   *
   * The idea of this regex is to match strings that are 1 to 63 characters long and do not start or end with dash character
   *
   * This part matches 2-63 character string that does not start or end with -
   * [^-.\/?:\s][^.\/?:\s]{0,61}[^-.\/?:\s]   <regexp1>
   *
   * For 1 character long string we use the part after |
   * [^-.\/?:\s]   <regexp2>
   *
   * This is terminated in a group with . character an repeated at least once
   * (<regexp1>|<regexp2>\.)+
   *
   * This covers everything but top level domain which is 2 to 63 characters long so we can just use the <regexp2> again
   * ^(<regexp1>|<regexp2>\.)+<regexp1>$
   */


  const DOMAIN_REGEX = /^(?:(?:[^-.\/?:\s][^.\/?:\s]{0,61}[^-.\/?:\s]|[^-.\/?:\s]{1,2})\.)+[^-.\/?:\s][^.\/?:\s]{0,61}[^-.\/?:\s]$/; // We are doing best-effort validation of domain here. The proper way would be to do validation using IDNA UTS64 standard
  // but that would give us high penalty to our dependencies as the library (idna-uts46-hx) that does this validation and translation
  // adds 160kB minified size which is significant. We expects that full validation will be done on Bee side.

  if (!DOMAIN_REGEX.test(value)) {
    throw new TypeError('ReferenceOrEns is not valid Reference, but also not valid ENS domain.');
  }
}
/**
 * Function that mainly converts Swarm CID into hex encoded Swarm Reference
 *
 * @param value
 * @param expectedCidType
 */

export function makeReferenceOrEns(value, expectedCidType) {
  var _a;

  if (typeof value !== 'string') {
    throw new TypeError('ReferenceCidOrEns has to be a string!');
  }

  try {
    const result = decodeCid(value);

    if (result.type !== expectedCidType) {
      throw new BeeError(`CID was expected to be of type ${expectedCidType}, but got instead ${(_a = result.type) !== null && _a !== void 0 ? _a : 'non-Swarm CID'}`);
    }

    return result.reference;
  } catch (e) {
    if (e instanceof BeeError) throw e;
  }

  assertReferenceOrEns(value);
  return value;
}
/**
 * Function that adds getter which converts the reference into CID base32 encoded string.
 * @param result
 * @param cidType Type as described in the @ethersphere/swarm-cids-js -> ReferenceType
 */

export function addCidConversionFunction(result, cidType) {
  return Object.assign(Object.assign({}, result), {
    get cid() {
      return encodeReference(result.reference, cidType).toString();
    }

  });
}
export function assertAddress(value) {
  assertHexString(value, ADDRESS_HEX_LENGTH, 'Address');
}
export function assertBatchId(value) {
  assertHexString(value, BATCH_ID_HEX_LENGTH, 'BatchId');
}
export function assertRequestOptions(value, name = 'RequestOptions') {
  if (value === undefined) {
    return;
  }

  if (!isStrictlyObject(value)) {
    throw new TypeError(`${name} has to be an object!`);
  }

  const options = value;

  if (options.retry) {
    assertNonNegativeInteger(options.retry, `${name}.retry`);
  }

  if (options.timeout) {
    assertNonNegativeInteger(options.timeout, `${name}.timeout`);
  }

  if (options.fetch && typeof options.fetch !== 'function') {
    throw new TypeError(`${name}.fetch has to be a function or undefined!`);
  }
}
export function assertUploadOptions(value, name = 'UploadOptions') {
  if (!isStrictlyObject(value)) {
    throw new TypeError(`${name} has to be an object!`);
  }

  assertRequestOptions(value, name);
  const options = value;

  if (options.pin && typeof options.pin !== 'boolean') {
    throw new TypeError(`options.pin property in ${name} has to be boolean or undefined!`);
  }

  if (options.encrypt && typeof options.encrypt !== 'boolean') {
    throw new TypeError(`options.encrypt property in ${name} has to be boolean or undefined!`);
  }

  if (options.tag) {
    if (typeof options.tag !== 'number') {
      throw new TypeError(`options.tag property in ${name} has to be number or undefined!`);
    }

    assertNonNegativeInteger(options.tag, 'options.tag');
  }
}
export function assertFileUploadOptions(value) {
  assertUploadOptions(value, 'FileUploadOptions');
  const options = value;

  if (options.size) {
    if (typeof options.size !== 'number') {
      throw new TypeError('tag property in FileUploadOptions has to be number or undefined!');
    }

    assertNonNegativeInteger(options.size, 'options.size');
  }

  if (options.contentType && typeof options.contentType !== 'string') {
    throw new TypeError('contentType property in FileUploadOptions has to be string or undefined!');
  }
}
export function assertCollectionUploadOptions(value) {
  assertUploadOptions(value, 'CollectionUploadOptions');
  const options = value;

  if (options.indexDocument && typeof options.indexDocument !== 'string') {
    throw new TypeError('indexDocument property in CollectionUploadOptions has to be string or undefined!');
  }

  if (options.errorDocument && typeof options.errorDocument !== 'string') {
    throw new TypeError('errorDocument property in CollectionUploadOptions has to be string or undefined!');
  }
}
export function isTag(value) {
  if (!isStrictlyObject(value)) {
    return false;
  }

  const tag = value;
  const numberProperties = ['total', 'processed', 'synced', 'uid'];
  const correctNumberProperties = numberProperties.every(numberProperty => typeof tag[numberProperty] === 'number');

  if (!correctNumberProperties || !tag.startedAt || typeof tag.startedAt !== 'string') {
    return false;
  }

  return true;
}
export function assertTag(value) {
  if (!isStrictlyObject(value)) {
    throw new TypeError('Tag is not an object!');
  }

  const tag = value;
  const numberProperties = ['total', 'processed', 'synced', 'uid'];

  for (const numberProperty of numberProperties) {
    if (!tag[numberProperty]) {
      throw new TypeError(`Tag's property '${numberProperty}' has to be specified!`);
    }

    if (typeof tag[numberProperty] !== 'number') {
      throw new TypeError(`Tag's property '${numberProperty}' has to be number!`);
    }
  }

  if (!tag.startedAt) {
    throw new TypeError("Tag's property 'startedAt' has to be specified!");
  }

  if (typeof tag.startedAt !== 'string') {
    throw new TypeError("Tag's property 'startedAt' has to be string!");
  }
}
export function assertAddressPrefix(value) {
  assertHexString(value, undefined, 'AddressPrefix');

  if (value.length > PSS_TARGET_HEX_LENGTH_MAX) {
    throw new BeeArgumentError(`AddressPrefix must have length of ${PSS_TARGET_HEX_LENGTH_MAX} at most! Got string with ${value.length}`, value);
  }
}
export function assertPssMessageHandler(value) {
  if (!isStrictlyObject(value)) {
    throw new TypeError('PssMessageHandler has to be object!');
  }

  const handler = value;

  if (typeof handler.onMessage !== 'function') {
    throw new TypeError('onMessage property of PssMessageHandler has to be function!');
  }

  if (typeof handler.onError !== 'function') {
    throw new TypeError('onError property of PssMessageHandler has to be function!');
  }
}
export function assertPublicKey(value) {
  assertHexString(value, PUBKEY_HEX_LENGTH, 'PublicKey');
}
export function assertPostageBatchOptions(value) {
  if (value === undefined) {
    return;
  }

  assertStrictlyObject(value);
  const options = value;
  assertRequestOptions(options, 'PostageBatchOptions');

  if (options === null || options === void 0 ? void 0 : options.gasPrice) {
    assertNonNegativeInteger(options.gasPrice);
  }

  if ((options === null || options === void 0 ? void 0 : options.immutableFlag) !== undefined) {
    assertBoolean(options.immutableFlag);
  }

  if ((options === null || options === void 0 ? void 0 : options.waitForUsable) !== undefined) {
    assertBoolean(options.waitForUsable);
  }

  if ((options === null || options === void 0 ? void 0 : options.waitForUsableTimeout) !== undefined) {
    assertNonNegativeInteger(options.waitForUsableTimeout, 'options.waitForUsableTimeout');
  }
}
export function assertCashoutOptions(value) {
  if (value === undefined) {
    return;
  }

  assertStrictlyObject(value);
  const options = value;
  assertRequestOptions(options, 'PostageBatchOptions');

  if (options === null || options === void 0 ? void 0 : options.gasLimit) {
    assertNonNegativeInteger(options.gasLimit);
  }

  if (options === null || options === void 0 ? void 0 : options.gasPrice) {
    assertNonNegativeInteger(options.gasPrice);
  }
}
/**
 * Check whether the given parameter is valid data to upload
 * @param value
 * @throws TypeError if not valid
 */

export function assertData(value) {
  if (typeof value !== 'string' && !(value instanceof Uint8Array)) {
    throw new TypeError('Data must be either string or Uint8Array!');
  }
}
/**
 * Check whether the given parameter is a correct file representation to file upload.
 * @param value
 * @throws TypeError if not valid
 */

export function assertFileData(value) {
  if (typeof value !== 'string' && !(value instanceof Uint8Array) && !isFile(value) && !isReadable(value)) {
    throw new TypeError('Data must be either string, Readable, Uint8Array or File!');
  }
}
/**
 * Checks whether optional options for AllTags query are valid
 * @param options
 */

export function assertAllTagsOptions(entry) {
  if (entry !== undefined && !isStrictlyObject(entry)) {
    throw new TypeError('options has to be an object or undefined!');
  }

  assertRequestOptions(entry, 'AllTagsOptions');
  const options = entry;

  if ((options === null || options === void 0 ? void 0 : options.limit) !== undefined) {
    if (typeof options.limit !== 'number') {
      throw new TypeError('AllTagsOptions.limit has to be a number or undefined!');
    }

    if (options.limit < TAGS_LIMIT_MIN) {
      throw new BeeArgumentError(`AllTagsOptions.limit has to be at least ${TAGS_LIMIT_MIN}`, options.limit);
    }

    if (options.limit > TAGS_LIMIT_MAX) {
      throw new BeeArgumentError(`AllTagsOptions.limit has to be at most ${TAGS_LIMIT_MAX}`, options.limit);
    }
  }

  if ((options === null || options === void 0 ? void 0 : options.offset) !== undefined) {
    assertNonNegativeInteger(options.offset, 'AllTagsOptions.offset');
  }
}
/**
 * Utility functions that return Tag UID
 * @param tagUid
 */

export function makeTagUid(tagUid) {
  if (tagUid === undefined || tagUid === null) {
    throw new TypeError('TagUid was expected but got undefined or null instead!');
  }

  if (isTag(tagUid)) {
    return tagUid.uid;
  } else if (typeof tagUid === 'number') {
    assertNonNegativeInteger(tagUid, 'UID');
    return tagUid;
  } else if (typeof tagUid === 'string') {
    const int = parseInt(tagUid);

    if (isNaN(int)) {
      throw new TypeError('Passed tagUid string is not valid integer!');
    }

    if (int < 0) {
      throw new TypeError(`TagUid was expected to be positive non-negative integer! Got ${int}`);
    }

    return int;
  }

  throw new TypeError('tagUid has to be either Tag or a number (UID)!');
}
export function assertTransactionHash(transactionHash) {
  if (typeof transactionHash !== 'string') {
    throw new TypeError('TransactionHash has to be a string!');
  }

  assertPrefixedHexString(transactionHash, 'TransactionHash'); // Hash is 64 long + '0x' prefix = 66

  if (transactionHash.length !== 66) {
    throw new TypeError('TransactionHash has to be prefixed hex string with total length 66 (prefix including)');
  }
}