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

const cameras = [];

//Создаёт событие принятия данных
const infoEmit = (info) => {
    emitter.emit('info', info);
};
//Создаёт событие ошибки
const errorEmit = (error) => {
    emitter.emit('error', error);
};
//Создаёт событие запуска стрима со всех камер
const startAllEmit = () => {
    emitter.emit('start-all');
};
//Создаёт событие остановки стрима со всех камер
const stopAllEmit = () => {
    emitter.emit('stop-all');
};


//Подписка на события
const on = (event, action) => {
    switch (event) {
        case 'info':
            emitter.on('info', (info) => action(info));
            break;
        case 'error':
            emitter.on('error', (error) => action(error));
            break;
        case 'start-all':
            emitter.on('start-all', () => action());
            break;
        case 'stop-all':
            emitter.on('stop-all', () => action());
            break;
        default:
            break;
    };
};

//Возвращает список подключенных камер
const getCameras = () => {
    return new Promise ((resolve, reject) => {
        if (!navigator) {
            reject('Использование модуля возможно только в браузере, необходим компонет Navigator');
            return;
        };

        try {
            // throw new Error('Бла бла бла');
            navigator.mediaDevices.enumerateDevices().then(devices => {
                //Пройдёмся по списку устройств и отсеем камеры
                const cameras = [];
                devices.forEach(device => {
                    if (device.kind === "videoinput" && device.label && device.deviceId) {
                        cameras.push({
                            name: device.label,
                            id: device.deviceId
                        });
                    };
                });
                resolve(cameras);
            }).catch(error => {
                reject(error);
            });
        } catch (error) {
            reject(error.message);
        };
    });
}

//Проверяет название и id камеры
const checkCamNameAndId = (camera) => {
    const camName = (camera && camera.name) ? camera.name : '';
    const camId = (camera && camera.id) ? camera.id : '';

    if (!camera || typeof camera !== 'object' || !camName || !camId) {
        let text = '';
        if (!camera || typeof camera !== 'object') {
            text = 'камера должна задаваться в виде объекта { name: "name", id: "id" }';
        } else {
            if (!camName) text = 'НАЗВАНИЕ НЕ ЗАДАНО название';
            if (!camId) text += text ? ' и id ' : 'не задан id';
            text += ' камеры';
        };

        return {
            status: false,
            text: `"${camName || 'НАЗВАНИЕ НЕ ЗАДАНО'}"${camId ? ` (${camId})` : ''}, ${text}`
        };
    } else {
        return {
            status: true,
            text: 'OK'
        };
    };        
}

//Ищет камеру по имени и идентификационному номеру
const findCamera = (camera) => {
        return new Promise ((resolve, reject) => {
        if (!navigator) {
            reject('Использование модуля возможно только в браузере, необходим компонет Navigator');
            return;
        };
    
        const checkCamStatus = checkCamNameAndId(camera);
        if (checkCamStatus.status) {
            try {
                navigator.mediaDevices.enumerateDevices().then(devices => {
                    //Пройдёмся по списку устройств и отсеем камеры с заданным названием
                    const cameras = [];
                    devices.forEach(device => {
                        if (device.kind === "videoinput" && device.label && device.deviceId) { //(device.label === name || device.deviceId === id)
                            cameras.push({
                                name: device.label,
                                id: device.deviceId
                            });
                        };
                    });
                    
                    const camerasByName = cameras.filter(elem => elem.name === camera.name); //Список камер с искомым названием
                    const camerasById = camerasByName.filter(elem => elem.id === camera.id); //Список камер с искомым названием и id
                    
                    //Если найдена только одна камера с искомым названием, возвращаем её
                    if (camerasByName.length === 1) resolve({
                        status: 'found',
                        camera: {
                            name: camerasByName[0].name,
                            id: camerasByName[0].id
                        },
                        info: `Камера "${camera.name || 'БЕЗ НАЗВАНИЯ'}"${camera.id ? ` (${camera.id})` : ''} обнаружена`
                    });

                    //Если камер с искомым названием несколько, но среди них есть камера с искомым id
                    if (camerasByName.length > 1 && camerasById.length === 1) resolve({
                        status: 'found',
                        camera: {
                            name: camerasById[0].name,
                            id: camerasById[0].id
                        },
                        info: `Камера "${camera.name || 'БЕЗ НАЗВАНИЯ'}"${camera.id ? ` (${camera.id})` : ''} обнаружена`
                    });

                    //Если найдены дубликаты искомой камеры
                    if (camerasById.length > 1) resolve({
                        status: 'dublicate',
                        camera: {
                            name: camerasById[0].name,
                            id: camerasById[0].id
                        },
                        info: `Камера "${camera.name || 'БЕЗ НАЗВАНИЯ'}"${camera.id ? ` (${camera.id})` : ''} найдена, но имеет дубликат с таким же id`
                    });

                    //Если к компьютеру не подключенна камера с искомым названием, сообщаем что камера не найдена
                    resolve({
                        status: 'not found',
                        camera: null,
                        info: `Камера "${camera.name || 'БЕЗ НАЗВАНИЯ'}"${camera.id ? ` (${camera.id})` : ''} не подключена к компьютеру`
                    });

                }).catch(error => {
                    reject(error.message);
                });
            } catch (error) {
                reject(error.message);
            };
        } else {
            reject({message: 'Не удалось найти камеру ' + checkCamStatus.text});
        };
    });  
}

// //Запускает стримы на всех подключенных камерах через метод "start"
// const startAll = () => {
//     try {
//         //Пройдёмся по всем стримам и остановим их
//         cameras.forEach(camera => {
//             //Включаем автоподключение к камере
//             camera.reconnect = true;

//             //Если стрим с заданой камеры уже запущен, просто возвращаем его
//             if (camera.stream && camera.stream.active === true) {
//                 this.#infoEmit(`Стрим с камеры "${camera.name}" (${camera.id}) уже запущен`);
//                 this.#streamEmit(camera.stream);
//                 return;
//             };

//             //Создаём стрим
//             this.#connect(camera);
//         });
//         startAllEmit();
//     } catch (error) {
//         errorEmit(`Не удалось запустить все стримы с камер. Ошибка: ${error.message}`);
//     };
// };

//Останавливает все стимы с камер
const stopAll = () => {
    try {
        //Пройдёмся по всем стримам и остановим их
        cameras.forEach(camera => {
            if (camera.stream.active) {
                //Останавливаем все треки стрима
                camera.stream.getTracks().forEach((track) => track.stop());
                //Убираем флаг переподключения стрима
                camera.reconnect = false;
                infoEmit(`Стрим с камеры "${camera.name}" (${camera.id}) успешно остановлен`);
            } else {
                infoEmit(`Стрим с камеры "${camera.name}" (${camera.id}) не запущен`);
            };
            //Убиваем таймер переподключения
            if (camera.reconnectTimer) clearTimeout(camera.reconnectTimer);
        });
        stopAllEmit();
    } catch (error) {
        errorEmit(`Не удалось остановить все стримы с камер. Ошибка: ${error.message}`);
    };
}


class Camera {

    #emitter;
    #camera;
    #stream;

    constructor() {
        this.#emitter = new emitterClass(); //транслятор событий внутри класса
        this.#camera = null; //объект названия и id камеры
        this.#stream = false; //запущен ли стрим с камеры
        emitter.on('start-all', () => { this.#stream = false }); //по остановки стрима на всех камерах, отмечаем что на нашеё камере стрим тоже отключился
    }

    //Создаёт событие принятия данных
    #infoEmit (info) {
        this.#emitter.emit('info', info);
    };
    //Создаёт событие ошибки
    #errorEmit (error) {
        this.#emitter.emit('error', error);
    };
    //Создаёт событие подключения
    #streamEmit = (stream) => {
        this.#emitter.emit('stream', stream);
    };
    //Создаёт событие потери подключения
    #disconnectEmit = (camera) => {
        this.#emitter.emit('disconnect', camera);
    };
    //Создаёт событие остановки стрима с камеры
    #stopEmit = (camera) => {
        this.#emitter.emit('stop', camera);
    };

    //Подписка на события
    on (event, action) {
        switch (event) {
            case 'info':
                this.#emitter.on('info', (info) => action(info));
                break;
            case 'error':
                this.#emitter.on('error', (error) => action(error));
                break;  
            case 'stream':
                this.#emitter.on('stream', (stream) => action(stream));
                break;
            case 'disconnect':
                this.#emitter.on('disconnect', (camera) => action(camera));
                break;
            case 'stop':
                this.#emitter.on('stop', (camera) => action(camera));
                break;
            default:
                break;
        };
    };


    //Подключение к камере
    start (camera) {
        if (!navigator) {
            this.#errorEmit('Использование модуля возможно только в браузере, необходим компонет Navigator');
            return;
        };

        const checkCamStatus = checkCamNameAndId(camera);
        if (checkCamStatus.status) {
            try {
                this.#camera = camera;

                //Проверяем, есть ли заданая камера в списке камер
                let camIndex = cameras.findIndex(c => c.name === camera.name && c.id === camera.id);
                if (camIndex === -1) {
                    //Если заданной камеры нет в списке, добавляем её
                    camIndex = cameras.length;
                    cameras.push({
                        name: camera.name,
                        id: camera.id,
                        reconnectTimer: null
                    });
                };

                //Включаем автоподключение к камере
                cameras[camIndex].reconnect = true;

                //Если стрим с заданой камеры уже запущен, просто возвращаем его
                if (cameras[camIndex].stream && cameras[camIndex].stream.active === true) {
                    this.#infoEmit(`Стрим с камеры "${camera.name}" (${camera.id}) уже запущен`);
                    this.#streamEmit(cameras[camIndex].stream);
                    return;
                };

                //Создаём стрим
                this.#connect(cameras[camIndex]);

                this.#stream = true;

            } catch (error) {
                this.#errorEmit(`Не удалось подключиться к камере "${camera.name}" (${camera.id}). Ошибка: ${error.message}`);
            };
        } else {
            this.#errorEmit(checkCamStatus.text);
        };
    }

    //Возвращает true, если запущен стрим с камеры
    isStarted () {
        const checkCamStatus = checkCamNameAndId(this.#camera);
        if (checkCamStatus.status) {
            //Найдём указанный стрим
            const camIndex = cameras.findIndex(c => c.name === this.#camera.name && c.id === this.#camera.id);
            if (camIndex > -1 && cameras[camIndex].stream && cameras[camIndex].stream.active && this.#stream) {
                return true;
            } else {
                return false;
            };
        } else {
            return false;
        };
    }

    //Возвращает стрим по событию "stream"
    #connect (camItem) {

        //Создаём стрим (микрофон для стрима берём по умолчанию системы)
        if (camItem.reconnect) navigator.mediaDevices.getUserMedia({ video: {deviceId: { exact: camItem.id }, width: { ideal: 4096 }, height: { ideal: 2160 } }, audio: true }).then(stream => {
            if (stream.active) {
                this.#infoEmit(`Стрим с камеры "${camItem.name}" (${camItem.id}) успешно запущен`);
                
                //Привязываем стрим к камере
                stream.camName = camItem.name;
                stream.camId = camItem.id;
                camItem.stream = stream;

                //И отдаём его
                this.#streamEmit(stream);

                //Вешаем событие переподключения на видеопоток при его обрыве
                const videoTracks = camItem.stream.getVideoTracks();
                if (videoTracks.length && videoTracks.length > 0) {
                    videoTracks[0].onended = () => {
                        this.#disconnectEmit({name: camItem.name, id: camItem.id});
                        //Убиваем все треки стрима
                        camItem.stream.getTracks().forEach((track) => track.stop());
                        //Если надо к нему переподключаться, запускаем таймер переподключения
                        if (camItem.reconnectTimer) clearTimeout(camItem.reconnectTimer);
                        camItem.reconnectTimer = setTimeout(() => {
                            this.#connect(camItem);
                        }, 1000);
                    };
                };
            } else {
                if (camItem.reconnectTimer) clearTimeout(camItem.reconnectTimer);
                camItem.reconnectTimer = setTimeout(() => {
                    this.#errorEmit(`Не удалось запустить стрим с камеры "${camItem.name}" (${camItem.id}). Следующая попытка через секунду`);
                    this.#connect(camItem);
                }, 1000);
            };
        }).catch(error => {
            if (camItem.reconnectTimer) clearTimeout(camItem.reconnectTimer);
            camItem.reconnectTimer = setTimeout(() => {
                this.#errorEmit(`Не удалось подключиться к камере "${camItem.name}" (${camItem.id}). Ошибка: ${error.message}`);
                this.#connect(camItem);
            }, 1000);
        });
    }
    
    //Останавливает стрим с камеры
    stop () {
        const camera = this.#camera;
        if (!camera) {
            this.#infoEmit(`Стрим не запущен`);
            return;
        };
        const checkCamStatus = checkCamNameAndId(camera);
        if (checkCamStatus.status) {
            try {
                //Найдём указанный стрим
                const camIndex = cameras.findIndex(c => c.name === camera.name && c.id === camera.id);
                if (camIndex > -1 && cameras[camIndex].stream && cameras[camIndex].stream.active) {
                    //Останавливаем все треки стрима
                    cameras[camIndex].stream.getTracks().forEach((track) => track.stop());
                    //Убираем флаг переподключения стрима
                    cameras[camIndex].reconnect = false;                    
                    this.#stopEmit({name: cameras[camIndex].name, id: cameras[camIndex].id});
                } else {
                    this.#infoEmit(`Стрим с камеры "${camera.name}" (${camera.id}) не запущен`);
                };
                //Убиваем таймер переподключения
                if (camIndex > -1 && cameras[camIndex].reconnectTimer) clearTimeout(cameras[camIndex].reconnectTimer);

                this.#stream = false;
            } catch (error) {
                this.#errorEmit(`Не удалось остановить стрим камеры "${camera.name || 'НАЗВАНИЕ НЕ ЗАДАНО'}"${camera.id ? ` (${camera.id})` : ''}. Ошибка: ${error.message}`);
            };
        } else {
            this.#errorEmit('Не удалось остановить стрим камеры ' + checkCamStatus.text);
        };
    }
}

module.exports = {
    on,
    getCameras,
    findCamera,
    stopAll,
    // startAll,
    Camera
}
