"use strict";

interface PRNG {
  (): number
}

interface ULID {
  (seedTime?: number): string
}

interface ExportedObject extends ULID {
  createMonotonic(): ULID
  prng: PRNG
  incrementBase32(str: string): string
  randomChar(): string
  encodeTime(now: number, len: number): string
  encodeRandom(len: number): string
  decodeTime(id: string): number
  factory(prng: PRNG): ExportedObject
}

const factory = (prng: PRNG): ExportedObject => {

  // These values should NEVER change. If
  // they do, we're no longer making ulids!
  const ENCODING = "0123456789ABCDEFGHJKMNPQRSTVWXYZ" // Crockford's Base32
  const ENCODING_LEN = ENCODING.length
  const TIME_MAX = Math.pow(2, 48) - 1
  const TIME_LEN = 10
  const RANDOM_LEN = 16

  function replaceCharAt(str: string, index: number, char: string) {
    if (index > str.length - 1) {
      return str;
    }
    return str.substr(0, index) + char + str.substr(index + 1);
  }

  function incrementBase32(str: string): string {
    let done: string = undefined
    let index = str.length
    let char
    let charIndex
    const maxCharIndex = ENCODING_LEN - 1
    while (!done && index-- >= 0) {
      char = str[index]
      charIndex = ENCODING.indexOf(char)
      if (charIndex === -1) {
        throw new Error("incorrectly encoded string")
      }
      if (charIndex === maxCharIndex) {
        str = replaceCharAt(str, index, ENCODING[0])
        continue
      }
      done = replaceCharAt(str, index, ENCODING[charIndex + 1])
    }
    if (typeof done === "string") {
      return done
    }
    throw new Error("cannot increment this string")
  }

  function randomChar(): string {
    let rand = Math.floor(prng() * ENCODING_LEN)
    if (rand === ENCODING_LEN) {
      rand = ENCODING_LEN - 1
    }
    return ENCODING.charAt(rand)
  }

  function encodeTime(now: number, len: number): string {
    if (isNaN(now)) {
      throw new Error(now + " must be a number")
    }
    if (now > TIME_MAX) {
      throw new Error("cannot encode time greater than " + TIME_MAX)
    }
    if (now < 0) {
      throw new Error("time must be positive")
    }
    if (Number.isInteger(now) === false) {
      throw new Error("time must be an integer")
    }
    let mod
    let str = ""
    for (let x = len; x > 0; x--) {
      mod = now % ENCODING_LEN
      str = ENCODING.charAt(mod) + str
      now = (now - mod) / ENCODING_LEN
    }
    return str
  }

  function encodeRandom(len: number): string {
    let str = ""
    for (; len > 0; len--) {
      str = randomChar() + str
    }
    return str
  }

  function decodeTime(id: string): number {
    if (id.length !== TIME_LEN + RANDOM_LEN) {
      throw new Error("malformed ulid")
    }
    var time = id
      .substr(0, TIME_LEN)
      .split('')
      .reverse()
      .reduce((carry, char, index) => {
        const encodingIndex = ENCODING.indexOf(char)
        if (encodingIndex === -1) {
          throw new Error("invalid character found: " + char)
        }
        return carry += encodingIndex * Math.pow(ENCODING_LEN, index)
      }, 0)
    if (time > TIME_MAX) {
      throw new Error("malformed ulid, timestamp too large")
    }
    return time
  }

  function createMonotonic(): ULID {
    let lastTime: number = 0
    let lastRandom: string
    return function ulid(seedTime?: number): string {
      if (isNaN(seedTime)) {
        seedTime = Date.now()
      }
      if (seedTime <= lastTime) {
        const incrementedRandom = lastRandom = incrementBase32(lastRandom)
        return encodeTime(lastTime, TIME_LEN) + incrementedRandom
      }
      lastTime = seedTime
      const newRandom = lastRandom = encodeRandom(RANDOM_LEN)
      return encodeTime(seedTime, TIME_LEN) + newRandom
    }
  }

  const ulid = function ulid(seedTime?: number): string {
    if (isNaN(seedTime)) {
      seedTime = Date.now()
    }
    return encodeTime(seedTime, TIME_LEN) + encodeRandom(RANDOM_LEN)
  } as ExportedObject

  ulid.prng = prng
  ulid.createMonotonic = createMonotonic
  ulid.incrementBase32 = incrementBase32
  ulid.randomChar = randomChar
  ulid.encodeTime = encodeTime
  ulid.encodeRandom = encodeRandom
  ulid.decodeTime = decodeTime
  ulid.factory = factory

  return ulid

}

/* istanbul ignore next */
function _prng(root): PRNG {
  const browserCrypto = root && (root.crypto || root.msCrypto)

  if (browserCrypto) {
    try {
      return () => browserCrypto.getRandomValues(new Uint8Array(1))[0] / 0xFF
    } catch (e) {}
  } else {
    try {
      const nodeCrypto = require("crypto")
      return () => nodeCrypto.randomBytes(1).readUInt8() / 0xFF
    } catch (e) {}
  }

  try {
    console.error("[ulid] secure crypto unusable, falling back to insecure Math.random()!")
  } catch (e) {}

  return () => Math.random()
}

const root = typeof window !== "undefined" ? window : null
const prng = _prng(root)
export = factory(prng)
