# Source: src/main/js/common/MoneroUtils.js

    const assert = require("assert");
    const BigInteger = require("./biginteger").BigInteger;
    const GenUtils = require("./GenUtils");
    const LibraryUtils = require("./LibraryUtils");
    const MoneroError = require("./MoneroError");
    const MoneroIntegratedAddress = require("../wallet/model/MoneroIntegratedAddress");
    const MoneroNetworkType = require("../daemon/model/MoneroNetworkType");

    /**
     * Collection of Monero utilities. Runs in a worker thread by default.
     *
     * @hideconstructor
     */
    class MoneroUtils {

      /**
       * <p>Get the version of the monero-javascript library.<p>
       *
       * @return {string} the version of this monero-javascript library
       */
      static getVersion() {
        return "0.8.3";
      }

      /**
       * Enable or disable proxying these utilities to a worker thread.
       *
       * @param {boolean} proxyToWorker - specifies if utilities should be proxied to a worker
       */
      static setProxyToWorker(proxyToWorker) {
        MoneroUtils.PROXY_TO_WORKER = proxyToWorker || false;
      }

      /**
       * Validate the given mnemonic, throw an error if invalid.
       *
       * TODO: improve validation, use network type
       *
       * @param {string} mnemonic - mnemonic to validate
       */
      static async validateMnemonic(mnemonic) {
        assert(mnemonic, "Mnemonic phrase is not initialized");
        let words = mnemonic.split(" ");
        if (words.length !== MoneroUtils.NUM_MNEMONIC_WORDS) throw new MoneroError("Mnemonic phrase is " + words.length + " words but must be " + MoneroUtils.NUM_MNEMONIC_WORDS);
      }

      /**
       * Indicates if a private view key is valid.
       *
       * @param {string} privateViewKey is the private view key to validate
       * @return {Promise<bool>} true if the private view key is valid, false otherwise
       */
      static async isValidPrivateViewKey(privateViewKey) {
        try {
          await MoneroUtils.validatePrivateViewKey(privateViewKey);
          return true;
        } catch (e) {
          return false;
        }
      }

      /**
       * Indicates if a public view key is valid.
       *
       * @param {string} publicViewKey is the public view key to validate
       * @return {Promise<bool>} true if the public view key is valid, false otherwise
       */
      static async isValidPublicViewKey(publicViewKey) {
        try {
          await MoneroUtils.validatePublicViewKey(publicViewKey);
          return true;
        } catch (e) {
          return false;
        }
      }

      /**
       * Indicates if a private spend key is valid.
       *
       * @param {string} privateSpendKey is the private spend key to validate
       * @return {Promise<bool>} true if the private spend key is valid, false otherwise
       */
      static async isValidPrivateSpendKey(privateSpendKey) {
        try {
          await MoneroUtils.validatePrivateSpendKey(privateSpendKey);
          return true;
        } catch (e) {
          return false;
        }
      }

      /**
       * Indicates if a public spend key is valid.
       *
       * @param {string} publicSpendKey is the public spend key to validate
       * @return {Promise<bool>} true if the public spend key is valid, false otherwise
       */
      static async isValidPublicSpendKey(publicSpendKey) {
        try {
          await MoneroUtils.validatePublicSpendKey(publicSpendKey);
          return true;
        } catch (e) {
          return false;
        }
      }

      /**
       * Validate the given private view key, throw an error if invalid.
       *
       * @param {string} privateViewKey - private view key to validate
       */
      static async validatePrivateViewKey(privateViewKey) {
        if (!MoneroUtils._isHex64(privateViewKey)) throw new MoneroError("private view key expected to be 64 hex characters");
      }

      /**
       * Validate the given public view key, throw an error if invalid.
       *
       * @param {string} publicViewKey - public view key to validate
       */
      static async validatePublicViewKey(publicViewKey) {
        if (!MoneroUtils._isHex64(publicViewKey)) throw new MoneroError("public view key expected to be 64 hex characters");
      }

      /**
       * Validate the given private spend key, throw an error if invalid.
       *
       * @param {string} privateSpendKey - private spend key to validate
       */
      static async validatePrivateSpendKey(privateSpendKey) {
        if (!MoneroUtils._isHex64(privateSpendKey)) throw new MoneroError("private spend key expected to be 64 hex characters");
      }

      /**
       * Validate the given public spend key, throw an error if invalid.
       *
       * @param {string} publicSpendKey - public spend key to validate
       */
      static async validatePublicSpendKey(publicSpendKey) {
        if (!MoneroUtils._isHex64(publicSpendKey)) throw new MoneroError("public spend key expected to be 64 hex characters");
      }

      /**
       * Get an integrated address.
       *
       * @param {MoneroNetworkType} networkType - network type of the integrated address
       * @param {string} standardAddress - address to derive the integrated address from
       * @param {string} paymentId - optionally specifies the integrated address's payment id (defaults to random payment id)
       * @return {Promise<MoneroIntegratedAddress>} the integrated address
       */
      static async getIntegratedAddress(networkType, standardAddress, paymentId) {
        if (MoneroUtils.PROXY_TO_WORKER) return new MoneroIntegratedAddress(await LibraryUtils.invokeWorker(undefined, "moneroUtilsGetIntegratedAddress", Array.from(arguments)));

        // validate inputs
        MoneroNetworkType.validate(networkType);
        assert(typeof standardAddress === "string", "Address is not string");
        assert(standardAddress.length > 0, "Address is empty");
        assert(GenUtils.isBase58(standardAddress), "Address is not base 58");

        // load keys module by default
        if (LibraryUtils.getWasmModule() === undefined) await LibraryUtils.loadKeysModule();

        // get integrated address in queue
        return LibraryUtils.getWasmModule().queueTask(async function() {
          let integratedAddressJson = LibraryUtils.getWasmModule().get_integrated_address_util(networkType, standardAddress, paymentId ? paymentId : "");
          if (integratedAddressJson.charAt(0) !== '{') throw new MoneroError(integratedAddressJson);
          return new MoneroIntegratedAddress(JSON.parse(integratedAddressJson));
        });
      }

      /**
       * Determine if the given address is valid.
       *
       * @param {string} address - address
       * @param {MoneroNetworkType} networkType - network type of the address to validate
       * @return {Promise<boolean>} true if the address is valid, false otherwise
       */
      static async isValidAddress(address, networkType) {
        try {
          await MoneroUtils.validateAddress(address, networkType);
          return true;
        } catch (err) {
          return false;
        }
      }

      /**
       * Validate the given address, throw an error if invalid.
       *
       * @param {string} address - address to validate
       * @param {MoneroNetworkType} networkType - network type of the address to validate
       */
      static async validateAddress(address, networkType) {
        if (MoneroUtils.PROXY_TO_WORKER) return LibraryUtils.invokeWorker(undefined, "moneroUtilsValidateAddress", Array.from(arguments));

        // validate inputs
        assert(typeof address === "string", "Address is not string");
        assert(address.length > 0, "Address is empty");
        assert(GenUtils.isBase58(address), "Address is not base 58");
        MoneroNetworkType.validate(networkType);

        // load keys module by default
        if (LibraryUtils.getWasmModule() === undefined) await LibraryUtils.loadKeysModule();

        // validate address in queue
        return LibraryUtils.getWasmModule().queueTask(async function() {
          let errMsg = LibraryUtils.getWasmModule().validate_address(address, networkType);
          if (errMsg) throw new MoneroError(errMsg);
        });
      }

      /**
       * Determine if the given payment id is valid.
       *
       * @param {string} paymentId - payment id to determine if valid
       * @return {Promise<bool>} true if the payment id is valid, false otherwise
       */
      static async isValidPaymentId(paymentId) {
        try {
          await MoneroUtils.validatePaymentId(paymentId);
          return true;
        } catch (e) {
          return false;
        }
      }

      /**
       * Validate the given payment id, throw an error if invalid.
       *
       * TODO: improve validation
       *
       * @param {string} paymentId - payment id to validate
       */
      static async validatePaymentId(paymentId) {
        assert.equal(typeof paymentId, "string");
        assert(paymentId.length === 16 || paymentId.length === 64);
      }

      /**
       * Decode tx extra according to https://cryptonote.org/cns/cns005.txt and
       * returns the last tx pub key.
       *
       * TODO: use c++ bridge for this
       *
       * @param [byte[]] txExtra - array of tx extra bytes
       * @return {string} the last pub key as a hexidecimal string
       */
      static async getLastTxPubKey(txExtra) {
        let lastPubKeyIdx;
        for (let i = 0; i < txExtra.length; i++) {
          let tag = txExtra[i];
          if (tag === 0 || tag === 2) {
            i += 1 + txExtra[i + 1];  // advance to next tag
          } else if (tag === 1) {
            lastPubKeyIdx = i + 1;
            i += 1 + 32;              // advance to next tag
          } else throw new MoneroError("Invalid sub-field tag: " + tag);
        }
        return Buffer.from(new Uint8Array(txExtra.slice(lastPubKeyIdx, lastPubKeyIdx + 32))).toString("hex");
      }

      /**
       * Determines if two payment ids are functionally equal.
       *
       * For example, 03284e41c342f032 and 03284e41c342f032000000000000000000000000000000000000000000000000 are considered equal.
       *
       * @param {string} paymentId1 is a payment id to compare
       * @param {string} paymentId2 is a payment id to compare
       * @return {bool} true if the payment ids are equal, false otherwise
       */
      static paymentIdsEqual(paymentId1, paymentId2) {
        let maxLength = Math.max(paymentId1.length, paymentId2.length);
        for (let i = 0; i < maxLength; i++) {
          if (i < paymentId1.length && i < paymentId2.length && paymentId1[i] !== paymentId2[i]) return false;
          if (i >= paymentId1.length && paymentId2[i] !== '0') return false;
          if (i >= paymentId2.length && paymentId1[i] !== '0') return false;
        }
        return true;
      }

      /**
       * Merges a transaction into a list of existing transactions.
       *
       * @param {MoneroTx[]} txs - existing transactions to merge into
       * @param {MoneroTx} tx - transaction to merge into the list
       */
      static mergeTx(txs, tx) {
        for (let aTx of txs) {
          if (aTx.getHash() === tx.getHash()) {
            aTx.merge(tx);
            return;
          }
        }
        txs.push(tx);
      }

      /**
       * Convert the given JSON to a binary Uint8Array using Monero's portable storage format.
       *
       * @param {object} json - json to convert to binary
       * @return {Promise<Uint8Array>} the json converted to portable storage binary
       */
      static async jsonToBinary(json) {
        if (MoneroUtils.PROXY_TO_WORKER) return LibraryUtils.invokeWorker(undefined, "moneroUtilsJsonToBinary", Array.from(arguments));

        // load keys module by default
        if (LibraryUtils.getWasmModule() === undefined) await LibraryUtils.loadKeysModule();

        // use wasm in queue
        return LibraryUtils.getWasmModule().queueTask(async function() {

          // serialize json to binary which is stored in c++ heap
          let binMemInfoStr = LibraryUtils.getWasmModule().malloc_binary_from_json(JSON.stringify(json));

          // sanitize binary memory address info
          let binMemInfo = JSON.parse(binMemInfoStr);
          binMemInfo.ptr = parseInt(binMemInfo.ptr);
          binMemInfo.length = parseInt(binMemInfo.length);

          // read binary data from heap to Uint8Array
          let view = new Uint8Array(binMemInfo.length);
          for (let i = 0; i < binMemInfo.length; i++) {
            view[i] = LibraryUtils.getWasmModule().HEAPU8[binMemInfo.ptr / Uint8Array.BYTES_PER_ELEMENT + i];
          }

          // free binary on heap
          LibraryUtils.getWasmModule()._free(binMemInfo.ptr);

          // return json from binary data
          return view;
        });
      }

      /**
       * Convert the given portable storage binary to JSON.
       *
       * @param {Uint8Array} uint8arr - binary data in Monero's portable storage format
       * @return {Promise<object>} JSON object converted from the binary data
       */
      static async binaryToJson(uint8arr) {
        if (MoneroUtils.PROXY_TO_WORKER) return LibraryUtils.invokeWorker(undefined, "moneroUtilsBinaryToJson", Array.from(arguments));

        // load keys module by default
        if (LibraryUtils.getWasmModule() === undefined) await LibraryUtils.loadKeysModule();

        // use wasm in queue
        return LibraryUtils.getWasmModule().queueTask(async function() {

          // allocate space in c++ heap for binary
          let ptr = LibraryUtils.getWasmModule()._malloc(uint8arr.length * uint8arr.BYTES_PER_ELEMENT);
          let heap = new Uint8Array(LibraryUtils.getWasmModule().HEAPU8.buffer, ptr, uint8arr.length * uint8arr.BYTES_PER_ELEMENT);
          if (ptr !== heap.byteOffset) throw new MoneroError("Memory ptr !== heap.byteOffset"); // should be equal

          // write binary to heap
          heap.set(new Uint8Array(uint8arr.buffer));

          // create object with binary memory address info
          let binMemInfo = { ptr: ptr, length: uint8arr.length };

          // convert binary to json str
          const ret_string = LibraryUtils.getWasmModule().binary_to_json(JSON.stringify(binMemInfo));

          // free binary on heap
          LibraryUtils.getWasmModule()._free(ptr);

          // parse and return json
          return JSON.parse(ret_string);
        });
      }

      /**
       * Convert the binary response from daemon RPC block retrieval to JSON.
       *
       * @param {Uint8Array} uint8arr - binary response from daemon RPC when getting blocks
       * @return {Promise<object>} JSON object with the blocks data
       */
      static async binaryBlocksToJson(uint8arr) {
        if (MoneroUtils.PROXY_TO_WORKER) return LibraryUtils.invokeWorker(undefined, "moneroUtilsBinaryBlocksToJson", Array.from(arguments));

        // load keys module by default
        if (LibraryUtils.getWasmModule() === undefined) await LibraryUtils.loadKeysModule();

        // use wasm in queue
        return LibraryUtils.getWasmModule().queueTask(async function() {

          // allocate space in c++ heap for binary
          let ptr = LibraryUtils.getWasmModule()._malloc(uint8arr.length * uint8arr.BYTES_PER_ELEMENT);
          let heap = new Uint8Array(LibraryUtils.getWasmModule().HEAPU8.buffer, ptr, uint8arr.length * uint8arr.BYTES_PER_ELEMENT);
          if (ptr !== heap.byteOffset) throw new MoneroError("Memory ptr !== heap.byteOffset"); // should be equal

          // write binary to heap
          heap.set(new Uint8Array(uint8arr.buffer));

          // create object with binary memory address info
          let binMemInfo = { ptr: ptr, length: uint8arr.length  }

          // convert binary to json str
          const json_str = LibraryUtils.getWasmModule().binary_blocks_to_json(JSON.stringify(binMemInfo));

          // free memory
          LibraryUtils.getWasmModule()._free(ptr);

          // parse result to json
          let json = JSON.parse(json_str);                                          // parsing json gives arrays of block and tx strings
          json.blocks = json.blocks.map(blockStr => JSON.parse(blockStr));          // replace block strings with parsed blocks
          json.txs = json.txs.map(txs => txs ? txs.map(tx => JSON.parse(tx.replace(",", "{") + "}")) : []); // modify tx string to proper json and parse // TODO: more efficient way than this json manipulation?
          return json;
        });
      }

      /**
       * Convert XMR to atomic units.
       *
       * @param {number|string} amountXmr - amount in XMR to convert to atomic units
       * @return {BigInteger} amount in atomic units
       */
      static xmrToAtomicUnits(amountXmr) {
        if (typeof amountXmr === "number") amountXmr = "" + amountXmr;
        else if (typeof amountXmr !== "string") throw new MoneroError("Must provide XMR amount as a string or js number to convert to atomic units");
        let decimalDivisor = 1;
        let decimalIdx = amountXmr.indexOf('.');
        if (decimalIdx > -1) {
          decimalDivisor = Math.pow(10, amountXmr.length - decimalIdx - 1);
          amountXmr = amountXmr.slice(0, decimalIdx) + amountXmr.slice(decimalIdx + 1);
        }
        return new BigInteger(amountXmr).multiply(new BigInteger(MoneroUtils.AU_PER_XMR)).divide(new BigInteger(decimalDivisor));
      }

      /**
       * Convert atomic units to XMR.
       *
       * @param {BigInteger|string} amountAtomicUnits - amount in atomic units to convert to XMR
       * @return {number} amount in XMR
       */
      static atomicUnitsToXmr(amountAtomicUnits) {
        if (typeof amountAtomicUnits === "string") amountAtomicUnits = new BigInteger(amountAtomicUnits);
        else if (!(amountAtomicUnits instanceof BigInteger)) throw new MoneroError("Must provide atomic units as BigInteger or string to convert to XMR");
        let quotientAndRemainder = amountAtomicUnits.divRem(new BigInteger(MoneroUtils.AU_PER_XMR));
        return Number(quotientAndRemainder[0].toJSValue() + quotientAndRemainder[1].toJSValue() / MoneroUtils.AU_PER_XMR);
      }

      static _isHex64(str) {
        return typeof str === "string" && str.length === 64 && GenUtils.isHex(str);
      }
    }

    MoneroUtils.PROXY_TO_WORKER = true;
    MoneroUtils.NUM_MNEMONIC_WORDS = 25;
    MoneroUtils.RING_SIZE = 12;
    MoneroUtils.MAX_REQUESTS_PER_SECOND = 50;
    MoneroUtils.AU_PER_XMR = 1000000000000;

    module.exports = MoneroUtils;