import MerklePath from './MerklePath.js';
import BeefTx from './BeefTx.js';
import { toHex, toArray, verifyNotNull, ReaderUint8Array, WriterUint8Array, toUint8Array } from '../primitives/utils.js';
import { hash256 } from '../primitives/Hash.js';
import { BEEF_V1, BEEF_V2, ATOMIC_BEEF } from './BeefConstants.js';
export { BEEF_V1, BEEF_V2, ATOMIC_BEEF, TX_DATA_FORMAT } from './BeefConstants.js';
/*
 * BEEF standard: BRC-62: Background Evaluation Extended Format (BEEF) Transactions
 * https://github.com/bsv-blockchain/BRCs/blob/master/transactions/0062.md
 *
 * BUMP standard: BRC-74: BSV Unified Merkle Path (BUMP) Format
 * https://github.com/bsv-blockchain/BRCs/blob/master/transactions/0074.md
 *
 * BRC-95: Atomic BEEF Transactions
 * https://github.com/bsv-blockchain/BRCs/blob/master/transactions/0095.md
 *
 * The Atomic BEEF format is supported by the binary deserialization static method `fromBinary`.
 *
 * BRC-96: BEEF V2, Txid Only Extension
 * https://github.com/bsv-blockchain/BRCs/blob/master/transactions/0096.md
 *
 * A valid serialized BEEF is the cornerstone of Simplified Payment Validation (SPV)
 * where they are exchanged between two non-trusting parties to establish the
 * validity of a newly constructed bitcoin transaction and its inputs from prior
 * transactions.
 *
 * A `Beef` is fundamentally an list of `BUMP`s and a list of transactions.
 *
 * A `BUMP` is a partial merkle tree for a 'mined' bitcoin block.
 * It can therefore be used to prove the validity of transaction data
 * for each transaction txid whose merkle path is included in the tree.
 *
 * To be valid, the list of transactions must be sorted in dependency order:
 * oldest transaction first;
 * and each transaction must either
 * have a merkle path in one of the BUMPs, or
 * have all of its input transactions included in the list of transactions.
 *
 * The `Beef` class supports the construction of valid BEEFs by allowing BUMPs
 * (merkle paths) and transactions to be merged sequentially.
 *
 * The `Beef` class also extends the standard by supporting 'known' transactions.
 * A 'known' transaction is represented solely by its txid.
 * To become valid, all the 'known' transactions in a `Beef` must be replaced by full
 * transactions and merkle paths, if they are mined.
 *
 * The purpose of supporting 'known' transactions is that one or both parties
 * generating and exchanging BEEFs often possess partial knowledge of valid transactions
 * due to their history.
 *
 * A valid `Beef` is only required when sent to a party with no shared history,
 * such as a transaction processor.
 *
 * IMPORTANT NOTE:
 * It is fundamental to the BEEF value proposition that only valid transactions and valid
 * merkle path (BUMP) data be added to it. Merging invalid data breaks the `verify` and `isValid`
 * functions. There is no support for removing invalid data. A `Beef` that becomes invalid
 * must be discarded.
 */
export class Beef {
    bumps = [];
    txs = [];
    version = BEEF_V2;
    atomicTxid = undefined;
    txidIndex = undefined;
    rawBytesCache;
    hexCache;
    needsSort = true;
    constructor(version = BEEF_V2) {
        this.version = version;
    }
    invalidateSerializationCaches() {
        this.rawBytesCache = undefined;
        this.hexCache = undefined;
    }
    markMutated(requiresSort = true) {
        this.invalidateSerializationCaches();
        if (requiresSort) {
            this.needsSort = true;
        }
    }
    ensureSerializableState() {
        for (const tx of this.txs) {
            // Access txid to ensure it is computed before serialization
            // eslint-disable-next-line @typescript-eslint/no-unused-expressions
            tx.txid;
        }
    }
    ensureSortedForSerialization() {
        if (this.needsSort) {
            this.sortTxs();
        }
    }
    getSerializedBytes() {
        this.ensureSerializableState();
        if (this.rawBytesCache == null) {
            this.ensureSortedForSerialization();
            const writer = new WriterUint8Array();
            this.toWriter(writer);
            this.rawBytesCache = writer.toUint8Array();
        }
        return this.rawBytesCache;
    }
    getBeefForAtomic(txid) {
        if (this.needsSort) {
            this.sortTxs();
        }
        const tx = this.findTxid(txid);
        if (tx == null) {
            throw new Error(`${txid} does not exist in this Beef`);
        }
        // If the transaction is not the last one, clone and modify
        const beef = (this.txs.at(-1) === tx) ? this : this.clone();
        if (beef !== this) {
            const i = this.txs.findIndex((t) => t.txid === txid);
            beef.txs.splice(i + 1);
        }
        const writer = new WriterUint8Array();
        writer.writeUInt32LE(ATOMIC_BEEF);
        writer.writeReverse(toArray(txid, 'hex'));
        return { beef, writer };
    }
    /**
     * @param txid of `beefTx` to find
     * @returns `BeefTx` in `txs` with `txid`.
     */
    findTxid(txid) {
        return this.ensureTxidIndex().get(txid);
    }
    ensureTxidIndex() {
        if (this.txidIndex == null) {
            this.txidIndex = new Map();
            for (const tx of this.txs) {
                this.txidIndex.set(tx.txid, tx);
            }
        }
        return this.txidIndex;
    }
    deleteFromIndex(txid) {
        this.txidIndex?.delete(txid);
    }
    addToIndex(tx) {
        this.txidIndex?.set(tx.txid, tx);
    }
    /**
     * Replaces `BeefTx` for this txid with txidOnly.
     *
     * Replacement is done so that a `clone()` can be
     * updated by this method without affecting the
     * original.
     *
     * @param txid
     * @returns undefined if txid is unknown.
     */
    makeTxidOnly(txid) {
        const i = this.txs.findIndex((tx) => tx.txid === txid);
        if (i === -1)
            return undefined;
        let btx = this.txs[i];
        if (btx.isTxidOnly) {
            return btx;
        }
        this.deleteFromIndex(txid);
        this.txs.splice(i, 1);
        this.markMutated(true);
        btx = this.mergeTxidOnly(txid);
        return btx;
    }
    /**
     * @returns `MerklePath` with level zero hash equal to txid or undefined.
     */
    findBump(txid) {
        return this.bumps.find((b) => b.path[0].some((leaf) => leaf.hash === txid));
    }
    /**
     * Finds a Transaction in this `Beef`
     * and adds any missing input SourceTransactions from this `Beef`.
     *
     * The result is suitable for signing.
     *
     * @param txid The id of the target transaction.
     * @returns Transaction with all available input `SourceTransaction`s from this Beef.
     */
    findTransactionForSigning(txid) {
        const beefTx = this.findTxid(txid);
        if ((beefTx == null) || (beefTx.tx == null))
            return undefined; // Ensure beefTx.tx exists before using it
        for (const i of beefTx.tx.inputs) {
            if (i.sourceTransaction == null) {
                const itx = this.findTxid(verifyNotNull(i.sourceTXID, 'sourceTXID must be valid'));
                if (itx != null) {
                    i.sourceTransaction = itx.tx;
                }
            }
        }
        return beefTx.tx;
    }
    /**
     * Builds the proof tree rooted at a specific `Transaction`.
     *
     * To succeed, the Beef must contain all the required transaction and merkle path data.
     *
     * @param txid The id of the target transaction.
     * @returns Transaction with input `SourceTransaction` and `MerklePath` populated from this Beef.
     */
    findAtomicTransaction(txid) {
        const beefTx = this.findTxid(txid);
        if ((beefTx == null) || (beefTx.tx == null))
            return undefined; // Ensure beefTx.tx exists before using it
        this.addInputProof(beefTx.tx);
        return beefTx.tx;
    }
    /** Recursively attach merkle paths and source transactions to all inputs. */
    addInputProof(tx) {
        const mp = this.findBump(tx.id('hex'));
        if (mp != null) {
            tx.merklePath = mp;
            return;
        }
        for (const i of tx.inputs) {
            this.resolveInputSource(i);
            if (i.sourceTransaction != null) {
                this.attachMerklePathOrRecurse(i.sourceTransaction);
            }
        }
    }
    resolveInputSource(i) {
        if (i.sourceTransaction == null) {
            const itx = this.findTxid(verifyNotNull(i.sourceTXID, 'sourceTXID must be valid'));
            if (itx != null) {
                i.sourceTransaction = itx.tx;
            }
        }
    }
    attachMerklePathOrRecurse(sourceTx) {
        const mp = this.findBump(sourceTx.id('hex'));
        if (mp != null) {
            sourceTx.merklePath = mp;
        }
        else {
            this.addInputProof(sourceTx);
        }
    }
    /**
     * Merge a MerklePath that is assumed to be fully valid.
     * @param bump
     * @returns index of merged bump
     */
    mergeBump(bump) {
        this.markMutated(false);
        const bumpIndex = this.findOrInsertBump(bump);
        // Review if any transactions are proven by this bump
        const b = this.bumps[bumpIndex];
        for (const tx of this.txs) {
            if (tx.bumpIndex == null) {
                this.tryMarkTxProvenByBump(tx, b, bumpIndex);
            }
        }
        return bumpIndex;
    }
    /**
     * Find an existing compatible bump or insert a new one; return its index.
     */
    findOrInsertBump(bump) {
        for (let i = 0; i < this.bumps.length; i++) {
            const b = this.bumps[i];
            if (b === bump)
                return i;
            if (b.blockHeight !== bump.blockHeight)
                continue;
            if (b.computeRoot() === bump.computeRoot()) {
                b.combine(bump);
                return i;
            }
        }
        this.bumps.push(bump);
        return this.bumps.length - 1;
    }
    /** If bump's level-0 path contains tx's txid, record the bumpIndex on tx. */
    tryMarkTxProvenByBump(tx, b, bumpIndex) {
        const txid = tx.txid;
        for (const n of b.path[0]) {
            if (n.hash === txid) {
                tx.bumpIndex = bumpIndex;
                n.txid = true;
                break;
            }
        }
    }
    /**
     * Merge a serialized transaction.
     *
     * Checks that a transaction with the same txid hasn't already been merged.
     *
     * Replaces existing transaction with same txid.
     *
     * @param rawTx
     * @param bumpIndex Optional. If a number, must be valid index into bumps array.
     * @returns txid of rawTx
     */
    mergeRawTx(rawTx, bumpIndex) {
        this.markMutated(true);
        const newTx = new BeefTx(rawTx, bumpIndex);
        this.removeExistingTxid(newTx.txid);
        this.txs.push(newTx);
        this.addToIndex(newTx);
        this.tryToValidateBumpIndex(newTx);
        return newTx;
    }
    /**
     * Merge a `Transaction` and any referenced `merklePath` and `sourceTransaction`, recursifely.
     *
     * Replaces existing transaction with same txid.
     *
     * Attempts to match an existing bump to the new transaction.
     *
     * @param tx
     * @returns txid of tx
     */
    mergeTransaction(tx) {
        this.markMutated(true);
        const txid = tx.id('hex');
        this.removeExistingTxid(txid);
        let bumpIndex;
        if (tx.merklePath != null) {
            bumpIndex = this.mergeBump(tx.merklePath);
        }
        const newTx = new BeefTx(tx, bumpIndex);
        this.txs.push(newTx);
        this.addToIndex(newTx);
        this.tryToValidateBumpIndex(newTx);
        bumpIndex = newTx.bumpIndex;
        if (bumpIndex === undefined) {
            for (const input of tx.inputs) {
                if (input.sourceTransaction != null) {
                    this.mergeTransaction(input.sourceTransaction);
                }
            }
        }
        return newTx;
    }
    /**
     * Removes an existing transaction from the BEEF, given its TXID
     * @param txid TXID of the transaction to remove
     */
    removeExistingTxid(txid) {
        const existingTxIndex = this.txs.findIndex((t) => t.txid === txid);
        if (existingTxIndex >= 0) {
            this.deleteFromIndex(txid);
            this.txs.splice(existingTxIndex, 1);
            this.markMutated(true);
        }
    }
    mergeTxidOnly(txid) {
        let tx = this.findTxid(txid);
        if (tx == null) {
            tx = new BeefTx(txid);
            this.txs.push(tx);
            this.addToIndex(tx);
            this.tryToValidateBumpIndex(tx);
            this.markMutated(true);
        }
        return tx;
    }
    mergeBeefTx(btx) {
        let beefTx = this.findTxid(btx.txid);
        if (btx.isTxidOnly && (beefTx == null)) {
            beefTx = this.mergeTxidOnly(btx.txid);
        }
        else if ((btx._tx != null) && ((beefTx == null) || beefTx.isTxidOnly)) {
            beefTx = this.mergeTransaction(btx._tx);
        }
        else if ((btx._rawTx != null) && ((beefTx == null) || beefTx.isTxidOnly)) {
            beefTx = this.mergeRawTx(btx._rawTx);
        }
        if (beefTx == null) {
            throw new Error(`Failed to merge BeefTx for txid: ${btx.txid}`);
        }
        return beefTx;
    }
    mergeBeef(beef) {
        const b = (beef instanceof Beef) ? beef : Beef.fromBinary(beef);
        for (const bump of b.bumps) {
            this.mergeBump(bump);
        }
        for (const tx of b.txs) {
            this.mergeBeefTx(tx);
        }
    }
    /**
     * Sorts `txs` and checks structural validity of beef.
     *
     * Does NOT verify merkle roots.
     *
     * Validity requirements:
     * 1. No 'known' txids, unless `allowTxidOnly` is true.
     * 2. All transactions have bumps or their inputs chain back to bumps (or are known).
     * 3. Order of transactions satisfies dependencies before dependents.
     * 4. No transactions with duplicate txids.
     *
     * @param allowTxidOnly optional. If true, transaction txid only is assumed valid
     */
    isValid(allowTxidOnly) {
        return this.verifyValid(allowTxidOnly).valid;
    }
    /**
     * Sorts `txs` and confirms validity of transaction data contained in beef
     * by validating structure of this beef and confirming computed merkle roots
     * using `chainTracker`.
     *
     * Validity requirements:
     * 1. No 'known' txids, unless `allowTxidOnly` is true.
     * 2. All transactions have bumps or their inputs chain back to bumps (or are known).
     * 3. Order of transactions satisfies dependencies before dependents.
     * 4. No transactions with duplicate txids.
     *
     * @param chainTracker Used to verify computed merkle path roots for all bump txids.
     * @param allowTxidOnly optional. If true, transaction txid is assumed valid
     */
    async verify(chainTracker, allowTxidOnly) {
        const r = this.verifyValid(allowTxidOnly);
        if (!r.valid)
            return false;
        for (const height of Object.keys(r.roots)) {
            const isValid = await chainTracker.isValidRootForHeight(r.roots[height], Number(height));
            if (!isValid) {
                return false;
            }
        }
        return true;
    }
    /**
     * Sorts `txs` and confirms validity of transaction data contained in beef
     * by validating structure of this beef.
     *
     * Returns block heights and merkle root values to be confirmed by a chaintracker.
     *
     * Validity requirements:
     * 1. No 'known' txids, unless `allowTxidOnly` is true.
     * 2. All transactions have bumps or their inputs chain back to bumps (or are known).
     * 3. Order of transactions satisfies dependencies before dependents.
     * 4. No transactions with duplicate txids.
     *
     * @param allowTxidOnly optional. If true, transaction txid is assumed valid
     * @returns {{valid: boolean, roots: Record<number, string>}}
     * `valid` is true iff this Beef is structuraly valid.
     * `roots` is a record where keys are block heights and values are the corresponding merkle roots to be validated.
     */
    verifyValid(allowTxidOnly) {
        const r = {
            valid: false,
            roots: {}
        };
        const sr = this.sortTxs();
        if (sr.missingInputs.length > 0 ||
            sr.notValid.length > 0 ||
            (sr.txidOnly.length > 0 && allowTxidOnly !== true) ||
            sr.withMissingInputs.length > 0) {
            return r;
        }
        // valid txids: only txids if allowed, bump txids, then txids with input's in txids
        const txids = {};
        if (!this.collectTxidOnlyTxids(txids, allowTxidOnly))
            return r;
        if (!this.collectBumpTxids(txids, r))
            return r;
        if (!this.verifyBumpIndexLeaves())
            return r;
        if (!this.verifyInputDependencies(txids))
            return r;
        r.valid = true;
        return r;
    }
    /** Add txidOnly transaction txids; return false if not allowed. */
    collectTxidOnlyTxids(txids, allowTxidOnly) {
        for (const tx of this.txs) {
            if (!tx.isTxidOnly)
                continue;
            if (allowTxidOnly !== true)
                return false;
            txids[tx.txid] = true;
        }
        return true;
    }
    /**
     * Record txids proven by bumps; validate all bump roots agree per block height.
     * Returns false if any root conflict is detected.
     */
    collectBumpTxids(txids, r) {
        for (const b of this.bumps) {
            for (const n of b.path[0]) {
                if (n.txid !== true || typeof n.hash !== 'string' || n.hash.length === 0)
                    continue;
                txids[n.hash] = true;
                if (!this.confirmComputedRoot(b, n.hash, r))
                    return false;
            }
        }
        return true;
    }
    /** Verify that every tx with a bumpIndex has a matching txid leaf in its bump. */
    verifyBumpIndexLeaves() {
        for (const t of this.txs) {
            if (t.bumpIndex === undefined)
                continue;
            const leaf = this.bumps[t.bumpIndex].path[0].find(l => l.hash === t.txid);
            if (leaf == null)
                return false;
        }
        return true;
    }
    /** Verify all input txids appear before the spending tx in sorted order. */
    verifyInputDependencies(txids) {
        for (const t of this.txs) {
            for (const i of t.inputTxids) {
                if (!txids[i])
                    return false;
            }
            txids[t.txid] = true;
        }
        return true;
    }
    /** Confirm the computed merkle root for txid in bump matches previously accepted root for that height. */
    confirmComputedRoot(b, txid, r) {
        const root = b.computeRoot(txid);
        if (r.roots[b.blockHeight] === undefined || r.roots[b.blockHeight] === '') {
            // accept the root as valid for this block and reuse for subsequent txids
            r.roots[b.blockHeight] = root;
        }
        return r.roots[b.blockHeight] === root;
    }
    /**
     * Serializes this data to `writer`
     * @param writer
     */
    toWriter(writer) {
        writer.writeUInt32LE(this.version);
        writer.writeVarIntNum(this.bumps.length);
        for (const b of this.bumps) {
            writer.write(b.toBinary());
        }
        writer.writeVarIntNum(this.txs.length);
        for (const tx of this.txs) {
            tx.toWriter(writer, this.version);
        }
    }
    /**
     * Returns a binary array representing the serialized BEEF
     * @returns A binary array representing the BEEF
     * @returns An array of byte values containing binary serialization of the BEEF
     */
    toBinary() {
        return Array.from(this.getSerializedBytes());
    }
    /**
     * Returns a binary array representing the serialized BEEF
     * @returns A Uint8Array containing binary serialization of the BEEF
     */
    toUint8Array() {
        return this.getSerializedBytes();
    }
    /**
     * Serialize this Beef as AtomicBEEF.
     *
     * `txid` must exist
     *
     * after sorting, if txid is not last txid, creates a clone and removes newer txs
     *
     * @param txid
     * @returns serialized contents of this Beef with AtomicBEEF prefix.
     */
    toBinaryAtomic(txid) {
        const { beef, writer } = this.getBeefForAtomic(txid);
        beef.toWriter(writer);
        return writer.toArray();
    }
    /**
     * Serialize this Beef as AtomicBEEF.
     *
     * `txid` must exist
     *
     * after sorting, if txid is not last txid, creates a clone and removes newer txs
     *
     * @param txid
     * @returns serialized contents of this Beef with AtomicBEEF prefix.
     */
    toUint8ArrayAtomic(txid) {
        const { beef, writer } = this.getBeefForAtomic(txid);
        const beefUint8 = beef.getSerializedBytes();
        const prefix = writer.toUint8Array();
        const atomic = new Uint8Array(prefix.length + beefUint8.length);
        atomic.set(prefix, 0);
        atomic.set(beefUint8, prefix.length);
        return atomic;
    }
    /**
     * Returns a hex string representing the serialized BEEF
     * @returns A hex string representing the BEEF
     */
    toHex() {
        if (this.hexCache != null) {
            return this.hexCache;
        }
        const bytes = this.getSerializedBytes();
        const hex = toHex(bytes);
        this.hexCache = hex;
        return hex;
    }
    static fromReader(br) {
        let version = br.readUInt32LE();
        let atomicTxid;
        if (version === ATOMIC_BEEF) {
            // Skip the txid and re-read the BEEF version
            atomicTxid = toHex(br.readReverse(32));
            version = br.readUInt32LE();
        }
        if (version !== BEEF_V1 && version !== BEEF_V2) {
            throw new Error(`Serialized BEEF must start with ${BEEF_V1} or ${BEEF_V2} but starts with ${version}`);
        }
        const beef = new Beef(version);
        const bumpsLength = br.readVarIntNum();
        for (let i = 0; i < bumpsLength; i++) {
            const bump = MerklePath.fromReader(br, false);
            beef.bumps.push(bump);
        }
        const txsLength = br.readVarIntNum();
        for (let i = 0; i < txsLength; i++) {
            const beefTx = BeefTx.fromReader(br, version);
            beef.txs.push(beefTx);
        }
        beef.atomicTxid = atomicTxid;
        return beef;
    }
    /**
     * Constructs an instance of the Beef class based on the provided binary array
     * @param bin The binary array or Uint8Array from which to construct BEEF
     * @returns An instance of the Beef class constructed from the binary data
     */
    static fromBinary(bin) {
        const br = ReaderUint8Array.makeReader(bin);
        return Beef.fromReader(br);
    }
    /**
     * Constructs an instance of the Beef class based on the provided string
     * @param s The string value from which to construct BEEF
     * @param enc The encoding of the string value from which BEEF should be constructed
     * @returns An instance of the Beef class constructed from the string
     */
    static fromString(s, enc = 'hex') {
        const bin = toUint8Array(s, enc);
        const br = new ReaderUint8Array(bin);
        return Beef.fromReader(br);
    }
    /**
     * Try to validate newTx.bumpIndex by looking for an existing bump
     * that proves newTx.txid
     *
     * @param newTx A new `BeefTx` that has been added to this.txs
     * @returns true if a bump was found, false otherwise
     */
    tryToValidateBumpIndex(newTx) {
        if (newTx.bumpIndex !== undefined) {
            return true;
        }
        const txid = newTx.txid;
        for (let i = 0; i < this.bumps.length; i++) {
            const j = this.bumps[i].path[0].findIndex((b) => b.hash === txid);
            if (j >= 0) {
                newTx.bumpIndex = i;
                this.bumps[i].path[0][j].txid = true;
                return true;
            }
        }
        return false;
    }
    /**
     * Sort the `txs` by input txid dependency order:
     * - Oldest Tx Anchored by Path or txid only
     * - Newer Txs depending on Older parents
     * - Newest Tx
     *
     * with proof (MerklePath) last, longest chain of dependencies first
     *
     * @returns `{ missingInputs, notValid, valid, withMissingInputs }`
     */
    sortTxs() {
        // Hashtable of valid txids (with proof or all inputs chain to proof)
        const validTxids = {};
        // Hashtable of all transaction txids to transaction
        const txidToTx = {};
        // sorted transactions: hasProof to with longest dependency chain
        const result = [];
        const txidOnly = [];
        // Partition into proven, txidOnly, and remaining queue
        let queue = this.partitionTxs(txidToTx, validTxids, result, txidOnly);
        // Separate queue entries that have missing inputs
        const { txsMissingInputs, missingInputs, remaining } = this.separateMissingInputs(queue, txidToTx);
        queue = remaining;
        // Topological sort of remaining queue
        const txsNotValid = this.topoSort(queue, validTxids, result);
        // New order of txs is unsortable (missing inputs or depends on missing inputs), txidOnly, sorted (so newest sorted is last)
        this.txs = txsMissingInputs
            .concat(txsNotValid)
            .concat(txidOnly)
            .concat(result);
        this.needsSort = false;
        this.invalidateSerializationCaches();
        return {
            missingInputs: Object.keys(missingInputs),
            notValid: txsNotValid.map((tx) => tx.txid),
            valid: Object.keys(validTxids),
            withMissingInputs: txsMissingInputs.map((tx) => tx.txid),
            txidOnly: txidOnly.map((tx) => tx.txid)
        };
    }
    /**
     * Partition txs into proven (result), txidOnly, and a queue of the rest.
     * Populates txidToTx and validTxids as side-effects.
     */
    partitionTxs(txidToTx, validTxids, result, txidOnly) {
        const queue = [];
        for (const tx of this.txs) {
            txidToTx[tx.txid] = tx;
            tx.isValid = tx.hasProof;
            if (tx.isValid) {
                validTxids[tx.txid] = true;
                result.push(tx);
            }
            else if (tx.isTxidOnly && tx.inputTxids.length === 0) {
                validTxids[tx.txid] = true;
                txidOnly.push(tx);
            }
            else {
                queue.push(tx);
            }
        }
        return queue;
    }
    /**
     * Separate queue entries that have at least one input txid not present in txidToTx.
     */
    separateMissingInputs(candidates, txidToTx) {
        const missingInputs = {};
        const txsMissingInputs = [];
        const remaining = [];
        for (const tx of candidates) {
            let hasMissingInput = false;
            for (const inputTxid of tx.inputTxids) {
                if (txidToTx[inputTxid] === undefined) {
                    missingInputs[inputTxid] = true;
                    hasMissingInput = true;
                }
            }
            if (hasMissingInput) {
                txsMissingInputs.push(tx);
            }
            else {
                remaining.push(tx);
            }
        }
        return { txsMissingInputs, missingInputs, remaining };
    }
    /**
     * Topologically sort queue into result; return anything that cannot be sorted.
     */
    topoSort(queue, validTxids, result) {
        while (queue.length > 0) {
            const oldQueue = queue;
            queue = [];
            for (const tx of oldQueue) {
                if (tx.inputTxids.every((txid) => validTxids[txid])) {
                    validTxids[tx.txid] = true;
                    result.push(tx);
                }
                else {
                    queue.push(tx);
                }
            }
            if (oldQueue.length === queue.length)
                break;
        }
        return queue;
    }
    /**
     * @returns a shallow copy of this beef
     */
    clone() {
        const c = new Beef();
        c.version = this.version;
        c.bumps = Array.from(this.bumps);
        c.txs = Array.from(this.txs);
        c.txidIndex = undefined;
        c.needsSort = this.needsSort;
        c.hexCache = this.hexCache;
        c.rawBytesCache = this.rawBytesCache;
        return c;
    }
    /**
     * Ensure that all the txids in `knownTxids` are txidOnly
     * @param knownTxids
     */
    trimKnownTxids(knownTxids) {
        let mutated = this.removeKnownTxidOnlyTxs(knownTxids);
        mutated = this.reindexBumps() || mutated;
        if (mutated) {
            this.markMutated(true);
        }
    }
    /** Remove txidOnly entries that appear in knownTxids; return true if any were removed. */
    removeKnownTxidOnlyTxs(knownTxids) {
        let mutated = false;
        for (let i = 0; i < this.txs.length;) {
            const tx = this.txs[i];
            if (tx.isTxidOnly && knownTxids.includes(tx.txid)) {
                this.deleteFromIndex(tx.txid);
                this.txs.splice(i, 1);
                mutated = true;
            }
            else {
                i++;
            }
        }
        return mutated;
    }
    /**
     * Remove bumps that are no longer referenced by any tx and update bumpIndex references.
     * Returns true if any bumps were removed.
     */
    reindexBumps() {
        const referencedBumpIndices = new Set();
        for (const tx of this.txs) {
            if (tx.bumpIndex !== undefined) {
                referencedBumpIndices.add(tx.bumpIndex);
            }
        }
        if (referencedBumpIndices.size >= this.bumps.length)
            return false;
        // Build mapping of old indices to new indices after removal
        const indexMap = new Map();
        let newIndex = 0;
        for (let i = 0; i < this.bumps.length; i++) {
            if (referencedBumpIndices.has(i)) {
                indexMap.set(i, newIndex);
                newIndex++;
            }
        }
        // Remove unreferenced bumps
        this.bumps = this.bumps.filter((_, i) => referencedBumpIndices.has(i));
        // Update all transaction bumpIndex references
        for (const tx of this.txs) {
            if (tx.bumpIndex === undefined)
                continue;
            const mapped = indexMap.get(tx.bumpIndex);
            if (mapped === undefined) {
                throw new Error(`Internal error: bumpIndex ${tx.bumpIndex} not found in indexMap`);
            }
            tx.bumpIndex = mapped;
        }
        return true;
    }
    /**
     * @returns array of transaction txids that either have a proof or whose inputs chain back to a proven transaction.
     */
    getValidTxids() {
        const r = this.sortTxs();
        return r.valid;
    }
    /**
     * @returns Summary of `Beef` contents as multi-line string.
     */
    toLogString() {
        let log = '';
        log += `BEEF with ${this.bumps.length} BUMPS and ${this.txs.length} Transactions, isValid ${this.isValid().toString()}\n`;
        let i = -1;
        for (const b of this.bumps) {
            i++;
            log += `  BUMP ${i}\n    block: ${b.blockHeight}\n    txids: [\n${b.path[0]
                .filter((n) => n.txid === true)
                .map((n) => `      '${n.hash ?? ''}'`)
                .join(',\n')}\n    ]\n`;
        }
        i = -1;
        for (const t of this.txs) {
            i++;
            log += `  TX ${i}\n    txid: ${t.txid}\n`;
            if (t.bumpIndex !== undefined) {
                log += `    bumpIndex: ${t.bumpIndex}\n`;
            }
            if (t.isTxidOnly) {
                log += '    txidOnly\n';
            }
            else {
                log += `    rawTx length=${t.rawTx?.length ?? 0}\n`;
            }
            if (t.inputTxids.length > 0) {
                log += `    inputs: [\n${t.inputTxids
                    .map((it) => `      '${it}'`)
                    .join(',\n')}\n    ]\n`;
            }
        }
        return log;
    }
    /**
   * In some circumstances it may be helpful for the BUMP MerklePaths to include
   * leaves that can be computed from row zero.
   */
    addComputedLeaves() {
        for (const bump of this.bumps) {
            for (let row = 1; row < bump.path.length; row++) {
                this.addComputedLeavesForRow(bump, row);
            }
        }
    }
    /** Add any missing computable leaf at `row` derived from two known leaves at `row - 1`. */
    addComputedLeavesForRow(bump, row) {
        const hashPair = (m) => toHex(hash256(toArray(m, 'hex').reverse()).reverse());
        for (const leafL of bump.path[row - 1]) {
            if (typeof leafL.hash !== 'string' || (leafL.offset & 1) !== 0)
                continue;
            const leafR = bump.path[row - 1].find((l) => l.offset === leafL.offset + 1);
            if (leafR === undefined || typeof leafR.hash !== 'string')
                continue;
            const offsetOnRow = leafL.offset >> 1;
            if (bump.path[row].every((l) => l.offset !== offsetOnRow)) {
                // Computable leaf is missing... add it.
                bump.path[row].push({
                    offset: offsetOnRow,
                    // String concatenation puts the right leaf on the left of the left leaf hash
                    hash: hashPair(leafR.hash + leafL.hash)
                });
            }
        }
    }
}
export default Beef;
//# sourceMappingURL=Beef.js.map