import logger from '../../logger';
import { MempoolTransactionExtended } from '../../mempool.interfaces';
import { IEsploraApi } from '../bitcoin/esplora-api.interface';

const BLOCK_WEIGHT_UNITS = 4_000_000;
const BLOCK_SIGOPS = 80_000;
const MAX_RELATIVE_GRAPH_SIZE = 200;
const BID_BOOST_WINDOW = 40_000;
const BID_BOOST_MIN_OFFSET = 10_000;
const BID_BOOST_MAX_OFFSET = 400_000;

type Acceleration = {
  txid: string;
  max_bid: number;
};

interface TxSummary {
  txid: string; // txid of the current transaction
  effectiveVsize: number; // Total vsize of the dependency tree
  effectiveFee: number;  // Total fee of the dependency tree in sats
  ancestorCount: number; // Number of ancestors
}

export interface AccelerationInfo {
  txSummary: TxSummary;
  targetFeeRate: number; // target fee rate (recommended next block fee, or median fee for mined block)
  nextBlockFee: number; // fee in sats required to be in the next block (using recommended next block fee, or median fee for mined block)
  cost: number; // additional cost to accelerate ((cost + txSummary.effectiveFee) / txSummary.effectiveVsize) >= targetFeeRate
}

interface GraphTx {
  txid: string;
  vsize: number;
  weight: number;
  fees: {
    base: number; // in sats
  };
  depends: string[];
  spentby: string[];
}

interface MempoolTx extends GraphTx {
  ancestorcount: number;
  ancestorsize: number;
  fees: { // in sats
    base: number;
    ancestor: number;
  };

  ancestors: Map<string, MempoolTx>,
  ancestorRate: number;
  individualRate: number;
  score: number;
}

class AccelerationCosts {
  /**
   * Takes a list of accelerations and verbose block data
   * Returns the "fair" boost rate to charge accelerations
   *
   * @param accelerationsx
   * @param verboseBlock
   */
  public calculateBoostRate(accelerations: Acceleration[], blockTxs: IEsploraApi.Transaction[]): number {
    // Run GBT ourselves to calculate accurate effective fee rates
    // the list of transactions comes from a mined block, so we already know everything fits within consensus limits
    const template = makeBlockTemplate(blockTxs, accelerations, 1, Infinity, Infinity);

    // initialize working maps for fast tx lookups
    const accMap = {};
    const txMap = {};
    for (const acceleration of accelerations) {
      accMap[acceleration.txid] = acceleration;
    }
    for (const tx of template) {
      txMap[tx.txid] = tx;
    }

    // Identify and exclude accelerated and otherwise prioritized transactions
    const excludeMap = {};
    let totalWeight = 0;
    let minAcceleratedPackage = Infinity;
    let lastEffectiveRate = 0;
    // Iterate over the mined template from bottom to top.
    // Transactions should appear in ascending order of mining priority.
    for (const blockTx of [...blockTxs].reverse()) {
      const txid = blockTx.txid;
      const tx = txMap[txid];
      totalWeight += tx.weight;
      const isAccelerated = accMap[txid] != null;
      // If a cluster has a in-band effective fee rate than the previous cluster,
      // it must have been prioritized out-of-band (in order to have a higher mining priority)
      // so exclude from the analysis.
      const isPrioritized = tx.effectiveFeePerVsize < lastEffectiveRate;
      if (isPrioritized || isAccelerated) {
        let packageWeight = 0;
        // exclude this whole CPFP cluster
        for (const clusterTxid of tx.cluster) {
          packageWeight += txMap[clusterTxid].weight;
          if (!excludeMap[clusterTxid]) {
            excludeMap[clusterTxid] = true;
          }
        }
        // keep track of the smallest accelerated CPFP cluster for later
        if (isAccelerated) {
          minAcceleratedPackage = Math.min(minAcceleratedPackage, packageWeight);
        }
      }
      if (!isPrioritized) {
        if (!isAccelerated) {
          lastEffectiveRate = tx.effectiveFeePerVsize;
        }
      }
    }

    // The Bid Boost Rate is calculated by disregarding the bottom X weight units of the block,
    // where X is the larger of BID_BOOST_MIN_OFFSET or the smallest accelerated package weight (the "offset"),
    // then taking the average fee rate of the following BID_BOOST_WINDOW weight units
    // (ignoring accelerated transactions and their ancestors).
    //
    // Transactions within the offset might pay less than the fair rate due to bin-packing effects
    // But the average rate paid by the next chunk of non-accelerated transactions provides a good
    // upper bound on the "next best rate" of alternatives to including the accelerated transactions
    // (since, if there were any better options, they would have been included instead)
    const spareWeight = BLOCK_WEIGHT_UNITS - totalWeight;
    const windowOffset = Math.min(Math.max(minAcceleratedPackage, BID_BOOST_MIN_OFFSET, spareWeight), BID_BOOST_MAX_OFFSET);
    const leftBound = windowOffset;
    const rightBound = windowOffset + BID_BOOST_WINDOW;
    let totalFeeInWindow = 0;
    let totalWeightInWindow = Math.max(0, spareWeight - leftBound);
    let txIndex = blockTxs.length - 1;
    for (let offset = spareWeight; offset < BLOCK_WEIGHT_UNITS && txIndex >= 0; txIndex--) {
      const txid = blockTxs[txIndex].txid;
      const tx = txMap[txid];
      if (excludeMap[txid]) {
        // skip prioritized transactions and their ancestors
        continue;
      }

      const left = offset;
      const right = offset + tx.weight;
      offset += tx.weight;
      if (right < leftBound) {
        // not within window yet
        continue;
      }
      if (left > rightBound) {
        // past window
        break;
      }
      // count fees for weight units within the window
      const overlapLeft = Math.max(leftBound, left);
      const overlapRight = Math.min(rightBound, right);
      const overlapUnits = overlapRight - overlapLeft;
      totalFeeInWindow += (tx.effectiveFeePerVsize * (overlapUnits / 4));
      totalWeightInWindow += overlapUnits;
    }

    if (totalWeightInWindow < BID_BOOST_WINDOW) {
      // not enough un-prioritized transactions to calculate a fair rate
      // just charge everyone their max bids
      return Infinity;
    }
    // Divide the total fee by the size of the BID_BOOST_WINDOW in vbytes
    const averageRate = totalFeeInWindow / (BID_BOOST_WINDOW / 4);
    return averageRate;
  }


  /**
   * Takes an accelerated mined txid and a target rate
   * Returns the total vsize, fees and acceleration cost (in sats) of the tx and all same-block ancestors
   * 
   * @param txid 
   * @param medianFeeRate 
   */
  public getAccelerationInfo(tx: MempoolTransactionExtended, targetFeeRate: number, transactions: MempoolTransactionExtended[]): AccelerationInfo {
    // Get same-block transaction ancestors
    const allRelatives = this.getSameBlockRelatives(tx, transactions);
    const relativesMap = this.initializeRelatives(allRelatives);
    const rootTx = relativesMap.get(tx.txid) as MempoolTx;

    // Calculate cost to boost
    return this.calculateAccelerationAncestors(rootTx, relativesMap, targetFeeRate);
  }

  /**
   * Takes a raw transaction, and builds a graph of same-block relatives,
   * and returns as a MempoolTx
   * 
   * @param tx 
   */
  private getSameBlockRelatives(tx: MempoolTransactionExtended, transactions: MempoolTransactionExtended[]): Map<string, GraphTx> {
    const blockTxs = new Map<string, MempoolTransactionExtended>(); // map of txs in this block
    const spendMap = new Map<string, string>(); // map of outpoints to spending txids
    for (const tx of transactions) {
      blockTxs.set(tx.txid, tx);
      for (const vin of tx.vin) {
        spendMap.set(`${vin.txid}:${vin.vout}`, tx.txid);
      }
    }

    const relatives: Map<string, GraphTx> = new Map();
    const stack: string[] = [tx.txid];

    // build set of same-block ancestors
    while (stack.length > 0) {
      const nextTxid = stack.pop();
      const nextTx = nextTxid ? blockTxs.get(nextTxid) : null;
      if (!nextTx || relatives.has(nextTx.txid)) {
        continue;
      }

      const mempoolTx = this.convertToGraphTx(nextTx);

      mempoolTx.fees.base = nextTx.fee || 0;
      mempoolTx.depends = nextTx.vin.map(vin => vin.txid).filter(inTxid => inTxid && blockTxs.has(inTxid)) as string[];
      mempoolTx.spentby = nextTx.vout.map((vout, index) => spendMap.get(`${nextTx.txid}:${index}`)).filter(outTxid => outTxid && blockTxs.has(outTxid)) as string[];

      for (const txid of [...mempoolTx.depends, ...mempoolTx.spentby]) {
        if (txid) {
          stack.push(txid);
        }
      }

      relatives.set(mempoolTx.txid, mempoolTx);
    }

    return relatives;
  }

  /**
   * Takes a raw transaction and converts it to MempoolTx format
   * fee and ancestor data is initialized with dummy/null values
   * 
   * @param tx 
   */
  private convertToGraphTx(tx: MempoolTransactionExtended): GraphTx {
    return {
      txid: tx.txid,
      vsize: Math.ceil(tx.weight / 4),
      weight: tx.weight,
      fees: {
        base: 0, // dummy
      },
      depends: [], // dummy
      spentby: [], //dummy
    };
  }

  private convertGraphToMempoolTx(tx: GraphTx): MempoolTx {
    return {
      ...tx,
      fees: {
        base: tx.fees.base,
        ancestor: tx.fees.base,
      },
      ancestorcount: 1,
      ancestorsize: Math.ceil(tx.weight / 4),
      ancestors: new Map<string, MempoolTx>(),
      ancestorRate: 0,
      individualRate: 0,
      score: 0,
    };
  }

  /**
   * Given a root transaction, a list of in-mempool ancestors, and a target fee rate,
   * Calculate the minimum set of transactions to fee-bump, their total vsize + fees
   * 
   * @param tx
   * @param ancestors
   */
  private calculateAccelerationAncestors(tx: MempoolTx, relatives: Map<string, MempoolTx>, targetFeeRate: number): AccelerationInfo {
    // add root tx to the ancestor map
    relatives.set(tx.txid, tx);

    // Check for high-sigop transactions (not supported)
    relatives.forEach(entry => {
      if (entry.vsize > Math.ceil(entry.weight / 4)) {
        throw new Error(`high_sigop_tx`);
      }
    });

    // Initialize individual & ancestor fee rates
    relatives.forEach(entry => this.setAncestorScores(entry));

    // Sort by descending ancestor score
    let sortedRelatives = Array.from(relatives.values()).sort(this.mempoolComparator);

    let includedInCluster: Map<string, MempoolTx> | null = null;

    // While highest score >= targetFeeRate
    let maxIterations = MAX_RELATIVE_GRAPH_SIZE;
    while (sortedRelatives.length && sortedRelatives[0].score && sortedRelatives[0].score >= targetFeeRate && maxIterations > 0) {
      maxIterations--;
      // Grab the highest scoring entry
      const best = sortedRelatives.shift();
      if (best) {
        const cluster = new Map<string, MempoolTx>(best.ancestors?.entries() || []);
        if (best.ancestors.has(tx.txid)) {
          includedInCluster = cluster;
        }
        cluster.set(best.txid, best);
        // Remove this cluster (it already pays over the target rate, so doesn't need to be boosted)
        // and update scores, ancestor totals and dependencies for the survivors
        this.removeAncestors(cluster, relatives);

        // re-sort
        sortedRelatives = Array.from(relatives.values()).sort(this.mempoolComparator);
      }
    }

    // sanity check for infinite loops / too many ancestors (should never happen)
    if (maxIterations <= 0) {
      logger.warn(`acceleration dependency calculation failed: calculateAccelerationAncestors loop exceeded ${MAX_RELATIVE_GRAPH_SIZE} iterations, unable to proceed`);
      throw new Error('invalid_tx_dependencies');
    }

    let totalFee = tx.fees.ancestor;

    // transaction is already CPFP-d above the target rate by some descendant
    if (includedInCluster) {
      let clusterSize = 0;
      let clusterFee = 0;
      includedInCluster.forEach(entry => {
        clusterSize += entry.vsize;
        clusterFee += entry.fees.base;
      });
      const clusterRate = clusterFee / clusterSize;
      totalFee = Math.ceil(tx.ancestorsize * clusterRate);
    }

    // Whatever remains in the accelerated tx's dependencies needs to be boosted to the targetFeeRate
    // Cost = (totalVsize * targetFeeRate) - totalFee
    return {
      txSummary: {
        txid: tx.txid,
        effectiveVsize: tx.ancestorsize,
        effectiveFee: totalFee,
        ancestorCount: tx.ancestorcount,
      },
      cost: Math.max(0, Math.ceil(tx.ancestorsize * targetFeeRate) - totalFee),
      targetFeeRate,
      nextBlockFee: Math.ceil(tx.ancestorsize * targetFeeRate),
    };
  }

  /**
   * Recursively traverses an in-mempool dependency graph, and sets a Map of in-mempool ancestors
   * for each transaction.
   * 
   * @param tx 
   * @param all 
   */
  private setAncestors(tx: MempoolTx, all: Map<string, MempoolTx>, visited: Map<string, Map<string, MempoolTx>>, depth: number = 0): Map<string, MempoolTx> {
    // sanity check for infinite recursion / too many ancestors (should never happen)
    if (depth >= 100) {
      logger.warn('acceleration dependency calculation failed: setAncestors reached depth of 100, unable to proceed', `Accelerator`);
      throw new Error('invalid_tx_dependencies');
    }
    
    // initialize the ancestor map for this tx
    tx.ancestors = new Map<string, MempoolTx>();
    tx.depends.forEach(parentId => {
      const parent = all.get(parentId);
      if (parent) {
        // add the parent
        tx.ancestors?.set(parentId, parent);
        // check for a cached copy of this parent's ancestors
        let ancestors = visited.get(parent.txid);
        if (!ancestors) {
          // recursively fetch the parent's ancestors
          ancestors = this.setAncestors(parent, all, visited, depth + 1);
        }
        // and add to this tx's map
        ancestors.forEach((ancestor, ancestorId) => {
          tx.ancestors?.set(ancestorId, ancestor);
        });
      }
    });
    visited.set(tx.txid, tx.ancestors);

    return tx.ancestors;
  }

  /**
   * Efficiently sets a Map of in-mempool ancestors for each member of an expanded relative graph
   * by running setAncestors on each leaf, and caching intermediate results.
   * then initializes ancestor data for each transaction
   * 
   * @param all 
   */
  private initializeRelatives(all: Map<string, GraphTx>): Map<string, MempoolTx> {
    const mempoolTxs = new Map<string, MempoolTx>();
    all.forEach(entry => {
      mempoolTxs.set(entry.txid, this.convertGraphToMempoolTx(entry));
    });
    const visited: Map<string, Map<string, MempoolTx>> = new Map();
    const leaves: MempoolTx[] = Array.from(mempoolTxs.values()).filter(entry => entry.spentby.length === 0);
    for (const leaf of leaves) {
      this.setAncestors(leaf, mempoolTxs, visited);
    }
    mempoolTxs.forEach(entry => {
      entry.ancestors?.forEach(ancestor => {
        entry.ancestorcount++;
        entry.ancestorsize += ancestor.vsize;
        entry.fees.ancestor += ancestor.fees.base;
      });
      this.setAncestorScores(entry);
    });
    return mempoolTxs;
  }

  /**
   * Remove a cluster of transactions from an in-mempool dependency graph
   * and update the survivors' scores and ancestors
   * 
   * @param cluster 
   * @param ancestors 
   */
  private removeAncestors(cluster: Map<string, MempoolTx>, all: Map<string, MempoolTx>): void {
    // remove
    cluster.forEach(tx => {
      all.delete(tx.txid);
    });

    // update survivors
    all.forEach(tx => {
      cluster.forEach(remove => {
        if (tx.ancestors?.has(remove.txid)) {
          // remove as dependency
          tx.ancestors.delete(remove.txid);
          tx.depends = tx.depends.filter(parent => parent !== remove.txid);
          // update ancestor sizes and fees
          tx.ancestorsize -= remove.vsize;
          tx.fees.ancestor -= remove.fees.base;
        }
      });
      // recalculate fee rates
      this.setAncestorScores(tx);
    });
  }

  /**
   * Take a mempool transaction, and set the fee rates and ancestor score
   * 
   * @param tx 
   */
  private setAncestorScores(tx: MempoolTx): void {
    tx.individualRate = tx.fees.base / tx.vsize;
    tx.ancestorRate = tx.fees.ancestor / tx.ancestorsize;
    tx.score = Math.min(tx.individualRate, tx.ancestorRate);
  }

  // Sort by descending score
  private mempoolComparator(a, b): number {
    return b.score - a.score;
  }
}

export default new AccelerationCosts;

interface TemplateTransaction {
  txid: string;
  order: number;
  weight: number;
  adjustedVsize: number; // sigop-adjusted vsize, rounded up to the nearest integer
  sigops: number;
  fee: number;
  feeDelta: number;
  ancestors: string[];
  cluster: string[];
  effectiveFeePerVsize: number;
}

interface MinerTransaction extends TemplateTransaction {
  inputs: string[];
  feePerVsize: number;
  relativesSet: boolean;
  ancestorMap: Map<string, MinerTransaction>;
  children: Set<MinerTransaction>;
  ancestorFee: number;
  ancestorVsize: number;
  ancestorSigops: number;
  score: number;
  used: boolean;
  modified: boolean;
  dependencyRate: number;
}

/*
* Build a block using an approximation of the transaction selection algorithm from Bitcoin Core
* (see BlockAssembler in https://github.com/bitcoin/bitcoin/blob/master/src/node/miner.cpp)
*/
export function makeBlockTemplate(candidates: IEsploraApi.Transaction[], accelerations: Acceleration[], maxBlocks: number = 8, weightLimit: number = BLOCK_WEIGHT_UNITS, sigopLimit: number = BLOCK_SIGOPS): TemplateTransaction[] {
  const auditPool: Map<string, MinerTransaction> = new Map();
  const mempoolArray: MinerTransaction[] = [];
  
  candidates.forEach(tx => {
    // initializing everything up front helps V8 optimize property access later
    const adjustedVsize = Math.ceil(Math.max(tx.weight / 4, 5 * (tx.sigops || 0)));
    const feePerVsize = (tx.fee / adjustedVsize);
    auditPool.set(tx.txid, {
      txid: tx.txid,
      order: txidToOrdering(tx.txid),
      fee: tx.fee,
      feeDelta: 0,
      weight: tx.weight,
      adjustedVsize,
      feePerVsize: feePerVsize,
      effectiveFeePerVsize: feePerVsize,
      dependencyRate: feePerVsize,
      sigops: tx.sigops || 0,
      inputs: (tx.vin?.map(vin => vin.txid) || []) as string[],
      relativesSet: false,
      ancestors: [],
      cluster: [],
      ancestorMap: new Map<string, MinerTransaction>(),
      children: new Set<MinerTransaction>(),
      ancestorFee: 0,
      ancestorVsize: 0,
      ancestorSigops: 0,
      score: 0,
      used: false,
      modified: false,
    });
    mempoolArray.push(auditPool.get(tx.txid) as MinerTransaction);
  });

  // set accelerated effective fee
  for (const acceleration of accelerations) {
    const tx = auditPool.get(acceleration.txid);
    if (tx) {
      tx.feeDelta = acceleration.max_bid;
      tx.feePerVsize = ((tx.fee + tx.feeDelta) / tx.adjustedVsize);
      tx.effectiveFeePerVsize = tx.feePerVsize;
      tx.dependencyRate = tx.feePerVsize;
    }
  }

  // Build relatives graph & calculate ancestor scores
  for (const tx of mempoolArray) {
    if (!tx.relativesSet) {
      setRelatives(tx, auditPool);
    }
  }

  // Sort by descending ancestor score
  mempoolArray.sort(priorityComparator);

  // Build blocks by greedily choosing the highest feerate package
  // (i.e. the package rooted in the transaction with the best ancestor score)
  const blocks: number[][] = [];
  let blockWeight = 0;
  let blockSigops = 0;
  const transactions: MinerTransaction[] = [];
  let modified: MinerTransaction[] = [];
  const overflow: MinerTransaction[] = [];
  let failures = 0;
  while (mempoolArray.length || modified.length) {
    // skip invalid transactions
    while (mempoolArray[0].used || mempoolArray[0].modified) {
      mempoolArray.shift();
    }

    // Select best next package
    let nextTx;
    const nextPoolTx = mempoolArray[0];
    const nextModifiedTx = modified[0];
    if (nextPoolTx && (!nextModifiedTx || (nextPoolTx.score || 0) > (nextModifiedTx.score || 0))) {
      nextTx = nextPoolTx;
      mempoolArray.shift();
    } else {
      modified.shift();
      if (nextModifiedTx) {
        nextTx = nextModifiedTx;
      }
    }

    if (nextTx && !nextTx?.used) {
      // Check if the package fits into this block
      if (blocks.length >= (maxBlocks - 1) || ((blockWeight + (4 * nextTx.ancestorVsize) < weightLimit) && (blockSigops + nextTx.ancestorSigops <= sigopLimit))) {
        const ancestors: MinerTransaction[] = Array.from(nextTx.ancestorMap.values());
        // sort ancestors by dependency graph (equivalent to sorting by ascending ancestor count)
        const sortedTxSet = [...ancestors.sort((a, b) => { return (a.ancestorMap.size || 0) - (b.ancestorMap.size || 0); }), nextTx];
        const clusterTxids = sortedTxSet.map(tx => tx.txid);
        const effectiveFeeRate = Math.min(nextTx.dependencyRate || Infinity, nextTx.ancestorFee / nextTx.ancestorVsize);
        const used: MinerTransaction[] = [];
        while (sortedTxSet.length) {
          const ancestor = sortedTxSet.pop();
          if (!ancestor) {
            continue;
          }
          ancestor.used = true;
          ancestor.usedBy = nextTx.txid;
          // update this tx with effective fee rate & relatives data
          if (ancestor.effectiveFeePerVsize !== effectiveFeeRate) {
            ancestor.effectiveFeePerVsize = effectiveFeeRate;
          }
          ancestor.cluster = clusterTxids;
          transactions.push(ancestor);
          blockWeight += ancestor.weight;
          blockSigops += ancestor.sigops;
          used.push(ancestor);
        }

        // remove these as valid package ancestors for any descendants remaining in the mempool
        if (used.length) {
          used.forEach(tx => {
            modified = updateDescendants(tx, auditPool, modified, effectiveFeeRate);
          });
        }

        failures = 0;
      } else {
        // hold this package in an overflow list while we check for smaller options
        overflow.push(nextTx);
        failures++;
      }
    }

    // this block is full
    const exceededPackageTries = failures > 1000 && blockWeight > (weightLimit - 4000);
    const queueEmpty = !mempoolArray.length && !modified.length;

    if (exceededPackageTries || queueEmpty) {
      break;
    }
  }

  for (const tx of transactions) {
    tx.ancestors = Object.values(tx.ancestorMap);
  }

  return transactions;
}

// traverse in-mempool ancestors
// recursion unavoidable, but should be limited to depth < 25 by mempool policy
function setRelatives(
  tx: MinerTransaction,
  mempool: Map<string, MinerTransaction>,
): void {
  for (const parent of tx.inputs) {
    const parentTx = mempool.get(parent);
    if (parentTx && !tx.ancestorMap?.has(parent)) {
      tx.ancestorMap.set(parent, parentTx);
      parentTx.children.add(tx);
      // visit each node only once
      if (!parentTx.relativesSet) {
        setRelatives(parentTx, mempool);
      }
      parentTx.ancestorMap.forEach((ancestor) => {
        tx.ancestorMap.set(ancestor.txid, ancestor);
      });
    }
  };
  tx.ancestorFee = (tx.fee + tx.feeDelta);
  tx.ancestorVsize = tx.adjustedVsize || 0;
  tx.ancestorSigops = tx.sigops || 0;
  tx.ancestorMap.forEach((ancestor) => {
    tx.ancestorFee += (ancestor.fee + ancestor.feeDelta);
    tx.ancestorVsize += ancestor.adjustedVsize;
    tx.ancestorSigops += ancestor.sigops;
  });
  tx.score = tx.ancestorFee / tx.ancestorVsize;
  tx.relativesSet = true;
}

// iterate over remaining descendants, removing the root as a valid ancestor & updating the ancestor score
// avoids recursion to limit call stack depth
function updateDescendants(
  rootTx: MinerTransaction,
  mempool: Map<string, MinerTransaction>,
  modified: MinerTransaction[],
  clusterRate: number,
): MinerTransaction[] {
  const descendantSet: Set<MinerTransaction> = new Set();
  // stack of nodes left to visit
  const descendants: MinerTransaction[] = [];
  let descendantTx: MinerTransaction | undefined;
  rootTx.children.forEach(childTx => {
    if (!descendantSet.has(childTx)) {
      descendants.push(childTx);
      descendantSet.add(childTx);
    }
  });
  while (descendants.length) {
    descendantTx = descendants.pop();
    if (descendantTx && descendantTx.ancestorMap && descendantTx.ancestorMap.has(rootTx.txid)) {
      // remove tx as ancestor
      descendantTx.ancestorMap.delete(rootTx.txid);
      descendantTx.ancestorFee -= (rootTx.fee + rootTx.feeDelta);
      descendantTx.ancestorVsize -= rootTx.adjustedVsize;
      descendantTx.ancestorSigops -= rootTx.sigops;
      descendantTx.score = descendantTx.ancestorFee / descendantTx.ancestorVsize;
      descendantTx.dependencyRate = descendantTx.dependencyRate ? Math.min(descendantTx.dependencyRate, clusterRate) : clusterRate;

      if (!descendantTx.modified) {
        descendantTx.modified = true;
        modified.push(descendantTx);
      }

      // add this node's children to the stack
      descendantTx.children.forEach(childTx => {
        // visit each node only once
        if (!descendantSet.has(childTx)) {
          descendants.push(childTx);
          descendantSet.add(childTx);
        }
      });
    }
  }
  // return new, resorted modified list
  return modified.sort(priorityComparator);
}

// Used to sort an array of MinerTransactions by descending ancestor score
function priorityComparator(a: MinerTransaction, b: MinerTransaction): number {
  if (b.score === a.score) {
    // tie-break by txid for stability
    return a.order - b.order;
  } else {
    return b.score - a.score;
  }
}

// returns the most significant 4 bytes of the txid as an integer
function txidToOrdering(txid: string): number {
  return parseInt(
    txid.substring(62, 64) +
      txid.substring(60, 62) +
      txid.substring(58, 60) +
      txid.substring(56, 58),
    16
  );
}
