import { BigNumber } from 'bignumber.js';
import { BigNumber as EthersBigNumber } from 'ethers';
import isNil from 'lodash/isNil';
import memoize from 'lodash/memoize';
import React from 'react';
import { getUserLocale } from './utils';

const MAX_FRACTION_DIGITS = 20;

export function toDecimal(numberOfDecimals: number) {
  return 1 / Math.pow(10, numberOfDecimals);
}

export function toBigNum(
  rawValue: string | number | EthersBigNumber,
  decimals: number
): BigNumber {
  return new BigNumber(
    rawValue instanceof EthersBigNumber ? rawValue.toString() : rawValue || 0
  ).dividedBy(Math.pow(10, decimals));
}

export function addDecimal(
  value: string | number | EthersBigNumber,
  decimals: number,
  decimalPrecision = decimals
): string {
  if (!decimals) return value.toString();
  if (!decimalPrecision || decimalPrecision < 0) {
    return toBigNum(value, decimals).toFixed(0);
  }
  return toBigNum(value, decimals).toFixed(decimalPrecision);
}

export function removeDecimal(value: string, decimals: number): string {
  if (!decimals) return value;
  return new BigNumber(value || 0).times(Math.pow(10, decimals)).toFixed(0);
}

// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat
export const getNumberFormat = memoize((digits: number) => {
  if (isNil(digits) || digits < 0) {
    return new Intl.NumberFormat(getUserLocale());
  }
  return new Intl.NumberFormat(getUserLocale(), {
    minimumFractionDigits: Math.min(Math.max(0, digits), MAX_FRACTION_DIGITS),
    maximumFractionDigits: Math.min(Math.max(0, digits), MAX_FRACTION_DIGITS),
  });
});

export const getDecimalSeparator = memoize(
  () =>
    getNumberFormat(1)
      .formatToParts(1.1)
      .find((part) => part.type === 'decimal')?.value
);

export const formatNumber = (
  rawValue: string | number | BigNumber,
  formatDecimals = 0
) => {
  return getNumberFormat(formatDecimals).format(Number(rawValue));
};

export const addDecimalsFormatNumber = (
  rawValue: string | number,
  decimalPlaces: number,
  formatDecimals: number = decimalPlaces
) => {
  const x = addDecimal(rawValue, decimalPlaces);

  return formatNumber(x, formatDecimals);
};

export const formatNumberPercentage = (value: BigNumber, decimals?: number) => {
  const decimalPlaces =
    typeof decimals === 'undefined' ? Math.max(value.dp() || 0, 2) : decimals;
  return `${formatNumber(value, decimalPlaces)}%`;
};

export const toNumberParts = (
  value: BigNumber | null | undefined,
  decimals = 18
): [integers: string, decimalPlaces: string] => {
  if (!value) {
    return ['0', '0'.repeat(decimals)];
  }
  const separator = getDecimalSeparator() || '.';
  const [integers, decimalsPlaces] = formatNumber(value, decimals)
    .toString()
    .split(separator);
  return [integers, decimalsPlaces || ''];
};

export const useNumberParts = (
  value: BigNumber | null | undefined,
  decimals: number
): [integers: string, decimalPlaces: string] => {
  return React.useMemo(() => toNumberParts(value, decimals), [decimals, value]);
};

export const isNumeric = (
  value?: string | number | BigNumber | null
): value is NonNullable<number | string> => /^-?\d*\.?\d+$/.test(String(value));

const INFINITY = '∞';
const DEFAULT_COMPACT_ABOVE = 1_000_000;
const DEFAULT_COMPACT_CAP = new BigNumber(1e24);
/**
 * Compacts given number to human readable format.
 * @param number
 * @param decimals Number of decimal places
 * @param compactDisplay Display mode; short -> 1e6 == 1M; ling -> 1e6 1 million
 * @param compactAbove Compact number above threshold
 * @param cap Use scientific notation above threshold
 */
export const compactNumber = (
  number: BigNumber,
  decimals: number | 'infer' = 'infer',
  compactDisplay: 'short' | 'long' = 'short',
  compactAbove = DEFAULT_COMPACT_ABOVE,
  cap = DEFAULT_COMPACT_CAP
) => {
  if (!number.isFinite()) return `${number.isNegative() ? '-' : ''}${INFINITY}`;

  const decimalPlaces =
    (decimals === 'infer' ? number.decimalPlaces() : decimals) || 0;

  if (number.isLessThan(DEFAULT_COMPACT_ABOVE)) {
    return formatNumber(number, decimalPlaces);
  }

  /**
   * Note: it compacts number up to 1_000_000_000_000 (1e12) -> 1T, all above is formatted as iteration of T.
   * Example: 1.579208923731619e59 -> 157,920,892,373,161,900,000,000,000,000,000,000,000,000,000,000T
   */
  const compactNumFormat = new Intl.NumberFormat(getUserLocale(), {
    minimumFractionDigits: Math.max(0, decimalPlaces),
    maximumFractionDigits: Math.max(0, decimalPlaces),
    notation: 'compact',
    compactDisplay,
  });
  const scientificNumFormat = new Intl.NumberFormat(getUserLocale(), {
    minimumFractionDigits: Math.max(0, decimalPlaces),
    maximumFractionDigits: Math.max(0, decimalPlaces),
    notation: 'scientific',
  });

  if (number.isGreaterThan(DEFAULT_COMPACT_CAP)) {
    const r = /E(\d+)$/i;
    const formatted = scientificNumFormat.format(Number(number));
    const eNotation = formatted.match(r);
    if (eNotation && eNotation.length > 1) {
      const power = eNotation[1];
      return (
        <span>
          {formatted.replace(r, '')}{' '}
          <span>
            &times; 10
            <sup>{power}</sup>
          </span>
        </span>
      );
    }
  }

  return compactNumFormat.format(Number(number));
};
