const bech32 = require('bech32')
const Buffer = require('safe-buffer').Buffer
const BN = require('bn.js')

// defaults for encode; default timestamp is current time at call
const DEFAULTNETWORK = {
  // default network is bitcoin
  bech32: 'bc',
  pubKeyHash: 0x00,
  scriptHash: 0x05,
  validWitnessVersions: [0]
}
const TESTNETWORK = {
  bech32: 'tb',
  pubKeyHash: 0x6f,
  scriptHash: 0xc4,
  validWitnessVersions: [0]
}
const REGTESTNETWORK = {
  bech32: 'bcrt',
  pubKeyHash: 0x6f,
  scriptHash: 0xc4,
  validWitnessVersions: [0]
}
const SIMNETWORK = {
  bech32: 'sb',
  pubKeyHash: 0x3f,
  scriptHash: 0x7b,
  validWitnessVersions: [0]
}

const FEATUREBIT_ORDER = [
  'option_data_loss_protect',
  'initial_routing_sync',
  'option_upfront_shutdown_script',
  'gossip_queries',
  'var_onion_optin',
  'gossip_queries_ex',
  'option_static_remotekey',
  'payment_secret',
  'basic_mpp',
  'option_support_large_channel'
]

const DIVISORS = {
  m: new BN(1e3, 10),
  u: new BN(1e6, 10),
  n: new BN(1e9, 10),
  p: new BN(1e12, 10)
}

const MAX_MILLISATS = new BN('2100000000000000000', 10)

const MILLISATS_PER_BTC = new BN(1e11, 10)

const TAGCODES = {
  payment_hash: 1,
  payment_secret: 16,
  description: 13,
  payee: 19,
  description_hash: 23, // commit to longer descriptions (used by lnurl-pay)
  expiry: 6, // default: 3600 (1 hour)
  min_final_cltv_expiry: 24, // default: 9
  fallback_address: 9,
  route_hint: 3, // for extra routing info (private etc.)
  feature_bits: 5,
  metadata: 27
}

// reverse the keys and values of TAGCODES and insert into TAGNAMES
const TAGNAMES = {}
for (let i = 0, keys = Object.keys(TAGCODES); i < keys.length; i++) {
  const currentName = keys[i]
  const currentCode = TAGCODES[keys[i]].toString()
  TAGNAMES[currentCode] = currentName
}

const TAGPARSERS = {
  1: words => wordsToBuffer(words, true).toString('hex'), // 256 bits
  16: words => wordsToBuffer(words, true).toString('hex'), // 256 bits
  13: words => wordsToBuffer(words, true).toString('utf8'), // string variable length
  19: words => wordsToBuffer(words, true).toString('hex'), // 264 bits
  23: words => wordsToBuffer(words, true).toString('hex'), // 256 bits
  27: words => wordsToBuffer(words, true).toString('hex'), // variable
  6: wordsToIntBE, // default: 3600 (1 hour)
  24: wordsToIntBE, // default: 9
  3: routingInfoParser, // for extra routing info (private etc.)
  5: featureBitsParser // keep feature bits as array of 5 bit words
}

function getUnknownParser(tagCode) {
  return words => ({
    tagCode: parseInt(tagCode),
    words: bech32.encode('unknown', words, Number.MAX_SAFE_INTEGER)
  })
}

function wordsToIntBE(words) {
  return words.reverse().reduce((total, item, index) => {
    return total + item * Math.pow(32, index)
  }, 0)
}

function convert(data, inBits, outBits) {
  let value = 0
  let bits = 0
  const maxV = (1 << outBits) - 1

  const result = []
  for (let i = 0; i < data.length; ++i) {
    value = (value << inBits) | data[i]
    bits += inBits

    while (bits >= outBits) {
      bits -= outBits
      result.push((value >> bits) & maxV)
    }
  }

  if (bits > 0) {
    result.push((value << (outBits - bits)) & maxV)
  }

  return result
}

function wordsToBuffer(words, trim) {
  let buffer = Buffer.from(convert(words, 5, 8, true))
  if (trim && (words.length * 5) % 8 !== 0) {
    buffer = buffer.slice(0, -1)
  }
  return buffer
}

// first convert from words to buffer, trimming padding where necessary
// parse in 51 byte chunks. See encoder for details.
function routingInfoParser(words) {
  const routes = []
  let pubkey,
    shortChannelId,
    feeBaseMSats,
    feeProportionalMillionths,
    cltvExpiryDelta
  let routesBuffer = wordsToBuffer(words, true)
  while (routesBuffer.length > 0) {
    pubkey = routesBuffer.slice(0, 33).toString('hex') // 33 bytes
    shortChannelId = routesBuffer.slice(33, 41).toString('hex') // 8 bytes
    feeBaseMSats = parseInt(routesBuffer.slice(41, 45).toString('hex'), 16) // 4 bytes
    feeProportionalMillionths = parseInt(
      routesBuffer.slice(45, 49).toString('hex'),
      16
    ) // 4 bytes
    cltvExpiryDelta = parseInt(routesBuffer.slice(49, 51).toString('hex'), 16) // 2 bytes

    routesBuffer = routesBuffer.slice(51)

    routes.push({
      pubkey,
      short_channel_id: shortChannelId,
      fee_base_msat: feeBaseMSats,
      fee_proportional_millionths: feeProportionalMillionths,
      cltv_expiry_delta: cltvExpiryDelta
    })
  }
  return routes
}

function featureBitsParser(words) {
  const bools = words
    .slice()
    .reverse()
    .map(word => [
      !!(word & 0b1),
      !!(word & 0b10),
      !!(word & 0b100),
      !!(word & 0b1000),
      !!(word & 0b10000)
    ])
    .reduce((finalArr, itemArr) => finalArr.concat(itemArr), [])
  while (bools.length < FEATUREBIT_ORDER.length * 2) {
    bools.push(false)
  }
  const featureBits = {
    word_length: words.length
  }
  FEATUREBIT_ORDER.forEach((featureName, index) => {
    featureBits[featureName] = {
      required: bools[index * 2],
      supported: bools[index * 2 + 1]
    }
  })
  if (bools.length > FEATUREBIT_ORDER.length * 2) {
    const extraBits = bools.slice(FEATUREBIT_ORDER.length * 2)
    featureBits.extra_bits = {
      start_bit: FEATUREBIT_ORDER.length * 2,
      bits: extraBits,
      has_required: extraBits.reduce(
        (result, bit, index) =>
          index % 2 !== 0 ? result || false : result || bit,
        false
      )
    }
  } else {
    featureBits.extra_bits = {
      start_bit: FEATUREBIT_ORDER.length * 2,
      bits: [],
      has_required: false
    }
  }
  return featureBits
}

function hrpToMillisat(hrpString, outputString) {
  let divisor, value
  if (hrpString.slice(-1).match(/^[munp]$/)) {
    divisor = hrpString.slice(-1)
    value = hrpString.slice(0, -1)
  } else if (hrpString.slice(-1).match(/^[^munp0-9]$/)) {
    throw new Error('Not a valid multiplier for the amount')
  } else {
    value = hrpString
  }

  if (!value.match(/^\d+$/))
    throw new Error('Not a valid human readable amount')

  const valueBN = new BN(value, 10)

  const millisatoshisBN = divisor
    ? valueBN.mul(MILLISATS_PER_BTC).div(DIVISORS[divisor])
    : valueBN.mul(MILLISATS_PER_BTC)

  if (
    (divisor === 'p' && !valueBN.mod(new BN(10, 10)).eq(new BN(0, 10))) ||
    millisatoshisBN.gt(MAX_MILLISATS)
  ) {
    throw new Error('Amount is outside of valid range')
  }

  return outputString ? millisatoshisBN.toString() : millisatoshisBN
}

// decode will only have extra comments that aren't covered in encode comments.
// also if anything is hard to read I'll comment.
function decode(paymentRequest, network) {
  if (typeof paymentRequest !== 'string')
    throw new Error('Lightning Payment Request must be string')
  if (paymentRequest.slice(0, 2).toLowerCase() !== 'ln')
    throw new Error('Not a proper lightning payment request')

  const sections = []
  const decoded = bech32.decode(paymentRequest, Number.MAX_SAFE_INTEGER)
  paymentRequest = paymentRequest.toLowerCase()
  const prefix = decoded.prefix
  let words = decoded.words
  let letters = paymentRequest.slice(prefix.length + 1)
  let sigWords = words.slice(-104)
  words = words.slice(0, -104)

  // Without reverse lookups, can't say that the multipier at the end must
  // have a number before it, so instead we parse, and if the second group
  // doesn't have anything, there's a good chance the last letter of the
  // coin type got captured by the third group, so just re-regex without
  // the number.
  let prefixMatches = prefix.match(/^ln(\S+?)(\d*)([a-zA-Z]?)$/)
  if (prefixMatches && !prefixMatches[2])
    prefixMatches = prefix.match(/^ln(\S+)$/)
  if (!prefixMatches) {
    throw new Error('Not a proper lightning payment request')
  }

  // "ln" section
  sections.push({
    name: 'lightning_network',
    letters: 'ln'
  })

  // "bc" section
  const bech32Prefix = prefixMatches[1]
  let coinNetwork
  if (!network) {
    switch (bech32Prefix) {
      case DEFAULTNETWORK.bech32:
        coinNetwork = DEFAULTNETWORK
        break
      case TESTNETWORK.bech32:
        coinNetwork = TESTNETWORK
        break
      case REGTESTNETWORK.bech32:
        coinNetwork = REGTESTNETWORK
        break
      case SIMNETWORK.bech32:
        coinNetwork = SIMNETWORK
        break
    }
  } else {
    if (
      network.bech32 === undefined ||
      network.pubKeyHash === undefined ||
      network.scriptHash === undefined ||
      !Array.isArray(network.validWitnessVersions)
    )
      throw new Error('Invalid network')
    coinNetwork = network
  }
  if (!coinNetwork || coinNetwork.bech32 !== bech32Prefix) {
    throw new Error('Unknown coin bech32 prefix')
  }
  sections.push({
    name: 'coin_network',
    letters: bech32Prefix,
    value: coinNetwork
  })

  // amount section
  const value = prefixMatches[2]
  let millisatoshis
  if (value) {
    const divisor = prefixMatches[3]
    millisatoshis = hrpToMillisat(value + divisor, true)
    sections.push({
      name: 'amount',
      letters: prefixMatches[2] + prefixMatches[3],
      value: millisatoshis
    })
  } else {
    millisatoshis = null
  }

  // "1" separator
  sections.push({
    name: 'separator',
    letters: '1'
  })

  // timestamp
  const timestamp = wordsToIntBE(words.slice(0, 7))
  words = words.slice(7) // trim off the left 7 words
  sections.push({
    name: 'timestamp',
    letters: letters.slice(0, 7),
    value: timestamp
  })
  letters = letters.slice(7)

  let tagName, parser, tagLength, tagWords
  // we have no tag count to go on, so just keep hacking off words
  // until we have none.
  while (words.length > 0) {
    const tagCode = words[0].toString()
    tagName = TAGNAMES[tagCode] || 'unknown_tag'
    parser = TAGPARSERS[tagCode] || getUnknownParser(tagCode)
    words = words.slice(1)

    tagLength = wordsToIntBE(words.slice(0, 2))
    words = words.slice(2)

    tagWords = words.slice(0, tagLength)
    words = words.slice(tagLength)

    sections.push({
      name: tagName,
      tag: letters[0],
      letters: letters.slice(0, 1 + 2 + tagLength),
      value: parser(tagWords) // see: parsers for more comments
    })
    letters = letters.slice(1 + 2 + tagLength)
  }

  // signature
  sections.push({
    name: 'signature',
    letters: letters.slice(0, 104),
    value: wordsToBuffer(sigWords, true).toString('hex')
  })
  letters = letters.slice(104)

  // checksum
  sections.push({
    name: 'checksum',
    letters: letters
  })

  let result = {
    paymentRequest,
    sections,

    get expiry() {
      let exp = sections.find(s => s.name === 'expiry')
      if (exp) return getValue('timestamp') + exp.value
    },

    get route_hints() {
      return sections.filter(s => s.name === 'route_hint').map(s => s.value)
    }
  }

  for (let name in TAGCODES) {
    if (name === 'route_hint') {
      // route hints can be multiple, so this won't work for them
      continue
    }

    Object.defineProperty(result, name, {
      get() {
        return getValue(name)
      }
    })
  }

  return result

  function getValue(name) {
    let section = sections.find(s => s.name === name)
    return section ? section.value : undefined
  }
}

module.exports = {
  decode,
  hrpToMillisat
}
