interface BaseIdentifier {
  toString(radix?: number): string
}

export interface TraceIdentifier extends BaseIdentifier {
  // We use a brand to distinguish between TraceIdentifier and SpanIdentifier, else TypeScript
  // considers them as the same type
  __brand: 'traceIdentifier'
}

export interface SpanIdentifier extends BaseIdentifier {
  __brand: 'spanIdentifier'
}

export function createTraceIdentifier() {
  return createIdentifier(64) as TraceIdentifier
}

export function createSpanIdentifier() {
  return createIdentifier(63) as SpanIdentifier
}

function createIdentifier(bits: 63 | 64): BaseIdentifier {
  const buffer = crypto.getRandomValues(new Uint32Array(2))
  if (bits === 63) {
    // eslint-disable-next-line no-bitwise
    buffer[buffer.length - 1] >>>= 1 // force 63-bit
  }

  // The `.toString` function is intentionally similar to Number and BigInt `.toString` method.
  //
  // JavaScript numbers can represent integers up to 48 bits, this is why we need two of them to
  // represent a 64 bits identifier. But BigInts don't have this limitation and can represent larger
  // integer values.
  //
  // In the future, when we drop browsers without BigInts support, we could use BigInts directly
  // represent identifiers by simply returning a BigInt from this function (as all we need is a
  // value with a `.toString` method).
  //
  // Examples:
  //   const buffer = getCrypto().getRandomValues(new Uint32Array(2))
  //   return BigInt(buffer[0]) + BigInt(buffer[1]) << 32n
  //
  //   // Alternative with BigUint64Array (different Browser support than plain bigints!):
  //   return crypto.getRandomValues(new BigUint64Array(1))[0]
  //
  // For now, let's keep using two plain numbers as having two different implementations (one for
  // browsers with BigInt support and one for older browsers) don't bring much value.
  return {
    toString(radix = 10) {
      let high = buffer[1]
      let low = buffer[0]
      let str = ''

      do {
        const mod = (high % radix) * 4294967296 + low
        high = Math.floor(high / radix)
        low = Math.floor(mod / radix)
        str = (mod % radix).toString(radix) + str
      } while (high || low)

      return str
    },
  }
}

export function toPaddedHexadecimalString(id: BaseIdentifier) {
  return id.toString(16).padStart(16, '0')
}
