import { bufferToHexString } from '@iotize/common/byte-converter';
import { promiseSerial } from '@iotize/common/promise';
import { QueueComProtocol } from '@iotize/tap/protocol/core';
import { Checksum } from '@iotize/tap/client/impl';
import { createDebugger } from '@iotize/common/debug';
import { CodeError } from '@iotize/common/error';
import { Observable, from } from 'rxjs';
import { share, first, map } from 'rxjs/operators';

const BleConfig = {
    maxPacketLengthWithoutOffset: 19,
    services: {
        upgrade: {
            service: '9e5d1e47-5c13-43a0-8635-82ad38a1386f',
            charac: '347f7608-2e2d-47eb-913b-75d4edc4de3b',
        },
        lwm2m: {
            // // SPP over BLE Service UUID (128-bit)
            service: '6c7b16c2-2a5b-8c9f-cf42-d31425470e7b',
            charac: 'cc5c5491-b3be-9287-cb42-f7a6a29a50d5',
        },
        fastLwm2m: {
            service: '7c7b16c2-2a5b-8c9f-cf42-d31425470e7b',
            charac: 'dc5c5491-b3be-9287-cb42-f7a6a29a50d5',
        },
        standardClientConfig: {
            service: '00002902-0000-1000-8000-00805f9b34fb',
        },
    },
};

/**
 * Created by IoTize on 19/04/2018.
 * <p>
 * Glue packet chunk to build the original ble packet
 * @param {number} bufferLength
 * @class
 */
class BLEPacketBuilder {
    constructor(bufferLength) {
        this.bufferOffset = 0;
        this.dataLength = 0;
        this.data = new Uint8Array(bufferLength);
        this.reset();
    }
    /**
     * Append new chunk of data into the builder
     * @param {Array} dataChunk the data chunk. The first byte must be the position offset of this chunk in the buffer (in bytes)
     */
    append(dataChunk) {
        if (!dataChunk || dataChunk.length < 2) {
            return;
        }
        this.bufferOffset = (dataChunk[0] & 0xff) - 1;
        if (this.bufferOffset + dataChunk.length > this.data.length) {
            throw new Error(// TODO ble error
            `Buffer size exceeded. Maximum size is ${this.data.length} byte(s).`);
        }
        for (let i = 1; i < dataChunk.length; i++) {
            this.data[this.bufferOffset + i] = dataChunk[i];
        }
        this.dataLength += dataChunk.length - 1;
    }
    reset() {
        this.dataLength = 0;
        this.bufferOffset = 0;
    }
    hasAllChunks() {
        return this.bufferOffset === -1;
    }
    isChecksumValid() {
        let computedChecksum = this.getComputedChecksum();
        let expectedChecksum = this.getExpectedChecksum();
        return expectedChecksum === computedChecksum;
    }
    getComputedChecksum() {
        return Checksum.compute(this.data.subarray(0, this.dataLength - 1)) & 0xff;
    }
    getExpectedChecksum() {
        return this.data[this.dataLength - 1];
    }
    /**
     * @return {Array} the result data without the checksum
     */
    getData() {
        return this.data.slice(0, this.dataLength - 1);
        // let result: number[] = (s => { let a = []; while (s-- > 0) a.push(0); return a; })(this.dataLength - 1);
        // /* arraycopy */((srcPts, srcOff, dstPts, dstOff, size) => { if (srcPts !== dstPts || dstOff >= srcOff + size) { while (--size >= 0) dstPts[dstOff++] = srcPts[srcOff++]; } else { let tmp = srcPts.slice(srcOff, srcOff + size); for (let i = 0; i < size; i++) dstPts[dstOff++] = tmp[i]; } })(this.data, 0, result, 0, result.length);
        // return result;
    }
    /**
     * @return {Array} the buffer (with the checksum)
     */
    getBuffer() {
        return this.data;
    }
}

/**
 * Created by IoTize on 19/04/2018.
 *
 * Split data into chunks with a size of maxPacketSize + 1
 * @param {Array} data
 * @param {number} maxPacketSize
 * @class
 */
class BLEPacketSplitter {
    /**
     *
     * @param data
     * @param maxPacketSize
     */
    constructor(data, maxPacketSize) {
        this.lastPacketSize = 0;
        this.maxPacketSize = 0;
        this.currentPacketIndex = 0;
        if (maxPacketSize < 1) {
            throw new Error('Packet size must be greater than 0');
        }
        this.data = data;
        this.maxPacketSize = maxPacketSize;
        this.currentPacketIndex = this.getTotalNumberOfPacket() - 1;
        this.lastPacketSize = this.getLastPacketSize();
    }
    /**
     * Create a BLEPacketSplitter instance from data and add the checksum at the end
     * @param {Array} data body data
     * @param {number} maxPacketSize packet size
     * @return {BLEPacketSplitter} the new instance
     */
    static wrapWithChecksum(data, maxPacketSize) {
        var checkSum = BLEPacketSplitter.computeChecksum(data);
        var wrappedData = new Uint8Array(data.length + 1);
        wrappedData.set(data);
        wrappedData.set([checkSum & 0xff], data.length);
        return new BLEPacketSplitter(wrappedData, maxPacketSize);
    }
    getTotalNumberOfPacket() {
        return (((this.data.length / this.maxPacketSize) | 0) +
            (this.data.length % this.maxPacketSize === 0 ? 0 : 1));
    }
    getLastPacketSize() {
        let result = this.data.length % this.maxPacketSize;
        if (result === 0) {
            result = this.maxPacketSize;
        }
        return result;
    }
    getPackets() {
        let packets = [];
        while (this.hasNextPacket()) {
            packets.push(this.getNextPacket());
        }
        return packets;
    }
    getNextPacket() {
        let packetSize;
        let offset;
        if (this.currentPacketIndex > 0) {
            offset =
                this.lastPacketSize +
                    (this.currentPacketIndex - 1) * this.maxPacketSize;
            packetSize = this.maxPacketSize;
        }
        else {
            offset = 0;
            packetSize = this.lastPacketSize;
        }
        let packet = new Uint8Array(packetSize + 1);
        packet[0] = offset | 0;
        for (let i = 0; i < packetSize; i++) {
            packet[i + 1] = this.data[offset + i];
        }
        this.currentPacketIndex--;
        return packet;
    }
    hasNextPacket() {
        return this.currentPacketIndex >= 0;
    }
    static computeChecksum(data) {
        return Checksum.compute(data) & 0xff;
    }
    getTotalSize() {
        return this.data.length;
    }
}

const prefix = '@iotize/tap/protocol/ble';
const debug = createDebugger(prefix);

class BleComError extends CodeError {
    static invalidBleChunkChecksum(packetBuilder) {
        return InvalidBleFrameChecksum.create(packetBuilder);
    }
    static writeSizeAboveMTU(data, mtu) {
        return new BleComError(BleComError.Code.BleWriteSizeAboveMTU, `Cannot write ${data.length} bytes as it's more than the Maximal Transfert Unit (${mtu})`);
    }
    static serviceNotFound(uuid) {
        return new BleComError(BleComError.Code.BleGattServiceNotFound, `Bluetooth service with id ${uuid} not found`);
    }
    static charcacteristicNotFound(uuid) {
        return new BleComError(BleComError.Code.BleGattCharacteristicNotFound, `Bluetooth characteristic with id ${uuid} not found`);
    }
    static bleNotAvailable(msg = 'BLE is not available on your device') {
        return new BleComError(BleComError.Code.NotAvailable, msg);
    }
    static gattServerNotAvailable() {
        return new BleComError(BleComError.Code.GATTServerNotAvailable, `Generic Attribute Profile (GATT) server is not available`);
    }
    static gattServerConnectionFailed() {
        return new BleComError(BleComError.Code.GATTServerConnectionFailed, `Gatt server connection failed`);
    }
    constructor(code, msg) {
        super(msg, code);
    }
}
class InvalidBleFrameChecksum extends BleComError {
    constructor(packetBuilder) {
        super(BleComError.Code.InvalidChecksum, `Invalid checksum`);
        this.packetBuilder = packetBuilder;
    }
    static create(packetBuilder) {
        return new InvalidBleFrameChecksum(packetBuilder);
    }
}
(function (BleComError) {
    let Code;
    (function (Code) {
        Code["InvalidChecksum"] = "InvalidChecksum";
        Code["BleGattServiceNotFound"] = "BleGattServiceNotFound";
        Code["BleGattCharacteristicNotFound"] = "BleGattCharacteristicNotFound";
        Code["BleWriteSizeAboveMTU"] = "BleWriteSizeAboveMTU";
        Code["GATTServerNotAvailable"] = "BleComErrorGATTServerNotAvailable";
        Code["GATTServerConnectionFailed"] = "BleComErrorGATTServerConnectionFailed";
        Code["NotAvailable"] = "BleComErrorNotAvailable";
    })(Code = BleComError.Code || (BleComError.Code = {}));
})(BleComError || (BleComError = {}));

var __awaiter$1 = (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());
    });
};
/**
 * @deprecated use UniversalBleProtocolAdpater
 * Abstract class for BLE communication
 *
 * With ble communication, data is split into sub packets.
 * This class handles creation of packet chunks.
 *
 * - You must only implement the function to send one packet chunk writeLwm2mPacketChunk()
 * -
 */
class AbstractBleProtocol extends QueueComProtocol {
    constructor(bleOptions = {
        mtu: BleConfig.maxPacketLengthWithoutOffset + 1,
    }) {
        super();
        this.bleOptions = bleOptions;
    }
    onNewLwm2mMessage(data) {
        if (this._readResolve) {
            debug('onNewLwm2mMessage()', bufferToHexString(data));
            this._readResolve(data);
        }
        else {
            console.warn(`No listener for new message... 0x${bufferToHexString(data)}`);
            this._pendingRead = data;
        }
    }
    onNewLwm2mMessageError(err) {
        console.error('Error lwm2m response...', err);
    }
    onLwm2mDataChunkError(error) {
        if (this._readReject) {
            this._readReject(error);
        }
        else {
            console.warn('No listener for read message error...', error);
        }
    }
    getLwm2mResponsePromise() {
        debug('getLwm2mResponsePromise');
        if (this._pendingRead) {
            let data = this._pendingRead;
            this._pendingRead = undefined;
            return Promise.resolve(data);
        }
        if (!this._readPromise) {
            this.createLwm2mResponsePromise();
        }
        return this._readPromise;
    }
    createLwm2mResponsePromise() {
        debug('createLwm2mResponsePromise');
        this._readPromise = new Promise((resolve, reject) => {
            this._readResolve = resolve;
            this._readReject = reject;
        });
        return this._readPromise;
    }
    // send(data: Uint8Array): Observable<Uint8Array> {
    //     debug("BLEProtocol:send() " + bufferToHexString(data));
    //     return from(
    //         this
    //             .write(data)
    //             .then(() => this.waitForLwm2mResponse())
    //             .catch((err) => {
    //                 console.error("ERROR SEND " + err);
    //                 throw err;
    //             })
    //     );
    // }
    read() {
        debug('read()...');
        if (this.packetBuilder) {
            if (this.packetBuilder.hasAllChunks()) {
                let data = this.packetBuilder.getData();
                this.packetBuilder = undefined;
                return Promise.resolve(data);
            }
            // TODO manage error
            console.warn('AbstractBleProtocol::read() There is already a read operation in progress but a new call to ::read has been made.');
        }
        return this.getLwm2mResponsePromise()
            .then((response) => {
            this.packetBuilder = undefined;
            return response;
        })
            .catch((err) => {
            this.packetBuilder = undefined;
            throw err;
        });
    }
    write(data) {
        return __awaiter$1(this, void 0, void 0, function* () {
            if (this.packetSplitter != null) {
                console.warn('BLEProtocol', 'write', 'There are already data being sent but a new call to ::write has been made');
            }
            this.packetSplitter = BLEPacketSplitter.wrapWithChecksum(data, this.bleOptions.mtu - 1);
            let packets = this.packetSplitter.getPackets();
            let promise;
            this._pendingRead = undefined;
            this.packetBuilder = new BLEPacketBuilder(AbstractBleProtocol.RECEIVED_BUFFER_LENGTH);
            if (packets.length > 0) {
                // this.createLwm2mResponsePromise()
                //     .catch((err) => {
                //         this.onNewLwm2mMessageError(err);
                //     })
                promise = promiseSerial(packets, (packet, index) => {
                    return this.writeLwm2mPacketChunk(packet).then((value) => {
                        debug(`Done sending packet ${index + 1}/${packets.length}`);
                        return value;
                    });
                });
                // packets.forEach((value: Uint8Array, index: number) => {
                //     if (!promise){
                //         promise = this.writeLwm2mPacketChunk(packets[index]);
                //     }
                //     else{
                //         promise = promise.then(() => this.writeLwm2mPacketChunk(packets[index]))
                //     }
                //     // promise.then(() => {
                //         // debug("BLEProtocol", "write", `Done sending packet ${index+1}/${packets.length}`);
                //     // });
                // });
            }
            else {
                console.warn('BLEProtocol', 'write', `Nothing to write...`);
                promise = Promise.resolve();
            }
            return promise
                .then(() => {
                this.packetSplitter = undefined;
            })
                .catch((err) => {
                this.packetSplitter = undefined;
                throw err;
            });
        });
    }
    /**
     *
     * @param data data chunk
     */
    onNewLwm2mPacket(data) {
        if (!this.packetBuilder) {
            debug(`Ignoring lwm2m packet 0x${bufferToHexString(data)} as not request has been made yet`);
            return;
        }
        this.packetBuilder.append(data);
        if (this.packetBuilder.hasAllChunks()) {
            if (this.packetBuilder.isChecksumValid()) {
                this.onNewLwm2mMessage(this.packetBuilder.getData());
            }
            else {
                let error = InvalidBleFrameChecksum.create(this.packetBuilder);
                this.onLwm2mDataChunkError(error);
            }
        }
    }
}
// TODO set an option instead ?
AbstractBleProtocol.RECEIVED_BUFFER_LENGTH = 255;

function sanitizeUUID(input) {
    return input.replace(/\-/g, '');
}

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());
    });
};
const DEFAULT_BLE_OPTIONS = {
    mtu: BleConfig.maxPacketLengthWithoutOffset + 1,
    maximumBufferLength: 255,
    waitForWriteAcknowledge: true,
    preferedComServiceType: 'large-frame',
    sanitizeUUID: false,
};
/**
 * BLE communication
 *
 * With ble communication, data is split into sub packets.
 * This class handles creation of packet chunks.
 *
 * - You must only implement the function to send one packet chunk writeLwm2mPacketChunk()
 * -
 */
class UniversalBleProtocolAdapter extends QueueComProtocol {
    constructor(peripheral, bleOptions = {}) {
        super();
        this.peripheral = peripheral;
        this._useSplitter = true;
        this.bleOptions = Object.assign(Object.assign({}, DEFAULT_BLE_OPTIONS), bleOptions);
        this.options.connect.timeout = 8000;
        this.options.send.timeout = 4000;
        this.options.disconnect.timeout = 8000;
        peripheral.stateChange.subscribe((newState) => {
            if (newState !== this.getConnectionState()) {
                this.setConnectionState(newState);
            }
        });
    }
    get lwm2mCharc() {
        if (!this._lwm2mCharc) {
            throw new Error(`Illegal state error, lwm2m characteristic is not set`);
        }
        return this._lwm2mCharc;
    }
    sanitizeUUID(uuid) {
        return this.bleOptions.sanitizeUUID ? sanitizeUUID(uuid) : uuid;
    }
    _connect() {
        return new Observable((emitter) => {
            (() => __awaiter(this, void 0, void 0, function* () {
                try {
                    yield this.peripheral.connect();
                    this._lwm2mCharc = yield this.setupLwm2mCharacteristic();
                    emitter.next(null);
                    emitter.complete();
                }
                catch (err) {
                    try {
                        yield this.peripheral.disconnect();
                    }
                    catch (err) {
                        debug('Failed to propertly disconnect after connection failed', err.message);
                    }
                    emitter.error(err);
                }
            }))();
        }).pipe(share());
    }
    _disconnect() {
        this._lwm2mCharc = undefined;
        if (this.peripheral) {
            return from(this.peripheral.disconnect());
        }
        return from(Promise.resolve());
    }
    setupLwm2mCharacteristic() {
        return __awaiter(this, void 0, void 0, function* () {
            const lwm2mServiceUUIDs = [
                this.sanitizeUUID(BleConfig.services.lwm2m.service),
            ];
            if (this.bleOptions.preferedComServiceType === 'large-frame') {
                lwm2mServiceUUIDs.push(this.sanitizeUUID(BleConfig.services.fastLwm2m.service));
            }
            const serviceMap = yield this.peripheral.discoverServices(lwm2mServiceUUIDs);
            debug('Found services ', Object.keys(serviceMap).join(', '), 'asked for services: ', lwm2mServiceUUIDs.join(', '));
            const charac = yield this._selectLwm2mCharacteristic(serviceMap);
            yield charac.enableNotifications(true);
            return charac;
        });
    }
    _selectLwm2mCharacteristic(serviceMap) {
        return __awaiter(this, void 0, void 0, function* () {
            const largeFrameServiceUUID = this.sanitizeUUID(BleConfig.services.fastLwm2m.service);
            const legacyLwm2mServiceUUID = this.sanitizeUUID(BleConfig.services.lwm2m.service);
            if (this.bleOptions.preferedComServiceType === 'legacy' &&
                serviceMap[legacyLwm2mServiceUUID]) {
                debug('Force usage of legacy lwm2m characteristic UUID: ' +
                    legacyLwm2mServiceUUID);
                return this._getLegacyLwm2mCharacteristic(serviceMap[legacyLwm2mServiceUUID]);
            }
            else if (serviceMap[largeFrameServiceUUID]) {
                debug('Found fast lwm2m characteristic UUID: ' + largeFrameServiceUUID);
                return this._getLargeFrameLwm2mCharacteristic(serviceMap[largeFrameServiceUUID]);
            }
            else if (serviceMap[legacyLwm2mServiceUUID]) {
                debug('Found legacy lwm2m characteristic UUID: ' + legacyLwm2mServiceUUID);
                return this._getLegacyLwm2mCharacteristic(serviceMap[legacyLwm2mServiceUUID]);
            }
            else {
                throw BleComError.serviceNotFound(legacyLwm2mServiceUUID);
            }
        });
    }
    _getLargeFrameLwm2mCharacteristic(service) {
        return __awaiter(this, void 0, void 0, function* () {
            this.bleOptions.mtu = 255; // TODO read from device
            this._useSplitter = false;
            return yield service.getCharacteristic(this.sanitizeUUID(BleConfig.services.fastLwm2m.charac));
        });
    }
    _getLegacyLwm2mCharacteristic(service) {
        return __awaiter(this, void 0, void 0, function* () {
            this.bleOptions.mtu = BleConfig.maxPacketLengthWithoutOffset + 1;
            this._useSplitter = true;
            return yield service.getCharacteristic(this.sanitizeUUID(BleConfig.services.lwm2m.charac));
        });
    }
    read() {
        return __awaiter(this, void 0, void 0, function* () {
            // debug('read()...');
            if (!this._readPromise) {
                this._readPromise = this._createReadPromise();
            }
            return this._readPromise;
        });
    }
    readUnit() {
        return __awaiter(this, void 0, void 0, function* () {
            return this.lwm2mCharc.data
                .pipe(first(), map((info) => info.data))
                .toPromise();
        });
    }
    write(data) {
        return __awaiter(this, void 0, void 0, function* () {
            // debug('write()...');
            this._readPromise = this._createReadPromise();
            if (this.useSplitter) {
                const chunks = BLEPacketSplitter.wrapWithChecksum(data, this.chunkSize).getPackets();
                for (const chunk of chunks) {
                    yield this.writeUnit(chunk);
                }
            }
            else {
                return this.writeUnit(data);
            }
        });
    }
    get useSplitter() {
        return this._useSplitter;
    }
    get chunkSize() {
        return this.bleOptions.mtu - 1;
    }
    writeUnit(data) {
        return __awaiter(this, void 0, void 0, function* () {
            if (data.length > this.bleOptions.mtu) {
                throw BleComError.writeSizeAboveMTU(data, this.bleOptions.mtu);
            }
            if (this.bleOptions.waitForWriteAcknowledge) {
                yield this.lwm2mCharc.write(data, true);
            }
            else {
                this.lwm2mCharc.write(data, true).catch((err) => {
                    console.warn(`Write error ignored`, err);
                });
            }
        });
    }
    _createReadPromise() {
        return __awaiter(this, void 0, void 0, function* () {
            // debug(`_createReadPromise()`);
            if (this.useSplitter) {
                const packetBuilder = new BLEPacketBuilder(this.bleOptions.maximumBufferLength);
                while (!packetBuilder.hasAllChunks()) {
                    const chunk = yield this.readUnit();
                    packetBuilder.append(chunk);
                }
                if (packetBuilder.isChecksumValid()) {
                    return packetBuilder.getData();
                }
                else {
                    throw BleComError.invalidBleChunkChecksum(packetBuilder);
                }
            }
            else {
                return this.readUnit();
            }
        });
    }
}

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

export { AbstractBleProtocol, BLEPacketBuilder, BLEPacketSplitter, BleComError, BleConfig, DEFAULT_BLE_OPTIONS, InvalidBleFrameChecksum, UniversalBleProtocolAdapter };
//# sourceMappingURL=iotize-tap-protocol-ble-common.js.map
