import { NUMBER_OF_COLUMNS } from "@lodestar/params";
import { byteArrayEquals, fromHex, prettyBytes, toRootHex, withTimeout } from "@lodestar/utils";
import { kzgCommitmentToVersionedHash } from "../../../util/blobs.js";
import { BlockInputError, BlockInputErrorCode } from "./errors.js";
import { DAType, } from "./types.js";
export function isBlockInputPreDeneb(blockInput) {
    return blockInput.type === DAType.PreData;
}
export function isBlockInputBlobs(blockInput) {
    return blockInput.type === DAType.Blobs;
}
export function isBlockInputColumns(blockInput) {
    return blockInput.type === DAType.Columns;
}
export function isBlockInputNoData(blockInput) {
    return blockInput.type === DAType.NoData;
}
function createPromise() {
    let resolve;
    let reject;
    const promise = new Promise((_resolve, _reject) => {
        resolve = _resolve;
        reject = _reject;
    });
    return {
        promise,
        resolve,
        reject,
    };
}
class AbstractBlockInput {
    daOutOfRange;
    timeCreatedSec;
    forkName;
    slot;
    blockRootHex;
    parentRootHex;
    blockPromise = createPromise();
    dataPromise = createPromise();
    constructor(init) {
        this.daOutOfRange = init.daOutOfRange;
        this.timeCreatedSec = init.timeCreated;
        this.forkName = init.forkName;
        this.slot = init.slot;
        this.blockRootHex = init.blockRootHex;
        this.parentRootHex = init.parentRootHex;
    }
    hasBlock() {
        return this.state.hasBlock;
    }
    getBlock() {
        if (!this.state.hasBlock) {
            throw new BlockInputError({
                code: BlockInputErrorCode.MISSING_BLOCK,
                blockRoot: this.blockRootHex,
            }, "Cannot getBlock from BlockInput without a block");
        }
        return this.state.block;
    }
    getBlockSource() {
        if (!this.state.hasBlock) {
            throw new BlockInputError({
                code: BlockInputErrorCode.MISSING_BLOCK,
                blockRoot: this.blockRootHex,
            }, "Cannot getBlockSource from BlockInput without a block");
        }
        return this.state.source;
    }
    hasAllData() {
        return this.state.hasAllData;
    }
    hasBlockAndAllData() {
        return this.state.hasBlock && this.state.hasAllData;
    }
    getLogMeta() {
        return {
            slot: this.slot,
            blockRoot: prettyBytes(this.blockRootHex),
            timeCreatedSec: this.timeCreatedSec,
        };
    }
    getTimeComplete() {
        if (!this.state.hasBlock || !this.state.hasAllData) {
            throw new BlockInputError({
                code: BlockInputErrorCode.MISSING_TIME_COMPLETE,
                blockRoot: this.blockRootHex,
            }, "Cannot getTimeComplete from BlockInput without a block and data");
        }
        return this.state.timeCompleteSec;
    }
    waitForBlock(timeout, signal) {
        if (!this.state.hasBlock) {
            return withTimeout(() => this.blockPromise.promise, timeout, signal);
        }
        return Promise.resolve(this.state.block);
    }
    waitForAllData(timeout, signal) {
        return withTimeout(() => this.dataPromise.promise, timeout, signal);
    }
    async waitForBlockAndAllData(timeout, signal) {
        if (!this.state.hasBlock || !this.state.hasAllData) {
            await withTimeout(() => Promise.all([this.blockPromise.promise, this.dataPromise.promise]), timeout, signal);
        }
        return this;
    }
}
/**
 * Pre-DA, BlockInput only has a single state.
 * - the block simply exists
 */
export class BlockInputPreData extends AbstractBlockInput {
    type = DAType.PreData;
    state;
    constructor(init, state) {
        super(init);
        this.state = state;
        this.dataPromise.resolve(null);
        this.blockPromise.resolve(state.block);
    }
    static createFromBlock(props) {
        const init = {
            daOutOfRange: props.daOutOfRange,
            timeCreated: props.seenTimestampSec,
            forkName: props.forkName,
            slot: props.block.message.slot,
            blockRootHex: props.blockRootHex,
            parentRootHex: toRootHex(props.block.message.parentRoot),
        };
        const state = {
            hasBlock: true,
            hasAllData: true,
            block: props.block,
            source: {
                source: props.source,
                seenTimestampSec: props.seenTimestampSec,
                peerIdStr: props.peerIdStr,
            },
            timeCompleteSec: props.seenTimestampSec,
        };
        return new BlockInputPreData(init, state);
    }
    addBlock(_, opts = { throwOnDuplicateAdd: true }) {
        if (opts.throwOnDuplicateAdd) {
            throw new BlockInputError({
                code: BlockInputErrorCode.INVALID_CONSTRUCTION,
                blockRoot: this.blockRootHex,
            }, "Cannot addBlock to BlockInputPreData");
        }
    }
    getSerializedCacheKeys() {
        return [this.state.block];
    }
}
/**
 * With blobs, BlockInput has several states:
 * - The block is seen and all blobs are seen
 * - The block is seen and all blobs are not yet seen
 * - The block is yet not seen and its unknown if all blobs are seen
 */
export class BlockInputBlobs extends AbstractBlockInput {
    type = DAType.Blobs;
    state;
    blobsCache = new Map();
    constructor(init, state) {
        super(init);
        this.state = state;
    }
    static createFromBlock(props) {
        const hasAllData = props.daOutOfRange || props.block.message.body.blobKzgCommitments.length === 0;
        const state = {
            hasBlock: true,
            hasAllData,
            versionedHashes: props.block.message.body.blobKzgCommitments.map(kzgCommitmentToVersionedHash),
            block: props.block,
            source: {
                source: props.source,
                seenTimestampSec: props.seenTimestampSec,
                peerIdStr: props.peerIdStr,
            },
            timeCompleteSec: hasAllData ? props.seenTimestampSec : undefined,
        };
        const init = {
            daOutOfRange: props.daOutOfRange,
            timeCreated: props.seenTimestampSec,
            forkName: props.forkName,
            slot: props.block.message.slot,
            blockRootHex: props.blockRootHex,
            parentRootHex: toRootHex(props.block.message.parentRoot),
        };
        const blockInput = new BlockInputBlobs(init, state);
        blockInput.blockPromise.resolve(props.block);
        if (hasAllData) {
            blockInput.dataPromise.resolve([]);
        }
        return blockInput;
    }
    static createFromBlob(props) {
        const state = {
            hasBlock: false,
            hasAllData: false,
        };
        const init = {
            daOutOfRange: props.daOutOfRange,
            timeCreated: props.seenTimestampSec,
            forkName: props.forkName,
            blockRootHex: props.blockRootHex,
            parentRootHex: toRootHex(props.blobSidecar.signedBlockHeader.message.parentRoot),
            slot: props.blobSidecar.signedBlockHeader.message.slot,
        };
        const blockInput = new BlockInputBlobs(init, state);
        blockInput.blobsCache.set(props.blobSidecar.index, {
            blobSidecar: props.blobSidecar,
            source: props.source,
            seenTimestampSec: props.seenTimestampSec,
            peerIdStr: props.peerIdStr,
        });
        return blockInput;
    }
    getLogMeta() {
        return {
            slot: this.slot,
            blockRoot: prettyBytes(this.blockRootHex),
            timeCreatedSec: this.timeCreatedSec,
            expectedBlobs: this.state.hasBlock ? this.state.block.message.body.blobKzgCommitments.length : "unknown",
            receivedBlobs: this.blobsCache.size,
        };
    }
    addBlock({ blockRootHex, block, source, seenTimestampSec, peerIdStr }, opts = { throwOnDuplicateAdd: true }) {
        // this check suffices for checking slot, parentRoot, and forkName
        if (blockRootHex !== this.blockRootHex) {
            throw new BlockInputError({
                code: BlockInputErrorCode.MISMATCHED_ROOT_HEX,
                blockInputRoot: this.blockRootHex,
                mismatchedRoot: blockRootHex,
                source,
                peerId: `${peerIdStr}`,
            }, "addBlock blockRootHex does not match BlockInput.blockRootHex");
        }
        if (!opts.throwOnDuplicateAdd) {
            return;
        }
        if (this.state.hasBlock) {
            throw new BlockInputError({
                code: BlockInputErrorCode.INVALID_CONSTRUCTION,
                blockRoot: this.blockRootHex,
            }, "Cannot addBlock to BlockInputBlobs after it already has a block");
        }
        for (const { blobSidecar } of this.blobsCache.values()) {
            if (!blockAndBlobArePaired(block, blobSidecar)) {
                this.blobsCache.delete(blobSidecar.index);
                // TODO: (@matthewkeil) spec says to ignore invalid blobs but should we downscore the peer maybe?
                // this.logger?.error(`Removing blobIndex=${blobSidecar.index} from BlockInput`, {}, err);
            }
        }
        const hasAllData = this.blobsCache.size === block.message.body.blobKzgCommitments.length;
        this.state = {
            ...this.state,
            hasBlock: true,
            hasAllData,
            block,
            versionedHashes: block.message.body.blobKzgCommitments.map(kzgCommitmentToVersionedHash),
            source: {
                source,
                seenTimestampSec,
                peerIdStr,
            },
            timeCompleteSec: hasAllData ? seenTimestampSec : undefined,
        };
        this.blockPromise.resolve(block);
        if (hasAllData) {
            this.dataPromise.resolve(this.getBlobs());
        }
    }
    hasBlob(blobIndex) {
        return this.blobsCache.has(blobIndex);
    }
    getBlob(blobIndex) {
        return this.blobsCache.get(blobIndex)?.blobSidecar;
    }
    addBlob({ blockRootHex, blobSidecar, source, peerIdStr, seenTimestampSec }, opts = { throwOnDuplicateAdd: true }) {
        // this check suffices for checking slot, parentRoot, and forkName
        if (blockRootHex !== this.blockRootHex) {
            throw new BlockInputError({
                code: BlockInputErrorCode.MISMATCHED_ROOT_HEX,
                blockInputRoot: this.blockRootHex,
                mismatchedRoot: blockRootHex,
                source: source,
                peerId: `${peerIdStr}`,
            }, "Blob BeaconBlockHeader blockRootHex does not match BlockInput.blockRootHex");
        }
        const isDuplicate = this.blobsCache.has(blobSidecar.index);
        if (isDuplicate && opts.throwOnDuplicateAdd) {
            throw new BlockInputError({
                code: BlockInputErrorCode.INVALID_CONSTRUCTION,
                blockRoot: this.blockRootHex,
            }, "Cannot addBlob to BlockInputBlobs with duplicate blobIndex");
        }
        if (this.state.hasBlock) {
            assertBlockAndBlobArePaired(this.blockRootHex, this.state.block, blobSidecar);
        }
        if (isDuplicate) {
            return;
        }
        this.blobsCache.set(blobSidecar.index, { blobSidecar, source, seenTimestampSec, peerIdStr });
        if (this.state.hasBlock && this.blobsCache.size === this.state.block.message.body.blobKzgCommitments.length) {
            this.state = {
                ...this.state,
                hasAllData: true,
                timeCompleteSec: seenTimestampSec,
            };
            this.dataPromise.resolve([...this.blobsCache.values()].map(({ blobSidecar }) => blobSidecar));
        }
    }
    getVersionedHashes() {
        if (!this.state.hasBlock) {
            throw new BlockInputError({
                code: BlockInputErrorCode.INCOMPLETE_DATA,
                ...this.getLogMeta(),
            }, "Cannot get versioned hashes. Block is unknown");
        }
        return this.state.versionedHashes;
    }
    getMissingBlobMeta() {
        if (!this.state.hasBlock) {
            throw new BlockInputError({
                code: BlockInputErrorCode.INCOMPLETE_DATA,
                ...this.getLogMeta(),
            }, "Cannot get missing blobs. Block is unknown");
        }
        if (this.state.hasAllData) {
            return [];
        }
        const blobMeta = [];
        const versionedHashes = this.state.versionedHashes;
        for (let index = 0; index < versionedHashes.length; index++) {
            if (!this.blobsCache.has(index)) {
                blobMeta.push({
                    index,
                    blockRoot: fromHex(this.blockRootHex),
                    versionedHash: versionedHashes[index],
                });
            }
        }
        return blobMeta;
    }
    getAllBlobsWithSource() {
        if (!this.state.hasAllData) {
            throw new BlockInputError({
                code: BlockInputErrorCode.INCOMPLETE_DATA,
                ...this.getLogMeta(),
            }, "Cannot get all blobs. DA status is not complete");
        }
        return [...this.blobsCache.values()];
    }
    getBlobs() {
        return this.getAllBlobsWithSource().map(({ blobSidecar }) => blobSidecar);
    }
    getSerializedCacheKeys() {
        const objects = [];
        if (this.state.hasBlock) {
            objects.push(this.state.block);
        }
        for (const { blobSidecar } of this.blobsCache.values()) {
            objects.push(blobSidecar);
        }
        return objects;
    }
}
function blockAndBlobArePaired(block, blobSidecar) {
    const blockCommitment = block.message.body.blobKzgCommitments[blobSidecar.index];
    if (!blockCommitment || !blobSidecar.kzgCommitment) {
        return false;
    }
    return byteArrayEquals(blockCommitment, blobSidecar.kzgCommitment);
}
function assertBlockAndBlobArePaired(blockRootHex, block, blobSidecar) {
    if (!blockAndBlobArePaired(block, blobSidecar)) {
        // TODO: (@matthewkeil) should this eject the bad blob instead? No way to tell if the blob or the block
        //       has the invalid commitment. Guessing it would be the blob though because we match via block
        //       hashTreeRoot and we do not take a hashTreeRoot of the BlobSidecar
        throw new BlockInputError({
            code: BlockInputErrorCode.MISMATCHED_KZG_COMMITMENT,
            blockRoot: blockRootHex,
            slot: block.message.slot,
            sidecarIndex: blobSidecar.index,
        }, "BlobSidecar commitment does not match block commitment");
    }
}
/**
 * With columns, BlockInput has several states:
 * - The block is seen and all required sampled columns are seen
 * - The block is seen and all required sampled columns are not yet seen
 * - The block is not yet seen and all required sampled columns are seen
 * - The block is not yet seen and all required sampled columns are not yet seen
 */
export class BlockInputColumns extends AbstractBlockInput {
    type = DAType.Columns;
    state;
    columnsCache = new Map();
    sampledColumns;
    custodyColumns;
    /**
     * This promise resolves when all sampled columns are available
     *
     * This is different from `dataPromise` which resolves when all data is available or could become available (e.g. through reconstruction)
     */
    computedDataPromise = createPromise();
    constructor(init, state, sampledColumns, custodyColumns) {
        super(init);
        this.state = state;
        this.sampledColumns = sampledColumns;
        this.custodyColumns = custodyColumns;
    }
    get columnCount() {
        return this.columnsCache.size;
    }
    static createFromBlock(props) {
        const hasAllData = props.daOutOfRange ||
            props.block.message.body.blobKzgCommitments.length === 0 ||
            props.sampledColumns.length === 0;
        const state = {
            hasBlock: true,
            hasAllData,
            hasComputedAllData: hasAllData,
            versionedHashes: props.block.message.body.blobKzgCommitments.map(kzgCommitmentToVersionedHash),
            block: props.block,
            source: {
                source: props.source,
                seenTimestampSec: props.seenTimestampSec,
                peerIdStr: props.peerIdStr,
            },
            timeCreated: props.seenTimestampSec,
            timeCompleteSec: hasAllData ? props.seenTimestampSec : undefined,
        };
        const init = {
            daOutOfRange: props.daOutOfRange,
            timeCreated: props.seenTimestampSec,
            forkName: props.forkName,
            blockRootHex: props.blockRootHex,
            parentRootHex: toRootHex(props.block.message.parentRoot),
            slot: props.block.message.slot,
        };
        const blockInput = new BlockInputColumns(init, state, props.sampledColumns, props.custodyColumns);
        blockInput.blockPromise.resolve(props.block);
        if (hasAllData) {
            blockInput.dataPromise.resolve([]);
            blockInput.computedDataPromise.resolve([]);
        }
        return blockInput;
    }
    static createFromColumn(props) {
        const hasAllData = props.daOutOfRange || props.columnSidecar.kzgCommitments.length === 0 || props.sampledColumns.length === 0;
        const state = {
            hasBlock: false,
            hasAllData,
            hasComputedAllData: hasAllData,
            versionedHashes: props.columnSidecar.kzgCommitments.map(kzgCommitmentToVersionedHash),
        };
        const init = {
            daOutOfRange: false,
            timeCreated: props.seenTimestampSec,
            forkName: props.forkName,
            blockRootHex: props.blockRootHex,
            parentRootHex: toRootHex(props.columnSidecar.signedBlockHeader.message.parentRoot),
            slot: props.columnSidecar.signedBlockHeader.message.slot,
        };
        const blockInput = new BlockInputColumns(init, state, props.sampledColumns, props.custodyColumns);
        if (hasAllData) {
            blockInput.dataPromise.resolve([]);
            blockInput.computedDataPromise.resolve([]);
        }
        return blockInput;
    }
    getLogMeta() {
        return {
            slot: this.slot,
            blockRoot: prettyBytes(this.blockRootHex),
            timeCreatedSec: this.timeCreatedSec,
            expectedColumns: this.state.hasBlock && this.state.block.message.body.blobKzgCommitments.length === 0
                ? 0
                : this.sampledColumns.length,
            receivedColumns: this.getSampledColumns().length,
        };
    }
    addBlock(props, opts = { throwOnDuplicateAdd: true }) {
        if (props.blockRootHex !== this.blockRootHex) {
            throw new BlockInputError({
                code: BlockInputErrorCode.MISMATCHED_ROOT_HEX,
                blockInputRoot: this.blockRootHex,
                mismatchedRoot: props.blockRootHex,
                source: props.source,
                peerId: `${props.peerIdStr}`,
            }, "addBlock blockRootHex does not match BlockInput.blockRootHex");
        }
        if (!opts.throwOnDuplicateAdd) {
            return;
        }
        if (this.state.hasBlock) {
            throw new BlockInputError({
                code: BlockInputErrorCode.INVALID_CONSTRUCTION,
                blockRoot: this.blockRootHex,
            }, "Cannot addBlock to BlockInputColumns after it already has a block");
        }
        const hasAllData = props.block.message.body.blobKzgCommitments.length === 0 ||
            this.state.hasAllData;
        const hasComputedAllData = props.block.message.body.blobKzgCommitments.length === 0 || this.state.hasComputedAllData;
        this.state = {
            ...this.state,
            hasBlock: true,
            hasAllData,
            hasComputedAllData,
            block: props.block,
            source: {
                source: props.source,
                seenTimestampSec: props.seenTimestampSec,
                peerIdStr: props.peerIdStr,
            },
            timeCompleteSec: hasAllData ? props.seenTimestampSec : undefined,
        };
        this.blockPromise.resolve(props.block);
    }
    addColumn({ blockRootHex, columnSidecar, source, seenTimestampSec, peerIdStr }, opts = { throwOnDuplicateAdd: true }) {
        if (blockRootHex !== this.blockRootHex) {
            throw new BlockInputError({
                code: BlockInputErrorCode.MISMATCHED_ROOT_HEX,
                blockInputRoot: this.blockRootHex,
                mismatchedRoot: blockRootHex,
                source: source,
                peerId: `${peerIdStr}`,
            }, "Column BeaconBlockHeader blockRootHex does not match BlockInput.blockRootHex");
        }
        const isDuplicate = this.columnsCache.has(columnSidecar.index);
        if (isDuplicate && opts.throwOnDuplicateAdd) {
            throw new BlockInputError({
                code: BlockInputErrorCode.INVALID_CONSTRUCTION,
                blockRoot: this.blockRootHex,
            }, "Cannot addColumn to BlockInputColumns with duplicate column index");
        }
        if (isDuplicate) {
            return;
        }
        this.columnsCache.set(columnSidecar.index, { columnSidecar, source, seenTimestampSec, peerIdStr });
        const sampledColumns = this.getSampledColumns();
        const hasAllData = 
        // already hasAllData
        this.state.hasAllData ||
            // has all sampled columns
            sampledColumns.length === this.sampledColumns.length ||
            // has enough columns to reconstruct the rest
            this.columnsCache.size >= NUMBER_OF_COLUMNS / 2;
        const hasComputedAllData = 
        // has all sampled columns
        sampledColumns.length === this.sampledColumns.length;
        this.state = {
            ...this.state,
            hasAllData: hasAllData || this.state.hasAllData,
            hasComputedAllData: hasComputedAllData || this.state.hasComputedAllData,
            timeCompleteSec: hasAllData ? seenTimestampSec : undefined,
        };
        if (hasAllData && sampledColumns !== null) {
            this.dataPromise.resolve(sampledColumns);
        }
        if (hasComputedAllData && sampledColumns !== null) {
            this.computedDataPromise.resolve(sampledColumns);
        }
    }
    hasColumn(columnIndex) {
        return this.columnsCache.has(columnIndex);
    }
    getColumn(columnIndex) {
        return this.columnsCache.get(columnIndex)?.columnSidecar;
    }
    getVersionedHashes() {
        return this.state.versionedHashes;
    }
    getCustodyColumns() {
        const columns = [];
        for (const index of this.custodyColumns) {
            const column = this.columnsCache.get(index);
            if (column) {
                columns.push(column.columnSidecar);
            }
        }
        return columns;
    }
    getSampledColumnsWithSource() {
        const columns = [];
        for (const index of this.sampledColumns) {
            const column = this.columnsCache.get(index);
            if (column) {
                columns.push(column);
            }
        }
        return columns;
    }
    getSampledColumns() {
        const columns = [];
        for (const index of this.sampledColumns) {
            const column = this.columnsCache.get(index);
            if (column) {
                columns.push(column.columnSidecar);
            }
        }
        return columns;
    }
    getAllColumnsWithSource() {
        return [...this.columnsCache.values()];
    }
    getAllColumns() {
        return this.getAllColumnsWithSource().map(({ columnSidecar }) => columnSidecar);
    }
    getMissingSampledColumnMeta() {
        if (this.state.hasAllData) {
            return {
                missing: [],
                versionedHashes: this.state.versionedHashes,
            };
        }
        const missing = [];
        for (const index of this.sampledColumns) {
            if (!this.columnsCache.has(index)) {
                missing.push(index);
            }
        }
        return {
            missing,
            versionedHashes: this.state.versionedHashes,
        };
    }
    hasComputedAllData() {
        return this.state.hasComputedAllData;
    }
    waitForComputedAllData(timeout, signal) {
        if (!this.state.hasComputedAllData) {
            return withTimeout(() => this.computedDataPromise.promise, timeout, signal);
        }
        return Promise.resolve(this.getSampledColumns());
    }
    getSerializedCacheKeys() {
        const objects = [];
        if (this.state.hasBlock) {
            objects.push(this.state.block);
        }
        objects.push(...this.getAllColumns());
        return objects;
    }
}
export class BlockInputNoData extends AbstractBlockInput {
    type = DAType.NoData;
    state;
    constructor(init, state) {
        super(init);
        this.state = state;
        this.dataPromise.resolve(null);
        this.blockPromise.resolve(state.block);
    }
    static createFromBlock(props) {
        const init = {
            daOutOfRange: props.daOutOfRange,
            timeCreated: props.seenTimestampSec,
            forkName: props.forkName,
            slot: props.block.message.slot,
            blockRootHex: props.blockRootHex,
            parentRootHex: toRootHex(props.block.message.parentRoot),
        };
        const state = {
            hasBlock: true,
            hasAllData: true,
            block: props.block,
            source: {
                source: props.source,
                seenTimestampSec: props.seenTimestampSec,
                peerIdStr: props.peerIdStr,
            },
            timeCompleteSec: props.seenTimestampSec,
        };
        return new BlockInputNoData(init, state);
    }
    addBlock(_, opts = { throwOnDuplicateAdd: true }) {
        if (opts.throwOnDuplicateAdd) {
            throw new BlockInputError({
                code: BlockInputErrorCode.INVALID_CONSTRUCTION,
                blockRoot: this.blockRootHex,
            }, "Cannot addBlock to BlockInputNoData - block already exists");
        }
    }
    getBlobKzgCommitments() {
        return this.state.block.message.body.signedExecutionPayloadBid.message
            .blobKzgCommitments;
    }
    getSerializedCacheKeys() {
        return [this.state.block];
    }
}
//# sourceMappingURL=blockInput.js.map