// the largest BigInt we can safely downcast to a Number
const MAX_SAFE_NUMBER_INTEGER = BigInt(Number.MAX_SAFE_INTEGER)
const MIN_SAFE_NUMBER_INTEGER = BigInt(Number.MIN_SAFE_INTEGER)

/**
 * Constructs new long bits.
 *
 * @classdesc Helper class for working with the low and high bits of a 64 bit value.
 * @memberof util
 * @function Object() { [native code] }
 * @param {number} lo - Low 32 bits, unsigned
 * @param {number} hi - High 32 bits, unsigned
 */
export class LongBits {
  public lo: number
  public hi: number

  constructor (lo: number, hi: number) {
    // note that the casts below are theoretically unnecessary as of today, but older statically
    // generated converter code might still call the ctor with signed 32bits. kept for compat.

    /**
     * Low bits
     */
    this.lo = lo | 0

    /**
     * High bits
     */
    this.hi = hi | 0
  }

  /**
   * Converts this long bits to a possibly unsafe JavaScript number
   */
  toNumber (unsigned: boolean = false): number {
    if (!unsigned && (this.hi >>> 31) > 0) {
      const lo = ~this.lo + 1 >>> 0
      let hi = ~this.hi >>> 0
      if (lo === 0) {
        hi = hi + 1 >>> 0
      }
      return -(lo + hi * 4294967296)
    }
    return this.lo + this.hi * 4294967296
  }

  /**
   * Converts this long bits to a bigint
   */
  toBigInt (unsigned: boolean = false): bigint {
    if (unsigned) {
      return BigInt(this.lo >>> 0) + (BigInt(this.hi >>> 0) << 32n)
    }

    if ((this.hi >>> 31) !== 0) {
      const lo = ~this.lo + 1 >>> 0
      let hi = ~this.hi >>> 0
      if (lo === 0) {
        hi = hi + 1 >>> 0
      }
      return -(BigInt(lo) + (BigInt(hi) << 32n))
    }

    return BigInt(this.lo >>> 0) + (BigInt(this.hi >>> 0) << 32n)
  }

  /**
   * Converts this long bits to a string
   */
  toString (unsigned: boolean = false): string {
    return this.toBigInt(unsigned).toString()
  }

  /**
   * Zig-zag encodes this long bits
   */
  zzEncode (): this {
    const mask = this.hi >> 31
    this.hi = ((this.hi << 1 | this.lo >>> 31) ^ mask) >>> 0
    this.lo = (this.lo << 1 ^ mask) >>> 0
    return this
  }

  /**
   * Zig-zag decodes this long bits
   */
  zzDecode (): this {
    const mask = -(this.lo & 1)
    this.lo = ((this.lo >>> 1 | this.hi << 31) ^ mask) >>> 0
    this.hi = (this.hi >>> 1 ^ mask) >>> 0
    return this
  }

  /**
   * Calculates the length of this longbits when encoded as a varint.
   */
  length (): number {
    const part0 = this.lo
    const part1 = (this.lo >>> 28 | this.hi << 4) >>> 0
    const part2 = this.hi >>> 24
    return part2 === 0
      ? part1 === 0
        ? part0 < 16384
          ? part0 < 128 ? 1 : 2
          : part0 < 2097152 ? 3 : 4
        : part1 < 16384
          ? part1 < 128 ? 5 : 6
          : part1 < 2097152 ? 7 : 8
      : part2 < 128 ? 9 : 10
  }

  /**
   * Constructs new long bits from the specified number
   */
  static fromBigInt (value: bigint): LongBits {
    if (value === 0n) {
      return zero
    }

    if (value < MAX_SAFE_NUMBER_INTEGER && value > MIN_SAFE_NUMBER_INTEGER) {
      return this.fromNumber(Number(value))
    }

    const negative = value < 0n

    if (negative) {
      value = -value
    }

    let hi = value >> 32n
    let lo = value - (hi << 32n)

    if (negative) {
      hi = ~hi | 0n
      lo = ~lo | 0n

      if (++lo > TWO_32) {
        lo = 0n
        if (++hi > TWO_32) { hi = 0n }
      }
    }

    return new LongBits(Number(lo), Number(hi))
  }

  /**
   * Constructs new long bits from the specified number
   */
  static fromNumber (value: number): LongBits {
    if (value === 0) { return zero }
    const sign = value < 0
    if (sign) { value = -value }
    let lo = value >>> 0
    let hi = (value - lo) / 4294967296 >>> 0
    if (sign) {
      hi = ~hi >>> 0
      lo = ~lo >>> 0
      if (++lo > 4294967295) {
        lo = 0
        if (++hi > 4294967295) { hi = 0 }
      }
    }
    return new LongBits(lo, hi)
  }

  /**
   * Constructs new long bits from a number, long or string
   */
  static from (value: bigint | number | string | { low: number, high: number }): LongBits {
    if (typeof value === 'number') {
      return LongBits.fromNumber(value)
    }
    if (typeof value === 'bigint') {
      return LongBits.fromBigInt(value)
    }
    if (typeof value === 'string') {
      return LongBits.fromBigInt(BigInt(value))
    }
    return value.low != null || value.high != null ? new LongBits(value.low >>> 0, value.high >>> 0) : zero
  }
}

const zero = new LongBits(0, 0)
zero.toBigInt = function () { return 0n }
zero.zzEncode = zero.zzDecode = function () { return this }
zero.length = function () { return 1 }

const TWO_32 = 4294967296n
