import { TapRequestFrame, TapApduRequest, ApduResponse, ResultCode } from '@iotize/tap/client/api';
import { typedArrayToBuffer, bufferToHexString } from '@iotize/common/byte-converter';
import { KaitaiStreamReader, KaitaiStreamWriter } from '@iotize/common/byte-stream';
import { createDebugger } from '@iotize/common/debug';
import { CodeError } from '@iotize/common/error';
import { Subject, throwError } from 'rxjs';
import { map } from 'rxjs/operators';
import { Buffer } from 'buffer';
import { TLV } from '@iotize/common/tlv';

const prefix = '@iotize/tap/client/impl';
const debug = createDebugger(prefix);

class ConverterError extends CodeError {
    constructor(code, msg, cause) {
        super(msg, code);
        this.cause = cause;
    }
    static unexpectedBufferSize(expected, buffer) {
        return new ConverterError(ConverterError.Code.UnexpectedBufferSizeError, 'Expected buffer of ' +
            expected +
            ' byte(s) but found only ' +
            buffer.length +
            ' byte(s)');
    }
    static nullBufferError(msg) {
        return new ConverterError(ConverterError.Code.NullBufferError, msg);
    }
}
(function (ConverterError) {
    let Code;
    (function (Code) {
        Code["UnexpectedBufferSizeError"] = "UnexpectedBufferSizeError";
        Code["NullBufferError"] = "NullBufferError";
    })(Code = ConverterError.Code || (ConverterError.Code = {}));
})(ConverterError || (ConverterError = {}));

class NumberConverter {
    constructor(options) {
        this.options = options;
    }
    /**
     * Number as unsigned 8 bits converter
     * @param lsb true for least significant bit first
     */
    static uint8(lsb = false) {
        return new NumberConverter({
            signed: false,
            sizeOf: 8,
            leastSignificantBitFirst: lsb,
        });
    }
    /**
     * Number as unsigned 16 bits converter
     * @param lsb true for least significant bit first
     */
    static uint16(lsb = false) {
        return new NumberConverter({
            signed: false,
            sizeOf: 16,
            leastSignificantBitFirst: lsb,
        });
    }
    /**
     * Number as unsigned 32 bits converter
     * @param lsb true for least significant bit first
     */
    static uint32(lsb = false) {
        return new NumberConverter({
            signed: false,
            sizeOf: 32,
            leastSignificantBitFirst: lsb,
        });
    }
    /**
     * Number as signed 8 bits converter
     * @param lsb  true for least significant bit first
     */
    static int8(lsb = false) {
        return new NumberConverter({
            signed: true,
            sizeOf: 8,
            leastSignificantBitFirst: lsb,
        });
    }
    /**
     * Number as signed 16 bits converter
     * @param lsb  true for least significant bit first
     */
    static int16(lsb = false) {
        return new NumberConverter({
            signed: true,
            sizeOf: 16,
            leastSignificantBitFirst: lsb,
        });
    }
    /**
     * Number as signed 32 bits converter
     * @param lsb  true for least significant bit first
     */
    static int32(lsb = false) {
        return new NumberConverter({
            signed: true,
            sizeOf: 32,
            leastSignificantBitFirst: lsb,
        });
    }
    decode(body) {
        if (body.length * 8 < this.options.sizeOf) {
            throw ConverterError.unexpectedBufferSize(this.options.sizeOf / 8, body);
        }
        return NumberConverter.fromBytes(body, this.options.sizeOf / 8, this.options.signed, this.options.leastSignificantBitFirst);
    }
    encode(value) {
        const result = new Uint8Array(this.options.sizeOf >> 3);
        for (var i = 0; i < result.length; i++) {
            const decalage = (result.length - i - 1) * 8;
            result[i] = (value >> decalage) & 0xff;
        }
        if (this.options.leastSignificantBitFirst) {
            result.reverse();
        }
        return result;
    }
    static toOpaqueMsb(input, sizeOf) {
        if (sizeOf === 8) {
            return new Uint8Array([input & 0xff]);
        }
        else if (sizeOf === 16) {
            return new Uint8Array([(input >> 8) & 0xff, input & 0xff]);
        }
        else if (sizeOf === 32) {
            return new Uint8Array([
                (input >> 24) & 0xff,
                (input >> 16) & 0xff,
                (input >> 8) & 0xff,
                input & 0xff,
            ]);
        }
        else {
            throw new Error(`Invalid size ${sizeOf}. Must be 8, 16 or 32`);
        }
    }
    static fromOpaqueMsb(data, sizeOf) {
        if (!sizeOf) {
            sizeOf = data.length * 8;
        }
        if (sizeOf === 8) {
            return data[0] & 0xff;
        }
        if (sizeOf === 16) {
            return (data[1] & 0xff) + ((data[0] & 0xff) << 8);
        }
        if (sizeOf === 32) {
            return ((data[3] & 0xff) +
                ((data[2] & 0xff) << 8) +
                ((data[1] & 0xff) << 16) +
                ((data[0] & 0xff) << 24));
        }
        throw new Error(`Invalid size ${sizeOf}. Must be 8, 16 or 32`);
    }
    /**
     * @param size size in bytes
     */
    static fromBytes(data, size, signed, lsb) {
        if (lsb) {
            data = data.reverse();
        }
        var result = 0;
        var last = 0;
        var decalage;
        for (var i = 0; i < size; i++) {
            decalage = (size - i - 1) * 8;
            result += ((data[i] & 0xff) << decalage) >>> 0;
            if (last > result && !signed) {
                throw new Error('Overflow. Last value: ' + last + ' but new is ' + result);
            }
            last = result;
        }
        if (signed && (data[0] & 0x80) === 0x80) {
            result = -(Math.pow(2, 8 * size) - result);
        }
        return result;
    }
}

let _mpeg2Instance;
class CRC {
    constructor(polynomial) {
        this.polynomial = polynomial;
    }
    static mpeg2() {
        if (!_mpeg2Instance) {
            _mpeg2Instance = new CRC(CRC.POLYNOMIAL_CRC32_MPEG2);
        }
        return _mpeg2Instance;
    }
    compute(buffer, firstValue = CRC.FIRST_VALUE) {
        if (buffer instanceof Uint32Array) {
            return CRC.fromWord(buffer, firstValue, this.polynomial);
        }
        else {
            return CRC.fromBytes(buffer, firstValue, this.polynomial);
        }
    }
    static fromBytes(data, crc = CRC.FIRST_VALUE, polynomial = CRC.POLYNOMIAL_CRC32_MPEG2) {
        if (data.length % CRC.MODULO !== 0) {
            throw new Error(`Cannot compute CRC, input size must be a multiple of 4. Length is ${data.length} % ${CRC.MODULO} = ${data.length % CRC.MODULO}`);
        }
        const uint32Data = new Uint32Array(data.length / 4);
        for (let offset = 0; offset < uint32Data.byteLength; offset++) {
            uint32Data[offset] = NumberConverter.fromOpaqueMsb(data.slice(offset * 4, (offset + 1) * 4), 32);
        }
        return CRC.fromWord(uint32Data, crc, polynomial);
    }
    // crc 32 calculation by word.
    static fromWord(buffer, crc = CRC.FIRST_VALUE, polynomial = CRC.POLYNOMIAL_CRC32_MPEG2) {
        // don't follow null pointers
        if (!buffer) {
            throw new Error('Buffer is null');
        }
        // don't accept buffers larger than 16MB
        if (buffer.byteLength > 16 * 1024 * 1024) {
            throw new Error('Buffers larger than 16MB');
        }
        let myword;
        for (let i = 0; i < buffer.length; i++) {
            myword = buffer[i]; // Get next word.
            crc = CRC.appendCrcNumber(crc, myword, polynomial);
        }
        return crc;
    }
    static appendCrcNumber(crc, data, polynomial) {
        crc = crc ^ data;
        for (let i = 0; i < 32; i++) {
            if (crc & 0x80000000) {
                crc = ((crc << 1) ^ polynomial) >>> 0;
            }
            else {
                crc = (crc << 1) >>> 0;
            }
        }
        return crc;
    }
}
CRC.POLYNOMIAL_CRC32_MPEG2 = 0x4c11db7;
CRC.MODULO = 4;
CRC.FIRST_VALUE = 0xffffffff;

function enumKeyOrValueToNumber(key, mapping) {
    if (typeof key === 'string') {
        // if (!(key in mapping)) {
        //     throw new Error(``)
        // }
        return mapping[key];
    }
    else {
        return key;
    }
}
class TapStreamReader extends KaitaiStreamReader {
    static fromArray(array) {
        const buffer = typedArrayToBuffer(array);
        return new TapStreamReader(buffer);
    }
    static create(packet) {
        return packet instanceof Uint8Array
            ? TapStreamReader.fromArray(packet)
            : packet;
    }
    readStr(n) {
        return String.fromCharCode.apply(null, this.readBytes(n));
    }
    /**
     * Read given byte size as string
     * @param n number of byte to read
     */
    readString(n = this.byteLeft) {
        return this.readStr(n);
    }
    /**
     * Read string until terminator character is reached
     */
    readStringTerminator(terminator) {
        let result = '';
        while (true) {
            const lastByte = this.readU1();
            const lastChar = String.fromCharCode(lastByte);
            if (lastChar !== terminator) {
                result += lastChar;
            }
            else {
                break;
            }
        }
        return result;
    }
    readU2() {
        return this._isBigEndian ? this.readU2be() : this.readU2le();
    }
    readU4() {
        return this._isBigEndian ? this.readU4be() : this.readU4le();
    }
    readF4() {
        return this._isBigEndian ? this.readF4be() : this.readF4le();
    }
    subStream(length) {
        return TapStreamReader.fromArray(this.readBytes(length));
    }
}
class TapStreamWriter extends KaitaiStreamWriter {
    constructor(lengthOrBuffer = 512, byteOffset, isBigEndian = true) {
        if (typeof lengthOrBuffer === 'number') {
            lengthOrBuffer = new ArrayBuffer(lengthOrBuffer);
        }
        super(lengthOrBuffer, byteOffset, isBigEndian);
    }
    static create(size) {
        return new TapStreamWriter(new ArrayBuffer(size));
    }
    writeFunction(type, options, fieldValue, fieldSize) {
        switch (type) {
            case 'crc32':
                return this.addCRC(options);
            case 'padding':
                if (fieldValue) {
                    debug('Padding is already set');
                    return this.writeBytes(fieldValue);
                }
                else {
                    if (typeof fieldSize == undefined) {
                        throw new Error(`Padding function expect padding size as a second argument. Args: ${JSON.stringify(arguments)}`);
                    }
                    return this.addPadding(fieldSize);
                }
            default:
                throw new Error(`Invalid writer function type: ${type}`);
        }
    }
    addPadding(padSize) {
        this.writeBytes(new Uint8Array(padSize));
        return this;
    }
    addCRC(options) {
        let buffer = this.toBytes;
        if (options && options.offset) {
            buffer = buffer.slice(options.offset);
        }
        const crc = CRC.fromBytes(buffer);
        this.writeU4(crc);
        // console.info(`TODO REMOVE Compute CRC from ${options ? options.offset : 0}:${buffer.length} 0x${bufferToHexString(buffer)} => 0x${crc.toString(16)}`)
        return this;
    }
    writeUnsigned(value, length) {
        this.writeU(value, length);
        return this;
    }
    writeU4(value) {
        this.writeU(value, 4);
        return this;
    }
    writeU2(value) {
        this.writeU(value, 2);
        return this;
    }
    writeU1(value) {
        this.writeU(value, 1);
        return this;
    }
    writeF4(value) {
        this.writeF(value, 4);
        return this;
    }
    writeFloat(value, length) {
        this.writeF(value, length);
        return this;
    }
    writeSigned(value, length) {
        this.writeSigned(value, length);
        return this;
    }
    writeString(value, length = (value === null || value === void 0 ? void 0 : value.length) || 0) {
        return this.writeStr(value || '', length);
    }
    writeStr(value, length = value.length) {
        let i = 1;
        const stringWriteLength = Math.min(value.length, length);
        for (; i <= stringWriteLength; i++) {
            this.writeByte(value.charCodeAt(i - 1));
        }
        const remaining = length - stringWriteLength;
        if (remaining > 0) {
            this.writeBytes(new Uint8Array(remaining));
        }
        return this;
    }
    writeBitsInt(value, n) {
        this.writeB(value, n);
        return this;
    }
    writeBits(value, n) {
        this.writeB(value, n);
        return this;
    }
    writeBoolean(value, n) {
        this.writeBits(value ? 1 : 0, n);
        return this;
    }
}

/**
 *   Generated file. Do not edit
 */
TapStreamReader.prototype.readApduRequest = function () {
    const model = {};
    model.header = this.readApduRequestHeader();
    model.data = this.readBytes(model.header.lc);
    return model;
};
TapStreamReader.prototype.readApduRequestHeader =
    function () {
        const model = {};
        model.cla = this.readUnsigned(1);
        model.ins = this.readUnsigned(1);
        model.p1 = this.readUnsigned(1);
        model.p2 = this.readUnsigned(1);
        model.lc = this.readUnsigned(1);
        return model;
    };
TapStreamReader.prototype.readApduResponse = function () {
    const model = {};
    model.data = this.readBytes(this.getStreamSize() - 2);
    model.status = this.readUnsigned(2);
    return model;
};
TapStreamReader.prototype.readTapRequestEncrypted =
    function () {
        const model = {};
        model.header = this.readApduRequestHeader();
        model.request = this.readTapRequestFrame();
        return model;
    };
TapStreamReader.prototype.readTapApduRequest = function () {
    const model = {};
    return model;
};
TapStreamReader.prototype.readTapEncryptedFrame =
    function () {
        const model = {};
        model.id = this.readUnsigned(2);
        model.len = this.readUnsigned(2);
        model.payload = this.readBytes(model.len);
        model.padding = this.readBytes((16 - ((2 + 2 + (model.len === undefined ? 0 : model.len) + 4) % 16)) % 16);
        model.crc = this.readUnsigned(4);
        return model;
    };
TapStreamReader.prototype.readTapRequestFrame = function () {
    const model = {};
    model.header = this.readTapRequestFrameHeader();
    model.payload = this.readBytes();
    return model;
};
TapStreamReader.prototype.readTapRequestFrameHeader =
    function () {
        const model = {};
        model.methodType = this.readUnsigned(1);
        model.path = this.readTapRequestFramePath();
        return model;
    };
TapStreamReader.prototype.readTapRequestFramePath =
    function () {
        const model = {};
        model.objectId = this.readUnsigned(2);
        model.objectInstanceId = this.readUnsigned(2);
        model.resourceId = this.readUnsigned(2);
        return model;
    };
TapStreamReader.prototype.readTapResponseFrame = function () {
    const model = {};
    model.status = this.readUnsigned(1);
    model.data = this.readBytes();
    return model;
};
// TapStreamWriter.prototype.write(model: ApduRequest) : TapStreamWriter{
//     return this.writeApduRequest(model)
// }
TapStreamWriter.prototype.writeApduRequest = function (model) {
    this.writeApduRequestHeader(model.header);
    this.writeBytes(model.data, model.header.lc);
    return this;
};
TapStreamWriter.prototype.writeApduRequestHeader = function (model) {
    this.writeUnsigned(model.cla, 1);
    this.writeUnsigned(model.ins, 1);
    this.writeUnsigned(model.p1, 1);
    this.writeUnsigned(model.p2, 1);
    this.writeUnsigned(model.lc, 1);
    return this;
};
// TapStreamWriter.prototype.write(model: ApduResponse) : TapStreamWriter{
//     return this.writeApduResponse(model)
// }
TapStreamWriter.prototype.writeApduResponse = function (model) {
    this.writeBytes(model.data, undefined);
    this.writeUnsigned(model.status, 2);
    return this;
};
// TapStreamWriter.prototype.write(model: TapRequestEncrypted) : TapStreamWriter{
//     return this.writeTapRequestEncrypted(model)
// }
TapStreamWriter.prototype.writeTapRequestEncrypted = function (model) {
    this.writeApduRequestHeader(model.header);
    this.writeTapRequestFrame(model.request);
    return this;
};
// TapStreamWriter.prototype.write(model: TapApduRequest) : TapStreamWriter{
//     return this.writeTapApduRequest(model)
// }
TapStreamWriter.prototype.writeTapApduRequest = function (model) {
    return this;
};
// TapStreamWriter.prototype.write(model: TapEncryptedFrame) : TapStreamWriter{
//     return this.writeTapEncryptedFrame(model)
// }
TapStreamWriter.prototype.writeTapEncryptedFrame = function (model) {
    this.writeUnsigned(model.id, 2);
    this.writeUnsigned(model.len, 2);
    this.writeBytes(model.payload, model.len);
    this.writeFunction('padding', undefined, model.padding, (16 - ((2 + 2 + (model.len === undefined ? 0 : model.len) + 4) % 16)) % 16);
    this.writeFunction('crc32', undefined, model.crc, 4);
    return this;
};
// TapStreamWriter.prototype.write(model: TapRequestFrame) : TapStreamWriter{
//     return this.writeTapRequestFrame(model)
// }
TapStreamWriter.prototype.writeTapRequestFrame = function (model) {
    this.writeTapRequestFrameHeader(model.header);
    this.writeBytes(model.payload);
    return this;
};
TapStreamWriter.prototype.writeTapRequestFrameHeader = function (model) {
    this.writeBitsInt(enumKeyOrValueToNumber(model.methodType, TapRequestFrame.MethodType), 8);
    this.writeTapRequestFramePath(model.path);
    return this;
};
TapStreamWriter.prototype.writeTapRequestFramePath = function (model) {
    this.writeUnsigned(model.objectId, 2);
    this.writeUnsigned(model.objectInstanceId, 2);
    this.writeUnsigned(model.resourceId, 2);
    return this;
};
// TapStreamWriter.prototype.write(model: TapResponseFrame) : TapStreamWriter{
//     return this.writeTapResponseFrame(model)
// }
TapStreamWriter.prototype.writeTapResponseFrame = function (model) {
    this.writeUnsigned(model.status, 1);
    this.writeBytes(model.data);
    return this;
};

/**
 *   Generated file. Do not edit
 */
class ApduRequestConverter {
    encode(model, stream = new TapStreamWriter()) {
        stream.writeApduRequest(model);
        return stream.toBytes;
    }
    decode(data) {
        const stream = data instanceof TapStreamReader ? data : TapStreamReader.create(data);
        return stream.readApduRequest();
    }
}
class ApduResponseConverter {
    encode(model, stream = new TapStreamWriter()) {
        stream.writeApduResponse(model);
        return stream.toBytes;
    }
    decode(data) {
        const stream = data instanceof TapStreamReader ? data : TapStreamReader.create(data);
        return stream.readApduResponse();
    }
}
class TapRequestEncryptedConverter {
    encode(model, stream = new TapStreamWriter()) {
        stream.writeTapRequestEncrypted(model);
        return stream.toBytes;
    }
    decode(data) {
        const stream = data instanceof TapStreamReader ? data : TapStreamReader.create(data);
        return stream.readTapRequestEncrypted();
    }
}
class TapApduRequestConverter$1 {
    encode(model, stream = new TapStreamWriter()) {
        stream.writeTapApduRequest(model);
        return stream.toBytes;
    }
    decode(data) {
        const stream = data instanceof TapStreamReader ? data : TapStreamReader.create(data);
        return stream.readTapApduRequest();
    }
}
class TapEncryptedFrameConverter {
    encode(model, stream = new TapStreamWriter()) {
        stream.writeTapEncryptedFrame(model);
        return stream.toBytes;
    }
    decode(data) {
        const stream = data instanceof TapStreamReader ? data : TapStreamReader.create(data);
        return stream.readTapEncryptedFrame();
    }
}
class TapRequestFrameConverter {
    encode(model, stream = new TapStreamWriter()) {
        stream.writeTapRequestFrame(model);
        return stream.toBytes;
    }
    decode(data) {
        const stream = data instanceof TapStreamReader ? data : TapStreamReader.create(data);
        return stream.readTapRequestFrame();
    }
}
class TapResponseFrameConverter {
    encode(model, stream = new TapStreamWriter()) {
        stream.writeTapResponseFrame(model);
        return stream.toBytes;
    }
    decode(data) {
        const stream = data instanceof TapStreamReader ? data : TapStreamReader.create(data);
        return stream.readTapResponseFrame();
    }
}

function createApduFromTapRequest(request) {
    const data = new TapRequestFrameConverter().encode(request); // TODO
    // TODO check if size > 255 ?
    let ins;
    switch (request.header.methodType) {
        case TapRequestFrame.MethodType.GET:
            ins = TapApduRequest.MethodType.GET;
            break;
        case TapRequestFrame.MethodType.PUT:
        case TapRequestFrame.MethodType.POST:
            ins = TapApduRequest.MethodType.PUT_OR_POST;
            break;
        default:
            throw new Error(`Internal error: request type "${request.header.methodType}" is invalid`);
    }
    return {
        header: {
            cla: TapApduRequest.Default.CLA,
            ins,
            p1: 0,
            p2: 0,
            lc: data.length,
        },
        data,
    };
}

class TapRequestFrameBuilder {
    // public static fromBytes(bytes: Uint8Array): Header {
    //     const path = TapStreamReader.fromArray(bytes).readTapRequestPath();
    //     const header = new Header(path);
    //     if (bytes.length == 8) {
    //         // What is that ???
    //         header.resourceInstance =
    //             ((bytes[6] << 8) & Header.MAX_ID) + (bytes[7] & 0xff);
    //         header.resourceInstance &= Header.MAX_ID;
    //     }
    //     // return header;
    //     return header;
    // }
    static parsePath(path) {
        if (!path || path.length === 0) {
            throw new Error('Missing LWM2M path, it must not be empty'); // TODO better error
        }
        path = path.trim();
        const result = path.match(TapRequestFrameBuilder.PATH_EXPRESSION);
        if (!result || result.length === 0) {
            throw new Error(`Invalid LWM2M path "${path}", it does not match expected format`); // TODO better error
        }
        const header = {
            objectId: this.validateId(parseInt(result[1])),
            objectInstanceId: result[2] ? parseInt(result[2]) : 0xffff,
            resourceId: parseInt(result[3]),
        };
        if (result[4]) {
            header['resourceInstanceId'] = parseInt(result[4], 10);
        }
        return header;
    }
    static GET(url, body) {
        return TapRequestFrameBuilder.create(TapRequestFrame.MethodType.GET, url, body);
    }
    static PUT(url, body) {
        return TapRequestFrameBuilder.create(TapRequestFrame.MethodType.PUT, url, body);
    }
    static POST(url, body) {
        return TapRequestFrameBuilder.create(TapRequestFrame.MethodType.POST, url, body);
    }
    static create(method, url, data) {
        return {
            header: {
                path: TapRequestFrameBuilder.parsePath(url),
                methodType: method,
            },
            payload: data || new Uint8Array(),
        };
    }
    static validateId(id) {
        if (id < 0 || id > TapRequestFrameBuilder.MAX_ID) {
            throw new Error(`Invalid identifier "${id}". It must be a number between ${id} and ${TapRequestFrameBuilder.MAX_ID}`);
        }
        return id;
    }
}
TapRequestFrameBuilder.MAX_ID = 0xffff;
(function (TapRequestFrameBuilder) {
    TapRequestFrameBuilder.PATH_EXPRESSION = /^\/?([0-9]+)\/([0-9]+)?\/([0-9]+)\/?([0-9]+)?$/;
})(TapRequestFrameBuilder || (TapRequestFrameBuilder = {}));

class TapRequestHelper {
    // static toString(request: TapRequestFrame) {
    //     return `${TapRequestFrame.MethodType[request.header.methodType]} ${TapRequestHelper.pathToString(request.header.path)}`;
    // }
    static toString(request, options) {
        let result = `${TapRequestFrame.MethodType[request.header.methodType]} ${TapRequestHelper.pathToString(request.header.path, options)}`;
        if (request.payload && request.payload.length > 0) {
            result += ` 0x${bufferToHexString(request.payload)}`;
        }
        return result;
    }
    static pathToString(path, options) {
        const resourceInstanceId = path['resourceInstanceId'];
        return ('/' +
            path.objectId +
            '/' +
            (path.objectInstanceId === TapRequestFrameBuilder.MAX_ID
                ? (options === null || options === void 0 ? void 0 : options.printMaxId)
                    ? path.objectInstanceId
                    : ''
                : path.objectInstanceId) +
            '/' +
            path.resourceId +
            (resourceInstanceId ? '/' + resourceInstanceId : ''));
    }
}

/**
 *   Generated file. Do not edit
 */
const ResultCodeTranslation = {
    /**
     * OK
     * decimal: 0
     */
    0x0: 'The request was successful',
    /**
     * CREATED
     * decimal: 65
     */
    0x41: 'Create request was successful',
    /**
     * DELETED
     * decimal: 66
     */
    0x42: 'Delete request was successful',
    /**
     * CHANGED
     * decimal: 68
     */
    0x44: 'Update request was successful',
    /**
     * CONTENT
     * decimal: 69
     */
    0x45: 'The server has fulfilled the request and sent a response',
    /**
     * BUSY
     * decimal: 70
     */
    0x46: 'The server is busy',
    /**
     * BAD_REQUEST
     * decimal: 128
     */
    0x80: 'Bad request. Meaning device cannot understand your request.',
    /**
     * UNAUTHORIZED
     * decimal: 129
     */
    0x81: 'Current user is not authorized to access this ressource',
    /**
     * NOT_FOUND
     * decimal: 132
     */
    0x84: 'The server has not found anything matching the Request',
    /**
     * METHOD_NOT_ALLOWED
     * decimal: 133
     */
    0x85: 'Current user is not authorized to access this ressource',
    /**
     * NOT_ACCEPTABLE
     * decimal: 134
     */
    0x86: 'Given parameters are not acceptable',
    /**
     * RESOURCE_LOCKED
     * decimal: 135
     */
    0x87: 'Given resource is not available with the current TAP configuration.',
    /**
     * INTERNAL_SERVER_ERROR
     * decimal: 160
     */
    0xa0: 'Internal server error',
    /**
     * NOT_IMPLEMENTED
     * decimal: 161
     */
    0xa1: 'This resource has not been implemented yet',
    /**
     * SERVICE_UNAVAILABLE
     * decimal: 163
     */
    0xa3: 'Service is currently unavailable. Try again later.',
    /**
     * NVM_ERROR
     * decimal: 164
     */
    0xa4: 'Writing non volatible memory failed',
    /**
     * UNABLE_TO_CONNECT_TO_TARGET
     * decimal: 165
     */
    0xa5: 'Tap was not able to connect to the target',
    /**
     * TARGET_POWER_FAILURE
     * decimal: 166
     */
    0xa6: 'Target power failure',
    /**
     * NVM_FULL
     * decimal: 167
     */
    0xa7: 'Non volatile memory is full',
    /**
     * DEVICE_UNAVAILABLE
     * decimal: 168
     */
    0xa8: "It means we can't access this resource because someone is already connected to this device and he has the priority over us.",
    /**
     * RESPONSE_TOO_LARGE
     * decimal: 169
     */
    0xa9: 'Response is bigger than you current protocol limit',
    /**
     * TARGET_PROTOCOL_ERROR
     * decimal: 176
     */
    0xb0: 'Target protocol error',
    /**
     * TARGET_PROTOCOL_BUSY
     * decimal: 177
     */
    0xb1: 'Target protocol is busy',
    /**
     * TARGET_PROTOCOL_REAL
     * decimal: 178
     */
    0xb2: 'Target protocol error',
    /**
     * TARGET_PROTOCOL_WRONG_PARAM
     * decimal: 179
     */
    0xb3: 'Target protocol parameters are not valid',
    /**
     * TARGET_PROTOCOL_FORBIDDEN
     * decimal: 180
     */
    0xb4: 'Target protocol is forbidden',
    /**
     * TARGET_PROTOCOL_DATA
     * decimal: 181
     */
    0xb5: 'Target protocol data are not the one expected. For example a CRC error.',
    /**
     * TARGET_PROTOCOL_COM
     * decimal: 182
     */
    0xb6: 'Target protocol communication error',
    /**
     * TARGET_PROTOCOL_LOST_COM
     * decimal: 183
     */
    0xb7: 'Target protocol lost communication',
    /**
     * TARGET_PROTOCOL_PARAM
     * decimal: 184
     */
    0xb8: 'Target protocol invalid parameter',
    /**
     * TARGET_PROTOCOL_INT
     * decimal: 185
     */
    0xb9: 'Internal error with target communication',
    /**
     * TARGET_PROTOCOL_ABORT
     * decimal: 186
     */
    0xba: 'Operation aborted because one of the command did not return the expected result code.',
    /**
     * TARGET_PROTOCOL_NOT_IMPLEMENTED
     * decimal: 187
     */
    0xbb: 'Target protocol is not implemented',
};

class TapClientError extends CodeError {
    constructor(code, message, cause) {
        super(message, code);
        this.cause = cause;
    }
    static illegalArgument(msg) {
        return new TapClientError(TapClientError.Code.IllegalArgumentError, msg);
    }
    static illegalStateError(msg) {
        return new TapClientError(TapClientError.Code.IllegalStateError, msg);
    }
    static encodeRequestError(command, cause) {
        return new TapClientError(TapClientError.Code.EncodeRequestError, 'Cannot encode this request: ' + command + '. Cause: ' + cause, cause);
    }
    static decodeResponseError(cause, frameOrCommand) {
        return new TapClientError(TapClientError.Code.DecodeResponseError, `Cannot decode response to command ${frameOrCommand instanceof Uint8Array
            ? `0x${bufferToHexString(frameOrCommand)}`
            : TapRequestHelper.toString(frameOrCommand)}. Cause: ` + cause, cause);
    }
    static notConnectedError() {
        return new TapClientError(TapClientError.Code.NotConnectedError, 'Trying to execute command but device is not connected');
    }
    static cannotDecodeResponseError(err, bufferData) {
        return new TapClientError(TapClientError.Code.UnexpectedTapResponse, `Received from device an unexpected response that cannot be decoded (${err.message}). Frame: 0x${bufferToHexString(bufferData)}`);
    }
    static unexpectedApduStatusCode(apduResponse) {
        return new TapClientError(TapClientError.Code.UnexpectedApduResponseStatus, `Received from device an unexpected response with an invalid APDU status code 0x${apduResponse.status.toString(16)}
            but expecting 0x${ApduResponse.Status.OK.toString(16)}`);
    }
    static cannotEncodeRequest(err, tapRequest) {
        return new TapClientError(TapClientError.Code.CannotEncodeRequest, `Cannot encode request ${tapRequest}: ${err.message}`);
    }
}
(function (TapClientError) {
    let Code;
    (function (Code) {
        Code["NotConnectedError"] = "TapClientErrorNotConnected";
        Code["EncodeRequestError"] = "TapClientErrorEncodeRequest";
        Code["IllegalArgumentError"] = "TapClientErrorIllegalArgument";
        Code["IllegalStateError"] = "TapClientErrorIllegalState";
        Code["DecodeResponseError"] = "TapClientErrorDecodeResponse";
        Code["UnexpectedTapResponse"] = "TapClientErrorUnexpectedTapResponse";
        Code["UnexpectedApduResponseStatus"] = "TapClientErrorUnexpectedApduResponseStatus";
        Code["CannotEncodeRequest"] = "TapClientErrorCannotEncodeRequest";
        Code["TapClientResponseStatusError"] = "TapClientErrorResponseStatus";
    })(Code = TapClientError.Code || (TapClientError.Code = {}));
})(TapClientError || (TapClientError = {}));
// export const TapResponseErrorCode = 'TapResponseError';
class TapClientResponseStatusError extends TapClientError {
    // public code: string;
    constructor(response, request) {
        super(TapClientError.Code.TapClientResponseStatusError, TapClientResponseStatusError.createErrorMessage(response, request));
        this.response = response;
        this.request = request;
    }
    static createErrorMessage(response, request) {
        const codeRet = response.status;
        let msg = 'Tap request failed. ';
        if (request) {
            msg = `Tap request ${TapRequestHelper.toString(request)} failed. `;
        }
        const errorExplained = codeRet in ResultCodeTranslation
            ? ResultCodeTranslation[codeRet]
            : 'an unknown error';
        msg += `${errorExplained} (code=0x${codeRet.toString(16)})`;
        return msg;
    }
}

class TapApduRequestConverter {
    encode(tapRequest) {
        try {
            const stream = new TapStreamWriter(5 + 1 + tapRequest.payload.length + 1);
            stream.writeApduRequest(createApduFromTapRequest(tapRequest));
            return stream.toBytes;
        }
        catch (err) {
            if (!(err instanceof TapClientError)) {
                throw TapClientError.cannotEncodeRequest(err, tapRequest);
            }
            else {
                throw err;
            }
        }
    }
    decode(bufferData) {
        const stream = bufferData instanceof TapStreamReader
            ? bufferData
            : TapStreamReader.create(bufferData);
        const apduHeader = stream.readApduRequestHeader();
        const tapRequestFrame = stream.readTapRequestFrame();
        return tapRequestFrame;
    }
}
class TapApduResponseConverter {
    encode(tapResponseFrame) {
        const stream = new TapStreamWriter();
        const tapRequestStream = new TapStreamWriter();
        tapRequestStream.writeTapResponseFrame(tapResponseFrame);
        stream.writeApduResponse({
            data: tapRequestStream.toBytes,
            status: ApduResponse.Status.OK,
        });
        return stream.toBytes;
    }
    decode(bufferData) {
        try {
            const stream = bufferData instanceof TapStreamReader
                ? bufferData
                : TapStreamReader.create(bufferData);
            const apduResponse = stream.readApduResponse();
            if (apduResponse.status !== ApduResponse.Status.OK) {
                throw TapClientError.unexpectedApduStatusCode(apduResponse);
            }
            return TapStreamReader.fromArray(apduResponse.data).readTapResponseFrame();
        }
        catch (err) {
            if (!(err instanceof TapClientError)) {
                throw TapClientError.cannotDecodeResponseError(err, bufferData instanceof TapStreamReader
                    ? bufferData.toBytes
                    : bufferData);
            }
            else {
                throw err;
            }
        }
    }
}
const TAP_REQUEST_FRAME_CONVERTER = new TapApduRequestConverter();
const TAP_RESPONSE_FRAME_CONVERTER = new TapApduResponseConverter();

class InterceptorChain {
    constructor(target) {
        this.target = target;
        this.interceptors = [];
    }
    addInterceptor(interceptor) {
        this.interceptors.push(typeof interceptor === 'function'
            ? {
                intercept: interceptor,
            }
            : interceptor);
        return this;
    }
    execute(context) {
        const runner = {
            index: 0,
            run: (context) => {
                if (runner.index < this.interceptors.length) {
                    runner.index++;
                    return this.interceptors[runner.index - 1].intercept(context, {
                        handle: (context) => {
                            return runner.run(context);
                        },
                    });
                }
                else {
                    return this.target(context);
                }
            },
        };
        return runner.run(context);
    }
}

const TAG$1 = 'Client';
class TapClient {
    constructor(requestEncoder, responseDecoder) {
        this._onProtocolChange = new Subject();
        this._commandEncoder = requestEncoder;
        this._responseDecoder = responseDecoder;
        this.protocols = {};
        this._interceptorChain = new InterceptorChain((context) => {
            try {
                const command = context.request;
                // const bodyDecoder = context.bodyDecoder;
                return this._command(command);
            }
            catch (err) {
                return throwError(err);
            }
        });
    }
    static create(protocol) {
        const client = new TapClient(TAP_REQUEST_FRAME_CONVERTER, TAP_RESPONSE_FRAME_CONVERTER);
        if (protocol) {
            client.addComProtocol(protocol);
        }
        return client;
    }
    // public isEncryptionEnabled(): boolean {
    //     return this._options.encryption;
    // }
    // public get cryptedFrameConverter() {
    //     return this._cryptedFrameConverter;
    // }
    get interceptorChain() {
        return this._interceptorChain;
    }
    addInterceptor(interceptor) {
        this.interceptorChain.addInterceptor(interceptor);
        return this;
    }
    // /**
    //  * Enable/Disable encrytion when communicating with a device
    //  * @param value true if requests must be encrypted
    //  * @param algo optional if you have already provided the algo with @{link setEncryptionAlgo(algo)} method
    //  */
    // public enableEncryption(value: boolean, algo?: EncryptionAlgo): Promise<any> {
    //     if (value) {
    //         algo = algo || this._encryptionAlgo;
    //         if (!algo) {
    //             return Promise.reject(
    //                 TapClientError.illegalArgument(
    //                     'Illegal argument, encryption algo must be set'
    //                 )
    //             );
    //         }
    //         return this._startEncryption(algo);
    //     }
    //     else {
    //         return this._stopEncryption();
    //     }
    // }
    isConnected() {
        return (this.hasProtocol(this.currentProtocolId) &&
            this.getCurrentProtocol().isConnected());
    }
    /**
     * see {@link getCurrentProtocol}
     */
    get protocol() {
        return this.getCurrentProtocol();
    }
    get commandEncoder() {
        return this._commandEncoder;
    }
    get responseDecoder() {
        return this._responseDecoder;
    }
    /**
     * Get current protocol or throw an error if no protocol
     */
    getCurrentProtocol() {
        if (!this.currentProtocolId) {
            throw TapClientError.illegalStateError('Tap client protocol is required');
        }
        if (!this.hasProtocol(this.currentProtocolId)) {
            throw TapClientError.illegalArgument('Protocol ' + this.currentProtocolId + ' does not exist');
        }
        return this.protocols[this.currentProtocolId];
    }
    /**
     * Register/replace a communication protocol with given id
     * If no communication protocol is selected yet, it will select it.
     * @param newProtocol
     * @param id
     */
    addComProtocol(newProtocol, id = 'default') {
        this.protocols[id] = newProtocol;
        if (!this.currentProtocolId) {
            this.currentProtocolId = id;
        }
        return this;
    }
    /**
     * Switch to given communication protocol identifier
     * @throws if protocol identifier does not exist
     * @param name
     */
    switchProtocol(name) {
        if (!this.hasProtocol(name)) {
            throw TapClientError.illegalArgument('Unkonwn protocol: ' + name);
        }
        const oldProtocol = this.getCurrentProtocol();
        this.currentProtocolId = name;
        debug(TAG$1, `Changing protocol to: ${name}`);
        this._onProtocolChange.next({
            newProtocol: this.getCurrentProtocol(),
            oldProtocol,
        });
        return this;
    }
    /**
     * Change communication protocol
     * @throws if protocol identifier does not exist
     * @param protocol can either be the communication protocol instance or a string identifier
     */
    useComProtocol(protocol) {
        let protocolId;
        if (typeof protocol == 'string') {
            protocolId = protocol;
        }
        else {
            protocolId = protocol.constructor.name;
            this.addComProtocol(protocol, protocolId);
        }
        this.switchProtocol(protocolId);
        return this;
    }
    /**
     * Return true if protocol identifier is registered
     * @param id
     */
    hasProtocol(id) {
        return id in this.protocols;
    }
    connect() {
        try {
            return this.getCurrentProtocol().connect();
        }
        catch (err) {
            return throwError(err);
        }
    }
    disconnect() {
        try {
            return this.getCurrentProtocol().disconnect();
        }
        catch (err) {
            return throwError(err);
        }
    }
    /**
     * Send request
     * @param request
     * @param bodyDecoder
     */
    request(request) {
        return this._interceptorChain.execute({
            // bodyDecoder,
            request,
            client: this,
        });
    }
    /**
     * Send raw request
     * No interceptors/No encryption will be triggered
     * @param dataIn request bytes
     * @param bodyDecoder optional body decoder to use
     */
    send(dataIn) {
        try {
            return this.getCurrentProtocol()
                .send(dataIn)
                .pipe(map((result) => {
                // debug(
                //     TAG,
                //     'Received result: ',
                //     bufferToHexString(result),
                //     'DECODER: ',
                //     bodyDecoder ? bodyDecoder.constructor.name : 'NO'
                // );
                try {
                    return this._responseDecoder.decode(result);
                }
                catch (err) {
                    throw TapClientError.decodeResponseError(err, dataIn);
                }
            }));
        }
        catch (err) {
            return throwError(err);
        }
    }
    /**
     * Listen to connection state change event
     * We have to resubscribe when protocol change..
     */
    onConnectionStateChange() {
        try {
            return this.getCurrentProtocol().onConnectionStateChange();
        }
        catch (err) {
            return throwError(err);
        }
    }
    onProtocolChange() {
        try {
            return this._onProtocolChange.asObservable();
        }
        catch (err) {
            return throwError(err);
        }
    }
    _command(command
    // bodyDecoder?: BodyDecoder<T>
    ) {
        try {
            if (!this.isConnected()) {
                return throwError(TapClientError.notConnectedError());
            }
            let dataIn;
            try {
                dataIn = this._commandEncoder.encode(command);
            }
            catch (e) {
                return throwError(TapClientError.encodeRequestError(command, e));
            }
            debug(TAG$1, `Sending ${dataIn.length} bytes 0x${bufferToHexString(dataIn)}`);
            return this.getCurrentProtocol()
                .send(dataIn)
                .pipe(map((result) => {
                // debug(
                //     TAG,
                //     'Received result: ',
                //     bufferToHexString(result),
                //     'DECODER: ',
                //     bodyDecoder ? bodyDecoder.constructor.name : 'NO'
                // );
                try {
                    const response = this._responseDecoder.decode(result);
                    // if (bodyDecoder) {
                    //     response.setBodyDecoder(bodyDecoder);
                    // }
                    return response;
                }
                catch (err) {
                    throw TapClientError.decodeResponseError(err, command);
                }
            })
            // map(response => {
            //     debug(
            //         TAG,
            //         `Command "${command.toString()}" => Response: ${response.toString()}`
            //     );
            //     if (response instanceof ResponseImpl) {
            //         (response as ResponseImpl<T>).setRequest(command);
            //     }
            //     return response;
            // })
            );
        }
        catch (err) {
            return throwError(err);
        }
    }
}

class TapResponseFrameBuilder {
    static SUCCESS(data) {
        return TapResponseFrameBuilder.create(ResultCode.OK, data);
    }
    static ERROR(status = ResultCode.BAD_REQUEST, data) {
        return TapResponseFrameBuilder.create(status, data);
    }
    static create(status, data) {
        return {
            status,
            data: data || new Uint8Array(),
        };
    }
}

function tapResponseStatusToString(status) {
    return ResultCodeTranslation[status]
        ? ResultCodeTranslation[status]
        : 'Unknown tap response status';
}

class ArrayConverter {
    constructor(_itemConverter, options) {
        this._itemConverter = _itemConverter;
        this.options = options;
    }
    get itemConverter() {
        return this._itemConverter;
    }
    decode(bytes) {
        let items = [];
        let sizeOfItem = this.options.sizeOfItem;
        for (let offset = 0; offset < bytes.length; offset += sizeOfItem) {
            let slice = bytes.subarray(offset, offset + sizeOfItem);
            let item = this._itemConverter.decode(slice);
            items.push(item);
        }
        return items;
    }
    encode(items) {
        let result = new Uint8Array(this.options.sizeOfItem * items.length);
        let offset = 0;
        for (let item of items) {
            let encodedItem = this._itemConverter.encode(item); // TODO
            if (encodedItem.length > this.options.sizeOfItem) {
                throw new Error(`Encoded item size overflow: ${encodedItem.length} (max is ${this.options.sizeOfItem})`);
            }
            result.set(encodedItem, offset);
            offset += this.options.sizeOfItem;
        }
        return result;
    }
}

class BooleanConverter {
    constructor(_mask = 0x1) {
        this._mask = _mask;
    }
    decode(data) {
        var value = NumberConverter.fromBytes(data, data.length, false, BooleanConverter.IS_LEAST_SIGNIFICANT_BIT_FIRST);
        return BooleanConverter.decodeFromNumber(this._mask, value);
    }
    encode(value) {
        return Uint8Array.from([value ? this._mask : ~this._mask]);
    }
    static decodeFromNumber(mask, value) {
        return (mask == 0 && value == 0) || (value & mask) != 0;
    }
    static instanceBit0() {
        if (!BooleanConverter._instance) {
            BooleanConverter._instance = new BooleanConverter(0b1);
        }
        return BooleanConverter._instance;
    }
}
BooleanConverter.IS_LEAST_SIGNIFICANT_BIT_FIRST = true;
BooleanConverter._instance = undefined;

const BYTE_SWAP_ORDER_REGEX = /^(B[0-9]+)(_B[0-9]+)*$/;
class ByteSwapConverter {
    constructor(order) {
        this.order = order;
    }
    encode(input) {
        return swapBytes(input, this.order);
    }
    decode(input) {
        return this.encode(input);
    }
}
/**
 * Swap byte position
 * B0 represents the first byte from the right
 * BN represents the N byte from the right
 *
 * This function is symetric
 *
 * Example:
 * ```typescript
 * const input = Uint8Array.from([1,2,3,4]);
 * expect(swapBytes(input, 'B2_B3_B0_B1')).to.be.deep.eq(Uint8Array.from([2,1,4,3]));
 * ```
 *
 * @param input array
 * @param order string that defined how to swap bytes
 * @return a new array with byte swap
 */
function swapBytes(input, order) {
    if (!order.match(BYTE_SWAP_ORDER_REGEX)) {
        throw new Error(`swatpBytes error, invalid byte order "${order}". Should match regex ${BYTE_SWAP_ORDER_REGEX}`);
    }
    const parts = order.split('_');
    // // Create a mapping between byte number and byte position
    // // For example B0_B3_B1
    // // Will give mapping
    // // { B0: 0, B3: 1, B1: 2 }
    // const indexMap: { [key: string]: number } = parts.reduce(
    //     (accumulator: { [key: string]: number }, current: string, index: number) => {
    //         accumulator[current] = index;
    //         return accumulator;
    //     },
    //     {}
    // );
    // if (parts.length !== input.length) {
    //     throw new Error(
    //         `swatpBytes error: ${parts.length} bytes but found ${input.length} bytes`
    //     );
    // }
    const result = new Uint8Array(parts.length);
    // let maxLength = Math.min(parts.length, input.length)
    for (let index = 0; index < result.length; index++) {
        const prevByteIndex = parseInt(parts[index].substr(1));
        if (prevByteIndex >= input.length) {
            throw new Error(`swapBytes error, byte index ${prevByteIndex} is out of range (input length is ${input.length} byte(s))`);
        }
        result[index] = input[input.length - prevByteIndex - 1];
        // const byteIndex = (result.length - index - 1);
        // const newIndex = indexMap['B' + byteIndex];
        // if (newIndex !== undefined) {
        //     result[newIndex] = input[index];
        // }
    }
    return result;
}

function createTapEncryptedFrame(id, payload, paddingGenerator) {
    const len = payload.length;
    // let padSize = computePadding(8 + len, 16)
    // let padding = paddingGenerator ? paddingGenerator(padSize) : undefined
    return {
        id,
        len,
        payload,
        // padding: padding
    };
}

/**
 *
 */
class CryptedFrameConverter {
    constructor(frameIdGenerator) {
        this.frameIdGenerator = frameIdGenerator;
    }
    encode(data) {
        const encryptedModel = createTapEncryptedFrame(this.frameIdGenerator.next(), data
        // this.paddingGenerator
        );
        return new TapStreamWriter().writeTapEncryptedFrame(encryptedModel).toBytes;
    }
    decode(frame) {
        if (!frame) {
            throw ConverterError.nullBufferError('Illegal data: frame is null');
        }
        const encryptedFrame = TapStreamReader.fromArray(frame).readTapEncryptedFrame();
        const subData = encryptedFrame.payload;
        // TODO add back crc
        // let crc = encryptedFrame.crc;
        // let computedCrc = this._computeCRC(byteBuffer.data);
        // if (crc != computedCrc){
        //     if (this.options.skipCRCCheck){
        //     }
        //     else{
        //         throw new Error(`Invalid crc read ${crc}. Should be ${computedCrc}`);
        //     }
        // }
        return subData;
    }
}

class EnumConverter {
    constructor(mapping) {
        this.mapping = mapping;
    }
    decode(value) {
        const literalValue = this.decodeToLiteral(value);
        const enumStringValue = this.mapping[literalValue];
        if (!enumStringValue) {
            throw new Error(`Unknown litteral value ${literalValue}.`);
        }
        return literalValue;
    }
    encode(value) {
        if (typeof value !== 'number') {
            throw Error(`Illegal argument when encoding enum value. Expecting  number but found ${typeof value}`);
        }
        return this.encodeToLiteral(value);
    }
}
class FixedSizeEnumConverter extends EnumConverter {
    constructor(mapping, size = 1) {
        super(mapping);
        this.mapping = mapping;
        this.size = size;
    }
    decodeToLiteral(value) {
        return TapStreamReader.fromArray(value).readU(this.size);
    }
    encodeToLiteral(value) {
        return new TapStreamWriter(this.size).writeU(value, this.size).toBytes;
    }
}
class EnumListConverter {
    constructor(size, mapping) {
        this.size = size;
        this.mapping = mapping;
    }
    decode(value) {
        const bits = this.decodeToLiteral(TapStreamReader.create(value));
        const results = [];
        const enumValues = Object.keys(this.mapping).filter((key) => !isNaN(Number(this.mapping[key])));
        // console.log("numval", enumValues)
        for (const enumName of enumValues) {
            const enumValue = this.mapping[enumName];
            const mask = parseInt(enumValue);
            if (mask == 0) {
                if (bits == 0) {
                    results.push(enumValue);
                }
            }
            else if ((bits & mask) == mask) {
                results.push(enumValue);
            }
        }
        return results;
    }
    encode(values) {
        let mask = 0;
        for (const value of values) {
            if (typeof value !== 'number') {
                throw Error(`Illegal argument when encoding enum value. Expecting number but found ${typeof value}`);
            }
            mask |= value;
        }
        const result = new TapStreamWriter(this.size).writeU(mask, this.size).toBytes;
        return result;
    }
    decodeToLiteral(stream) {
        return stream.readU(this.size);
    }
}

class FloatConverter {
    constructor(options) {
        this.options = options;
    }
    encode(value) {
        return NumberConverter.toOpaqueMsb(FloatConverter.toArray(value), 32);
    }
    decode(body) {
        return FloatConverter.toFloat(body);
    }
    /**
     *
     * @see https://stackoverflow.com/questions/3096646/how-to-convert-a-floating-point-number-to-its-binary-representation-ieee-754-i
     * @param number
     */
    static toArray(number) {
        var n = +number, status = n !== n || n == -Infinity || n == +Infinity ? n : 0, exp = 0, len = 281, // 2 * 127 + 1 + 23 + 3,
        bin = new Array(len), signal = (n = status !== 0 ? 0 : n) < 0, n = Math.abs(n), intPart = Math.floor(n), floatPart = n - intPart, i, lastBit, rounded, j, exponent;
        if (status !== 0) {
            if (n !== n) {
                return 0x7fc00000;
            }
            if (n === Infinity) {
                return 0x7f800000;
            }
            if (n === -Infinity) {
                return 0xff800000;
            }
        }
        i = len;
        while (i) {
            bin[--i] = 0;
        }
        i = 129;
        while (intPart && i) {
            bin[--i] = intPart % 2;
            intPart = Math.floor(intPart / 2);
        }
        i = 128;
        while (floatPart > 0 && i) {
            (bin[++i] = (floatPart *= 2) >= 1 ? 1 : 0) && --floatPart;
        }
        i = -1;
        while (++i < len && !bin[i])
            ;
        if (bin[(lastBit =
            22 +
                (i =
                    (exp = 128 - i) >= -126 && exp <= 127
                        ? i + 1
                        : 128 - (exp = -127))) + 1]) {
            if (!(rounded = bin[lastBit])) {
                j = lastBit + 2;
                while (!rounded && j < len) {
                    rounded = bin[j++];
                }
            }
            j = lastBit + 1;
            while (rounded && --j >= 0) {
                (bin[j] = !bin[j] - 0) && (rounded = 0);
            }
        }
        i = i - 2 < 0 ? -1 : i - 3;
        while (++i < len && !bin[i])
            ;
        (exp = 128 - i) >= -126 && exp <= 127
            ? ++i
            : exp < -126 && ((i = 255), (exp = -127));
        (intPart || status !== 0) &&
            ((exp = 128),
                (i = 129),
                status == -Infinity
                    ? (signal = true)
                    : status !== status && (bin[i] = 1));
        n = Math.abs(exp + 127);
        exponent = 0;
        j = 0;
        while (j < 8) {
            exponent += n % 2 << j;
            n >>= 1;
            j++;
        }
        var mantissa = 0;
        n = i + 23;
        for (; i < n; i++) {
            mantissa = (mantissa << 1) + bin[i];
        }
        return ((signal ? 0x80000000 : 0) + (exponent << 23) + mantissa) | 0;
    }
    static toFloat(val) {
        return FloatConverter.numberToFloat(NumberConverter.fromOpaqueMsb(val, 32));
    }
    static numberToFloat(val) {
        var bits = 1 * val;
        var sign = (bits & 0x80000000) == 0 ? 1 : -1;
        var exponent = (bits & 0x7f800000) >> 23;
        var mantissa = bits & 0x007fffff;
        mantissa |= 0x00800000;
        var f = 1.0 * (sign * mantissa * Math.pow(2, exponent - 150));
        return parseFloat(f.toFixed(5));
    }
    static instance32() {
        if (!FloatConverter._instance32) {
            FloatConverter._instance32 = new FloatConverter();
        }
        return FloatConverter._instance32;
    }
}
FloatConverter._instance32 = undefined;

class HexStringConverter {
    decode(value) {
        return bufferToHexString(value);
    }
    encode(value) {
        if (!value || value.length == 0) {
            return new Uint8Array(0);
        }
        if (value.length % 2 > 0) {
            // Prefix with 0
            value = '0' + value;
        }
        var result = [];
        for (var i = 0, len = value.length; i < len; i += 2) {
            result.push(parseInt(value.substr(i, 2), 16));
        }
        return new Uint8Array(result);
    }
    static instance() {
        if (!HexStringConverter._instance) {
            HexStringConverter._instance = new HexStringConverter();
        }
        return HexStringConverter._instance;
    }
}
HexStringConverter._instance = undefined;

class Ipv4StringConverter {
    decode(body) {
        if (body == null || body.length != 4) {
            throw ConverterError.unexpectedBufferSize(4, body);
        }
        var result = '';
        var values = Array.from(body);
        for (var part of values) {
            result += part.toString() + '.';
        }
        return result.substr(0, result.length - 1);
    }
    encode(ip) {
        var parts = ip.split('.');
        if (parts.length != 4) {
            throw new Error('Invalid ip format. Should be x.x.x.x');
        }
        var result = new Uint8Array(4);
        parts.map((value, index) => {
            result[index] = parseInt(value) & 0xff;
        });
        return result;
    }
    static instance() {
        if (!Ipv4StringConverter._instance) {
            Ipv4StringConverter._instance = new Ipv4StringConverter();
        }
        return Ipv4StringConverter._instance;
    }
}
Ipv4StringConverter._instance = undefined;

const EXPECTED_ARRAY_LENGTH = 6;
class MacAddressStringConverter {
    decode(body) {
        if (!body || body.length !== EXPECTED_ARRAY_LENGTH) {
            throw ConverterError.unexpectedBufferSize(EXPECTED_ARRAY_LENGTH, body);
        }
        let result = '';
        for (var i = 0; i < body.length; i++) {
            const part = body.slice(i, i + 1);
            result += bufferToHexString(part).toUpperCase() + ':';
        }
        return result.substr(0, result.length - 1);
    }
    encode(value) {
        const values = value.split(':').map((v) => parseInt(v, 16) & 0xff);
        if (values.length !== EXPECTED_ARRAY_LENGTH) {
            throw new Error('Invalid mac address format: ' + value);
        }
        return Uint8Array.from(values);
    }
    static instance() {
        if (!MacAddressStringConverter._instance) {
            MacAddressStringConverter._instance = new MacAddressStringConverter();
        }
        return MacAddressStringConverter._instance;
    }
}
MacAddressStringConverter._instance = undefined;

function swapMap(val) {
    var ret = {};
    for (var key in val) {
        ret[val[key]] = key;
    }
    return ret;
}
class MultipleMaskConverter {
    // protected static _instance: UniqMaskDecoder;
    constructor(mapping) {
        this._mapping = mapping;
        // this._mapping[0x00] = (null as any as OutputType);
        // this._mapping[0x01] = (null as any as OutputType);
    }
    decode(body) {
        return MultipleMaskConverter.decodeAll(body, this._mapping);
    }
    /**
     * TODO check that not use more than the first 7 bits
     * @param types
     */
    encode(types) {
        return MultipleMaskConverter.encodeAll(types, this._mapping);
    }
    static encodeAll(values, mapping) {
        var _reverseMapping = swapMap(mapping);
        var result = 0x0;
        for (var type of values) {
            if (type in _reverseMapping) {
                var mask = parseInt(_reverseMapping[type]);
                result = result | mask;
            }
        }
        return Uint8Array.from([result & 0xff]);
    }
    static decodeAll(value, mapping) {
        var results = [];
        if (typeof value !== 'number') {
            value = NumberConverter.fromOpaqueMsb(value, Math.min(value.length * 8, 32));
        }
        for (var key in mapping) {
            var mask = parseInt(key);
            if (mask == 0 && value == 0) {
                results.push(mapping[mask]);
            }
            else if ((mask & value) != 0) {
                results.push(mapping[mask]);
            }
        }
        return results;
    }
}

class PipeConverterBuilder {
    constructor(aliases = {}) {
        this.aliases = aliases;
    }
    pipe(converter) {
        return new PipeConverter([], this.aliases).pipe(converter);
    }
}
/**
 * Pipe multiple converter one after another
 */
class PipeConverter {
    constructor(converters, aliases = {}) {
        this.converters = converters;
        this.aliases = aliases;
    }
    decode(body) {
        let output = body;
        for (let converter of this.converters) {
            if (typeof converter == 'function') {
                output = converter(output);
            }
            else {
                output = converter.decode(output);
            }
        }
        return output;
    }
    encode(value) {
        let output = value;
        const reversedConverters = [...this.converters].reverse();
        for (let converter of reversedConverters) {
            if (typeof converter == 'function') {
                output = converter(output);
            }
            else {
                output = converter.encode(output);
            }
        }
        return output;
    }
    pipe(converter) {
        if (typeof converter === 'string') {
            if (!(converter in this.aliases)) {
                throw new Error(`Illegal argument, missing converter id: ${converter}`);
            }
            converter = this.aliases[converter];
        }
        this.converters.push(converter);
        return this;
    }
    static pipe(converter) {
        return new PipeConverter([converter], {});
    }
}

class StringConverter {
    constructor(encoding = 'ascii', options = {
        stripEndOfStringCharacter: true,
    }) {
        this.encoding = encoding;
        this.options = options;
    }
    encode(value) {
        let buffer = Buffer.from(value, this.encoding);
        return Uint8Array.from(buffer);
    }
    decode(value) {
        let str = Buffer.from(value).toString(this.encoding);
        if (this.options.stripEndOfStringCharacter) {
            const indexOfEndOfString = str.indexOf('\0');
            if (indexOfEndOfString >= 0) {
                str = str.substr(0, indexOfEndOfString);
            }
        }
        return str;
    }
    /**
     * Get ascii instance
     * @deprecated use constructor instead
     */
    static ascii() {
        if (!StringConverter._ascii) {
            StringConverter._ascii = new StringConverter('ascii');
        }
        return StringConverter._ascii;
    }
}
StringConverter._ascii = undefined;

class TLVConverter {
    constructor() {
        this.serializer = new TLV.Serializer();
        this.parser = new TLV.Parser();
    }
    encode(data) {
        return this.serializer.serialize(data);
    }
    decode(data) {
        let tlvTree = this.parser.parse(data);
        return tlvTree;
    }
}

class TlvBundleConverter {
    constructor(variables = [], tlvConverter = new TLVConverter()) {
        this.variables = variables;
        this.tlvConverter = tlvConverter;
        this.bundleId = 0;
    }
    decode(data, context) {
        if (data instanceof Uint8Array) {
            // Why this?
            // const buffer = Buffer.from(data.buffer);
            data = this.tlvConverter.decode(data);
        }
        const result = this._convertFromTlvTree(data);
        if (!result || typeof result !== 'object' || result instanceof Uint8Array) {
            throw new Error(`Invalid TLV frame`); // TODO proper error code
        }
        return result;
    }
    // {
    //     "header": {
    //         "id": 1,
    //         "type": 2,
    //         "len": 12
    //     },
    //     "children": [
    //         {
    //             "header": {
    //                 "id": 1,
    //                 "type": 3,
    //                 "len": 4
    //             },
    //             "payload": {
    //                 "type": "Buffer",
    //                 "data": [
    //                     64,
    //                     83,
    //                     127,
    //                     58
    //                 ]
    //             }
    //         },
    //         {
    //             "header": {
    //                 "id": 2,
    //                 "type": 3,
    //                 "len": 4
    //             },
    //             "payload": {
    //                 "type": "Buffer",
    //                 "data": [
    //                     65,
    //                     243,
    //                     141,
    //                     114
    //                 ]
    //             }
    //         }
    //     ]
    // }
    encode(data) {
        var _a;
        const tree = {
            header: {
                id: this.bundleId,
                len: 0,
                type: TLV.IdentifierType.MULTIPLE_RESOURCE,
            },
            children: [],
        };
        let totalLength = 0;
        for (const key in data) {
            const value = data[key];
            let encodedValue;
            let meta = this.variables.find((m) => m.name === key);
            if (!meta) {
                meta = this.variables.find((m) => m.id === parseInt(key, 10));
            }
            // if (!meta) {
            //     // TODO use proper error with ErrorCode
            //     throw new Error(`Cannot find variable by identifier "${key}"`);
            // }
            if (meta === null || meta === void 0 ? void 0 : meta.converter) {
                encodedValue = (_a = meta.converter) === null || _a === void 0 ? void 0 : _a.encode(value);
            }
            else {
                if (!(value instanceof Uint8Array)) {
                    throw new Error(`Add converter for variable with identifier "${key}"`);
                }
                encodedValue = value;
            }
            if (!tree.children) {
                tree.children = [];
            }
            const id = (meta === null || meta === void 0 ? void 0 : meta.id) || parseInt(key, 10);
            if (isNaN(id)) {
                throw new Error(`Cannot find variable by identifier "${key}"`);
            }
            tree.children.push({
                header: {
                    id,
                    len: encodedValue.length,
                    type: TLV.IdentifierType.RESOURCE_VALUE,
                },
                payload: encodedValue,
            });
            totalLength += encodedValue.length + 2; // 2 for header length
        }
        tree.header.len = totalLength;
        return this.tlvConverter.encode(tree);
    }
    /**
     * Converter tlv tree to variable name => value
     */
    _convertFromTlvTree(tree, variableMeta) {
        // console.log('TLV =>', JSON.stringify(tree, null, 4));
        let result = {};
        // Recursive call to decode children
        if (tree.children) {
            result = {};
            for (const child of tree.children) {
                const variableMetas = this.variables.filter((v) => v.id == child.header.id);
                if (variableMetas.length == 0) {
                    variableMetas.push({
                        id: child.header.id,
                    });
                }
                for (const variableMeta of variableMetas) {
                    const key = variableMeta && variableMeta.name
                        ? variableMeta.name
                        : child.header.id.toString();
                    result[key] = this._convertFromTlvTree(child, variableMeta);
                }
            }
        }
        // Decode current node
        else if (tree.payload) {
            const converter = variableMeta ? variableMeta.converter : undefined;
            if (converter) {
                result = converter.decode(tree.payload);
            }
            else {
                result = Uint8Array.from(tree.payload);
            }
        }
        return result;
    }
}

const TAG = 'Converter';
/**
 * TODO improv it . We don't know the size of input buffer...
 */
class UniqMaskConverter {
    // protected _reverseMapping: { [key: number]: OutputType };
    constructor(mapping) {
        this._mapping = mapping;
    }
    decode(body) {
        return UniqMaskConverter.decodeOne(body, this._mapping);
    }
    encode(value) {
        return UniqMaskConverter.encodeOneAsBuffer(value, this._mapping);
    }
    static decodeOneExact(data, mapping) {
        if (typeof data !== 'number') {
            data = NumberConverter.fromOpaqueMsb(data);
        }
        for (var key in mapping) {
            var mask = parseInt(key);
            if (data == mask) {
                return mapping[mask];
            }
        }
        throw new Error(`Cannot decode ${typeof data === 'number'
            ? 'number ' + data
            : '0x' + bufferToHexString(data)}. Available keys are ${Object.keys(mapping)}`);
    }
    static decodeOne(data, mapping) {
        var results = MultipleMaskConverter.decodeAll(data, mapping);
        if (results.length == 0) {
            throw new Error(`Cannot decode value ${typeof data === 'number' ? data : '0x' + bufferToHexString(data)}. Available keys are ${Object.keys(mapping)}`);
        }
        else if (results.length > 1) {
            debug(TAG, 'Multiple result for data : ', results, data, mapping);
        }
        return results[0];
    }
    static encodeOneAsNumber(data, mapping) {
        let swappedMap = UniqMaskConverter.swapKeyValue(mapping);
        let dataAsString = data.toString();
        if (!(dataAsString in swappedMap)) {
            throw new Error(`Key ${dataAsString} does not exist in mapping. Available keys are ${Object.keys(swappedMap)}`);
        }
        return parseInt(swappedMap[dataAsString]);
    }
    static encodeOneAsBuffer(data, mapping) {
        let result = new Uint8Array(1); // TODO change according to conf
        let encodedValue = UniqMaskConverter.encodeOneAsNumber(data, mapping);
        result[0] = encodedValue & 0xff;
        return result;
    }
    static swapKeyValue(input) {
        var ret = {};
        for (var key in input) {
            ret[input[key]] = key;
        }
        return ret;
    }
}

function safeParseInt(v) {
    const res = parseInt(v);
    if (isNaN(res)) {
        throw invalidStringVersionFormatError(v);
    }
    return res;
}
function invalidStringVersionFormatError(version) {
    return new Error(`Invalid string version format for "${version}". Should be x.x.x. For example: 1.0.3.`);
}
class StringVersionConverter {
    encode(version, stream = new TapStreamWriter()) {
        const parts = version.split('.');
        if (parts.length !== 3) {
            throw invalidStringVersionFormatError(version);
        }
        stream.writeUnsigned(safeParseInt(parts[0]), 1);
        stream.writeUnsigned(safeParseInt(parts[1]), 1);
        stream.writeUnsigned(safeParseInt(parts[2]), 2);
        return stream.toBytes;
    }
    decode(data) {
        // if (!data || data.length !== 4) {
        //     throw ConverterError.unexpectedBufferSize(4, data);
        // }
        const stream = data instanceof TapStreamReader ? data : TapStreamReader.create(data);
        return `${stream.readUnsigned(1)}.${stream.readUnsigned(1)}.${stream.readUnsigned(2)}`;
    }
}

class Checksum {
    static compute(data) {
        let computedChecksum = 0;
        for (let i = 0; i < data.length; i++) {
            computedChecksum = computedChecksum + data[i];
        }
        return computedChecksum;
    }
}

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

export { ApduRequestConverter, ApduResponseConverter, ArrayConverter, BooleanConverter, ByteSwapConverter, CRC, Checksum, ConverterError, CryptedFrameConverter, EnumConverter, EnumListConverter, FixedSizeEnumConverter, FloatConverter, HexStringConverter, Ipv4StringConverter, MacAddressStringConverter, MultipleMaskConverter, NumberConverter, PipeConverter, PipeConverterBuilder, ResultCodeTranslation, StringConverter, StringVersionConverter, TAP_REQUEST_FRAME_CONVERTER, TAP_RESPONSE_FRAME_CONVERTER, TLVConverter, TapApduRequestConverter, TapApduResponseConverter, TapClient, TapClientError, TapEncryptedFrameConverter, TapRequestEncryptedConverter, TapRequestFrameBuilder, TapRequestFrameConverter, TapRequestHelper, TapResponseFrameBuilder, TapResponseFrameConverter, TapStreamReader, TapStreamWriter, TlvBundleConverter, UniqMaskConverter, createApduFromTapRequest, createTapEncryptedFrame, enumKeyOrValueToNumber, tapResponseStatusToString };
//# sourceMappingURL=iotize-tap-client-impl.js.map
