var _a;
import { __awaiter, __extends, __generator } from "tslib";
import assert from "assert";
import { clamp, sortedIndexBy } from "lodash";
import { BlockFinder } from "../../utils/BlockFinder";
import { isDefined } from "../../utils/TypeGuards";
import { getCurrentTime } from "../../utils/TimeUtils";
import { CHAIN_IDs } from "../../constants";
import { getNearestSlotTime } from "./utils";
var now = getCurrentTime();
var averageBlockTimes = (_a = {},
    _a[CHAIN_IDs.SOLANA] = { average: 0.4, timestamp: now, blockRange: 1 },
    _a);
/**
 * @description Compute the average slot time over a slot range.
 * @dev Solana slots are all defined to be ~400ms away from each other += a small deviation, so we can hardcode this.
 * @returns Average number of seconds per slot
 */
export function averageBlockTime() {
    // @todo This may need to be expanded to work without assuming that chainId = CHAIN_IDs.SOLANA.
    return averageBlockTimes[CHAIN_IDs.SOLANA];
}
function estimateBlocksElapsed(seconds, cushionPercentage, _provider) {
    if (cushionPercentage === void 0) { cushionPercentage = 0.0; }
    var cushionMultiplier = cushionPercentage + 1.0;
    var average = averageBlockTime().average;
    return Math.floor((seconds * cushionMultiplier) / average);
}
var SVMBlockFinder = /** @class */ (function (_super) {
    __extends(SVMBlockFinder, _super);
    function SVMBlockFinder(provider, blocks) {
        if (blocks === void 0) { blocks = []; }
        var _this = _super.call(this) || this;
        _this.provider = provider;
        _this.blocks = blocks;
        return _this;
    }
    /**
     * @notice Gets the latest slot whose timestamp is <= the provided timestamp.
     * @param number Timestamp timestamp to search.
     * @param hints Optional low and high slot to bound the search space.
     */
    SVMBlockFinder.prototype.getBlockForTimestamp = function (timestamp, hints) {
        if (hints === void 0) { hints = {}; }
        return __awaiter(this, void 0, void 0, function () {
            var block, initialBlock, cushion, incrementDistance, multiplier, distance, blockNumber, block, index;
            var _this = this;
            return __generator(this, function (_a) {
                switch (_a.label) {
                    case 0:
                        timestamp = Number(timestamp);
                        assert(timestamp !== undefined && timestamp !== null, "timestamp must be provided");
                        if (!(this.blocks.length === 0 || this.blocks[this.blocks.length - 1].timestamp < timestamp)) return [3 /*break*/, 2];
                        return [4 /*yield*/, this.getLatestBlock()];
                    case 1:
                        block = _a.sent();
                        if (timestamp >= block.timestamp)
                            return [2 /*return*/, block];
                        _a.label = 2;
                    case 2: 
                    // Prime the BlockFinder cache with any supplied hints.
                    // If the hint is accurate, then this will bypass the subsequent estimation.
                    return [4 /*yield*/, Promise.all(Object.values(hints)
                            .filter(isDefined)
                            .map(function (blockNumber) { return _this.getBlock(blockNumber); }))];
                    case 3:
                        // Prime the BlockFinder cache with any supplied hints.
                        // If the hint is accurate, then this will bypass the subsequent estimation.
                        _a.sent();
                        if (!(this.blocks[0].timestamp > timestamp)) return [3 /*break*/, 7];
                        initialBlock = this.blocks[0];
                        cushion = 1;
                        incrementDistance = Math.max(
                        // Ensure the increment slot distance is _at least_ a single slot to prevent an infinite loop.
                        estimateBlocksElapsed(initialBlock.timestamp - timestamp, cushion, this.provider), 1);
                        multiplier = 1;
                        _a.label = 4;
                    case 4:
                        distance = multiplier * incrementDistance;
                        blockNumber = Math.max(0, initialBlock.number - distance);
                        return [4 /*yield*/, this.getBlock(blockNumber)];
                    case 5:
                        block = _a.sent();
                        if (block.timestamp <= timestamp)
                            return [3 /*break*/, 7]; // Found an earlier block.
                        assert(blockNumber > 0, "timestamp is before block 0");
                        _a.label = 6;
                    case 6:
                        multiplier++;
                        return [3 /*break*/, 4];
                    case 7:
                        index = sortedIndexBy(this.blocks, { timestamp: timestamp }, "timestamp");
                        return [2 /*return*/, this.findBlock(this.blocks[index - 1], this.blocks[index], timestamp)];
                }
            });
        });
    };
    /**
     * For a given slot, resolve the current or preceding timestamp.
     * Not all Solana slots have an associated block timestamp; in case of no block at the requested slot, the most
     * immediate preceding block timestamp will be used. Note that this may return an eventually-incorrect timestamp for
     * future slots.
     */
    SVMBlockFinder.prototype.getBlockTime = function (slot) {
        var opts = isDefined(slot) ? { slot: slot } : undefined;
        return getNearestSlotTime(this.provider, opts);
    };
    // Grabs the most recent slot and caches it.
    SVMBlockFinder.prototype.getLatestBlock = function () {
        var _a;
        return __awaiter(this, void 0, void 0, function () {
            var _b, slot, timestamp, block, index;
            return __generator(this, function (_c) {
                switch (_c.label) {
                    case 0: return [4 /*yield*/, this.getBlockTime()];
                    case 1:
                        _b = _c.sent(), slot = _b.slot, timestamp = _b.timestamp;
                        block = {
                            timestamp: timestamp,
                            number: Number(slot),
                        };
                        index = sortedIndexBy(this.blocks, block, "number");
                        if (((_a = this.blocks[index]) === null || _a === void 0 ? void 0 : _a.number) !== block.number)
                            this.blocks.splice(index, 0, block);
                        return [2 /*return*/, this.blocks[index]];
                }
            });
        });
    };
    // Grabs the slot for a particular number and caches it.
    SVMBlockFinder.prototype.getBlock = function (number) {
        var _a, _b;
        return __awaiter(this, void 0, void 0, function () {
            var index, _c, _slot, timestamp, slot, block;
            return __generator(this, function (_d) {
                switch (_d.label) {
                    case 0:
                        index = sortedIndexBy(this.blocks, { number: number }, "number");
                        if (((_a = this.blocks[index]) === null || _a === void 0 ? void 0 : _a.number) === number)
                            return [2 /*return*/, this.blocks[index]]; // Return early if block already exists.
                        return [4 /*yield*/, this.getBlockTime(BigInt(number))];
                    case 1:
                        _c = _d.sent(), _slot = _c.slot, timestamp = _c.timestamp;
                        slot = Number(_slot);
                        block = {
                            timestamp: timestamp,
                            number: slot,
                        };
                        // Recompute the index after the async call since the state of this.blocks could have changed!
                        index = sortedIndexBy(this.blocks, { number: slot }, "number");
                        // Rerun this check to avoid duplicate insertion.
                        if (((_b = this.blocks[index]) === null || _b === void 0 ? void 0 : _b.number) === slot)
                            return [2 /*return*/, this.blocks[index]];
                        this.blocks.splice(index, 0, block); // A simple insert at index.
                        return [2 /*return*/, block];
                }
            });
        });
    };
    // Return the latest slot, between startSlot and endSlot, whose timestamp is <= timestamp.
    // Effectively, this is an interpolation search algorithm to minimize slot requests.
    // Note: startSlot and endSlot _must_ be different slots.
    SVMBlockFinder.prototype.findBlock = function (_startSlot, _endSlot, timestamp) {
        return __awaiter(this, void 0, void 0, function () {
            var _a, startBlock, endBlock, totalTimeDifference, totalBlockDistance, blockPercentile, estimatedBlock, newBlock;
            return __generator(this, function (_b) {
                switch (_b.label) {
                    case 0:
                        _a = [_startSlot, _endSlot], startBlock = _a[0], endBlock = _a[1];
                        // In the case of equality, the endBlock is expected to be passed as the one whose timestamp === the requested
                        // timestamp.
                        if (endBlock.timestamp === timestamp)
                            return [2 /*return*/, endBlock];
                        // If there's no equality, but the blocks are adjacent, return the startBlock, since we want the returned slot's
                        // timestamp to be <= the requested timestamp.
                        if (endBlock.number === startBlock.number + 1)
                            return [2 /*return*/, startBlock];
                        assert(endBlock.number !== startBlock.number, "startBlock cannot equal endBlock");
                        assert(timestamp < endBlock.timestamp && timestamp > startBlock.timestamp, "timestamp not in between start and end blocks");
                        totalTimeDifference = endBlock.timestamp - startBlock.timestamp;
                        totalBlockDistance = endBlock.number - startBlock.number;
                        blockPercentile = (timestamp - startBlock.timestamp) / totalTimeDifference;
                        estimatedBlock = startBlock.number + Math.round(blockPercentile * totalBlockDistance);
                        return [4 /*yield*/, this.getBlock(clamp(estimatedBlock, startBlock.number + 1, endBlock.number - 1))];
                    case 1:
                        newBlock = _b.sent();
                        // Depending on whether the new slot is below or above the timestamp, narrow the search space accordingly.
                        if (newBlock.timestamp < timestamp) {
                            return [2 /*return*/, this.findBlock(newBlock, endBlock, timestamp)];
                        }
                        else {
                            return [2 /*return*/, this.findBlock(startBlock, newBlock, timestamp)];
                        }
                        return [2 /*return*/];
                }
            });
        });
    };
    return SVMBlockFinder;
}(BlockFinder));
export { SVMBlockFinder };
//# sourceMappingURL=BlockUtils.js.map