import { __assign, __awaiter, __generator, __spreadArray } from "tslib";
import { delay } from "./common";
import { isDefined, toBN } from "./";
var maxRetries = 3;
var retrySleepTime = 10;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function spreadEvent(args) {
    var keys = Object.keys(args).filter(function (key) { return isNaN(+key); }); // Extract non-numeric keys.
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    var returnedObject = {};
    keys.forEach(function (key) {
        switch (typeof args[key]) {
            case "boolean": // fallthrough
            case "number":
            case "string":
                returnedObject[key] = args[key];
                break;
            case "object":
                if (Array.isArray(args[key])) {
                    returnedObject[key] =
                        Object.keys(args[key]).filter(function (key) { return isNaN(+key); }).length > 0
                            ? spreadEvent(args[key]) // Record/array hybrid...
                            : args[key]; // Just an array
                }
                else {
                    returnedObject[key] = args[key];
                }
                break;
        }
    });
    // ID information, if included in an event, should be cast to a number rather than a BigNumber.
    if (returnedObject.groupIndex) {
        returnedObject.groupIndex = Number(returnedObject.groupIndex);
    }
    if (returnedObject.leafId) {
        returnedObject.leafId = Number(returnedObject.leafId);
    }
    if (returnedObject.chainId) {
        returnedObject.chainId = Number(returnedObject.chainId);
    }
    if (returnedObject.destinationChainId) {
        returnedObject.destinationChainId = Number(returnedObject.destinationChainId);
    }
    if (returnedObject.originChainId) {
        returnedObject.originChainId = Number(returnedObject.originChainId);
    }
    if (returnedObject.repaymentChainId) {
        returnedObject.repaymentChainId = Number(returnedObject.repaymentChainId);
    }
    if (returnedObject.l2ChainId) {
        returnedObject.l2ChainId = Number(returnedObject.l2ChainId);
    }
    if (returnedObject.rootBundleId) {
        returnedObject.rootBundleId = Number(returnedObject.rootBundleId);
    }
    // If depositId is included in the event, cast it to a BigNumber. Need to check if it is defined since the deposit ID can
    // be 0, which would still make this evaluate as false.
    if (isDefined(returnedObject.depositId)) {
        // Assuming a numeric output, we can safely cast the unknown to BigNumberish since the depositId will either be a uint32 (and therefore a TypeScript `number`),
        // or a uint256 (and therefore an ethers BigNumber).
        returnedObject.depositId = toBN(returnedObject.depositId);
    }
    return returnedObject;
}
export var eventToLog = function (event) { return (__assign(__assign({}, event), { event: event.event, args: spreadEvent(event.args) })); };
export function paginatedEventQuery(contract, filter, searchConfig, retryCount) {
    if (retryCount === void 0) { retryCount = 0; }
    return __awaiter(this, void 0, void 0, function () {
        var events, paginatedRanges, error_1;
        return __generator(this, function (_a) {
            switch (_a.label) {
                case 0:
                    if (!(searchConfig.maxLookBack === 0)) return [3 /*break*/, 2];
                    return [4 /*yield*/, contract.queryFilter(filter, searchConfig.from, searchConfig.to)];
                case 1:
                    events = _a.sent();
                    return [2 /*return*/, events.map(eventToLog)];
                case 2:
                    paginatedRanges = getPaginatedBlockRanges(searchConfig);
                    _a.label = 3;
                case 3:
                    _a.trys.push([3, 5, , 10]);
                    return [4 /*yield*/, Promise.all(paginatedRanges.map(function (_a) {
                            var fromBlock = _a[0], toBlock = _a[1];
                            return contract.queryFilter(filter, fromBlock, toBlock);
                        }))];
                case 4: return [2 /*return*/, ((_a.sent())
                        .flat()
                        // Filter events by block number because ranges can include blocks that are outside the range specified for caching reasons.
                        .filter(function (event) { return event.blockNumber >= searchConfig.from && event.blockNumber <= searchConfig.to; })
                        .map(eventToLog))];
                case 5:
                    error_1 = _a.sent();
                    if (!(retryCount < maxRetries)) return [3 /*break*/, 8];
                    return [4 /*yield*/, delay(retrySleepTime)];
                case 6:
                    _a.sent();
                    return [4 /*yield*/, paginatedEventQuery(contract, filter, searchConfig, retryCount + 1)];
                case 7: return [2 /*return*/, _a.sent()];
                case 8: throw error_1;
                case 9: return [3 /*break*/, 10];
                case 10: return [2 /*return*/];
            }
        });
    });
}
/**
 * @dev Warning: this is a specialized function!! Its functionality is not obvious.
 * This function attempts to return block ranges to repeat ranges as much as possible. To do so, it may include blocks that
 * are outside the provided range. The guarantee is that it will always include _at least_ the blocks requested.
 * @param eventSearchConfig contains fromBlock, toBlock, and maxBlockLookBack.
 * The range is inclusive, so the results will include events in the fromBlock and in the toBlock.
 * maxBlockLookback defined the maximum number of blocks to search. Because the range is inclusive, the maximum diff
 * in the returned pairs is maxBlockLookBack - 1. This is a bit non-intuitive here, but this is meant so that this
 * parameter more closely aligns with the more commonly understood definition of a max query range that node providers
 * use.
 * @returns an array of disjoint fromBlock, toBlock ranges that should be queried. These cover at least the entire
 * input range, but can include blocks outside of the desired range, so results should be filtered. Results
 * are ordered from smallest to largest.
 */
export function getPaginatedBlockRanges(_a) {
    var from = _a.from, to = _a.to, maxLookBack = _a.maxLookBack;
    var fromBlock = from;
    var toBlock = to;
    var maxBlockLookBack = maxLookBack;
    // If the maxBlockLookBack is undefined, we can look back as far as we like. Just return the entire range.
    if (maxBlockLookBack === undefined) {
        return [[fromBlock, toBlock]];
    }
    // If the fromBlock is > toBlock, then return no ranges.
    if (fromBlock > toBlock) {
        return [];
    }
    // A maxBlockLookBack of 0 is not allowed.
    if (maxBlockLookBack <= 0) {
        throw new Error("Cannot set maxBlockLookBack <= 0");
    }
    // Floor the requestedFromBlock to the nearest smaller multiple of the maxBlockLookBack to enhance caching.
    // This means that a range like 5 - 45 with a maxBlockLookBack of 20 would look like:
    // 0-19, 20-39, 40-45.
    // This allows us to get the max number of repeated node queries. The maximum number of "nonstandard" queries per
    // call of this function is 1.
    var flooredStartBlock = Math.floor(fromBlock / maxBlockLookBack) * maxBlockLookBack;
    // Note: range is inclusive, so we have to add one to the number of blocks to query.
    var iterations = Math.ceil((toBlock + 1 - flooredStartBlock) / maxBlockLookBack);
    var ranges = [];
    for (var i = 0; i < iterations; i++) {
        // Each inner range start is just a multiple of the maxBlockLookBack added to the start block.
        var innerFromBlock = flooredStartBlock + maxBlockLookBack * i;
        // The innerFromBlock is just the max range from the innerFromBlock or the outer toBlock, whichever is smaller.
        // The end block should never be larger than the outer toBlock. This is to avoid querying blocks that are in the
        // future.
        var innerToBlock = Math.min(innerFromBlock + maxBlockLookBack - 1, toBlock);
        ranges.push([innerFromBlock, innerToBlock]);
    }
    return ranges;
}
export function logToSortableEvent(log) {
    return {
        txnIndex: log.transactionIndex,
        txnRef: log.transactionHash,
        logIndex: log.logIndex,
        blockNumber: log.blockNumber,
    };
}
export function spreadEventWithBlockNumber(event) {
    return __assign(__assign({}, spreadEvent(event.args)), logToSortableEvent(event));
}
// This copies the array and sorts it, returning a new array with the new ordering.
export function sortEventsAscending(events) {
    return sortEventsAscendingInPlace(__spreadArray([], events, true));
}
// This sorts the events in place, meaning it modifies the passed array and returns a reference to the same array.
// Note: this method should only be used in cases where modifications are acceptable.
export function sortEventsAscendingInPlace(events) {
    return events.sort(function (ex, ey) {
        return ex.blockNumber === ey.blockNumber ? ex.logIndex - ey.logIndex : ex.blockNumber - ey.blockNumber;
    });
}
// This copies the array and sorts it, returning a new array with the new ordering.
export function sortEventsDescending(events) {
    return sortEventsDescendingInPlace(__spreadArray([], events, true));
}
// This sorts the events in place, meaning it modifies the passed array and returns a reference to the same array.
// Note: this method should only be used in cases where modifications are acceptable.
export function sortEventsDescendingInPlace(events) {
    return events.sort(function (ex, ey) {
        return ex.blockNumber === ey.blockNumber ? ey.logIndex - ex.logIndex : ey.blockNumber - ex.blockNumber;
    });
}
// Returns true if ex is older than ey.
export function isEventOlder(ex, ey) {
    return ex.blockNumber === ey.blockNumber ? ex.logIndex < ey.logIndex : ex.blockNumber < ey.blockNumber;
}
export function getTransactionRefs(events) {
    return __spreadArray([], Array.from(new Set(events.map(function (e) { return e.txnRef; }))), true);
}
export function duplicateEvent(a, b) {
    return a.txnRef === b.txnRef && a.logIndex === b.logIndex;
}
//# sourceMappingURL=EventUtils.js.map