/* eslint-disable @typescript-eslint/no-explicit-any */
import { abs, max2, min2, pow10, toPrecision } from './math'

export enum Rounding {
  /**
   * Rounds away from zero
   * Example: 1.5 -> 2, -1.5 -> -2
   */
  ROUND_UP,

  /**
   * Rounds towards zero
   * Example: 1.5 -> 1, -1.5 -> -1
   */
  ROUND_DOWN,

  /**
   * Rounds towards Infinity
   * Example: 1.5 -> 2, -1.5 -> -1
   */
  ROUND_CEIL,

  /**
   * Rounds towards -Infinity
   * Example: 1.5 -> 1, -1.5 -> -2
   */
  ROUND_FLOOR,

  /**
   * Rounds towards nearest neighbour.
   * If equidistant, rounds away from zero
   * Example: 1.5 -> 2, -1.5 -> -2
   */
  ROUND_HALF_UP,

  /**
   * Rounds towards nearest neighbour.
   * If equidistant, rounds towards zero
   * Example: 1.5 -> 1, -1.5 -> -1
   */
  ROUND_HALF_DOWN,

  /**
   * Rounds towards nearest neighbour.
   * If equidistant, rounds towards even neighbour
   * Example: 1.5 -> 2, 2.5 -> 2
   */
  ROUND_HALF_EVEN,

  /**
   * Rounds towards nearest neighbour.
   * If equidistant, rounds towards Infinity
   * Example: 1.5 -> 2, -1.5 -> -1
   */
  ROUND_HALF_CEIL,

  /**
   * Rounds towards nearest neighbour.
   * If equidistant, rounds towards -Infinity
   * Example: 1.5 -> 1, -1.5 -> -2
   */
  ROUND_HALF_FLOOR
}

export enum Decimals {
  left = 'left',
  right = 'right',
  min = 'min',
  max = 'max',
  add = 'add',
  sub = 'sub'
}

export type PrecisionResolution = Decimals | number | bigint

const pickPrecision = (
  aPrecision: bigint,
  bPrecision: bigint,
  precisionResolution: PrecisionResolution,
): bigint => {
  if (typeof precisionResolution !== 'string') {
    return BigInt(precisionResolution)
  }
  switch (precisionResolution) {
    case Decimals.left:
      return aPrecision
    case Decimals.right:
      return bPrecision
    case Decimals.min:
      return min2(aPrecision, bPrecision)
    case Decimals.max:
      return max2(aPrecision, bPrecision)
    case Decimals.add:
      return aPrecision + bPrecision
    case Decimals.sub:
      return max2(aPrecision, bPrecision) - min2(aPrecision, bPrecision)
  }
}

/**
 * Integer division `numerator / denominator` rounded according to the given mode.
 * The remainder is the discarded fractional part; rounding decides whether the
 * truncated quotient is moved one step away from zero.
 */
const roundDiv = (
  numerator: bigint,
  denominator: bigint,
  rounding: Rounding,
): bigint => {
  if (denominator < 0n) {
    numerator = -numerator
    denominator = -denominator
  }
  const isNegative = numerator < 0n
  const absNumerator = isNegative ? -numerator : numerator
  const quotient = absNumerator / denominator
  const remainder = absNumerator % denominator
  if (remainder === 0n) {
    return isNegative ? -quotient : quotient
  }
  const twiceRemainder = remainder * 2n
  let roundUp = false
  switch (rounding) {
    case Rounding.ROUND_UP: // Away from zero
      roundUp = true
      break
    case Rounding.ROUND_DOWN: // Towards zero
      roundUp = false
      break
    case Rounding.ROUND_CEIL: // Towards Infinity
      roundUp = !isNegative
      break
    case Rounding.ROUND_FLOOR: // Towards -Infinity
      roundUp = isNegative
      break
    case Rounding.ROUND_HALF_UP: // If halfway, away from zero
      roundUp = twiceRemainder >= denominator
      break
    case Rounding.ROUND_HALF_DOWN: // If halfway, towards zero
      roundUp = twiceRemainder > denominator
      break
    case Rounding.ROUND_HALF_EVEN: // If halfway, towards even neighbour
      roundUp = twiceRemainder > denominator || (twiceRemainder === denominator && quotient % 2n === 1n)
      break
    case Rounding.ROUND_HALF_CEIL: // If halfway, towards Infinity
      roundUp = twiceRemainder > denominator || (twiceRemainder === denominator && !isNegative)
      break
    case Rounding.ROUND_HALF_FLOOR: // If halfway, towards -Infinity
      roundUp = twiceRemainder > denominator || (twiceRemainder === denominator && isNegative)
      break
  }
  const rounded = roundUp ? quotient + 1n : quotient
  return isNegative ? -rounded : rounded
}

/**
 * Like {@link toPrecision}, but when precision is reduced the discarded digits
 * are rounded according to the given mode instead of always being truncated.
 */
const toPrecisionRounded = (
  base: bigint,
  to: bigint,
  from: bigint,
  rounding?: Rounding,
): bigint => {
  if (to >= from) {
    return toPrecision(base, to, from)
  }
  if (rounding === undefined || rounding === Rounding.ROUND_DOWN) {
    return base / pow10(from - to)
  }
  return roundDiv(base, pow10(from - to), rounding)
}

export class FixedPoint {

  static min(arg0: FixedPoint, ...args: FixedPoint[]): FixedPoint {
    let min = arg0
    for (const arg of args) {
      if (arg.lt(min)) {
        min = arg
      }
    }
    return min
  }

  static max(arg0: FixedPoint, ...args: FixedPoint[]): FixedPoint {
    let max = arg0
    for (const arg of args) {
      if (arg.gt(max)) {
        max = arg
      }
    }
    return max
  }

  base: bigint

  precision: bigint

  constructor(base: bigint, precision: bigint) {
    this.base = base
    this.precision = precision
  }

  add(arg: FixedPoint, resultPrecision?: PrecisionResolution): FixedPoint {
    if (resultPrecision === undefined && this.precision === arg.precision) {
      return new FixedPoint(this.base + arg.base, this.precision)
    }
    const aPrecision = this.precision
    const bPrecision = arg.precision
    const calcPrecision = max2(aPrecision, bPrecision)
    const targetPrecision = pickPrecision(aPrecision, bPrecision, resultPrecision ?? Decimals.left)
    const aBase = toPrecision(this.base, calcPrecision, aPrecision)
    const bBase = toPrecision(arg.base, calcPrecision, bPrecision)
    return new FixedPoint(toPrecision(aBase + bBase, targetPrecision, calcPrecision), targetPrecision)
  }

  sub(arg: FixedPoint, resultPrecision?: PrecisionResolution): FixedPoint {
    if (resultPrecision === undefined && this.precision === arg.precision) {
      return new FixedPoint(this.base - arg.base, this.precision)
    }
    const aPrecision = this.precision
    const bPrecision = arg.precision
    const calcPrecision = max2(aPrecision, bPrecision)
    const targetPrecision = pickPrecision(aPrecision, bPrecision, resultPrecision ?? Decimals.left)
    const aBase = toPrecision(this.base, calcPrecision, aPrecision)
    const bBase = toPrecision(arg.base, calcPrecision, bPrecision)
    return new FixedPoint(toPrecision(aBase - bBase, targetPrecision, calcPrecision), targetPrecision)
  }

  mul(
    arg: FixedPoint,
    resultPrecision?: PrecisionResolution,
    rounding?: Rounding,
  ): FixedPoint {
    // Hot path: kept tiny and byte-identical to the original so V8 keeps inlining it.
    // Any rounding/precision request is delegated to the (cold) general path.
    if (rounding === undefined && resultPrecision === undefined && this.precision === arg.precision) {
      return new FixedPoint((this.base * arg.base) / pow10(this.precision), this.precision)
    }
    return this.mulGeneral(arg, resultPrecision, rounding)
  }

  private mulGeneral(
    arg: FixedPoint,
    resultPrecision: PrecisionResolution | undefined,
    rounding: Rounding | undefined,
  ): FixedPoint {
    const aPrecision = this.precision
    const bPrecision = arg.precision
    const calcPrecision = aPrecision + bPrecision
    const targetPrecision = pickPrecision(aPrecision, bPrecision, resultPrecision ?? Decimals.max)
    const rawBase = this.base * arg.base // at calcPrecision
    return new FixedPoint(toPrecisionRounded(rawBase, targetPrecision, calcPrecision, rounding), targetPrecision)
  }

  div(
    arg: FixedPoint,
    resultPrecision?: PrecisionResolution,
    rounding?: Rounding,
  ): FixedPoint {
    // Hot path: kept tiny and byte-identical to the original so V8 keeps inlining it.
    // Any rounding/precision request is delegated to the (cold) general path.
    if (rounding === undefined && resultPrecision === undefined && this.precision === arg.precision) {
      return new FixedPoint((this.base * pow10(this.precision)) / arg.base, this.precision)
    }
    return this.divGeneral(arg, resultPrecision, rounding)
  }

  private divGeneral(
    arg: FixedPoint,
    resultPrecision: PrecisionResolution | undefined,
    rounding: Rounding | undefined,
  ): FixedPoint {
    const aPrecision = this.precision
    const bPrecision = arg.precision
    const targetPrecision = pickPrecision(aPrecision, bPrecision, resultPrecision ?? Decimals.max)
    // result at targetPrecision: this.base * 10^(bPrecision + targetPrecision - aPrecision) / arg.base
    const shift = bPrecision + targetPrecision - aPrecision
    let numerator: bigint
    let denominator: bigint
    if (shift >= 0n) {
      numerator = this.base * pow10(shift)
      denominator = arg.base
    } else {
      numerator = this.base
      denominator = arg.base * pow10(-shift)
    }
    if (rounding === undefined || rounding === Rounding.ROUND_DOWN) {
      return new FixedPoint(numerator / denominator, targetPrecision)
    }
    return new FixedPoint(roundDiv(numerator, denominator, rounding), targetPrecision)
  }

  cmp(arg: FixedPoint, comparator: (a: bigint, b: bigint) => boolean): boolean {
    const aPrecision = this.precision
    const bPrecision = arg.precision
    const newPrecision = max2(aPrecision, bPrecision)
    const aBase = toPrecision(this.base, newPrecision, aPrecision)
    const bBase = toPrecision(arg.base, newPrecision, bPrecision)
    return comparator(aBase, bBase)
  }

  eq(arg: FixedPoint): boolean {
    if (this.precision === arg.precision) {return this.base === arg.base}
    return this.cmp(arg, (a, b) => a === b)
  }

  gt(arg: FixedPoint): boolean {
    if (this.precision === arg.precision) {return this.base > arg.base}
    return this.cmp(arg, (a, b) => a > b)
  }

  lt(arg: FixedPoint): boolean {
    if (this.precision === arg.precision) {return this.base < arg.base}
    return this.cmp(arg, (a, b) => a < b)
  }

  gte(arg: FixedPoint): boolean {
    if (this.precision === arg.precision) {return this.base >= arg.base}
    return this.cmp(arg, (a, b) => a >= b)
  }

  lte(arg: FixedPoint): boolean {
    if (this.precision === arg.precision) {return this.base <= arg.base}
    return this.cmp(arg, (a, b) => a <= b)
  }

  neg(): FixedPoint {
    return new FixedPoint(-this.base, this.precision)
  }

  abs(): FixedPoint {
    return new FixedPoint(abs(this.base), this.precision)
  }

  sqrt(): FixedPoint {
    if (this.isNegative()) {
      throw new Error('Cannot calculate square root of negative number')
    }

    if (this.isZero()) {
      return new FixedPoint(0n, this.precision)
    }

    // For Newton-Raphson method, we need higher precision for intermediate calculations
    const workingPrecision = this.precision + 10n
    const workingThis = new FixedPoint(toPrecision(this.base, workingPrecision, this.precision), workingPrecision)

    // Initial guess: use the number shifted right by half the precision
    // This gives us a reasonable starting point for the Newton-Raphson method
    let x = new FixedPoint(workingThis.base >> (workingPrecision / 2n), workingPrecision)

    // Handle case where initial guess is zero (for very small numbers)
    if (x.isZero()) {
      x = new FixedPoint(10n ** (workingPrecision / 2n), workingPrecision)
    }

    const two = new FixedPoint(2n * (10n ** workingPrecision), workingPrecision)
    const epsilon = new FixedPoint(1n, workingPrecision) // Minimum precision unit

    // Newton-Raphson iteration: x_{n+1} = (x_n + a/x_n) / 2
    for (let i = 0; i < 50; i++) { // Maximum 50 iterations to prevent infinite loops
      const quotient = workingThis.div(x, workingPrecision)
      const newX = x.add(quotient, workingPrecision).div(two, workingPrecision)

      // Check for convergence
      if (newX.sub(x, workingPrecision).abs().lte(epsilon)) {
        break
      }

      x = newX
    }

    // Convert back to original precision
    return x.toPrecision(this.precision)
  }

  isZero(): boolean {
    return this.base === 0n
  }

  isPositive(): boolean {
    return this.base > 0n
  }

  isNegative(): boolean {
    return this.base < 0n
  }

  floor() {
    return this.round(Rounding.ROUND_FLOOR)
  }

  ceil() {
    return this.round(Rounding.ROUND_CEIL)
  }

  round(mode: Rounding = Rounding.ROUND_HALF_UP): FixedPoint {
    // No rounding needed for zero precision
    if (this.precision === 0n) {
      return new FixedPoint(this.base, this.precision)
    }

    const isNegative = this.isNegative()
    const absBase = abs(this.base)

    const divisor = pow10(this.precision)

    const integerPart = absBase / divisor
    const fractionalPart = absBase % divisor

    const isHalfwayCase = fractionalPart * 2n === divisor

    let rounded = integerPart

    switch (mode) {
      case Rounding.ROUND_UP: // Away from zero
        // Round up if there's any fractional part
        if (fractionalPart > 0n) {
          rounded = integerPart + 1n
        }
        break

      case Rounding.ROUND_DOWN: // Towards zero
        // Keep the integer part (truncate)
        rounded = integerPart
        break

      case Rounding.ROUND_CEIL: // Towards Infinity
        if (fractionalPart > 0n) {
          if (!isNegative) {
            rounded = integerPart + 1n
          } else {
            rounded = integerPart
          }
        }
        break

      case Rounding.ROUND_FLOOR: // Towards -Infinity
        if (fractionalPart > 0n) {
          if (!isNegative) {
            rounded = integerPart
          } else {
            rounded = integerPart + 1n
          }
        }
        break

      case Rounding.ROUND_HALF_UP: // If halfway, away from zero
        if (fractionalPart > divisor / 2n || (isHalfwayCase)) {
          rounded = integerPart + 1n
        }
        break

      case Rounding.ROUND_HALF_DOWN: // If halfway, towards zero
        if (fractionalPart > divisor / 2n) {
          rounded = integerPart + 1n
        }
        break

      case Rounding.ROUND_HALF_EVEN: // If halfway, towards even neighbor
        if (fractionalPart > divisor / 2n) {
          rounded = integerPart + 1n
        } else if (isHalfwayCase) {
          // If integerPart is even, keep it; if odd, round up
          if (integerPart % 2n === 1n) {
            rounded = integerPart + 1n
          }
        }
        break

      case Rounding.ROUND_HALF_CEIL: // If halfway, towards Infinity
        if (fractionalPart > divisor / 2n) {
          rounded = integerPart + 1n
        } else if (isHalfwayCase) {
          if (!isNegative) {
            rounded = integerPart + 1n
          }
        }
        break

      case Rounding.ROUND_HALF_FLOOR: // If halfway, towards -Infinity
        if (fractionalPart > divisor / 2n) {
          rounded = integerPart + 1n
        } else if (isHalfwayCase) {
          if (isNegative) {
            rounded = integerPart + 1n
          }
        }
        break
    }

    // Apply sign and create new FixedPoint instance with the same precision
    const roundedBase = isNegative ? -rounded * divisor : rounded * divisor
    return new FixedPoint(roundedBase, this.precision)
  }

  setPrecision(newPrecision: bigint, rounding: Rounding = Rounding.ROUND_DOWN): void {
    if (newPrecision < this.precision) {
      if (rounding === Rounding.ROUND_DOWN) {
        // Fast path: ROUND_DOWN is just truncation — BigInt division truncates towards zero
        this.base = this.base / pow10(this.precision - newPrecision)
        this.precision = newPrecision
      } else {
        const rounded = new FixedPoint(this.base, this.precision - newPrecision).round(rounding)
        this.base = toPrecision(rounded.base, newPrecision, this.precision)
        this.precision = newPrecision
      }
    } else if (newPrecision > this.precision) {
      this.base = toPrecision(this.base, newPrecision, this.precision)
      this.precision = newPrecision
    }
  }

  toPrecision(resultPrecision: number | bigint, rounding: Rounding = Rounding.ROUND_DOWN): FixedPoint {
    const newPrecision = BigInt(resultPrecision)
    if (newPrecision < this.precision) {
      if (rounding === Rounding.ROUND_DOWN) {
        return new FixedPoint(this.base / pow10(this.precision - newPrecision), newPrecision)
      }
      const rounded = new FixedPoint(this.base, this.precision - newPrecision).round(rounding)
      return new FixedPoint(toPrecision(rounded.base, newPrecision, this.precision), newPrecision)
    } else {
      return new FixedPoint(toPrecision(this.base, newPrecision, this.precision), newPrecision)
    }
  }

  toString() {
    return this.base.toString()
  }

  toJSON() {
    return this.toString()
  }

  toDecimalString(trimTrailingZeros = false) {
    const isNegative = this.isNegative()
    let str = abs(this.base).toString().padStart(Number(this.precision) + 1, '0')
    if (isNegative) {
      str = `-${str}`
    }
    if (this.precision === 0n) {
      return str
    }
    const intPart = str.slice(0, -Number(this.precision))
    const fracPart = str.slice(-Number(this.precision))
    if (!trimTrailingZeros) {
      return `${intPart}.${fracPart}`
    }
    let end = fracPart.length
    while (end > 0 && fracPart.charCodeAt(end - 1) === 48) {
      end -= 1
    }
    return end === 0 ? intPart : `${intPart}.${fracPart.slice(0, end)}`
  }

  toDecimal() {
    return Number(this.toDecimalString())
  }

  valueOf() {
    return this.toDecimal()
  }
}

// eslint-disable-next-line @typescript-eslint/no-empty-interface,@typescript-eslint/consistent-type-definitions,no-redeclare
export interface FixedPoint {
  plus: FixedPoint['add'],
  minus: FixedPoint['sub'],
  times: FixedPoint['mul'],
  multipliedBy: FixedPoint['mul'],
  dividedBy: FixedPoint['div'],
  isEqualTo: FixedPoint['eq'],
  isGreaterThan: FixedPoint['gt'],
  isLessThan: FixedPoint['lt'],
  isGreaterThanOrEqualTo: FixedPoint['gte'],
  isLessThanOrEqualTo: FixedPoint['lte'],
  negated: FixedPoint['neg'],
  absoluteValue: FixedPoint['abs'],
  squareRoot: FixedPoint['sqrt'],
}

const proto = FixedPoint.prototype
proto.plus = proto.add
proto.minus = proto.sub
proto.times = proto.mul
proto.multipliedBy = proto.mul
proto.dividedBy = proto.div
proto.isEqualTo = proto.eq
proto.isGreaterThan = proto.gt
proto.isLessThan = proto.lt
proto.isGreaterThanOrEqualTo = proto.gte
proto.isLessThanOrEqualTo = proto.lte
proto.negated = proto.neg
proto.absoluteValue = proto.abs
proto.squareRoot = proto.sqrt
