import { AbstractService, extendServiceContainer, TapError } from '@iotize/tap';
import { TapRequestFrame, ResultCode } from '@iotize/tap/client/api';
import { TapStreamReader, TapStreamWriter, enumKeyOrValueToNumber, CRC } from '@iotize/tap/client/impl';
import { converters } from '@iotize/tap/service/core';
import { CodeError } from '@iotize/common/error';
import { bufferToHexString } from '@iotize/common/byte-converter';
import { AesEcb128Converter } from '@iotize/common/crypto';
import { ScramAuth } from '@iotize/tap/auth';
import { hmacSHA256 } from '@iotize/tap/crypto';
import { StepOperations } from '@iotize/common/task-manager';
import { sleep } from '@iotize/common/utility';
import { createDebugger } from '@iotize/common/debug';
import { chunkArray } from '@iotize/common/array';
import { KaitaiStreamWriter } from '@iotize/common/byte-stream';

var SinglePacketStoreInfo;
(function (SinglePacketStoreInfo) {
    /* tslint:disable */
    /**
     * This file was automatically generated by json-schema-to-typescript.
     * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file,
     * and run json-schema-to-typescript to regenerate this file.
     */
    let State;
    (function (State) {
        State[State["UNAVAILABLE"] = 0] = "UNAVAILABLE";
        State[State["EMPTY"] = 1] = "EMPTY";
        State[State["USED"] = 2] = "USED";
        State[State["FULL"] = 3] = "FULL";
        State[State["ERROR"] = 4] = "ERROR";
    })(State = SinglePacketStoreInfo.State || (SinglePacketStoreInfo.State = {}));
})(SinglePacketStoreInfo || (SinglePacketStoreInfo = {}));
var SinglePacket;
(function (SinglePacket) {
    /* tslint:disable */
    /**
     * This file was automatically generated by json-schema-to-typescript.
     * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file,
     * and run json-schema-to-typescript to regenerate this file.
     */
    /* tslint:disable */
    /**
     * This file was automatically generated by json-schema-to-typescript.
     * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file,
     * and run json-schema-to-typescript to regenerate this file.
     */
    let PacketType;
    (function (PacketType) {
        PacketType[PacketType["DATA_LOG"] = 1] = "DATA_LOG";
        PacketType[PacketType["COMMAND"] = 2] = "COMMAND";
        PacketType[PacketType["CODE_EXEC"] = 3] = "CODE_EXEC";
        PacketType[PacketType["CONF_UPDATE"] = 4] = "CONF_UPDATE";
        PacketType[PacketType["SECURITY_TOKEN"] = 5] = "SECURITY_TOKEN";
        PacketType[PacketType["TARGET_FW_UPDATE"] = 6] = "TARGET_FW_UPDATE";
        PacketType[PacketType["MASK"] = 7] = "MASK";
    })(PacketType = SinglePacket.PacketType || (SinglePacket.PacketType = {}));
})(SinglePacket || (SinglePacket = {}));
var SinglePacketChunk;
(function (SinglePacketChunk) {
    /* tslint:disable */
    /**
     * This file was automatically generated by json-schema-to-typescript.
     * DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file,
     * and run json-schema-to-typescript to regenerate this file.
     */
    let Format;
    (function (Format) {
        Format[Format["OFFSET_ON_2_BYTES"] = 0] = "OFFSET_ON_2_BYTES";
        Format[Format["OFFSET_ON_4_BYTES"] = 8] = "OFFSET_ON_4_BYTES";
    })(Format = SinglePacketChunk.Format || (SinglePacketChunk.Format = {}));
})(SinglePacketChunk || (SinglePacketChunk = {}));

/**
 *   Generated file. Do not edit
 */
TapStreamReader.prototype.readSinglePacketStoreInfo =
    function () {
        const model = {};
        this.forwardBits(24);
        model.state = this.readUnsigned(1);
        return model;
    };
TapStreamReader.prototype.readSinglePacket = function () {
    const model = {};
    model.sendTime = this.readUnsigned(4);
    model.header = this.readSinglePacketHeader();
    model.payload = this.readSinglePacketPayload();
    return model;
};
TapStreamReader.prototype.readSinglePacketHeader =
    function () {
        const model = {};
        model.packetLength = this.readUnsigned(2);
        model.packetId = this.readUnsigned(2);
        model.configVersion = this.readUnsigned(4);
        model.messageType = this.readBits(4);
        this.forwardBits(1);
        model.isExtended = this.readBoolean(1);
        model.encryption = this.readBoolean(1);
        model.ack = this.readBoolean(1);
        model.senderId = this.readUnsigned(1);
        model.salt = this.readUnsigned(2);
        return model;
    };
TapStreamReader.prototype.readSinglePacketPayload =
    function () {
        const model = {};
        model.logTime = this.readUnsigned(4);
        model.dataSize = this.readUnsigned(2);
        model.data = this.readBytes((model.dataSize === undefined ? 0 : model.dataSize) == 0
            ? this.getStreamSize() - 4
            : model.dataSize === undefined
                ? 0
                : model.dataSize);
        model.padding = this.readBytes((4 - ((model.data.length + 10) % 4)) % 4);
        model.crc = this.readUnsigned(4);
        return model;
    };
TapStreamReader.prototype.readSinglePacketChunk =
    function () {
        const model = {};
        model.format = this.readBits(4);
        model.length = this.readBits(12);
        model.offset = this.readUnsigned(model.format == 0b1000 ? 4 : 2);
        model.data = this.readBytes((model.length === undefined ? 0 : model.length) * 16);
        return model;
    };
// TapStreamWriter.prototype.write(model: SinglePacketStoreInfo) : TapStreamWriter{
//     return this.writeSinglePacketStoreInfo(model)
// }
TapStreamWriter.prototype.writeSinglePacketStoreInfo = function (model) {
    this.forwardBits(24);
    this.writeBitsInt(enumKeyOrValueToNumber(model.state, SinglePacketStoreInfo.State), 8);
    return this;
};
// TapStreamWriter.prototype.write(model: SinglePacket) : TapStreamWriter{
//     return this.writeSinglePacket(model)
// }
TapStreamWriter.prototype.writeSinglePacket = function (model) {
    this.writeUnsigned(model.sendTime, 4);
    this.writeSinglePacketHeader(model.header);
    this.writeSinglePacketPayload(model.payload);
    return this;
};
TapStreamWriter.prototype.writeSinglePacketHeader = function (model) {
    this.writeUnsigned(model.packetLength, 2);
    this.writeUnsigned(model.packetId, 2);
    this.writeUnsigned(model.configVersion, 4);
    this.writeBitsInt(enumKeyOrValueToNumber(model.messageType, SinglePacket.PacketType), 4);
    this.forwardBits(1);
    this.writeBoolean(model.isExtended, 1);
    this.writeBoolean(model.encryption, 1);
    this.writeBoolean(model.ack, 1);
    this.writeUnsigned(model.senderId, 1);
    this.writeUnsigned(model.salt, 2);
    return this;
};
TapStreamWriter.prototype.writeSinglePacketPayload = function (model) {
    this.writeUnsigned(model.logTime, 4);
    this.writeUnsigned(model.dataSize !== undefined
        ? model.dataSize
        : model.data.length > 0xffff
            ? 0
            : model.data.length, 2);
    this.writeBytes(model.data, (model.dataSize === undefined ? 0 : model.dataSize) == 0
        ? model.data.length
        : model.dataSize === undefined
            ? 0
            : model.dataSize);
    this.writeFunction('padding', undefined, model.padding, (4 - ((model.data.length + 10) % 4)) % 4);
    this.writeFunction('crc32', {
        offset: 4,
    }, model.crc, 4);
    return this;
};
// TapStreamWriter.prototype.write(model: SinglePacketChunk) : TapStreamWriter{
//     return this.writeSinglePacketChunk(model)
// }
TapStreamWriter.prototype.writeSinglePacketChunk = function (model) {
    this.writeBitsInt(enumKeyOrValueToNumber(model.format, SinglePacketChunk.Format), 4);
    this.writeBits(model.length !== undefined ? model.length : model.data.length / 16, 12);
    this.writeUnsigned(model.offset, model.format == 0b1000 ? 4 : 2);
    this.writeBytes(model.data, (model.length === undefined ? 0 : model.length) == 0
        ? model.data.length
        : (model.length === undefined ? 0 : model.length) * 16);
    return this;
};

/**
 *   Generated file. Do not edit
 */
class SinglePacketStoreInfoConverter {
    encode(model, stream = new TapStreamWriter()) {
        stream.writeSinglePacketStoreInfo(model);
        return stream.toBytes;
    }
    decode(data) {
        const stream = data instanceof TapStreamReader ? data : TapStreamReader.create(data);
        return stream.readSinglePacketStoreInfo();
    }
}
class SinglePacketConverter {
    encode(model, stream = new TapStreamWriter()) {
        stream.writeSinglePacket(model);
        return stream.toBytes;
    }
    decode(data) {
        const stream = data instanceof TapStreamReader ? data : TapStreamReader.create(data);
        return stream.readSinglePacket();
    }
}
class SinglePacketChunkConverter {
    encode(model, stream = new TapStreamWriter()) {
        stream.writeSinglePacketChunk(model);
        return stream.toBytes;
    }
    decode(data) {
        const stream = data instanceof TapStreamReader ? data : TapStreamReader.create(data);
        return stream.readSinglePacketChunk();
    }
}

/**
 *   Generated file. Do not edit
 */
const singlePacketStoreInfo = new SinglePacketStoreInfoConverter();
const singlePacket = new SinglePacketConverter();
const singlePacketChunk = new SinglePacketChunkConverter();

var serviceConverters = /*#__PURE__*/Object.freeze({
    __proto__: null,
    singlePacketStoreInfo: singlePacketStoreInfo,
    singlePacket: singlePacket,
    singlePacketChunk: singlePacketChunk
});

/**
 *   Generated file. Do not edit
 */
const allConverters = Object.assign(Object.assign({}, converters), serviceConverters);
const SERVICE_CALLS = {
    getInfo: {
        method: TapRequestFrame.MethodType.GET,
        pathAlias: '/single-packet/info',
        path: '/1024//81',
        responseBodyDecoder: allConverters.singlePacketStoreInfo,
    },
    writeAndExecuteBytes: {
        method: TapRequestFrame.MethodType.POST,
        pathAlias: '/single-packet/write-and-execute-bytes',
        path: '/1024//80',
    },
    writeAndExecute: {
        method: TapRequestFrame.MethodType.POST,
        pathAlias: '/single-packet/write-and-execute',
        path: '/1024//80',
        bodyEncoder: allConverters.singlePacket,
    },
    writeChunk: {
        method: TapRequestFrame.MethodType.POST,
        pathAlias: '/single-packet/write-chunk',
        path: '/1024//82',
        bodyEncoder: allConverters.singlePacketChunk,
    },
    executeSinglePacketInStore: {
        method: TapRequestFrame.MethodType.POST,
        pathAlias: '/single-packet/execute-single-packet-in-store',
        path: '/1024//83',
        bodyEncoder: allConverters.uint32,
    },
};

class SinglePacketService extends AbstractService {
    constructor() {
        super(...arguments);
        this.resources = SERVICE_CALLS;
    }
    /**
     * Get information concerning Single Packet Store Status
     *
     * LWM2M path: /1024//81
     *
     * @tapVersion(">=1.13")
     * @return
     */
    getInfo() {
        return this.serviceCallRunner.execute(this.getInfoCall());
    }
    /**
     *
     *
     * LWM2M path: /1024//81
     *
     * @tapVersion(">=1.13")
     * @return call options
     */
    getInfoCall() {
        const callOptions = Object.assign({}, this.resources.getInfo);
        return callOptions;
    }
    /**
     * Send a complete Small Single Packet to the TAP as bytes. Size of the packet < 120 bytes
     *
     * LWM2M path: /1024//80
     *
     * @tapVersion(">=1.13")
     * @param data input
     * @return
     */
    writeAndExecuteBytes(
    /*
     *
     */
    data) {
        return this.serviceCallRunner.execute(this.writeAndExecuteBytesCall(data));
    }
    /**
     *
     *
     * LWM2M path: /1024//80
     *
     * @tapVersion(">=1.13")
     * @param data input
     * @return call options
     */
    writeAndExecuteBytesCall(
    /*
     *
     */
    data) {
        const callOptions = Object.assign({}, this.resources.writeAndExecuteBytes);
        callOptions.body = data;
        return callOptions;
    }
    /**
     * Send and execute the given Single Packet in one request
     *
     * LWM2M path: /1024//80
     *
     * @tapVersion(">=1.13")
     * @param data input
     * @return
     */
    writeAndExecute(
    /*
     *
     */
    data) {
        return this.serviceCallRunner.execute(this.writeAndExecuteCall(data));
    }
    /**
     *
     *
     * LWM2M path: /1024//80
     *
     * @tapVersion(">=1.13")
     * @param data input
     * @return call options
     */
    writeAndExecuteCall(
    /*
     *
     */
    data) {
        const callOptions = Object.assign({}, this.resources.writeAndExecute);
        callOptions.body = data;
        return callOptions;
    }
    /**
     * Write single packet chunk
     *
     * LWM2M path: /1024//82
     *
     * @tapVersion(">=1.13")
     * @param data input
     * @return
     */
    writeChunk(
    /*
     *
     */
    data) {
        return this.serviceCallRunner.execute(this.writeChunkCall(data));
    }
    /**
     *
     *
     * LWM2M path: /1024//82
     *
     * @tapVersion(">=1.13")
     * @param data input
     * @return call options
     */
    writeChunkCall(
    /*
     *
     */
    data) {
        const callOptions = Object.assign({}, this.resources.writeChunk);
        callOptions.body = data;
        return callOptions;
    }
    /**
     * Execute single packet stored in Tap storage
     *
     * LWM2M path: /1024//83
     *
     * @tapVersion(">=1.13")
     * @param crc input
     * @return
     */
    executeSinglePacketInStore(
    /*
     * CRC of the single packet stored in Tap storage.
     */
    crc) {
        return this.serviceCallRunner.execute(this.executeSinglePacketInStoreCall(crc));
    }
    /**
     *
     *
     * LWM2M path: /1024//83
     *
     * @tapVersion(">=1.13")
     * @param crc input
     * @return call options
     */
    executeSinglePacketInStoreCall(
    /*
     * CRC of the single packet stored in Tap storage.
     */
    crc) {
        const callOptions = Object.assign({}, this.resources.executeSinglePacketInStore);
        callOptions.body = crc;
        return callOptions;
    }
}

const _TAP_SERVICE_EXTENSION_SINGLEPACKET_ = extendServiceContainer('singlePacket', SinglePacketService);

class SinglePacketError extends CodeError {
    constructor(code, msg) {
        super(msg, code);
    }
    static authSessionRequiredToEncryptSinglePacket() {
        return new SinglePacketError(SinglePacketError.Code.AuthSessionRequiredToEncryptSinglePacket, `A valid authenticated Tap session is required to encrypt single packet`);
    }
    static credentialsRequiredForEncryptedSinglePacket() {
        return new SinglePacketError(SinglePacketError.Code.CredentialsRequiredForEncryptedSinglePacket, `Single packet encryption is enabled, you need to provid encryption credentials`);
    }
    static internalError(msg) {
        return new SinglePacketError(SinglePacketError.Code.SinglePacketInternalError, `Single packet related internal error: ${msg}`);
    }
    static maxSinglePacketPartSizeExceeded(packet, max) {
        return new SinglePacketError(SinglePacketError.Code.MaxSinglePacketPartSizeExceeded, `Single packet part size of ${packet.data.length} bytes is above single packet maximum size of ${max} bytes`);
    }
    static maxSinglePacketSizeExceeded(length, max) {
        return new SinglePacketError(SinglePacketError.Code.MaxSinglePacketSizeExceeded, `Single packet size of ${length} bytes is above single packet maximum size of ${max} bytes`);
    }
}
(function (SinglePacketError) {
    let Code;
    (function (Code) {
        Code["SinglePacketInternalError"] = "SinglePacketErrorInternalError";
        Code["AuthSessionRequiredToEncryptSinglePacket"] = "SinglePacketErrorAuthSessionRequiredToEncryptSinglePacket";
        Code["MaxSinglePacketPartSizeExceeded"] = "SinglePacketErrorMaxSinglePacketPartSizeExceeded";
        Code["MaxSinglePacketSizeExceeded"] = "SinglePacketErrorMaxSinglePacketSizeExceeded";
        Code["CredentialsRequiredForEncryptedSinglePacket"] = "SinglePacketErrorCredentialsRequiredForEncryptedSinglePacket";
    })(Code = SinglePacketError.Code || (SinglePacketError.Code = {}));
})(SinglePacketError || (SinglePacketError = {}));

class DefaultSinglePacketKeyGenerator {
    /**
     * HMsg = UserName | IoTizeSN | PacketHeader1
     */
    generate(options) {
        const stream = new TapStreamWriter();
        if (options.username.length > 16) {
            // TODO constant
            throw new Error(`Invalid username too long: ${options.username}`);
        }
        stream.writeStr(options.username);
        if (options.serialNumber.length !== 18) {
            // TODO constant
            // TODO constant
            throw new Error(`Invalid sn: ${options.serialNumber}. Length must be 18`);
        }
        stream.writeStr(options.serialNumber, 18);
        if (options.headerBytes.length !== 12) {
            throw new Error(`Invalid header length ${options.headerBytes.length}. Expected 12`);
        }
        stream.writeBytes(options.headerBytes);
        const data = stream.toBytes;
        const key = hmacSHA256(data, options.hashKey).slice(0, 16);
        return key;
    }
}

const TAG$1 = 'SinglePacket';
TapStreamReader.prototype.readSinglePacketPayloadFullData =
    function () {
        const model = {};
        model.logTime = this.readU4();
        model.dataSize = this.readU2();
        model.data = this.readBytes(-4);
        model.crc = this.readU4();
        return model;
    };
class SinglePacketEncryptionOptionsBuilder {
    static fromBlankKey(username) {
        return {
            hashKey: new Uint8Array(32),
            serialNumber: 'IoTize_FWUpdate_SP',
            username,
        };
    }
    static fromHashKey(hashKey, username) {
        return {
            hashKey,
            serialNumber: 'IoTize_FWUpdate_SP',
            username,
        };
    }
    static fromUserCredentials(credentials, options) {
        const keys = ScramAuth.computeBaseKeys(credentials.password, options);
        return SinglePacketEncryptionOptionsBuilder.fromCurrentSessionData({
            username: credentials.username,
            serverKey: keys.serverKey,
            storedKey: keys.storedKey,
        });
    }
    static fromCurrentSessionData(sessionData) {
        const hashKey = TapStreamWriter.create(32)
            .writeBytes(sessionData.storedKey)
            .writeBytes(sessionData.serverKey).toBytes;
        if (hashKey.length !== 32) {
            // TODO constant
            throw new Error(`Single packet encryption not available`);
        }
        return {
            hashKey,
            serialNumber: 'IoTize_FWUpdate_SP',
            username: sessionData.username,
        };
    }
}
class EncryptedSinglePacketConverter {
    constructor(options, encryptionAlgo = new AesEcb128Converter(), keyGenerator = new DefaultSinglePacketKeyGenerator()) {
        this.options = options;
        this.encryptionAlgo = encryptionAlgo;
        this.keyGenerator = keyGenerator;
    }
    static createWithBlankKey() {
        return new EncryptedSinglePacketConverter(SinglePacketEncryptionOptionsBuilder.fromBlankKey('IoTize'));
    }
    setEncryptionOptions(options) {
        this.options = Object.assign(Object.assign({}, this.options), options);
    }
    encode(packet) {
        const stream = new TapStreamWriter();
        stream.writeU4(packet.sendTime);
        stream.writeSinglePacketHeader(packet.header);
        const payloadBytesPosition = stream.pos;
        stream.writeSinglePacketPayload(packet.payload);
        if (packet.header.encryption) {
            const bytes = stream.toBytes;
            // debug(`SinglePacket before encryption: ${toHexString(bytes)}`);
            const encryptionAlgo = this._getEncryptionAlgo(packet.header);
            let encryptedPayloadBytes = bytes.slice(payloadBytesPosition);
            encryptedPayloadBytes = encryptionAlgo.encode(encryptedPayloadBytes);
            stream.pos = payloadBytesPosition;
            stream.writeBytes(encryptedPayloadBytes);
        }
        return stream.toBytes;
    }
    decode(packetData) {
        const stream = TapStreamReader.fromArray(packetData);
        const sendTime = stream.readU4();
        const header = stream.readSinglePacketHeader();
        let payload;
        if (header.encryption) {
            const encryptedData = stream.readBytesFull();
            const encryptionAlgo = this._getEncryptionAlgo(header);
            const decryptedData = encryptionAlgo.decode(encryptedData);
            const payloadStream = TapStreamReader.fromArray(decryptedData);
            // if (header.messageType == SinglePacketModel.PacketType.CODE_EXEC) {
            payload = payloadStream.readSinglePacketPayloadFullData();
            // } else {
            //     payload = payloadStream.readSinglePacketPayload();
            // }
        }
        else {
            payload = stream.readSinglePacketPayload();
        }
        const packet = {
            sendTime,
            header: header,
            payload,
        };
        // TODO check CRC
        // let computedCrc = 0;
        // if (computedCrc !== packet.payload.crc){
        //     CRC.fromBytes()
        // }
        return packet;
    }
    _getEncryptionAlgo(header) {
        const encryptionKey = this.computeKey(header);
        this.encryptionAlgo.setOptions({
            iv: '00000000000000000000000000000000',
            key: bufferToHexString(encryptionKey),
        });
        return this.encryptionAlgo;
    }
    computeKey(header) {
        if (!(header instanceof Uint8Array)) {
            const stream = new TapStreamWriter(12);
            header = stream.writeSinglePacketHeader(header).toBytes;
        }
        const generateOptions = Object.assign(Object.assign({}, this.options), {
            headerBytes: header,
        });
        const key = this.keyGenerator.generate(generateOptions);
        return key;
    }
}

const debug = createDebugger('@iotize/tap/single-packet');

function computePadding(size, modulo) {
    return (modulo - (size % modulo)) % modulo;
}
function senderIdToUserId(senderId) {
    if (senderId >= 0x80) {
        senderId += 0xff00;
    }
    return senderId;
}
function userIdToSenderId(userId) {
    if (userId > 0xffff) {
        throw new Error(`Invalid user id. Should be between 0 and ${0xffff}`); // TODO custom error
    }
    if (userId > 0x7f && userId < 0xff80) {
        throw new Error(`Invalid user id. Should be between 0 and ${0} and ${0x7f} or ${0xffff} and ${0xff80}`);
    } // TODO custom error
    return 0xff & userId;
}
function usernameToSenderId(username) {
    switch (username) {
        case 'IoTize':
            return 0xfd;
        case 'admin':
            return userIdToSenderId(0xffff); // TODO constant
        case 'anonymous':
            return userIdToSenderId(0x0);
        case 'supervisor':
            return userIdToSenderId(0xfffe);
        default:
            return 0;
    }
}
// function createCredentialsFromCurrentSessionData() {
//     const authSessionState = this.tap.auth.sessionStateSnapshot;
//     const authSessionData = this.tap.auth.sessionDataSnapshot;
//     console.info('Session data: ', authSessionState);
//     if (!authSessionData.serverKey || !authSessionData.storedKey || !authSessionState.name) {
//         throw SinglePacketError.authSessionRequiredToEncryptSinglePacket();
//     }
//     return SinglePacketEncryptionOptionsBuilder.fromCurrentSessionData({
//         username: authSessionState.name,
//         serverKey: authSessionData.serverKey,
//         storedKey: authSessionData.storedKey
//     });
// }

const TAG = 'SinglePacketBuilder';
const ZEROS_PADDING_GENERATOR = (length) => new Uint8Array(length);
class SinglePacketBuilder {
    constructor(type, paddingGenerator = ZEROS_PADDING_GENERATOR) {
        this.paddingGenerator = paddingGenerator;
        this.model = {
            sendTime: 0,
            header: {
                ack: false,
                configVersion: 0,
                encryption: false,
                isExtended: false,
                messageType: type,
                packetId: 0,
                packetLength: 0,
                salt: Math.round(Math.random() * 0xffff),
                senderId: 0,
            },
            payload: {
                data: new Uint8Array(),
                dataSize: 0,
                logTime: SinglePacketBuilder.getTimestampInSeconds(),
                padding: undefined, // TODO
            },
        };
    }
    // static fillComputedField(packet: SinglePacket): SinglePacket {
    //     if (!packet.sendTime) {
    //         packet.sendTime = SinglePacketBuilder.getTimestampInSeconds();
    //     }
    //     packet.payload.dataSize = packet.payload.data.length;
    //     packet.header.packetLength =
    //         computePadding(packet.payload.data.length + 10, 4) +
    //         packet.payload.data.length +
    //         SinglePacketBuilder.PACKET_HEADER_SIZE_WITH_SEND_TIME;
    //     let multiplicator = SinglePacketBuilder.getLengthMultiplicator(packet);
    //     if (packet.header.packetLength % multiplicator !==  0){
    //         throw new Error(`Packet length must be a multiple of ${multiplicator}. But length is ${packet.header.packetLength}`);
    //     }
    //     return packet;
    // }
    static getTimestampInSeconds() {
        return Math.round(new Date().getTime() / 1000);
    }
    static getLengthMultiplicator(packet) {
        if (packet.header.isExtended) {
            return 256;
        }
        else if (packet.header.encryption ||
            packet.header.messageType === SinglePacket.PacketType.CODE_EXEC) {
            return 16;
        }
        else {
            return 1;
        }
    }
    static create(data, type, paddingGenertor = ZEROS_PADDING_GENERATOR) {
        const builder = new SinglePacketBuilder(type, paddingGenertor);
        builder.setData(data);
        return builder;
    }
    setLogTime(logTime) {
        this.model.payload.logTime = logTime;
        return this;
    }
    /**
     *
     * @param id
     */
    setPacketId(id) {
        // TODO check length ?
        this.model.header.packetId = id;
        return this;
    }
    setSalt(salt) {
        this.model.header.salt = salt;
        return this;
    }
    setExtended(value) {
        this.model.header.isExtended = value;
        return this;
    }
    /**
     *
     * @param id
     */
    setEncryption(enabled) {
        // TODO check length ?
        this.model.header.encryption = enabled;
        return this;
    }
    /**
     *
     * @param id
     */
    setSenderId(id) {
        // TODO check length ?
        this.model.header.senderId = id;
        return this;
    }
    /**
     *
     * @param id
     */
    setSendTime(time) {
        this.model.sendTime = time;
        return this;
    }
    /**
     *
     * @param id
     */
    setConfigVersion(configVersion) {
        this.model.header.configVersion = configVersion;
        return this;
    }
    setData(data) {
        this.model.payload.data = data;
        return this;
    }
    getLengthMultiplicator() {
        return SinglePacketBuilder.getLengthMultiplicator(this.model);
    }
    build() {
        if (!this.model.sendTime) {
            this.model.sendTime = SinglePacketBuilder.getTimestampInSeconds();
        }
        let multiplicator = this.getLengthMultiplicator();
        if (this.model.payload.data.length > 0xffff * multiplicator) {
            this.model.header.isExtended = true;
        }
        multiplicator = this.getLengthMultiplicator(); // Refresh multiplicator if isExtended
        const paddingMultiple = Math.max(4, multiplicator); // TODO constant
        // debug(TAG, 'Multiplicator', multiplicator, 'padding mult', paddingMultiple);
        const requiredPadding = computePadding(this.model.payload.data.length +
            SinglePacketBuilder.PACKET_PAYLOAD_HEADER_SIZE, paddingMultiple);
        // debug(TAG, 'requiredPadding', requiredPadding);
        this.model.payload.padding = this.paddingGenerator(requiredPadding);
        // debug(TAG, 'this.model.payload.padding', this.model.payload.padding);
        const payloadLengthInBytes = this.model.payload.data.length +
            SinglePacketBuilder.PACKET_PAYLOAD_HEADER_SIZE +
            this.model.payload.padding.length;
        const packetLengthInBytes = SinglePacketBuilder.PACKET_HEADER_SIZE +
            this.model.payload.data.length +
            this.model.payload.padding.length;
        this.model.payload.dataSize =
            this.model.payload.data.length > 0xffff
                ? undefined
                : this.model.payload.data.length;
        if (payloadLengthInBytes % multiplicator !== 0) {
            throw SinglePacketError.internalError(`Packet length ${packetLengthInBytes} must be a multiple of ${multiplicator}`);
        }
        if (packetLengthInBytes > SinglePacketBuilder.MAX_FULL_SIZE) {
            throw SinglePacketError.maxSinglePacketSizeExceeded(packetLengthInBytes, SinglePacketBuilder.MAX_FULL_SIZE);
        }
        this.model.header.packetLength = payloadLengthInBytes / multiplicator;
        if (multiplicator === 1) {
            this.model.header.packetLength +=
                SinglePacketBuilder.PACKET_OUTER_HEADER_SIZE;
        }
        return this.model;
    }
}
SinglePacketBuilder.MAX_FULL_SIZE = 0x200000;
SinglePacketBuilder.MAX_PART_SIZE = 0xc0;
SinglePacketBuilder.PACKET_PAYLOAD_HEADER_SIZE = 4 + 2 + 4;
SinglePacketBuilder.PACKET_OUTER_HEADER_SIZE = 12;
SinglePacketBuilder.PACKET_HEADER_SIZE = SinglePacketBuilder.PACKET_OUTER_HEADER_SIZE +
    SinglePacketBuilder.PACKET_PAYLOAD_HEADER_SIZE;
SinglePacketBuilder.PACKET_HEADER_SIZE_WITH_SEND_TIME = SinglePacketBuilder.PACKET_HEADER_SIZE + 4;
SinglePacketBuilder.MAX_DATA_SIZE = SinglePacketBuilder.MAX_FULL_SIZE -
    (SinglePacketBuilder.PACKET_HEADER_SIZE_WITH_SEND_TIME + 0xff);

/**
 * Utility class to split a big single packet data into {@link SinglePacketChunk}
 */
class SinglePacketSplitter {
    /**
     * @throws {@link #chunkSize} < {@link #unitBlockSize}
     */
    constructor(options = {}) {
        if (!options.sizeMultiplicator) {
            options.sizeMultiplicator = SinglePacketSplitter.SIZE_MULTIPLICATOR;
        }
        if (!options.chunkSize) {
            options.chunkSize = SinglePacketBuilder.MAX_PART_SIZE;
        }
        if (!options.format) {
            options.format = SinglePacketChunk.Format.OFFSET_ON_2_BYTES;
        }
        if (!options.pad) {
            options.pad = false;
        }
        this.options = options;
        this.checkOptions();
    }
    get sizeMultiplicator() {
        return this.options.sizeMultiplicator;
    }
    checkOptions() {
        // Maybe we should force chunk size to be a multiple of unitBlockSize ?
        if (this.options.chunkSize < this.sizeMultiplicator) {
            throw new Error(`Chunk size must be greater than ${this.sizeMultiplicator}. Given: ${this.options.chunkSize}`);
        }
        if (!this.options.pad &&
            this.options.chunkSize % this.sizeMultiplicator !== 0) {
            throw new Error(`Chunk size "${this.options.chunkSize}" is not a multiple of length multiplicator "${this.sizeMultiplicator}"`);
        }
    }
    /**
     * @return an array of single packet parts
     */
    chunk(data) {
        if (!this.options.pad) {
            if (data.length % this.sizeMultiplicator !== 0) {
                throw new Error(`Padding is disabled and data length (${data.length} byte(s)) is not a multiple of length multiplicator ${this.sizeMultiplicator})`);
            }
        }
        else {
            const padding = computePadding(data.length, this.sizeMultiplicator);
            if (padding > 0) {
                data = KaitaiStreamWriter.mergeArrays(data, new Uint8Array(padding));
            }
        }
        const packets = chunkArray(Array.from(data), this.options.chunkSize).map((value) => Uint8Array.from(value));
        let offset = 0;
        return packets.map((chunk) => {
            const part = {
                data: chunk,
                length: Math.ceil(chunk.length / this.sizeMultiplicator),
                offset: Math.ceil(offset / this.sizeMultiplicator),
                format: this.options.format,
            };
            offset += chunk.length;
            return part;
        });
    }
}
SinglePacketSplitter.SIZE_MULTIPLICATOR = 16;

var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
class SinglePacketStoreLoader extends StepOperations {
    constructor(steps, 
    // public readonly crc: number,
    singlePacketService) {
        super(StepOperations.createSequentialContainers(steps));
        this.singlePacketService = singlePacketService;
    }
    static createOperationsFromBytes(data, singlePacketService, options) {
        const chunkSize = (options === null || options === void 0 ? void 0 : options.chunkSize) || SinglePacketBuilder.MAX_PART_SIZE;
        const offsetFitsOn2Bytes = data.length / chunkSize <= 0xffff;
        const singlePacketSplitter = new SinglePacketSplitter({
            chunkSize,
            format: offsetFitsOn2Bytes
                ? SinglePacketChunk.Format.OFFSET_ON_2_BYTES
                : SinglePacketChunk.Format.OFFSET_ON_4_BYTES,
            pad: options && 'pad' in options ? options.pad : true,
        });
        const packets = singlePacketSplitter.chunk(data);
        return SinglePacketStoreLoader.createOperationsFromChunk(packets, singlePacketService, options);
    }
    static createFromBytes(data, singlePacketService, options) {
        const steps = SinglePacketStoreLoader.createOperationsFromBytes(data, singlePacketService, options);
        return new SinglePacketStoreLoader(steps, 
        // expectedCRC,
        singlePacketService);
    }
    /**
     * Create single packet loading operations
     * @param packets Single packet chunks to load to the Tap
     * @param expectedCRC expected CRC sent to the Tap when executing the single packet. if you don't plan to execute single packet, you pass a dummy value
     * @param singlePacketService
     * @param options
     */
    static createOperationsFromChunk(packets, singlePacketService, options, output = {
        expectedCRC: 0,
    }) {
        output.expectedCRC = CRC.FIRST_VALUE;
        const operations = packets.map((packet, index) => {
            return (observer, context) => __awaiter(this, void 0, void 0, function* () {
                if (packet.data.length > SinglePacketBuilder.MAX_PART_SIZE) {
                    throw SinglePacketError.maxSinglePacketPartSizeExceeded(packet, SinglePacketBuilder.MAX_PART_SIZE);
                }
                debug('createSinglePacketLoadOperations', `sendSinglePacket`, `sending packet ${index + 1}/${packets.length} offset: ${packet.offset}...${packet.offset + (packet.length || 0)}`);
                let retryCount = 0;
                const maxRetry = (options === null || options === void 0 ? void 0 : options.maxRetry) || 3;
                output.expectedCRC = CRC.fromBytes(packet.data, output.expectedCRC);
                while (true) {
                    retryCount++;
                    // if (userIncrementalSinglePacketPart) {
                    // const call = { ...this.tap.service.singlePacket.resources.writeChunk };
                    // call.bodyEncoder = undefined;
                    // call.body = encodePartialSinglePacketBig(packet.data, packet.offset * this.singlePacketSplitter.options.sizeMultiplicator);
                    // response = await this.tap.lwm2m.execute(call);
                    const response = yield singlePacketService.writeChunk(packet);
                    // }
                    // else {
                    //     response = await singlePacketService.writeChunk(packet);
                    // }
                    if (retryCount < maxRetry &&
                        (response.status === ResultCode.SERVICE_UNAVAILABLE ||
                            response.status === ResultCode.NVM_ERROR)) {
                        debug('createSinglePacketLoadOperations', `Attempt ${retryCount + 1}/${maxRetry} failed with error ${TapError.reponseStatusError(response).toString()}`);
                        yield sleep((options === null || options === void 0 ? void 0 : options.retryDelay) || 500);
                        continue;
                    }
                    response.successful();
                    observer.next({
                        step: SinglePacketStoreLoader.Step.loadPacketChunk,
                        progress: {
                            total: packets.length,
                            loaded: index + 1,
                        },
                    });
                    return;
                }
                // debug(TAG, `sendSinglePacket`, `Packet ${index + 1}/${packets.length} SENT!`);
            });
        });
        if (options === null || options === void 0 ? void 0 : options.execute) {
            operations.push((observer, context) => __awaiter(this, void 0, void 0, function* () {
                observer.next({
                    step: SinglePacketStoreLoader.Step.executeSinglePacket,
                    expectedCRC: output.expectedCRC,
                });
                (yield singlePacketService.executeSinglePacketInStore(output.expectedCRC)).successful();
            }));
        }
        return operations;
    }
    static createFromChunks(packets, singlePacketService, options) {
        return new SinglePacketStoreLoader(SinglePacketStoreLoader.createOperationsFromChunk(packets, singlePacketService, options), singlePacketService);
    }
    static computeCRC(data) {
        if (data instanceof Uint8Array) {
            return CRC.mpeg2().compute(data);
        }
        else {
            return data.reduce((crc, packet) => {
                return CRC.mpeg2().compute(packet.data, crc);
            }, CRC.FIRST_VALUE);
        }
    }
}
(function (SinglePacketStoreLoader) {
    let Step;
    (function (Step) {
        Step["loadPacketChunk"] = "loadPacketChunk";
        Step["executeSinglePacket"] = "executeSinglePacket";
    })(Step = SinglePacketStoreLoader.Step || (SinglePacketStoreLoader.Step = {}));
})(SinglePacketStoreLoader || (SinglePacketStoreLoader = {}));

/**
 * Load single packet model to store
 * SinglePacket will be encoded to a single packet frame.
 * If credentials parameter is given, it will be encrypted with it and sender id will be filled
 * If no credential is given and SinglePacket.header.encryption is set to true, it will throw an error
 *
 * @param packet
 * @param credentials
 */
SinglePacketService.prototype.createStoreLoader = function (packet, options, credentials) {
    const encoder = EncryptedSinglePacketConverter.createWithBlankKey();
    if (credentials) {
        encoder.setEncryptionOptions(credentials);
        packet.header.encryption = true;
        packet.header.senderId = usernameToSenderId(credentials.username);
    }
    else if (packet.header.encryption) {
        throw SinglePacketError.credentialsRequiredForEncryptedSinglePacket();
    }
    const encodedPacket = encoder.encode(packet);
    return this.createStoreLoaderFromBytes(encodedPacket, options);
};
SinglePacketService.prototype.createStoreLoaderFromBytes = function (data, options) {
    return SinglePacketStoreLoader.createFromBytes(data, this, options);
};
// SinglePacketService.prototype.loadChunksToStore = function (
//     this: SinglePacketService,
//     packets: SinglePacketChunk[]
// ): Observable<any> {
//     return this.createChunksToStoreLoader.asObservable();
// };
// SinglePacketService.prototype.createChunksToStoreLoader = function (
//     this: SinglePacketService,
//     packets: SinglePacketChunk[]
// ): SinglePacketStoreLoader {
//     return SinglePacketStoreLoader.createFromChunks(
//         packets,
//         this
//     );
// };

/**
 * Generated bundle index. Do not edit.
 */

export { EncryptedSinglePacketConverter, SERVICE_CALLS as SINGLEPACKET_SERVICE_CALLS, SinglePacket, SinglePacketBuilder, SinglePacketChunk, SinglePacketChunkConverter, SinglePacketConverter, SinglePacketError, SinglePacketService, SinglePacketSplitter, SinglePacketStoreInfo, SinglePacketStoreInfoConverter, SinglePacketStoreLoader, _TAP_SERVICE_EXTENSION_SINGLEPACKET_, serviceConverters as singlePacketConverters, SinglePacketEncryptionOptionsBuilder as ɵa };
//# sourceMappingURL=iotize-tap-service-impl-single-packet.js.map
