import assert from 'node:assert';

import type ADBReader from './adb.ts';
import type { MissingKeyBlock } from './blte.ts';

const WDC5_MAGIC = 0x57444335;

interface SectionHeader {
    tactKeyHash: bigint,
    fileOffset: number,
    recordCount: number,
    stringTableSize: number,
    offsetRecordsEnd: number,
    idListSize: number,
    relationshipDataSize: number,
    offsetMapIDCount: number,
    copyTableCount: number,
}

interface FieldStructure {
    size: number,
    position: number,
}

interface FieldStorageInfoCompressionNone {
    fieldOffsetBits: number,
    fieldSizeBits: number,
    additionalDataSize: number,
    storageType: 'none',
}

interface FieldStorageInfoCompressionBitpacked {
    fieldOffsetBits: number,
    fieldSizeBits: number,
    additionalDataSize: number,
    storageType: 'bitpacked',
    bitpackingOffsetBits: number,
    bitpackingSizeBits: number,
    flags: number,
}

interface FieldStorageInfoCompressionCommonData {
    fieldOffsetBits: number,
    fieldSizeBits: number,
    additionalDataSize: number,
    storageType: 'commonData',
    defaultValue: number,
}

interface FieldStorageInfoCompressionBitpackedIndexed {
    fieldOffsetBits: number,
    fieldSizeBits: number,
    additionalDataSize: number,
    storageType: 'bitpackedIndexed',
    bitpackingOffsetBits: number,
    bitpackingSizeBits: number,
}

interface FieldStorageInfoCompressionBitpackedIndexedArray {
    fieldOffsetBits: number,
    fieldSizeBits: number,
    additionalDataSize: number,
    storageType: 'bitpackedIndexedArray',
    bitpackingOffsetBits: number,
    bitpackingSizeBits: number,
    arrayCount: number,
}

interface FieldStorageInfoCompressionBitpackedSigned {
    fieldOffsetBits: number,
    fieldSizeBits: number,
    additionalDataSize: number,
    storageType: 'bitpackedSigned',
    bitpackingOffsetBits: number,
    bitpackingSizeBits: number,
    flags: number,
}

type FieldStorageInfo = FieldStorageInfoCompressionNone
    | FieldStorageInfoCompressionBitpacked
    | FieldStorageInfoCompressionCommonData
    | FieldStorageInfoCompressionBitpackedIndexed
    | FieldStorageInfoCompressionBitpackedIndexedArray
    | FieldStorageInfoCompressionBitpackedSigned;

interface OffsetMapEntry {
    offset: number,
    size: number,
    data: Buffer,
}

interface ParsedFieldNone {
    type: 'none',
    data: number | bigint,
    string?: string,
}

interface ParsedFieldCommonData {
    type: 'commonData',
    data: number,
}

interface ParsedFieldBitpacked {
    type: 'bitpacked',
    data: number,
}

interface ParsedFieldBitpackedArray {
    type: 'bitpackedArray',
    data: number[],
}

type ParsedField = ParsedFieldNone
    | ParsedFieldCommonData
    | ParsedFieldBitpacked
    | ParsedFieldBitpackedArray;

interface SparseRow {
    type: 'sparse',
    data: Buffer,
}

interface Section {
    header: SectionHeader,
    isZeroed: boolean,
    recordDataSize: number,
    records: Buffer[],
    idList: number[],
    offsetMap: OffsetMapEntry[],
    relationshipMap: Map<number, number>,
}

interface HotfixModify {
    type: 'modify',
    data: Buffer,
}

interface HotfixDelete {
    type: 'delete',
}

type Hotfix = HotfixModify | HotfixDelete;

/* eslint-disable no-bitwise */
const readBitpackedValue = (
    buffer: Buffer,
    fieldOffsetBits: number,
    fieldSizeBits: number,
    signed = false,
) => {
    const offsetBytes = fieldOffsetBits >>> 3;
    const bitOffset = fieldOffsetBits & 0x7;
    const sizeBytes = Math.ceil((fieldSizeBits + bitOffset) / 8);

    if (sizeBytes <= 6) {
        // safe to be number
        const rawValue = buffer.readUIntLE(offsetBytes, sizeBytes);
        return Number(
            signed
                ? BigInt.asIntN(fieldSizeBits, BigInt(rawValue >>> bitOffset))
                : BigInt.asUintN(fieldSizeBits, BigInt(rawValue >>> bitOffset)),
        );
    }

    // need to be bigint
    let value = 0n;

    for (let i = sizeBytes - 1; i >= 0; i -= 1) {
        const byte = buffer.readUInt8(offsetBytes + i);
        value = (value << 8n) | BigInt(byte);
    }

    return signed
        ? BigInt.asIntN(fieldSizeBits, value >> BigInt(bitOffset))
        : BigInt.asUintN(fieldSizeBits, value >> BigInt(bitOffset));
};
/* eslint-enable no-bitwise */

export default class WDCReader {
    public readonly tableHash: number;

    public readonly layoutHash: number;

    public readonly locale: number;

    public readonly isNormal: boolean;

    public readonly hasRelationshipData: boolean;

    public readonly fields: FieldStructure[];

    public readonly fieldsInfo: FieldStorageInfo[];

    public readonly rows = new Map<number, ParsedField[] | SparseRow>();

    public readonly relationships = new Map<number, number>();

    public readonly copyTable = new Map<number, number>();

    public readonly hotfixes = new Map<number, Hotfix>();

    constructor(buffer: Buffer, blocks: MissingKeyBlock[] = [], adb?: ADBReader) {
        const magic = buffer.readUInt32BE(0);
        const version = buffer.readUInt32LE(4);
        // const schema = buffer.toString('ascii', 8, 136);
        // const recordCount = buffer.readUInt32LE(136);
        const fieldCount = buffer.readUInt32LE(140);
        const recordSize = buffer.readUInt32LE(144);
        // const stringTableSize = buffer.readUInt32LE(148);
        const tableHash = buffer.readUInt32LE(152);
        const layoutHash = buffer.readUInt32LE(156);
        // const minID = buffer.readUInt32LE(160);
        // const maxID = buffer.readUInt32LE(164);
        const locale = buffer.readUInt32LE(168);
        const flags = buffer.readUInt16LE(172);
        const idIndex = buffer.readUInt16LE(174);
        // const totalFieldCount = buffer.readUInt32LE(176);
        // const bitpackedDataOffset = buffer.readUInt32LE(180);
        // const lookupColumnCount = buffer.readUInt32LE(184);
        const fieldStorageInfoSize = buffer.readUInt32LE(188);
        const commonDataSize = buffer.readUInt32LE(192);
        const palletDataSize = buffer.readUInt32LE(196);
        const sectionCount = buffer.readUInt32LE(200);

        assert(magic === WDC5_MAGIC, `Invalid WDC5 magic: ${magic.toString(16).padStart(8, '0')}`);
        assert(version === 5, `Invalid WDC5 version: ${version.toString()}`);

        this.tableHash = tableHash;
        this.layoutHash = layoutHash;
        this.locale = locale;

        // eslint-disable-next-line no-bitwise
        const isNormal = !(flags & 0x1);
        // eslint-disable-next-line no-bitwise
        const hasRelationshipData = !!(flags & 0x2);

        this.isNormal = isNormal;
        this.hasRelationshipData = hasRelationshipData;

        const sectionHeaders: SectionHeader[] = [];
        const sectionHeadersOffset = 204;
        for (let i = 0; i < sectionCount; i += 1) {
            const sectionHeaderOffset = sectionHeadersOffset + i * 40;

            sectionHeaders.push({
                tactKeyHash: buffer.readBigUInt64LE(sectionHeaderOffset),
                fileOffset: buffer.readUInt32LE(sectionHeaderOffset + 8),
                recordCount: buffer.readUInt32LE(sectionHeaderOffset + 12),
                stringTableSize: buffer.readUInt32LE(sectionHeaderOffset + 16),
                offsetRecordsEnd: buffer.readUInt32LE(sectionHeaderOffset + 20),
                idListSize: buffer.readUInt32LE(sectionHeaderOffset + 24),
                relationshipDataSize: buffer.readUInt32LE(sectionHeaderOffset + 28),
                offsetMapIDCount: buffer.readUInt32LE(sectionHeaderOffset + 32),
                copyTableCount: buffer.readUInt32LE(sectionHeaderOffset + 36),
            });
        }

        const fields: FieldStructure[] = [];
        const fieldsOffset = 204 + sectionCount * 40;
        for (let i = 0; i < fieldCount; i += 1) {
            const fieldOffset = fieldsOffset + i * 4;
            fields.push({
                size: buffer.readInt16LE(fieldOffset),
                position: buffer.readUInt16LE(fieldOffset + 2),
            });
        }
        this.fields = fields;

        const fieldsInfo: FieldStorageInfo[] = [];
        const fieldsInfoOffset = fieldsOffset + fieldCount * 4;
        for (let i = 0; i < fieldStorageInfoSize / 24; i += 1) {
            const fieldInfoOffset = fieldsInfoOffset + i * 24;

            const fieldOffsetBits = buffer.readUInt16LE(fieldInfoOffset);
            const fieldSizeBits = buffer.readUInt16LE(fieldInfoOffset + 2);
            const additionalDataSize = buffer.readUInt32LE(fieldInfoOffset + 4);
            const storageType = buffer.readUInt32LE(fieldInfoOffset + 8);
            const arg1 = buffer.readUInt32LE(fieldInfoOffset + 12);
            const arg2 = buffer.readUInt32LE(fieldInfoOffset + 16);
            const arg3 = buffer.readUInt32LE(fieldInfoOffset + 20);

            switch (storageType) {
                case 0:
                    fieldsInfo.push({
                        fieldOffsetBits,
                        fieldSizeBits,
                        additionalDataSize,
                        storageType: 'none',
                    });
                    break;
                case 1:
                    fieldsInfo.push({
                        fieldOffsetBits,
                        fieldSizeBits,
                        additionalDataSize,
                        storageType: 'bitpacked',
                        bitpackingOffsetBits: arg1,
                        bitpackingSizeBits: arg2,
                        flags: arg3,
                    });
                    break;
                case 2:
                    fieldsInfo.push({
                        fieldOffsetBits,
                        fieldSizeBits,
                        additionalDataSize,
                        storageType: 'commonData',
                        defaultValue: arg1,
                    });
                    break;
                case 3:
                    fieldsInfo.push({
                        fieldOffsetBits,
                        fieldSizeBits,
                        additionalDataSize,
                        storageType: 'bitpackedIndexed',
                        bitpackingOffsetBits: arg1,
                        bitpackingSizeBits: arg2,
                    });
                    break;
                case 4:
                    fieldsInfo.push({
                        fieldOffsetBits,
                        fieldSizeBits,
                        additionalDataSize,
                        storageType: 'bitpackedIndexedArray',
                        bitpackingOffsetBits: arg1,
                        bitpackingSizeBits: arg2,
                        arrayCount: arg3,
                    });
                    break;
                case 5:
                    fieldsInfo.push({
                        fieldOffsetBits,
                        fieldSizeBits,
                        additionalDataSize,
                        storageType: 'bitpackedSigned',
                        bitpackingOffsetBits: arg1,
                        bitpackingSizeBits: arg2,
                        flags: arg3,
                    });
                    break;
                default:
                    throw new Error(`Unknown storage type: ${storageType.toString(16).padStart(8, '0')}`);
            }
        }
        this.fieldsInfo = fieldsInfo;

        const palletData = new Map<number, number[]>();
        const palletDataOffset = fieldsInfoOffset + fieldStorageInfoSize;
        let palletDataPointer = palletDataOffset;
        for (let i = 0; i < fieldsInfo.length; i += 1) {
            const fieldInfo = fieldsInfo[i];
            if (fieldInfo.storageType === 'bitpackedIndexed' || fieldInfo.storageType === 'bitpackedIndexedArray') {
                const data: number[] = [];
                for (let j = 0; j < fieldInfo.additionalDataSize / 4; j += 1) {
                    data.push(buffer.readUInt32LE(palletDataPointer));
                    palletDataPointer += 4;
                }
                palletData.set(i, data);
            }
        }

        assert(
            palletDataPointer === palletDataOffset + palletDataSize,
            `Invalid pallet data size: ${(palletDataPointer - palletDataOffset).toString()} != ${palletDataSize.toString()}`,
        );

        const commonData = new Map<number, Map<number, number>>();
        const commonDataOffset = palletDataPointer;
        let commonDataPointer = commonDataOffset;
        for (let i = 0; i < fieldsInfo.length; i += 1) {
            const fieldInfo = fieldsInfo[i];
            if (fieldInfo.storageType === 'commonData') {
                const map = new Map<number, number>();
                for (let j = 0; j < fieldInfo.additionalDataSize / 8; j += 1) {
                    map.set(
                        buffer.readUInt32LE(commonDataPointer),
                        buffer.readUInt32LE(commonDataPointer + 4),
                    );
                    commonDataPointer += 8;
                }
                commonData.set(i, map);
            }
        }

        assert(
            commonDataPointer === commonDataOffset + commonDataSize,
            `Invalid common data size: ${(commonDataPointer - commonDataOffset).toString()} != ${commonDataSize.toString()}`,
        );

        const encryptedIDs = new Map<number, number[]>();
        const encryptedRecordsOffset = commonDataPointer;
        let encryptedRecordsPointer = encryptedRecordsOffset;
        for (let i = 0; i < sectionHeaders.length; i += 1) {
            const sectionHeader = sectionHeaders[i];
            if (sectionHeader.tactKeyHash !== 0n) {
                const count = buffer.readUInt32LE(encryptedRecordsPointer);
                encryptedRecordsPointer += 4;

                const data: number[] = [];
                for (let j = 0; j < count; j += 1) {
                    data.push(buffer.readUInt32LE(encryptedRecordsPointer));
                    encryptedRecordsPointer += 4;
                }
                encryptedIDs.set(i, data);
            }
        }

        const stringTable = new Map<number, string>();
        let stringTableDelta = 0;

        const sectionsOffset = encryptedRecordsPointer;
        let sectionPointer = sectionsOffset;
        const sections = sectionHeaders.map((sectionHeader): Section => {
            assert(
                sectionPointer === sectionHeader.fileOffset,
                `Invalid section offset: ${sectionPointer.toString()} != ${sectionHeader.fileOffset.toString()}`,
            );

            const sectionRecordSize = isNormal
                ? (sectionHeader.recordCount * recordSize + sectionHeader.stringTableSize)
                : (sectionHeader.offsetRecordsEnd - sectionPointer);

            const sectionSize = sectionRecordSize
                + sectionHeader.idListSize
                + sectionHeader.copyTableCount * 8
                + sectionHeader.offsetMapIDCount * 10
                + sectionHeader.relationshipDataSize;

            const recordDataSize = isNormal
                ? recordSize * sectionHeader.recordCount
                : sectionHeader.offsetRecordsEnd - sectionHeader.fileOffset;

            const isZeroed = blocks.some((block) => {
                const sectionStart = sectionHeader.fileOffset;
                const sectionEnd = sectionStart + sectionSize;
                const blockStart = block.offset;
                const blockEnd = blockStart + block.size;

                return sectionStart >= blockStart && sectionEnd <= blockEnd;
            });

            if (isZeroed) {
                sectionPointer += sectionSize;
                if (isNormal) {
                    stringTableDelta += sectionHeader.stringTableSize;
                }
                return {
                    header: sectionHeader,
                    isZeroed,
                    recordDataSize,
                    records: [],
                    idList: [],
                    offsetMap: [],
                    relationshipMap: new Map(),
                };
            }

            const records: Buffer[] = [];
            if (isNormal) {
                for (let j = 0; j < sectionHeader.recordCount; j += 1) {
                    records.push(buffer.subarray(sectionPointer, sectionPointer + recordSize));
                    sectionPointer += recordSize;
                }

                const stringTableOffset = sectionPointer;
                let stringStartPointer = stringTableOffset;
                while (sectionPointer < stringTableOffset + sectionHeader.stringTableSize) {
                    if (buffer[sectionPointer] === 0x00) {
                        if (sectionPointer - stringStartPointer > 0) {
                            const string = buffer.toString('utf-8', stringStartPointer, sectionPointer);
                            stringTable.set(
                                stringStartPointer - stringTableOffset + stringTableDelta,
                                string,
                            );
                        }

                        stringStartPointer = sectionPointer + 1;
                    }
                    sectionPointer += 1;
                }
                stringTableDelta += sectionHeader.stringTableSize;
            } else {
                sectionPointer = sectionHeader.offsetRecordsEnd;
            }

            const idList: number[] = [];
            for (let j = 0; j < sectionHeader.idListSize / 4; j += 1) {
                idList.push(buffer.readUInt32LE(sectionPointer));
                sectionPointer += 4;
            }

            for (let j = 0; j < sectionHeader.copyTableCount; j += 1) {
                const dst = buffer.readUInt32LE(sectionPointer);
                const src = buffer.readUInt32LE(sectionPointer + 4);
                this.copyTable.set(dst, src);

                sectionPointer += 8;
            }

            const offsetMap: OffsetMapEntry[] = [];
            for (let j = 0; j < sectionHeader.offsetMapIDCount; j += 1) {
                const offset = buffer.readUInt32LE(sectionPointer);
                const size = buffer.readUInt16LE(sectionPointer + 4);
                const data = buffer.subarray(offset, offset + size);

                sectionPointer += 6;

                offsetMap.push({
                    offset,
                    size,
                    data,
                });
            }

            const offsetMapIDList: number[] = [];

            if (hasRelationshipData) {
                // Note, if flag 0x02 is set,
                // offset_map_id_list will appear before relationship_map instead
                for (let j = 0; j < sectionHeader.offsetMapIDCount; j += 1) {
                    offsetMapIDList.push(buffer.readUInt32LE(sectionPointer));
                    sectionPointer += 4;
                }
            }

            const relationshipMap = new Map<number, number>();
            if (sectionHeader.relationshipDataSize > 0) {
                const numEntries = buffer.readUInt32LE(sectionPointer);
                // const relationshipMinID = buffer.readUInt32LE(sectionPointer + 4);
                // const relationshipMaxID = buffer.readUInt32LE(sectionPointer + 8);

                sectionPointer += 12;

                for (let j = 0; j < numEntries; j += 1) {
                    const foreignID = buffer.readUInt32LE(sectionPointer);
                    const recordIndex = buffer.readUInt32LE(sectionPointer + 4);
                    sectionPointer += 8;

                    relationshipMap.set(recordIndex, foreignID);
                }
            }

            if (!hasRelationshipData) {
                // see if (hasRelationshipData)
                for (let j = 0; j < sectionHeader.offsetMapIDCount; j += 1) {
                    offsetMapIDList.push(buffer.readUInt32LE(sectionPointer));
                    sectionPointer += 4;
                }
            }

            return {
                header: sectionHeader,
                isZeroed,
                recordDataSize,
                records,
                idList,
                offsetMap,
                relationshipMap,
            };
        });

        const totalRecordDataSize = sections
            .reduce((acc, section) => acc + section.recordDataSize, 0);
        sections.forEach((section) => {
            const {
                header, isZeroed, records, idList, offsetMap, relationshipMap,
            } = section;

            const prevRecordDataSize = sections
                .filter((s) => s.header.fileOffset < header.fileOffset)
                .reduce((acc, s) => acc + s.recordDataSize, 0);

            if (isZeroed) {
                return;
            }

            for (let recordIndex = 0; recordIndex < header.recordCount; recordIndex += 1) {
                let recordID = idList.length > 0 ? idList[recordIndex] : undefined;
                const recordBuffer = isNormal
                    ? records[recordIndex]
                    : offsetMap[recordIndex].data;

                if (isNormal) {
                    const recordData = fieldsInfo.map((fieldInfo, fieldIndex): ParsedField => {
                        switch (fieldInfo.storageType) {
                            case 'none': {
                                const value = readBitpackedValue(
                                    recordBuffer,
                                    fieldInfo.fieldOffsetBits,
                                    fieldInfo.fieldSizeBits,
                                );

                                if (typeof value === 'bigint') {
                                    return {
                                        type: 'none',
                                        data: value,
                                    };
                                }

                                if (recordID === undefined && fieldIndex === idIndex) {
                                    recordID = value;
                                }

                                // eslint-disable-next-line no-bitwise
                                const fieldOffset = fieldInfo.fieldOffsetBits >>> 3;
                                const offset = prevRecordDataSize - totalRecordDataSize
                                    + (recordSize * recordIndex) + fieldOffset + value;

                                return {
                                    type: 'none',
                                    data: value,
                                    string: stringTable.get(offset),
                                };
                            }
                            case 'commonData': {
                                const value = recordID !== undefined
                                    ? commonData.get(fieldIndex)?.get(recordID)
                                    : undefined;

                                return {
                                    type: 'commonData',
                                    data: value ?? fieldInfo.defaultValue,
                                };
                            }
                            case 'bitpacked':
                            case 'bitpackedSigned':
                            case 'bitpackedIndexed':
                            case 'bitpackedIndexedArray': {
                                let value = readBitpackedValue(
                                    recordBuffer,
                                    fieldInfo.fieldOffsetBits,
                                    fieldInfo.fieldSizeBits,
                                    fieldInfo.storageType === 'bitpackedSigned',
                                );

                                assert(typeof value === 'number', 'Bitpacked value must be a number');

                                if (fieldInfo.storageType === 'bitpackedIndexedArray') {
                                    const fieldPalletData = palletData.get(fieldIndex);

                                    assert(fieldPalletData, `No pallet data for field ${fieldIndex.toString()}`);

                                    const data: number[] = [];
                                    const palletStart = value * fieldInfo.arrayCount;

                                    for (let j = 0; j < fieldInfo.arrayCount; j += 1) {
                                        data.push(fieldPalletData[palletStart + j]);
                                    }

                                    return {
                                        type: 'bitpackedArray',
                                        data,
                                    };
                                }

                                if (fieldInfo.storageType === 'bitpackedIndexed') {
                                    const fieldPalletData = palletData.get(fieldIndex);

                                    assert(fieldPalletData, `No pallet data for field ${fieldIndex.toString()}`);

                                    value = fieldPalletData[value];
                                }

                                if (recordID === undefined && fieldIndex === idIndex) {
                                    recordID = value;
                                }

                                return {
                                    type: 'bitpacked',
                                    data: value,
                                };
                            }
                            default:
                                fieldInfo satisfies never;
                                throw new Error('Unreachable');
                        }
                    });

                    assert(recordID !== undefined, 'No record ID found');

                    this.rows.set(recordID, recordData);

                    const foreignID = relationshipMap.get(recordIndex);
                    if (foreignID !== undefined) {
                        this.relationships.set(recordID, foreignID);
                    }
                } else {
                    const recordData = {
                        type: 'sparse',
                        data: recordBuffer,
                    } satisfies SparseRow;

                    // for now (10.2.5), every sparse table has idList
                    // so we can safely assume recordID is not undefined
                    assert(recordID !== undefined, 'No record ID found');

                    this.rows.set(recordID, recordData);

                    const foreignID = relationshipMap.get(recordIndex);
                    if (foreignID !== undefined) {
                        this.relationships.set(recordID, foreignID);
                    }
                }
            }
        });

        const entries = adb?.tableEntries.get(tableHash);
        entries
            ?.filter((entry) => entry.pushID !== -1)
            .sort((a, b) => a.pushID - b.pushID)
            .forEach((entry) => {
                switch (entry.recordState) {
                    case 1: // Valid
                        this.hotfixes.set(entry.recordID, { type: 'modify', data: entry.data });
                        break;
                    case 2: // Delete
                        this.hotfixes.set(entry.recordID, { type: 'delete' });
                        break;
                    case 3: // Invalid
                        this.hotfixes.delete(entry.recordID);
                        break;
                    case 4: // NotPublic
                        break;
                    default:
                        throw new Error(`Unknown record state: ${entry.recordState.toString()}`);
                }
            });
    }

    getAllIDs(): number[] {
        return [...this.rows.keys(), ...this.copyTable.keys()];
    }

    getRowData(id: number): ParsedField[] | SparseRow | undefined {
        const hotfix = this.hotfixes.get(id);
        if (hotfix) {
            switch (hotfix.type) {
                case 'modify':
                    return {
                        type: 'sparse',
                        data: hotfix.data,
                    };
                case 'delete':
                    return undefined;
                default:
                    hotfix satisfies never;
                    throw new Error('Unreachable');
            }
        }

        const dst = this.copyTable.get(id);
        if (dst !== undefined) {
            return this.rows.get(dst);
        }
        return this.rows.get(id);
    }

    getRowRelationship(id: number): number | undefined {
        const dst = this.copyTable.get(id);
        if (dst !== undefined) {
            return this.relationships.get(dst);
        }
        return this.relationships.get(id);
    }
}

export type {
    FieldStructure,
    FieldStorageInfo,
    FieldStorageInfoCompressionNone,
    FieldStorageInfoCompressionBitpacked,
    FieldStorageInfoCompressionCommonData,
    FieldStorageInfoCompressionBitpackedIndexed,
    FieldStorageInfoCompressionBitpackedIndexedArray,
    FieldStorageInfoCompressionBitpackedSigned,
    ParsedField,
    ParsedFieldNone,
    ParsedFieldCommonData,
    ParsedFieldBitpacked,
    ParsedFieldBitpackedArray,
    SparseRow,
    Hotfix,
    HotfixModify,
    HotfixDelete,
};
