import { __awaiter, __generator } from "tslib";
import { isSVMSpokePoolClient } from "../../SpokePoolClient";
import { bnZero, chainIsEvm, chainIsSvm, compareAddresses, EvmAddress, isDefined } from "../../../utils";
import { getLatestFinalizedSlotWithBlock } from "../../../arch/svm";
import assert from "assert";
// This returns a possible next block range that could be submitted as a new root bundle, or used as a reference
// when evaluating  pending root bundle. The block end numbers must be less than the latest blocks for each chain ID
// (because we can't evaluate events in the future), and greater than the expected start blocks, which are the
// greater of 0 and the latest bundle end block for an executed root bundle proposal + 1.
export function getWidestPossibleExpectedBlockRange(chainIds, spokeClients, endBlockBuffers, clients, latestMainnetBlock, enabledChains, optimistic) {
    if (optimistic === void 0) { optimistic = false; }
    return __awaiter(this, void 0, void 0, function () {
        var resolveEndBlock, resolveSVMEndBlock, latestPossibleBundleEndBlockNumbers;
        return __generator(this, function (_a) {
            switch (_a.label) {
                case 0:
                    resolveEndBlock = function (chainId, idx) {
                        return Math.max(spokeClients[chainId].latestHeightSearched - endBlockBuffers[idx], 0);
                    };
                    resolveSVMEndBlock = function (chainId, idx) {
                        var spokePoolClient = spokeClients[chainId];
                        assert(isSVMSpokePoolClient(spokePoolClient));
                        var maxSlot = resolveEndBlock(chainId, idx); // Respect any configured buffer for Solana.
                        return getLatestFinalizedSlotWithBlock(spokePoolClient.svmEventsClient.getRpc(), BigInt(maxSlot));
                    };
                    return [4 /*yield*/, Promise.all(chainIds.map(function (chainId, idx) {
                            if (!enabledChains.includes(chainId) || !isDefined(spokeClients[chainId])) {
                                return -1; // Chain is disabled; end block is redundant and will be overridden later.
                            }
                            if (chainIsEvm(chainId)) {
                                return Promise.resolve(resolveEndBlock(chainId, idx));
                            }
                            if (chainIsSvm(chainId)) {
                                return resolveSVMEndBlock(chainId, idx);
                            }
                            assert(false, "Unsupported chainId: ".concat(chainId));
                        }))];
                case 1:
                    latestPossibleBundleEndBlockNumbers = _a.sent();
                    return [2 /*return*/, chainIds.map(function (chainId, index) {
                            var lastEndBlockForChain = clients.hubPoolClient.getLatestBundleEndBlockForChain(chainIds, latestMainnetBlock, chainId);
                            // If chain is disabled, re-use the latest bundle end block for the chain as both the start
                            // and end block.
                            if (!enabledChains.includes(chainId)) {
                                return [lastEndBlockForChain, lastEndBlockForChain];
                            }
                            // If the latest block hasn't advanced enough from the previous proposed end block, then re-use it. It will
                            // be regarded as disabled by the Dataworker clients. Otherwise, add 1 to the previous proposed end block.
                            if (lastEndBlockForChain >= latestPossibleBundleEndBlockNumbers[index]) {
                                // @dev: Without this check, then `getNextBundleStartBlockNumber` could return `latestBlock+1` even when the
                                // latest block for the chain hasn't advanced, resulting in an invalid range being produced.
                                return [lastEndBlockForChain, lastEndBlockForChain];
                            }
                            // Chain has advanced far enough including the buffer, return range from previous proposed end block + 1 to
                            // latest block for chain minus buffer.
                            return [
                                optimistic
                                    ? clients.hubPoolClient.getOptimisticBundleStartBlockNumber(chainIds, latestMainnetBlock, chainId)
                                    : clients.hubPoolClient.getNextBundleStartBlockNumber(chainIds, latestMainnetBlock, chainId),
                                latestPossibleBundleEndBlockNumbers[index],
                            ];
                        })];
            }
        });
    });
}
export function isChainDisabled(blockRangeForChain) {
    return blockRangeForChain[0] === blockRangeForChain[1];
}
// Note: this function computes the intended transfer amount before considering the transfer threshold.
// A positive number indicates a transfer from hub to spoke.
export function computeDesiredTransferAmountToSpoke(runningBalance, spokePoolTargetBalance) {
    // Transfer is always desired if hub owes spoke.
    if (runningBalance.gte(0)) {
        return runningBalance;
    }
    // Running balance is negative, but its absolute value is less than the spoke pool target balance threshold.
    // In this case, we transfer nothing.
    if (runningBalance.abs().lt(spokePoolTargetBalance.threshold)) {
        return bnZero;
    }
    // We are left with the case where the spoke pool is beyond the threshold.
    // A transfer needs to be initiated to bring it down to the target.
    var transferSize = runningBalance.abs().sub(spokePoolTargetBalance.target);
    // If the transferSize is < 0, this indicates that the target is still above the running balance.
    // This can only happen if the threshold is less than the target. This is likely due to a misconfiguration.
    // In this case, we transfer nothing until the target is exceeded.
    if (transferSize.lt(0)) {
        return bnZero;
    }
    // Negate the transfer size because a transfer from spoke to hub is indicated by a negative number.
    return transferSize.mul(-1);
}
// If the running balance is greater than the token transfer threshold, then set the net send amount
// equal to the running balance and reset the running balance to 0. Otherwise, the net send amount should be
// 0, indicating that we do not want the data worker to trigger a token transfer between hub pool and spoke
// pool when executing this leaf.
export function getNetSendAmountForL1Token(spokePoolTargetBalance, runningBalance) {
    return computeDesiredTransferAmountToSpoke(runningBalance, spokePoolTargetBalance);
}
export function getRunningBalanceForL1Token(spokePoolTargetBalance, runningBalance) {
    var desiredTransferAmount = computeDesiredTransferAmountToSpoke(runningBalance, spokePoolTargetBalance);
    return runningBalance.sub(desiredTransferAmount);
}
export function updateRunningBalance(runningBalances, l2ChainId, l1Token, updateAmount) {
    // Initialize dictionary if empty.
    if (!runningBalances[l2ChainId]) {
        runningBalances[l2ChainId] = {};
    }
    var runningBalance = runningBalances[l2ChainId][l1Token];
    if (runningBalance) {
        runningBalances[l2ChainId][l1Token] = runningBalance.add(updateAmount);
    }
    else {
        runningBalances[l2ChainId][l1Token] = updateAmount;
    }
}
export function addLastRunningBalance(latestMainnetBlock, runningBalances, hubPoolClient) {
    Object.keys(runningBalances).forEach(function (repaymentChainId) {
        Object.keys(runningBalances[Number(repaymentChainId)]).forEach(function (l1TokenAddress) {
            var runningBalance = hubPoolClient.getRunningBalanceBeforeBlockForChain(latestMainnetBlock, Number(repaymentChainId), EvmAddress.from(l1TokenAddress)).runningBalance;
            if (!runningBalance.eq(bnZero)) {
                updateRunningBalance(runningBalances, Number(repaymentChainId), l1TokenAddress, runningBalance);
            }
        });
    });
}
export function updateRunningBalanceForDeposit(runningBalances, hubPoolClient, deposit, updateAmount, mainnetBundleEndBlock) {
    var l1TokenCounterpart = hubPoolClient.getL1TokenForL2TokenAtBlock(deposit.inputToken, deposit.originChainId, mainnetBundleEndBlock);
    updateRunningBalance(runningBalances, deposit.originChainId, l1TokenCounterpart.toEvmAddress(), updateAmount);
}
export function constructPoolRebalanceLeaves(latestMainnetBlock, runningBalances, realizedLpFees, chainsWithRefundsOnly, configStoreClient, maxL1TokenCount) {
    // Add a leaf for each chain ID with no L1 tokens or running balances
    assert(chainsWithRefundsOnly.every(function (chainId) { return runningBalances[chainId] === undefined; }) &&
        chainsWithRefundsOnly.every(function (chainId) { return realizedLpFees[chainId] === undefined; }), "Refund-only chains should not have running balances or realized LP fees.");
    // Create one leaf per L2 chain ID. First we'll create a leaf with all L1 tokens for each chain ID, and then
    // we'll split up any leaves with too many L1 tokens.
    var leaves = [];
    Object.keys(runningBalances)
        .map(function (chainId) { return Number(chainId); })
        .concat(chainsWithRefundsOnly)
        // Leaves should be sorted by ascending chain ID
        .sort(function (chainIdA, chainIdB) { return chainIdA - chainIdB; })
        .map(function (chainId) {
        if (chainsWithRefundsOnly.includes(chainId)) {
            leaves.push({
                chainId: chainId,
                bundleLpFees: [],
                netSendAmounts: [],
                runningBalances: [],
                groupIndex: 0,
                leafId: leaves.length,
                l1Tokens: [],
            });
            return;
        }
        // Sort addresses.
        var sortedL1Tokens = Object.keys(runningBalances[chainId]).sort(function (addressA, addressB) {
            return compareAddresses(addressA, addressB);
        });
        // This begins at 0 and increments for each leaf for this { chainId, L1Token } combination.
        var groupIndexForChainId = 0;
        // Split addresses into multiple leaves if there are more L1 tokens than allowed per leaf.
        var maxL1TokensPerLeaf = maxL1TokenCount || configStoreClient.getMaxRefundCountForRelayerRefundLeafForBlock(latestMainnetBlock);
        var _loop_1 = function (i) {
            var l1TokensToIncludeInThisLeaf = sortedL1Tokens.slice(i, i + maxL1TokensPerLeaf);
            var spokeTargetBalances = l1TokensToIncludeInThisLeaf.map(function (l1Token) {
                return configStoreClient.getSpokeTargetBalancesForBlock(l1Token, chainId, latestMainnetBlock);
            });
            // Build leaves using running balances and realized lp fees data for l1Token + chain, or default to
            // zero if undefined.
            var leafBundleLpFees = l1TokensToIncludeInThisLeaf.map(function (l1Token) { var _a, _b; return (_b = (_a = realizedLpFees[chainId]) === null || _a === void 0 ? void 0 : _a[l1Token]) !== null && _b !== void 0 ? _b : bnZero; });
            var leafNetSendAmounts = l1TokensToIncludeInThisLeaf.map(function (l1Token, index) {
                return runningBalances[chainId] && runningBalances[chainId][l1Token]
                    ? getNetSendAmountForL1Token(spokeTargetBalances[index], runningBalances[chainId][l1Token])
                    : bnZero;
            });
            var leafRunningBalances = l1TokensToIncludeInThisLeaf.map(function (l1Token, index) {
                var _a;
                return ((_a = runningBalances[chainId]) === null || _a === void 0 ? void 0 : _a[l1Token])
                    ? getRunningBalanceForL1Token(spokeTargetBalances[index], runningBalances[chainId][l1Token])
                    : bnZero;
            });
            leaves.push({
                chainId: chainId,
                bundleLpFees: leafBundleLpFees,
                netSendAmounts: leafNetSendAmounts,
                runningBalances: leafRunningBalances,
                groupIndex: groupIndexForChainId++,
                leafId: leaves.length,
                l1Tokens: l1TokensToIncludeInThisLeaf.map(function (l1TokenAddr) { return EvmAddress.from(l1TokenAddr); }),
            });
        };
        for (var i = 0; i < sortedL1Tokens.length; i += maxL1TokensPerLeaf) {
            _loop_1(i);
        }
    });
    return leaves;
}
//# sourceMappingURL=PoolRebalanceUtils.js.map