const bech32CharValues = 'qpzry9x8gf2tvdw0s3jn54khce6mua7l';

module.exports = {
  decode: function(paymentRequest) {
  	  let input = paymentRequest.toLowerCase();
      let splitPosition = input.lastIndexOf('1');
      let humanReadablePart = input.substring(0, splitPosition);
      let data = input.substring(splitPosition + 1, input.length - 6);
      let checksum = input.substring(input.length - 6, input.length);

      if (!this.verify_checksum(humanReadablePart, this.bech32ToFiveBitArray(data + checksum))) {
          return 'error';
      }

      return {
          'human_readable_part': this.decodeHumanReadablePart(humanReadablePart),
          'data': this.decodeData(data, humanReadablePart),
          'checksum': checksum
      }
  },

  decodeHumanReadablePart: function(humanReadablePart) {
      let prefixes = ['lnbc', 'lntb', 'lnbcrt'];
      let prefix;
      prefixes.forEach(value => {
          if (humanReadablePart.substring(0, value.length) === value) {
              prefix = value;
          }
      });
      if (prefix == null) return 'error'; // A reader MUST fail if it does not understand the prefix.
      let amount = this.decodeAmount(humanReadablePart.substring(prefix.length, humanReadablePart.length));

      return {
          'prefix': prefix,
          'amount': amount
      }
  },

  decodeData: function(data, humanReadablePart) {
      let date32 = data.substring(0, 7);
      let dateEpoch = this.bech32ToInt(date32);
      let signature = data.substring(data.length - 104, data.length);
      let tagData = data.substring(7, data.length - 104);
      let decodedTags = this.decodeTags(tagData);
      let value = this.bech32ToFiveBitArray(date32 + tagData);
      value = this.fiveBitArrayTo8BitArray(value, true);
      value = this.textToHexString(humanReadablePart).concat(this.byteArrayToHexString(value));

  		return {
          'time_stamp': dateEpoch,
          'tags': decodedTags,
          'signature': this.decodeSignature(signature),
          'signing_data': value
      }
  },

  decodeSignature: function(signature) {
      let data = this.fiveBitArrayTo8BitArray(this.bech32ToFiveBitArray(signature));
      let recoveryFlag = data[data.length - 1];
      let r = this.byteArrayToHexString(data.slice(0, 32));
      let s = this.byteArrayToHexString(data.slice(32, data.length - 1));
      return {
          'r': r,
          's': s,
          'recovery_flag': recoveryFlag
      }
  },

  decodeAmount: function(str) {
      let multiplier = str.charAt(str.length - 1);
      let amount = str.substring(0, str.length - 1);
      if (amount.substring(0, 1) === '0') {
          return 'error';
      }
      amount = Number(amount);
      if (amount < 0 || !Number.isInteger(amount)) {
          return 'error';
      }

      switch (multiplier) {
          case '':
              return 'Any amount'; // A reader SHOULD indicate if amount is unspecified
          case 'p':
              return amount / 10;
          case 'n':
              return amount * 100;
          case 'u':
              return amount * 100000;
          case 'm':
              return amount * 100000000;
          default:
              // A reader SHOULD fail if amount is followed by anything except a defined multiplier.
              return 'error';
      }
  },

  decodeTags: function(tagData) {
      let tags = this.extractTags(tagData);
      let decodedTags = [];
      tags.forEach(value => decodedTags.push(this.decodeTag(value.type, value.length, value.data)));
      return decodedTags;
  },

  extractTags: function(str) {
      let tags = [];
      while (str.length > 0) {
          let type = str.charAt(0);
          let dataLength = this.bech32ToInt(str.substring(1, 3));
          let data = str.substring(3, dataLength + 3);
          tags.push({
              'type': type,
              'length': dataLength,
              'data': data
          });
          str = str.substring(3 + dataLength, str.length);
      }
      return tags;
  },

  decodeTag: function(type, length, data) {
      switch (type) {
          case 'p':
              if (length !== 52) break; // A reader MUST skip over a 'p' field that does not have data_length 52
              return {
                  'type': type,
                  'length': length,
                  'description': 'payment_hash',
                  'value': this.byteArrayToHexString(this.fiveBitArrayTo8BitArray(this.bech32ToFiveBitArray(data)))
              };
          case 'd':
              return {
                  'type': type,
                  'length': length,
                  'description': 'description',
                  'value': this.bech32ToUTF8String(data)
              };
          case 'n':
              if (length !== 53) break; // A reader MUST skip over a 'n' field that does not have data_length 53
              return {
                  'type': type,
                  'length': length,
                  'description': 'payee_public_key',
                  'value': this.byteArrayToHexString(this.fiveBitArrayTo8BitArray(this.bech32ToFiveBitArray(data)))
              };
          case 'h':
              if (length !== 52) break; // A reader MUST skip over a 'h' field that does not have data_length 52
              return {
                  'type': type,
                  'length': length,
                  'description': 'description_hash',
                  'value': data
              };
          case 'x':
              return {
                  'type': type,
                  'length': length,
                  'description': 'expiry',
                  'value': this.bech32ToInt(data)
              };
          case 'c':
              return {
                  'type': type,
                  'length': length,
                  'description': 'min_final_cltv_expiry',
                  'value': this.bech32ToInt(data)
              };
          case 'f':
              let version = this.bech32ToFiveBitArray(data.charAt(0))[0];
              if (version < 0 || version > 18) break; // a reader MUST skip over an f field with unknown version.
              data = data.substring(1, data.length);
              return {
                  'type': type,
                  'length': length,
                  'description': 'fallback_address',
                  'value': {
                      'version': version,
                      'fallback_address': data
                  }
              };
          case 'r':
              data = this.fiveBitArrayTo8BitArray(this.bech32ToFiveBitArray(data));
              let pubkey = data.slice(0, 33);
              let shortChannelId = data.slice(33, 41);
              let feeBaseMsat = data.slice(41, 45);
              let feeProportionalMillionths = data.slice(45, 49);
              let cltvExpiryDelta = data.slice(49, 51);
              return {
                  'type': type,
                  'length': length,
                  'description': 'routing_information',
                  'value': {
                      'public_key': this.byteArrayToHexString(pubkey),
                      'short_channel_id': this.byteArrayToHexString(shortChannelId),
                      'fee_base_msat': this.byteArrayToInt(feeBaseMsat),
                      'fee_proportional_millionths': this.byteArrayToInt(feeProportionalMillionths),
                      'cltv_expiry_delta': this.byteArrayToInt(cltvExpiryDelta)
                  }
              };
          default:
          // reader MUST skip over unknown fields
      }
  },

  polymod: function(values) {
      let GEN = [0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3];
      let chk = 1;
      values.forEach((value) => {
          let b = (chk >> 25);
          chk = (chk & 0x1ffffff) << 5 ^ value;
          for (let i = 0; i < 5; i++) {
              if (((b >> i) & 1) === 1) {
                  chk ^= GEN[i];
              } else {
                  chk ^= 0;
              }
          }
      });
      return chk;
  },

  expand: function(str) {
      let array = [];
      for (let i = 0; i < str.length; i++) {
          array.push(str.charCodeAt(i) >> 5);
      }
      array.push(0);
      for (let i = 0; i < str.length; i++) {
          array.push(str.charCodeAt(i) & 31);
      }
      return array;
  },

  verify_checksum: function(hrp, data) {
      hrp = this.expand(hrp);
      let all = hrp.concat(data);
      let bool = this.polymod(all);
      return bool === 1;
  },

  byteArrayToInt: function(byteArray) {
      let value = 0;
      for (let i = 0; i < byteArray.length; ++i) {
          value = (value << 8) + byteArray[i];
      }
      return value;
  },

  bech32ToInt: function(str) {
      let sum = 0;
      for (let i = 0; i < str.length; i++) {
          sum = sum * 32;
          sum = sum + bech32CharValues.indexOf(str.charAt(i));
      }
      return sum;
  },

  bech32ToFiveBitArray: function(str) {
      let array = [];
      for (let i = 0; i < str.length; i++) {
          array.push(bech32CharValues.indexOf(str.charAt(i)));
      }
      return array;
  },

  fiveBitArrayTo8BitArray: function(int5Array, includeOverflow) {
      let count = 0;
      let buffer = 0;
      let byteArray = [];
      int5Array.forEach((value) => {
          buffer = (buffer << 5) + value;
          count += 5;
          if (count >= 8) {
              byteArray.push(buffer >> (count - 8) & 255);
              count -= 8;
          }
      });
      if (includeOverflow && count > 0) {
          byteArray.push(buffer << (8 - count) & 255);
      }
      return byteArray;
  },

  bech32ToUTF8String: function(str) {
      let int5Array = this.bech32ToFiveBitArray(str);
      let byteArray = this.fiveBitArrayTo8BitArray(int5Array);

      let utf8String = '';
      for (let i = 0; i < byteArray.length; i++) {
          utf8String += '%' + ('0' + byteArray[i].toString(16)).slice(-2);
      }
      return decodeURIComponent(utf8String);
  },

  byteArrayToHexString: function(byteArray) {
      return Array.prototype.map.call(byteArray, function (byte) {
          return ('0' + (byte & 0xFF).toString(16)).slice(-2);
      }).join('');
  },

  textToHexString: function(text) {
      let hexString = '';
      for (let i = 0; i < text.length; i++) {
          hexString += text.charCodeAt(i).toString(16);
      }
      return hexString;
  },

  epochToDate: function(int) {
      let date = new Date(int * 1000);
      return date.toUTCString();
  }
}