'use strict';

/**
 * Executes a Rule of Three calculation.
 *
 * @param {number} val1 - The first reference value (numerator in direct proportion, denominator in inverse).
 * @param {number} val2 - The second reference value (denominator in direct proportion, numerator in inverse).
 * @param {number} val3 - The third value (numerator in direct proportion, denominator in inverse).
 * @param {boolean} [inverse] - Whether the calculation should use inverse proportion (true for inverse, false for direct).
 * @returns {number} The result of the Rule of Three operation.
 *
 * Rule of Three Formula (Direct Proportion):
 *      val1 / val2 = val3 / result
 *
 * For Inverse Proportion:
 *      val1 / val3 = val2 / result
 *
 * Visual Representation:
 *
 * For Direct Proportion:
 *      val1      val2
 *      -----  =  ------
 *      val3      result
 *
 * For Inverse Proportion:
 *      val1      val2
 *      -----  =  ------
 *      val3      result
 *
 * @example
 * // Direct proportion:
 * ruleOfThree.execute(2, 6, 3, false); // → 9
 *
 * @example
 * // Inverse proportion:
 * ruleOfThree.execute(2, 6, 3, true); // → 4
 */
function ruleOfThree(val1, val2, val3, inverse = false) {
  return inverse ? Number(val1 * val2) / val3 : Number(val3 * val2) / val1;
}

/**
 * Calculates the actual value that corresponds to a percentage of a base number.
 * Unlike `getPercentage`, which tells how much something represents in percent,
 * this function tells how much a given percentage *is worth* in value.
 *
 * @param {number} price - The base number to apply the percentage to.
 * @param {number} percentage - The percentage to calculate from the base.
 * @returns {number} The resulting value of the percentage.
 *
 * @example
 * getSimplePerc(200, 15); // 30
 */
function getSimplePerc(price, percentage) {
  return price * (percentage / 100);
}

/**
 * Calculates how much percent a partial value represents of the total value.
 *
 * @param {number} part - The partial value to compare.
 * @param {number} total - The total or maximum value.
 * @returns {number} The percentage that 'part' represents of 'total'.
 *
 * @example
 * getPercentage(5, 100); // 5
 */
function getPercentage(part, total) {
  if (total === 0) return 0;
  return (part / total) * 100;
}

/**
 * Calculates the age based on the given date.
 *
 * @param {number|string|Date} timeData - The birth date (can be a timestamp, ISO string, or Date object).
 * @param {Date|null} [now=null] - The Date object representing the current date. Defaults to the current date and time if not provided.
 * @returns {number|null} The age in years, or null if `timeData` is not provided or invalid.
 */
function getAge(timeData = 0, now = null) {
  if (typeof timeData !== 'undefined' && timeData !== null && timeData !== 0) {
    const birthDate = new Date(timeData);
    if (Number.isNaN(birthDate.getTime())) return null;

    const currentDate = now instanceof Date ? now : new Date();

    let age = currentDate.getFullYear() - birthDate.getFullYear();

    const currentMonth = currentDate.getMonth();
    const birthMonth = birthDate.getMonth();

    const currentDay = currentDate.getDate();
    const birthDay = birthDate.getDate();

    // Adjust if birthday hasn't occurred yet this year
    if (currentMonth < birthMonth || (currentMonth === birthMonth && currentDay < birthDay)) age--;

    return Math.abs(age);
  }

  return null;
}

/**
 * @typedef {Object} FormattedByteResult
 * @property {string|null} unit - The resulting unit (e.g., 'MB', 'GB') or null if input is invalid.
 * @property {number|null} value - The numerical value in the chosen unit, or null if input is invalid.
 */

/**
 * Converts a byte value into a human-readable format with unit and value separated.
 *
 * @param {number} bytes - The number of bytes to format. Must be a non-negative number.
 * @param {number|null} [decimals=null] - The number of decimal places to include in the result. Defaults to null. If negative, it will be treated as 0. If null, no rounding is applied.
 * @param {string|null} [maxUnit=null] - Optional unit limit. If provided, restricts conversion to this unit at most (e.g., 'MB' prevents conversion to 'GB' or higher). Must be one of: 'Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'.
 * @returns {FormattedByteResult} An object with the converted value and its corresponding unit. Returns nulls if input is invalid.
 *
 * @example
 * formatBytes(123456789);
 * // → { unit: 'MB', value: 117.74 }
 *
 * @example
 * formatBytes(1073741824, 2, 'MB');
 * // → { unit: 'MB', value: 1024 }
 */
function formatBytes(bytes, decimals = null, maxUnit = null) {
  if (typeof bytes !== 'number' || bytes < 0) return { unit: null, value: null };
  if (bytes === 0) return { unit: 'Bytes', value: 0 };

  const k = 1024;
  const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];

  const maxIndex = maxUnit && sizes.includes(maxUnit) ? sizes.indexOf(maxUnit) : sizes.length - 1;
  const i = Math.min(Math.floor(Math.log(bytes) / Math.log(k)), maxIndex);

  let value = bytes / Math.pow(k, i);

  if (decimals !== null) {
    const dm = decimals < 0 ? 0 : decimals;
    value = parseFloat(value.toFixed(dm));
  }

  const unit = sizes[i];
  return { unit, value };
}

/**
 * Generates a Fibonacci-like sequence as an array of vectors.
 *
 * @param {Object} [settings={}]
 * @param {number[]} [settings.baseValues=[0, 1]] - An array of two starting numbers (e.g. [0, 1] or [1, 1]).
 * @param {number} [settings.length=10] - Total number of items to generate in the sequence.
 * @param {(a: number, b: number, index: number) => number} [settings.combiner=((a, b) => a + b)] - A custom function to combine previous two numbers.
 * @returns {number[]} The resulting Fibonacci sequence.
 *
 * FibonacciVectors2D
 * @example
 * generateFibonacciSequence({
 *   baseValues: [[0, 1], [1, 1]],
 *   length: 10,
 *   combiner: ([x1, y1], [x2, y2]) => [x1 + x2, y1 + y2]
 * });
 *
 * @beta
 */
function genFibonacciSeq({
  baseValues = [0, 1],
  length = 10,
  combiner = (a, b) => a + b,
} = {}) {
  if (!Array.isArray(baseValues) || baseValues.length !== 2)
    throw new Error('baseValues must be an array of exactly two numbers');

  const sequence = [...baseValues.slice(0, 2)];

  for (let i = 2; i < length; i++) {
    const next = combiner(sequence[i - 2], sequence[i - 1], i);
    sequence.push(next);
  }

  return sequence;
}

/**
 * Calculates the unit price of a coin or token based on the market capitalization and circulating supply.
 *
 * This function is typically used in financial contexts to determine the price
 * of an asset by dividing its total market capitalization by its circulating supply.
 *
 * @param {number} originalMarketCap - The total market capitalization (e.g., in USD).
 * @param {number} circulatingSupply - The number of coins/tokens currently in circulation.
 * @returns {number} The calculated price per unit of the asset.
 */
function calculateMarketcap(originalMarketCap, circulatingSupply) {
  if (Number.isNaN(originalMarketCap) || !Number.isFinite(originalMarketCap))
    throw new TypeError('Original market cap must be a number.');
  if (Number.isNaN(circulatingSupply) || !Number.isFinite(circulatingSupply))
    throw new TypeError('Circulating supply must be a number.');
  if (circulatingSupply <= 0) throw new Error('Circulating supply must be greater than zero.');
  return originalMarketCap / circulatingSupply;
}

/**
 * Calculates the new price of a coin when the market cap changes.
 * @param {number} originalMarketCap - The original market cap.
 * @param {number} circulatingSupply - The circulating supply.
 * @param {number} newMarketCap - The new market cap.
 * @returns {{
 *   originalPrice: number,
 *   newPrice: number,
 *   priceChangePercent: number
 * }}
 */
function compareMarketcap(originalMarketCap, circulatingSupply, newMarketCap) {
  if (Number.isNaN(newMarketCap) || !Number.isFinite(newMarketCap))
    throw new TypeError('New market cap must be a number.');
  const originalPrice = calculateMarketcap(originalMarketCap, circulatingSupply);
  const newPrice = typeof newMarketCap === 'number' ? newMarketCap / circulatingSupply : NaN;
  const priceChangePercent =
    typeof newMarketCap === 'number' ? ((newPrice - originalPrice) / originalPrice) * 100 : NaN;
  return {
    originalPrice,
    newPrice,
    priceChangePercent,
  };
}

exports.calculateMarketcap = calculateMarketcap;
exports.compareMarketcap = compareMarketcap;
exports.formatBytes = formatBytes;
exports.genFibonacciSeq = genFibonacciSeq;
exports.getAge = getAge;
exports.getPercentage = getPercentage;
exports.getSimplePerc = getSimplePerc;
exports.ruleOfThree = ruleOfThree;
