const SerialPort = require('serialport');
const childProcess = require('child_process');
const EventEmitter = require('events');
class emitterClass extends EventEmitter {};
const emitter = new emitterClass(); //транслятор событий

let watchdog = null; //COM-порт к которому подключен Watchdog
let connected = false; //Подключен ли модуль
let started = false; //Запущино ли вещание в модуль
let settings = ''; //Настройки модуля
let sendAliveInterval = null; //Интервал вещания в модуль
let connectInterval = null; //Интервал ожижания переподключения модуля

//Преобразует строку с настройками Watchdog'а в читабельный вид
const parseSettings = (data) => {
    try {
        if (typeof data === 'string' && data.length === 11) {
            const [
                waitTime, // Ожидание сигнала перезагрузки t1
                t2, // Длительность импульса сигнала "Reset" t2
                t3, // Длительность импульса сигнала "Power" t3
                t4, // Длительность ожиданий t4
                t5, // Длительность импульса сигнала "Power" t5
                channelMode1, // Режим канала: 1: 0-выкл, 1-RESET, 2-POWER, 3-Управлсемый(нач.сост.-открыт), 4-Управлсемый(нач.сост.-закрыт)
                channelMode2, // Режим канала: 2: 0-выкл, 1-RESET, 2-POWER, 3-Управлсемый(нач.сост.-открыт), 4-Управлсемый(нач.сост.-закрыт)
                resetCount, // Ограничение кол-ва перезагрузок. 0-нет ограничений
                channelMode3, // Режим канала 3 (Bx/Ln): 0-выкл, 1-дискретный вход, 3-вход датчика температуры ds18b20
                maxHeap1, maxHeap2// Пороговое значение температуры для автоматического перезапуска. Нктуально при канале 3 (Bx/Ln) = 3. в шестнадцатеричном формате, например: 32 градуса - 20, 80 градусов - 50, 00 - отключено
            ] = data;
            const maxHeap = maxHeap1 + maxHeap2;
            
            //Запоминает текущие настройки устройства
            settings = data;

            const props = `${settings}
                ${waitTime}  Ожидание сигнала перезагрузки (1 мин)
                ${t2}  Длительность импульса сигнала "Reset" (100 мс)
                ${t3}  Длительность импульса сигнала "Power" (1 с)
                ${t4}  Длительность ожиданий (1 с)
                ${t5}  Длительность импульса сигнала "Power" (100 мс)
                ${channelMode1}  Режим канала: 1: 0-выкл, 1-RESET, 2-POWER, 3-Управляемый (нач.сост.-открыт), 4-Управлсемый (нач.сост.-закрыт)
                ${channelMode2}  Режим канала: 2: 0-выкл, 1-RESET, 2-POWER, 3-Управляемый (нач.сост.-открыт), 4-Управлсемый (нач.сост.-закрыт)
                ${resetCount}  Ограничение кол-ва перезагрузок. 0-нет ограничений
                ${channelMode3}  Режим канала 3 (Bx/Ln): 0-выкл, 1-дискретный вход, 3-вход датчика температуры ds18b20
                ${maxHeap} Пороговое значение температуры для автоматического перезапуска. Неактуально при канале 3 (Bx/Ln) = 3. в шестнадцатеричном формате, например: 32 градуса - 20, 80 градусов - 50, 00 - отключено
            `;
            return props;
        } else {
            return `не удалось разобрать. Некорректный ответ: ${data}`;
        };
    } catch (error) {
        return `не удалось разобрать. Ошибка: ${error.message}`
    };
};

//Создаёт событие подключения для передачи в this.on
const connectEmit = () => {
    emitter.emit('connect');
};
//Создаёт событие потери подключения для передачи в this.on
const disconnectEmit = () => {
    emitter.emit('disconnect');
};
//Создаёт событие ошибки для передачи в this.on
const errorEmit = (error) => {
    emitter.emit('error', error);
};
//Создаёт событие принятия данных для передачи в this.on
const dataEmit = (data) => {
    emitter.emit('data', data);
};

//Проверяет действительно ли к порту подключен Watchdog
const testPort = (port) => {
    return new Promise ((resolve, reject) => {
        let awaitAnswerTimeout = null;
        port.on('data', (data) => {
            if (awaitAnswerTimeout) {
                clearTimeout(awaitAnswerTimeout);
                awaitAnswerTimeout = null;
                if (/~F/i.test(data.toString())) {
                    //Переводим ответ порта в строку и убираем название команды и перенос строки
                    resolve(data.toString().slice(2).replace(/\n+/g, ''));
                } else {
                    reject(`К порту "${port.path}" не подключено устройство Watchdog`);
                    port.close();
                };
            };
        });

        port.write('~F', (error) => {
            if (error) {
                reject(`Не удалось отправить сообщение на порт "${port.path}". Ошибка: ${error.message}`);
                port.close();
            } else {
                awaitAnswerTimeout = setTimeout(() => {
                    reject(`К порту "${port.path}" не подключено устройство Watchdog`);
                    port.close();
                    awaitAnswerTimeout = null;
                }, 2000);
            };
        }); 
    });
};
/**
* @param {string} portPath Путь COM-порта вида "COM0" для Windows или "/dev/ttyACM0" для Linux
*/
//Поиск COM-порта watchdog'а и подключение к нему
exports.connect = (portPath) => {
    clearInterval(sendAliveInterval);
    sendAliveInterval = null;
    clearInterval(connectInterval);
    connectInterval = setInterval(() => {
        findPort(portPath);
    }, 1000);
    connected = false;
};

const findPort = (portPath) => {
    if (connected) return;
    //Если порт задан, пытаемся подключиться к нему
    if (portPath) {
        watchdog = new SerialPort(portPath, { autoOpen: false });
        watchdog.open((error) => {
            if (error) {
                errorEmit(`Не удалось подключиться к порту "${portPath}". Ошибка: ${error.message}`);
                findPort();
            } else {
                testPort(watchdog).then(data => {
                    dataEmit(`Успешно подключились к порту watchdog (${portPath}). Настройки устройства: ${parseSettings(data)}`);
                    connecting(watchdog);
                    connectEmit();
                }).catch(error => {
                    errorEmit(error);
                });
            };
        });
    } else {
        //Иначе проходимся по всем портам и ищем "Watchdog"
        SerialPort.list().then(ports => {
            const port = process.platform === 'linux' ? ports.find(port => /Watchdog/i.test(port.pnpId)) : ports.find(port => /VID_0483&PID_A26D/i.test(port.pnpId));
            if (!port) {
                errorEmit('Устройство Watchdog не подключено к компьютеру');
                return;
            };
            portPath = port.path;
            watchdog = new SerialPort(portPath, { autoOpen: false });
            watchdog.open((error) => {
                if (error) {
                    errorEmit(`Модуль Watchdog найден на порту "${portPath}", однако подключиться к нему не удалось. Ошибка: ${error.message}`);
                } else {
                    testPort(watchdog).then(data => {
                        dataEmit(`Успешно подключились к порту watchdog (${port.vendorId}:${port.productId}) на порту ${portPath}. Настройки устройства: ${parseSettings(data)}`);
                        connecting(watchdog);
                        connectEmit();
                    }).catch(error => {
                        errorEmit(error);
                    });
                };
            });      
        }).catch(error => errorEmit(`Не удалось получить список COM-портов компьютера. Ошибка: ${error.message}`));
    };
};

exports.on = (event, action) => {
    switch (event) {
        case 'connect':
            emitter.on('connect', () => action());
            break;
        case 'disconnect':
            emitter.on('disconnect', () => action());
            break;
        case 'data':
            emitter.on('data', (data) => action(data));
            break;
        case 'error':
            emitter.on('error', (error) => action(error));
            break;    
        default:
            break;
    };
};

//Отправляет команду на модуль
const doCommand = (command) => {
    if (!watchdog || !connected) {
        errorEmit(`Не возможно отправить команду "${command}", Watchdog не подключен`);
        return;
    };

    dataEmit('=> ' + command);
    watchdog.write(command, (error) => {
        if (error) {
            errorEmit(`Не удалось отправить команду на порт "${watchdog.path}". Ошибка: ${error.message}`);
        };
    }); 
};

/**
* Настройки модуля принимаются в виде строки из 11 символов (0 - 9, A - F), где:

*    1 - Ожидание сигнала перезагрузки (1 мин*);

*    2 - Длительность импульса сигнала "Reset" (100 мс*);

*    3 - Длительность импульса сигнала "Power" (1 с*);

*    4 - Длительность ожиданий (1 с*);

*    5 - Длительность импульса сигнала "Power" (100 мс*);

*    6 - Режим канала: 1: 0-выкл, 1-RESET, 2-POWER, 3-Управлсемый(нач.сост.-открыт), 4-Управлсемый(нач.сост.-закрыт);

*    7 - Режим канала: 2: 0-выкл, 1-RESET, 2-POWER, 3-Управлсемый(нач.сост.-открыт), 4-Управлсемый(нач.сост.-закрыт);

*    8 - Ограничение кол-ва перезагрузок. 0-нет ограничений;

*    9 - Режим канала 3 (Bx/Ln): 0-выкл, 1-дискретный вход, 3-вход датчика температуры ds18b20;

*    10, 11 - Пороговое значение температуры для автоматического перезапуска. Неактуально при канале 3 (Bx/Ln) = 3. в шестнадцатеричном формате, например: 32 градуса - 20, 80 градусов - 50, 00 - отключено.

* *значения параметров 1-5 могут быть в диапазоне 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, A(10), B(11), C(12), D(13), E(14), F(15).
* @param {string} params Параметры настройки модуля
*/
//Задать настройки модулю
exports.setSettings = (params) => {
    if (typeof params !== 'string' || params.length !== 11 || 
        //Проверка что первая пятёрка в A-Z, 0-9
        !/^[A-F0-9]+$/.test(params.slice(0, 5)) ||
        //Проверка что 6, 7 в диапазоне 0-4
        !/^[0-4]+$/.test(params.slice(5, 7)) ||
        //Проверка что 8 в диапазоне 0-9
        !/^[0-9]+$/.test(params[7]) ||
        //Проверка что 9 в имеет значение 0, 1 или 3
        !/^[0, 1, 3]+$/.test(params[8]) ||
        //Проверка что последняя пара в A-Z, 0-9
        !/^[A-F0-9]+$/.test(params.slice(9, 11))) {
        errorEmit(`Настройки модуля (${params}) заданы в неверном формате. Смотрите описание функции`);
        return;
    };

    if (settings && settings === params) {
        dataEmit('=> ~W' + params);
        dataEmit(`<= ~F${params}\nНастройки устройства не изменились`); // + parseSettings(params)
    } else {
        doCommand('~W' + params);
    };  
};

//Запускает вещание в модуль
exports.start = (interval) => {
    if (!watchdog || !connected) {
        errorEmit(`Не возможно запустить вещание в модуль, Watchdog не подключен`);
        return;
    };

    clearInterval(sendAliveInterval);
    sendAliveInterval = setInterval(() => {
        doCommand('~U');
    }, (interval && parseInt(interval) > 0) ? interval * 1000 : 1000);
    started = true; 
};

//Прекращаяет вещание в модуль
exports.stop = () => {
    clearInterval(sendAliveInterval);
    sendAliveInterval = null;
    started = false;
    //Ставим модуль на паузу
    doCommand('~P1');
};

//Подключение к модулю Watchdog
const connecting = (watchdog) => {
    clearInterval(connectInterval);
    connectInterval = null;
    connected = true;

    //При подключении убиваем сервис который ставит модуль на паузу и даём комманду сняться с паузы
    childProcess.exec(`sudo systemctl stop watchdog-pause.service`, () => { });
    doCommand('~P0');

    watchdog.on('open', () => {
        connected = true;
        dataEmit(`Порт ${watchdog.path} открыт`);
        clearInterval(connectInterval);
        connectInterval = null;
        if (started) {
            this.start(1);
        };
    });

    watchdog.on('close', () => {
        if (connected) {
            errorEmit(`Связь с модулем Watchdog потеряна`);
            clearInterval(sendAliveInterval);
            sendAliveInterval = null;
            clearInterval(connectInterval);
            connectInterval = setInterval(() => {
                findPort(watchdog.path);
            });
        } else {
            dataEmit(`Порт ${watchdog.path} закрыт`);
        };
    });

    watchdog.on('data', (data) => {
        const result = data.toString().split('~');
        // console.log(result);
        result.forEach(answer => {
            if (answer[0]) switch (answer[0]) {
                case 'F':
                    const newSettings = answer.slice(1).replace(/\n+/g, '');
                    if (settings && settings !== newSettings) {
                        dataEmit(`<= ~${answer}Настройки устройства изменены: ${settings} -> ` + parseSettings(newSettings));
                    } else {
                        dataEmit('Настройки устройства: ' + parseSettings(newSettings));
                    };
                    break;
                case 'A':
                    dataEmit('<= ~' + answer);
                    break;
                case 'P':
                    dataEmit('<= ~' + answer);
                    if (answer[1] === '1' && started) doCommand('~P0');
                    break;
                case 'G':
                    dataEmit('Температура: ' + answer);
                    break;
                default:
                    errorEmit('Неверный формат ответа: ' + answer);
                    break;
            };
        });
    });
};