import { ConnectionState, ComProtocol } from '@iotize/tap/protocol/api';
import { Subject, defer, of, throwError } from 'rxjs';
import { createDebugger } from '@iotize/common/debug';
import { promiseTimeout } from '@iotize/common/promise';
import { TaskQueue } from '@iotize/common/task-manager';
import { share, timeoutWith, tap, finalize, switchMap } from 'rxjs/operators';

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

const TAG$1 = 'ComProtocol';
class AbstractComProtocol {
    constructor() {
        this.connectionState = ConnectionState.DISCONNECTED;
        this._options = {
            connect: {
                timeout: 1000,
            },
            disconnect: {
                timeout: 1000,
            },
            send: {
                timeout: 500,
            },
        };
    }
    get options() {
        return this._options;
    }
    set options(options) {
        this._options = options;
    }
    getConnectionState() {
        return this.connectionState;
    }
    isConnected() {
        return this.connectionState == ConnectionState.CONNECTED;
    }
    setConnectionState(connectionState) {
        debug(TAG$1, 'setConnectionState', ConnectionState[connectionState], 'Old: ', ConnectionState[this.connectionState]);
        let event = {
            newState: connectionState,
            oldState: this.connectionState,
        };
        this.connectionState = connectionState;
        if (this._connectionStateChange) {
            this._connectionStateChange.next(event);
        }
        return this;
    }
    /**
     * Must be implemented in childs
     */
    receiveStream() {
        throw new Error('Method not implemented.');
    }
    onConnectionStateChange() {
        if (!this._connectionStateChange) {
            this._connectionStateChange = new Subject();
        }
        return this._connectionStateChange;
    }
}

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 TAG = 'ComProtocol';
/**
 *
 */
class QueueComProtocol extends AbstractComProtocol {
    constructor() {
        super();
        this._jobQueue = new TaskQueue();
    }
    _send(data, options) {
        return defer(() => __awaiter(this, void 0, void 0, function* () {
            if (!this.isConnected()) {
                throw ComProtocol.Errors.notConnected({
                    protocol: this,
                });
            }
            let startTime = new Date();
            yield promiseTimeout(options.timeout, this.write(data), (info) => {
                return ComProtocol.Errors.timeoutError({
                    msg: `Communication timeout. Send request to device device timeout (${info.timeout}ms).`,
                    protocol: this,
                    timeout: info.timeout,
                    startTime: info.startTime,
                });
            });
            let elapsedTime = new Date().getTime() - startTime.getTime();
            let newTimeout = options.timeout - elapsedTime;
            const result = yield promiseTimeout(newTimeout, this.read(), (info) => {
                return ComProtocol.Errors.timeoutError({
                    msg: `Communication timeout. Read response from device timeout (${newTimeout}ms).`,
                    protocol: this,
                    timeout: newTimeout,
                    startTime: info.startTime,
                });
            });
            return result;
        })).pipe(share());
    }
    /**
     * Cancel pending requests
     */
    cancel() {
        debug(TAG, `Canceling operations`);
        this._jobQueue.cancelAll(ComProtocol.Errors.operationCanceled());
    }
    /**
     *
     * @param data
     * @param options
     */
    send(data, options) {
        // debug(TAG, `QueueComProtocol`, `::send() ENQUEUE request (${data.length} bytes) ${toHexString(data)}. Queue position: ${this._queue.size}`);
        if (true) {
            // Copy data ? TODO can be removed ?
            data = data.slice(0);
        }
        return this._jobQueue.addExecutor(() => {
            let timeout = options ? options.timeout : this._options.send.timeout;
            return this._send(data, {
                timeout: timeout,
            });
        });
    }
    /**
     * Connect with timeout
     *
     * If connect has already been called or is in progress / no further action
     *
     * @param options
     */
    connect(options) {
        if (this.connectionState === ConnectionState.CONNECTED) {
            debug(TAG, `QueueComProtocol is already connected`);
            return of(null);
        }
        if (this._connect$) {
            debug(TAG, `QueueComProtocol is already connecting...`);
            return this._connect$;
        }
        if (this._disconnect$) {
            return throwError(ComProtocol.Errors.operationInProgress(`Failed to connect as disconnection is in progress`));
        }
        let connectTimeout = options
            ? options.timeout
            : this._options.connect.timeout;
        this._connect$ = defer(() => {
            this.setConnectionState(ConnectionState.CONNECTING);
            let startTime = new Date();
            return this._connect(options).pipe(timeoutWith(connectTimeout, throwError(ComProtocol.Errors.timeoutError({
                msg: 'Protocol connection timeout',
                protocol: this,
                timeout: connectTimeout,
                startTime: startTime,
            }))));
        }).pipe(tap({
            complete: () => {
                this.setConnectionState(ConnectionState.CONNECTED);
            },
            error: () => {
                this.setConnectionState(ConnectionState.DISCONNECTED);
            },
        }), finalize(() => {
            this._connect$ = undefined;
        }), share());
        return this._connect$;
    }
    disconnect(options) {
        if (this.connectionState === ConnectionState.DISCONNECTED) {
            debug(TAG, `QueueComProtocol is already disconnected`);
            return of();
        }
        if (this._disconnect$) {
            return this._disconnect$;
        }
        if (this._connect$) {
            return throwError(ComProtocol.Errors.operationInProgress(`Failed to disconnect as connection is in progress`));
        }
        let disconnectTimeout = options
            ? options.timeout
            : this._options.disconnect.timeout;
        this._disconnect$ = defer(() => __awaiter(this, void 0, void 0, function* () {
            this.setConnectionState(ConnectionState.DISCONNECTING);
        })).pipe(switchMap(() => {
            let startTime = new Date();
            return this._disconnect(options).pipe(timeoutWith(disconnectTimeout, throwError(ComProtocol.Errors.timeoutError({
                msg: 'Protocol disconnection timeout',
                protocol: this,
                timeout: disconnectTimeout,
                startTime: startTime,
            }))), finalize(() => {
                this.setConnectionState(ConnectionState.DISCONNECTED);
                this._disconnect$ = undefined;
            }));
        }), share());
        return this._disconnect$;
    }
}

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

export { AbstractComProtocol, QueueComProtocol };
//# sourceMappingURL=iotize-tap-protocol-core.js.map
