import debugFactory from 'debug';

const debug = debugFactory('serialport/binding-mock');
let ports = {};
let serialNumber = 0;
function resolveNextTick() {
    return new Promise(resolve => process.nextTick(() => resolve()));
}
class CanceledError extends Error {
    constructor(message) {
        super(message);
        this.canceled = true;
    }
}
const MockBinding = {
    reset() {
        ports = {};
        serialNumber = 0;
    },
    // Create a mock port
    createPort(path, options = {}) {
        serialNumber++;
        const optWithDefaults = Object.assign({ echo: false, record: false, manufacturer: 'The J5 Robotics Company', vendorId: undefined, productId: undefined, maxReadSize: 1024 }, options);
        ports[path] = {
            data: Buffer.alloc(0),
            echo: optWithDefaults.echo,
            record: optWithDefaults.record,
            readyData: optWithDefaults.readyData,
            maxReadSize: optWithDefaults.maxReadSize,
            info: {
                path,
                manufacturer: optWithDefaults.manufacturer,
                serialNumber: `${serialNumber}`,
                pnpId: undefined,
                locationId: undefined,
                vendorId: optWithDefaults.vendorId,
                productId: optWithDefaults.productId,
            },
        };
        debug(serialNumber, 'created port', JSON.stringify({ path, opt: options }));
    },
    async list() {
        debug(null, 'list');
        return Object.values(ports).map(port => port.info);
    },
    async open(options) {
        var _a;
        if (!options || typeof options !== 'object' || Array.isArray(options)) {
            throw new TypeError('"options" is not an object');
        }
        if (!options.path) {
            throw new TypeError('"path" is not a valid port');
        }
        if (!options.baudRate) {
            throw new TypeError('"baudRate" is not a valid baudRate');
        }
        const openOptions = Object.assign({ dataBits: 8, lock: true, stopBits: 1, parity: 'none', rtscts: false, xon: false, xoff: false, xany: false, hupcl: true }, options);
        const { path } = openOptions;
        debug(null, `open: opening path ${path}`);
        const port = ports[path];
        await resolveNextTick();
        if (!port) {
            throw new Error(`Port does not exist - please call MockBinding.createPort('${path}') first`);
        }
        const serialNumber = port.info.serialNumber;
        if ((_a = port.openOpt) === null || _a === void 0 ? void 0 : _a.lock) {
            debug(serialNumber, 'open: Port is locked cannot open');
            throw new Error('Port is locked cannot open');
        }
        debug(serialNumber, `open: opened path ${path}`);
        port.openOpt = Object.assign({}, openOptions);
        return new MockPortBinding(port, openOptions);
    },
};
/**
 * Mock bindings for pretend serialport access
 */
class MockPortBinding {
    constructor(port, openOptions) {
        this.port = port;
        this.openOptions = openOptions;
        this.pendingRead = null;
        this.isOpen = true;
        this.lastWrite = null;
        this.recording = Buffer.alloc(0);
        this.writeOperation = null; // in flight promise or null
        this.serialNumber = port.info.serialNumber;
        if (port.readyData) {
            const data = port.readyData;
            process.nextTick(() => {
                if (this.isOpen) {
                    debug(this.serialNumber, 'emitting ready data');
                    this.emitData(data);
                }
            });
        }
    }
    // Emit data on a mock port
    emitData(data) {
        if (!this.isOpen || !this.port) {
            throw new Error('Port must be open to pretend to receive data');
        }
        const bufferData = Buffer.isBuffer(data) ? data : Buffer.from(data);
        debug(this.serialNumber, 'emitting data - pending read:', Boolean(this.pendingRead));
        this.port.data = Buffer.concat([this.port.data, bufferData]);
        if (this.pendingRead) {
            process.nextTick(this.pendingRead);
            this.pendingRead = null;
        }
    }
    async close() {
        debug(this.serialNumber, 'close');
        if (!this.isOpen) {
            throw new Error('Port is not open');
        }
        const port = this.port;
        if (!port) {
            throw new Error('already closed');
        }
        port.openOpt = undefined;
        // reset data on close
        port.data = Buffer.alloc(0);
        debug(this.serialNumber, 'port is closed');
        this.serialNumber = undefined;
        this.isOpen = false;
        if (this.pendingRead) {
            this.pendingRead(new CanceledError('port is closed'));
        }
    }
    async read(buffer, offset, length) {
        if (!Buffer.isBuffer(buffer)) {
            throw new TypeError('"buffer" is not a Buffer');
        }
        if (typeof offset !== 'number' || isNaN(offset)) {
            throw new TypeError(`"offset" is not an integer got "${isNaN(offset) ? 'NaN' : typeof offset}"`);
        }
        if (typeof length !== 'number' || isNaN(length)) {
            throw new TypeError(`"length" is not an integer got "${isNaN(length) ? 'NaN' : typeof length}"`);
        }
        if (buffer.length < offset + length) {
            throw new Error('buffer is too small');
        }
        if (!this.isOpen) {
            throw new Error('Port is not open');
        }
        debug(this.serialNumber, 'read', length, 'bytes');
        await resolveNextTick();
        if (!this.isOpen || !this.port) {
            throw new CanceledError('Read canceled');
        }
        if (this.port.data.length <= 0) {
            return new Promise((resolve, reject) => {
                this.pendingRead = err => {
                    if (err) {
                        return reject(err);
                    }
                    this.read(buffer, offset, length).then(resolve, reject);
                };
            });
        }
        const lengthToRead = this.port.maxReadSize > length ? length : this.port.maxReadSize;
        const data = this.port.data.slice(0, lengthToRead);
        const bytesRead = data.copy(buffer, offset);
        this.port.data = this.port.data.slice(lengthToRead);
        debug(this.serialNumber, 'read', bytesRead, 'bytes');
        return { bytesRead, buffer };
    }
    async write(buffer) {
        if (!Buffer.isBuffer(buffer)) {
            throw new TypeError('"buffer" is not a Buffer');
        }
        if (!this.isOpen || !this.port) {
            debug('write', 'error port is not open');
            throw new Error('Port is not open');
        }
        debug(this.serialNumber, 'write', buffer.length, 'bytes');
        if (this.writeOperation) {
            throw new Error('Overlapping writes are not supported and should be queued by the serialport object');
        }
        this.writeOperation = (async () => {
            await resolveNextTick();
            if (!this.isOpen || !this.port) {
                throw new Error('Write canceled');
            }
            const data = (this.lastWrite = Buffer.from(buffer)); // copy
            if (this.port.record) {
                this.recording = Buffer.concat([this.recording, data]);
            }
            if (this.port.echo) {
                process.nextTick(() => {
                    if (this.isOpen) {
                        this.emitData(data);
                    }
                });
            }
            this.writeOperation = null;
            debug(this.serialNumber, 'writing finished');
        })();
        return this.writeOperation;
    }
    async update(options) {
        if (typeof options !== 'object') {
            throw TypeError('"options" is not an object');
        }
        if (typeof options.baudRate !== 'number') {
            throw new TypeError('"options.baudRate" is not a number');
        }
        debug(this.serialNumber, 'update');
        if (!this.isOpen || !this.port) {
            throw new Error('Port is not open');
        }
        await resolveNextTick();
        if (this.port.openOpt) {
            this.port.openOpt.baudRate = options.baudRate;
        }
    }
    async set(options) {
        if (typeof options !== 'object') {
            throw new TypeError('"options" is not an object');
        }
        debug(this.serialNumber, 'set');
        if (!this.isOpen) {
            throw new Error('Port is not open');
        }
        await resolveNextTick();
    }
    async get() {
        debug(this.serialNumber, 'get');
        if (!this.isOpen) {
            throw new Error('Port is not open');
        }
        await resolveNextTick();
        return {
            cts: true,
            dsr: false,
            dcd: false,
        };
    }
    async getBaudRate() {
        var _a;
        debug(this.serialNumber, 'getBaudRate');
        if (!this.isOpen || !this.port) {
            throw new Error('Port is not open');
        }
        await resolveNextTick();
        if (!((_a = this.port.openOpt) === null || _a === void 0 ? void 0 : _a.baudRate)) {
            throw new Error('Internal Error');
        }
        return {
            baudRate: this.port.openOpt.baudRate,
        };
    }
    async flush() {
        debug(this.serialNumber, 'flush');
        if (!this.isOpen || !this.port) {
            throw new Error('Port is not open');
        }
        await resolveNextTick();
        this.port.data = Buffer.alloc(0);
    }
    async drain() {
        debug(this.serialNumber, 'drain');
        if (!this.isOpen) {
            throw new Error('Port is not open');
        }
        await this.writeOperation;
        await resolveNextTick();
    }
}

export { CanceledError, MockBinding, MockPortBinding };
