"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Struct = void 0;
/**
 * Struct provides a builder-like interface to create Buffer-based memory
 * structures for read/write interfacing with data structures from adapters.
 */
class Struct {
    /**
     * Creates an empty struct. Further calls to `member()` and `method()` functions will form the structure.
     * Finally call to `build()` will type the resulting structure appropriately without internal functions.
     */
    static new() {
        return new Struct();
    }
    constructor() {
        this.members = [];
        this.childStructs = {};
        this.length = 0;
        this.paddingByte = 0x00;
    }
    /**
     * Returns raw contents of the structure as a sliced Buffer.
     * Mutations to the returned buffer will not be reflected within struct.
     */
    serialize(alignment = "unaligned", padLength = true, parentOffset = 0) {
        switch (alignment) {
            case "unaligned": {
                /* update child struct values and return as-is (unaligned) */
                /* istanbul ignore next */
                for (const key of Object.keys(this.childStructs)) {
                    const child = this.childStructs[key];
                    this.buffer.set(child.struct.serialize(alignment), child.offset);
                }
                return Buffer.from(this.buffer);
            }
            case "aligned": {
                /* create 16-bit aligned buffer */
                const aligned = Buffer.alloc(this.getLength(alignment, padLength, parentOffset), this.paddingByte);
                let offset = 0;
                for (const member of this.members) {
                    switch (member.type) {
                        case "uint8":
                            aligned.set(this.buffer.slice(member.offset, member.offset + 1), offset);
                            offset += 1;
                            break;
                        case "uint16":
                            offset += offset % 2;
                            aligned.set(this.buffer.slice(member.offset, member.offset + 2), offset);
                            offset += 2;
                            break;
                        case "uint32":
                            offset += offset % 2;
                            aligned.set(this.buffer.slice(member.offset, member.offset + 4), offset);
                            offset += 4;
                            break;
                        case "uint8array":
                        case "uint8array-reversed":
                            aligned.set(this.buffer.slice(member.offset, member.offset + member.length), offset);
                            offset += member.length;
                            break;
                        case "struct":
                            const structData = this.childStructs[member.key].struct.serialize(alignment, false, offset);
                            aligned.set(structData, offset);
                            offset += structData.length;
                            break;
                    }
                }
                return aligned;
            }
        }
    }
    /**
     * Returns total length of the struct. Struct length is always fixed and configured
     * by calls to `member()` methods.
     */
    /* istanbul ignore next */
    getLength(alignment = "unaligned", padLength = true, parentOffset = 0) {
        switch (alignment) {
            case "unaligned": {
                /* return actual length */
                return this.length;
            }
            case "aligned": {
                /* compute aligned length and return */
                let length = this.members.reduce((offset, member) => {
                    switch (member.type) {
                        case "uint8":
                            offset += 1;
                            break;
                        case "uint16":
                            offset += ((parentOffset + offset) % 2) + 2;
                            break;
                        case "uint32":
                            offset += ((parentOffset + offset) % 2) + 4;
                            break;
                        case "uint8array":
                        case "uint8array-reversed":
                            offset += member.length;
                            break;
                        case "struct":
                            offset += this.childStructs[member.key].struct.getLength(alignment, false);
                            break;
                    }
                    return offset;
                }, 0);
                if (padLength) {
                    length += length % 2;
                }
                return length;
            }
        }
    }
    /**
     * Returns structure contents in JS object format.
     */
    // eslint-disable-next-line max-len
    // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types,@typescript-eslint/explicit-function-return-type
    toJSON() {
        // eslint-disable-next-line @typescript-eslint/no-explicit-any
        return this.members.reduce((a, c) => { a[c.key] = this[c.key]; return a; }, {});
    }
    member(type, name, lengthOrStructFactory) {
        const offset = this.length;
        const structFactory = type === "struct" ? lengthOrStructFactory : undefined;
        const length = type === "struct" ?
            structFactory().length :
            lengthOrStructFactory;
        switch (type) {
            case "uint8": {
                Object.defineProperty(this, name, {
                    enumerable: true,
                    get: () => this.buffer.readUInt8(offset),
                    set: (value) => this.buffer.writeUInt8(value, offset)
                });
                this.length += 1;
                break;
            }
            case "uint16": {
                Object.defineProperty(this, name, {
                    enumerable: true,
                    get: () => this.buffer.readUInt16LE(offset),
                    set: (value) => this.buffer.writeUInt16LE(value, offset)
                });
                this.length += 2;
                break;
            }
            case "uint32": {
                Object.defineProperty(this, name, {
                    enumerable: true,
                    get: () => this.buffer.readUInt32LE(offset),
                    set: (value) => this.buffer.writeUInt32LE(value, offset)
                });
                this.length += 4;
                break;
            }
            case "uint8array":
            case "uint8array-reversed": {
                /* istanbul ignore next */
                if (!length) {
                    throw new Error("Struct builder requires length for `uint8array` and `uint8array-reversed` type");
                }
                Object.defineProperty(this, name, {
                    enumerable: true,
                    get: () => type === "uint8array-reversed" ?
                        Buffer.from(this.buffer.slice(offset, offset + length)).reverse() :
                        Buffer.from(this.buffer.slice(offset, offset + length)),
                    set: (value) => {
                        if (value.length !== length) {
                            throw new Error(`Invalid length for member ${name} (expected=${length}, got=${value.length})`);
                        }
                        if (type === "uint8array-reversed") {
                            value = Buffer.from(value).reverse();
                        }
                        for (let i = 0; i < length; i++) {
                            this.buffer[offset + i] = value[i];
                        }
                    }
                });
                this.length += length;
                break;
            }
            case "struct": {
                this.childStructs[name] = { offset, struct: structFactory() };
                Object.defineProperty(this, name, {
                    enumerable: true,
                    get: () => this.childStructs[name].struct
                });
                this.length += length;
            }
        }
        this.members.push({ key: name, offset, type, length });
        return this;
    }
    /**
     * Adds a custom method to the struct.
     *
     * *This method is stripped from type on struct `build()`.*
     *
     * @param name Name of the method to be appended.
     * @param returnType Return type (eg. `Buffer.prototype`).
     * @param body Function implementation. Takes struct as a first and single input parameter.
     */
    method(name, returnType, body) {
        Object.defineProperty(this, name, {
            enumerable: true,
            configurable: false,
            writable: false,
            value: () => body.bind(this)(this)
        });
        return this;
    }
    /**
     * Sets default data to initialize empty struct with.
     *
     * @param data Data to initialize empty struct with.
     */
    /* istanbul ignore next */
    default(data) {
        if (data.length !== this.length) {
            throw new Error("Default value needs to have the length of unaligned structure.");
        }
        this.defaultData = Buffer.from(data);
        return this;
    }
    /**
     * Sets byte to use for padding.
     *
     * @param padding Byte to use for padding
     */
    /* istanbul ignore next */
    padding(padding = 0x00) {
        this.paddingByte = padding;
        return this;
    }
    /**
     * Creates the struct and optionally fills it with data. If data is provided, the length
     * of the provided buffer needs to match the structure length.
     *
     * *This method is stripped from type on struct `build()`.*
     */
    build(data) {
        if (data) {
            if (data.length === this.getLength("unaligned")) {
                this.buffer = Buffer.from(data);
                for (const key of Object.keys(this.childStructs)) {
                    const child = this.childStructs[key];
                    child.struct.build(this.buffer.slice(child.offset, child.offset + child.struct.length));
                }
            }
            else if (data.length === this.getLength("aligned")) {
                this.buffer = Buffer.alloc(this.length, this.paddingByte);
                let offset = 0;
                for (const member of this.members) {
                    switch (member.type) {
                        case "uint8":
                            this.buffer.set(data.slice(offset, offset + 1), member.offset);
                            offset += 1;
                            break;
                        case "uint16":
                            offset += offset % 2;
                            this.buffer.set(data.slice(offset, offset + 2), member.offset);
                            offset += 2;
                            break;
                        case "uint32":
                            offset += offset % 2;
                            this.buffer.set(data.slice(offset, offset + 4), member.offset);
                            offset += 4;
                            break;
                        case "uint8array":
                        case "uint8array-reversed":
                            this.buffer.set(data.slice(offset, offset + member.length), member.offset);
                            offset += member.length;
                            break;
                        case "struct":
                            const child = this.childStructs[member.key];
                            child.struct.build(data.slice(offset, offset + child.struct.length));
                            this.buffer.set(child.struct.serialize(), member.offset);
                            offset += child.struct.length;
                            break;
                    }
                }
            }
            else {
                const expectedLengths = `${this.getLength("unaligned")}/${this.getLength("aligned")}`;
                throw new Error(`Struct length mismatch (expected=${expectedLengths}, got=${data.length})`);
            }
        }
        else {
            this.buffer = this.defaultData ? Buffer.from(this.defaultData) : Buffer.alloc(this.length);
        }
        return this;
    }
}
exports.Struct = Struct;
//# sourceMappingURL=struct.js.map