# Source: src/main/js/wallet/model/MoneroTxQuery.js

    const assert = require("assert");
    const MoneroOutputQuery = require("./MoneroOutputQuery");
    const MoneroTransferQuery = require("./MoneroTransferQuery");
    const MoneroTxWallet = require("./MoneroTxWallet");

    /**
     * <p>Configuration to query transactions.</p>
     *
     * @class
     * @extends {MoneroTxWallet}
     */
    class MoneroTxQuery extends MoneroTxWallet {

      /**
       * <p>Construct the transaction query.</p>
       *
       * <p>Example:</p>
       *
       * <code>
       * &sol;&sol; get transactions with unlocked incoming transfers to account 0<br>
       * let txs = await wallet.getTxs({<br>
       * &nbsp;&nbsp; isLocked: false,<br>
       * &nbsp;&nbsp; transferQuery: {<br>
       * &nbsp;&nbsp;&nbsp;&nbsp; isIncoming: true,<br>
       * &nbsp;&nbsp;&nbsp;&nbsp; accountIndex: 0<br>
       * &nbsp;&nbsp; }<br>
       * });
       * </code>
       *
       * <p>All configuration is optional.  All transactions are returned except those that don't meet criteria defined in this query.</p>
       *
       * @param {object} config - tx query configuration
       * @param {string} config.hash - get a tx with this hash
       * @param {string[]} config.txHashes - get txs with these hashes
       * @param {int} config.height - get txs with this height
       * @param {int} config.minHeight - get txs with height greater than or equal to this height
       * @param {int} config.maxHeight - get txs with height less than or equal to this height
       * @param {boolean} config.isConfirmed - get confirmed or unconfirmed txs
       * @param {boolean} config.inTxPool - get txs in or out of the tx pool
       * @param {boolean} config.relay - get txs with the same relay status
       * @param {boolean} config.isRelayed - get relayed or non-relayed txs
       * @param {boolean} config.isFailed - get failed or non-failed txs
       * @param {boolean} config.isMinerTx - get miner or non-miner txs
       * @param {boolean} config.isLocked - get locked or unlocked txs
       * @param {boolean} config.isIncoming - get txs with or without incoming transfers
       * @param {boolean} config.isOutgoing - get txs with or without outgoing transfers
       * @param {string} config.paymentId - get txs with this payment ID
       * @param {string} config.paymentIds - get txs with a payment ID among these payment IDs
       * @param {boolean} config.hasPaymentId - get txs with or without payment IDs
       * @param {object|MoneroTransferQuery} config.transferQuery - get txs with transfers matching this transfer query
       * @param {object|MoneroOutputQuery} config.inputQuery - get txs with inputs matching this input query
       * @param {object|MoneroOutputQuery} config.outputQuery - get txs with outputs matching this output query
       */
      constructor(config) {
        super(config);

        // deserialize if necessary
        if (this.state.transferQuery && !(this.state.transferQuery instanceof MoneroTransferQuery)) this.state.transferQuery = new MoneroTransferQuery(this.state.transferQuery);
        if (this.state.inputQuery && !(this.state.inputQuery instanceof MoneroOutputQuery)) this.state.inputQuery = new MoneroOutputQuery(this.state.inputQuery);
        if (this.state.outputQuery && !(this.state.outputQuery instanceof MoneroOutputQuery)) this.state.outputQuery = new MoneroOutputQuery(this.state.outputQuery);

        // link cycles
        if (this.state.transferQuery) this.state.transferQuery.setTxQuery(this);
        if (this.state.inputQuery) this.state.inputQuery.setTxQuery(this);
        if (this.state.outputQuery) this.state.outputQuery.setTxQuery(this);

        // alias 'hash' to hashes
        if (this.state.hash) {
          this.setHashes([this.state.hash]);
          delete this.state.hash;
        }
      }

      copy() {
        return new MoneroTxQuery(this);
      }

      toJson() {
        let json = Object.assign({}, this.state, super.toJson()); // merge json onto inherited state
        if (this.getTransferQuery()) json.transferQuery = this.getTransferQuery().toJson();
        if (this.getInputQuery()) json.inputQuery = this.getInputQuery().toJson();
        if (this.getOutputQuery()) json.outputQuery = this.getOutputQuery().toJson();
        delete json.block;  // do not serialize parent block
        return json;
      }

      isIncoming() {
        return this.state.isIncoming;
      }

      setIsIncoming(isIncoming) {
        this.state.isIncoming = isIncoming;
        return this;
      }

      isOutgoing() {
        return this.state.isOutgoing;
      }

      setIsOutgoing(isOutgoing) {
        this.state.isOutgoing = isOutgoing;
        return this;
      }

      getHashes() {
        return this.state.hashes;
      }

      setHashes(hashes) {
        this.state.hashes = hashes;
        return this;
      }

      setHash(hash) {
        if (hash === undefined) return this.setHashes(undefined);
        assert(typeof hash === "string");
        return this.setHashes([hash]);
      }

      hasPaymentId() {
        return this.state.hasPaymentId;
      }

      setHasPaymentId() {
        this.state.hasPaymentId = hasPaymentId;
        return this;
      }

      getPaymentIds() {
        return this.state.paymentIds;
      }

      setPaymentIds(paymentIds) {
        this.state.paymentIds = paymentIds;
        return this;
      }

      setPaymentId(paymentId) {
        if (paymentId === undefined) return this.setPaymentIds(undefined);
        assert(typeof paymentId === "string");
        return this.setPaymentIds([paymentId]);
      }

      getHeight() {
        return this.state.height;
      }

      setHeight(height) {
        this.state.height = height;
        return this;
      }

      getMinHeight() {
        return this.state.minHeight;
      }

      setMinHeight(minHeight) {
        this.state.minHeight = minHeight;
        return this;
      }

      getMaxHeight() {
        return this.state.maxHeight;
      }

      setMaxHeight(maxHeight) {
        this.state.maxHeight = maxHeight;
        return this;
      }

      getIncludeOutputs() {
        return this.state.includeOutputs;
      }

      setIncludeOutputs(includeOutputs) {
        this.state.includeOutputs = includeOutputs;
        return this;
      }

      getTransferQuery() {
        return this.state.transferQuery;
      }

      setTransferQuery(transferQuery) {
        this.state.transferQuery = transferQuery;
        if (transferQuery) transferQuery.state.txQuery = this;
        return this;
      }

      getInputQuery() {
        return this.state.inputQuery;
      }

      setInputQuery(inputQuery) {
        this.state.inputQuery = inputQuery;
        if (inputQuery) inputQuery.state.txQuery = this;
        return this;
      }

      getOutputQuery() {
        return this.state.outputQuery;
      }

      setOutputQuery(outputQuery) {
        this.state.outputQuery = outputQuery;
        if (outputQuery) outputQuery.state.txQuery = this;
        return this;
      }

      meetsCriteria(tx, queryChildren) {
        if (!(tx instanceof MoneroTxWallet)) throw new Error("Tx not given to MoneroTxQuery.meetsCriteria(tx)");
        if (queryChildren === undefined) queryChildren = true;

        // filter on tx
        if (this.getHash() !== undefined && this.getHash() !== tx.getHash()) return false;
        if (this.getPaymentId() !== undefined && this.getPaymentId() !== tx.getPaymentId()) return false;
        if (this.isConfirmed() !== undefined && this.isConfirmed() !== tx.isConfirmed()) return false;
        if (this.inTxPool() !== undefined && this.inTxPool() !== tx.inTxPool()) return false;
        if (this.getRelay() !== undefined && this.getRelay() !== tx.getRelay()) return false;
        if (this.isRelayed() !== undefined && this.isRelayed() !== tx.isRelayed()) return false;
        if (this.isFailed() !== undefined && this.isFailed() !== tx.isFailed()) return false;
        if (this.isMinerTx() !== undefined && this.isMinerTx() !== tx.isMinerTx()) return false;
        if (this.isLocked() !== undefined && this.isLocked() !== tx.isLocked()) return false;

        // filter on having a payment id
        if (this.hasPaymentId() !== undefined) {
          if (this.hasPaymentId() && tx.getPaymentId() === undefined) return false;
          if (!this.hasPaymentId() && tx.getPaymentId() !== undefined) return false;
        }

        // filter on incoming
        if (this.isIncoming() !== undefined) {
          if (this.isIncoming() && !tx.isIncoming()) return false;
          if (!this.isIncoming() && tx.isIncoming()) return false;
        }

        // filter on outgoing
        if (this.isOutgoing() !== undefined) {
          if (this.isOutgoing() && !tx.isOutgoing()) return false;
          if (!this.isOutgoing() && tx.isOutgoing()) return false;
        }

        // filter on remaining fields
        let txHeight = tx.getBlock() === undefined ? undefined : tx.getBlock().getHeight();
        if (this.getHashes() !== undefined && !this.getHashes().includes(tx.getHash())) return false;
        if (this.getPaymentIds() !== undefined && !this.getPaymentIds().includes(tx.getPaymentId())) return false;
        if (this.getHeight() !== undefined && (txHeight === undefined || txHeight !== this.getHeight())) return false;
        if (this.getMinHeight() !== undefined && txHeight !== undefined && txHeight < this.getMinHeight()) return false; // do not filter unconfirmed
        if (this.getMaxHeight() !== undefined && (txHeight === undefined || txHeight > this.getMaxHeight())) return false;
        // TODO: filtering not complete

        // done if not querying transfers or outputs
        if (!queryChildren) return true;

        // at least one transfer must meet transfer filter if defined
        if (this.getTransferQuery()) {
          let matchFound = false;
          if (tx.getOutgoingTransfer() && this.getTransferQuery().meetsCriteria(tx.getOutgoingTransfer(), false)) matchFound = true;
          else if (tx.getIncomingTransfers()) {
            for (let incomingTransfer of tx.getIncomingTransfers()) {
              if (this.getTransferQuery().meetsCriteria(incomingTransfer, false)) {
                matchFound = true;
                break;
              }
            }
          }
          if (!matchFound) return false;
        }

        // at least one input must meet input query if defined
        if (this.getInputQuery() !== undefined) {
          if (tx.getInputs() === undefined || tx.getInputs().length === 0) return false;
          let matchFound = false;
          for (let input of tx.getInputs()) {
            if (this.getInputQuery().meetsCriteria(input, false)) {
              matchFound = true;
              break;
            }
          }
          if (!matchFound) return false;
        }

        // at least one output must meet output query if defined
        if (this.getOutputQuery() !== undefined) {
          if (tx.getOutputs() === undefined || tx.getOutputs().length === 0) return false;
          let matchFound = false;
          for (let output of tx.getOutputs()) {
            if (this.getOutputQuery().meetsCriteria(output, false)) {
              matchFound = true;
              break;
            }
          }
          if (!matchFound) return false;
        }

        return true;  // transaction meets filter criteria
      }
    }

    module.exports = MoneroTxQuery;