/**
 * Snowflake module.
 * Contains functions/data relating to snowflakes.
 * Discord utilizes Twitter's snowflake format for uniquely identifiable descriptors (IDs).
 * These IDs are guaranteed to be unique across all of Discord, except in some unique
 * scenarios in which child objects share their parent's ID. Because Snowflake IDs are up
 * to 64 bits in size (e.g. a uint64), they are always returned as strings in the HTTP API
 * to prevent integer overflows in some languages. See Gateway ETF/JSON for more information
 * regarding Gateway encoding.
 * @see {@link https://discord.com/developers/docs/reference#snowflakes|Discord}
 * @module Snowflake
 * @memberof Discord
 * @namespace Discord.Snowflake
 */

/* eslint-disable no-unused-vars */
import { log, warn, error } from '@vizality/util/logger';
import { assertNumber } from '@vizality/util/number';
import { assertString } from '@vizality/util/string';
import { isDate } from '@vizality/util/time';
/**
 * @todo The "long" package is only used here. It may be worth stripping that particular function
 * from the package, rather than including the entire package, as it is over 170 kb.
 */
import Long from 'long';

/**
 * Discord epoch (2015-01-01T00:00:00.000Z)
 */
const EPOCH = 1420070400000;

/**
 * @private
 */
const _labels = [ 'Discord', 'Snowflake' ];
const _log = (labels, ...message) => log({ labels, message });
const _warn = (labels, ...message) => warn({ labels, message });
const _error = (labels, ...message) => error({ labels, message });

/**
 * @private
 */
const _pad = (v, n, c = '0') => {
  return String(v).length >= n ? String(v) : (String(c).repeat(n) + v).slice(-n);
};

/**
 * Generates a snowflake from a timestamp.
 * <info>This hardcodes the worker ID as 1 and the process ID as 0.</info>
 * Alternatively, you can also perform this action directly using internals (less performant):
 * $vz.webpack.getModule('DISCORD_EPOCH').default.fromTimestamp(new Date())
 * @see {@link https://discord.js.org|discord.js}
 * @param {timestamp|Date} [timestamp=Date.now()] Timestamp or date of the snowflake to generate
 * @returns {snowflake|void}
 */
export const getSnowflake = (timestamp = Date.now()) => {
  try {
    /**
     * Check if the provided argument is a Date.
     * If it is, get the timestamp.
     */
    if (isDate(timestamp)) {
      timestamp = timestamp.getTime();
    }
    assertNumber(timestamp);
    let INCREMENT = 0;
    if (INCREMENT >= 4095) {
      INCREMENT = 0;
    }
    const BINARY = `${_pad((timestamp - EPOCH).toString(2), 42)}0000100000${_pad((INCREMENT++).toString(2), 12)}`;
    return Long.fromString(BINARY, 2)?.toString();
  } catch (err) {
    return _error(_labels.concat('getSnowflake'), err);
  }
};

/**
 * Deconstructs a snowflake.
 * @see {@link https://discord.js.org|discord.js}
 * @param {snowflake} snowflake Snowflake to deconstruct
 * @returns {DeconstructedSnowflake} Deconstructed snowflake
 */
export const getDeconstructedSnowflake = snowflake => {
  try {
    assertString(snowflake);
    const BINARY = _pad(Long.fromString(snowflake).toString(2), 64);
    const res = {
      timestamp: parseInt(BINARY.substring(0, 42), 2) + EPOCH,
      workerID: parseInt(BINARY.substring(42, 47), 2),
      processID: parseInt(BINARY.substring(47, 52), 2),
      increment: parseInt(BINARY.substring(52, 64), 2),
      binary: BINARY
    };
    Object.defineProperty(res, 'date', {
      get: function get () {
        return new Date(this.timestamp);
      },
      enumerable: true
    });
    return res;
  } catch (err) {
    return _error(_labels.concat('getDeconstructedSnowflake'), err);
  }
};

/**
 * Breaks the snowflake down into binary.
 * @param {snowflake} snowflake Snowflake
 * @returns {Date|void}
 */
export const getSnowflakeBinary = snowflake => {
  try {
    assertString(snowflake);
    return this.getDeconstructedSnowflake(snowflake)?.binary;
  } catch (err) {
    return _error(_labels.concat('getSnowflakeBinary'), err);
  }
};

/**
 * Extracts a date from the snowflake.
 * @param {snowflake} snowflake Snowflake
 * @returns {Date|void}
 */
export const getSnowflakeDate = snowflake => {
  try {
    assertString(snowflake);
    return this.getDeconstructedSnowflake(snowflake)?.date;
  } catch (err) {
    return _error(_labels.concat('getSnowflakeDate'), err);
  }
};

/**
 * Extracts a timestamp from the snowflake.
 * @param {snowflake} snowflake Snowflake
 * @returns {timestamp|void}
 */
export const getSnowflakeTimestamp = snowflake => {
  try {
    assertString(snowflake);
    return this.getDeconstructedSnowflake(snowflake)?.timestamp;
  } catch (err) {
    return _error(_labels.concat('getSnowflakeTimestamp'), err);
  }
};
