const faceApi = require('./face-api.min');
const fs = require('fs');
const path = require('path');

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

let camOptions = { //Настройки камеры по умолчанию
    rotate: 0, //поворот
    verticalInvert: false, //отражение по вертикали (слева направо)
    horizontalInvert: false //отражение по горизотали (сверху вниз)
};

const detector = {
    name: 'ssdMobilenetv1',
    started: false,
    options: new faceApi.SsdMobilenetv1Options()
};

let awaitDetectTimer = null; //Таймер заглушка на необьяснимое зависание в работе модуля "face-api" при определении лиц
let recognizeDelayTimer = null; //Таймер задержки следующего распознавания

let canvas = null;
let context = null;
let testFace = null;
if (document) {
    canvas = document.createElement('canvas'); //холст для снимков и поворотов при необходимости видео
    context = canvas.getContext('2d');
    testFace = document.createElement('img');
};

let usersDescriptors = []; //Массив дескрипторов с привязкой к именам
let faceMatcher = null; //Инструмент для поиска пользователей по дескрипторам лица
const currentPath = path.dirname(__filename);
let recognizeQuality = 0.5;

let initStartTime;
let recognizeTimeout = 5 * 1000; //секунд между распознаваниями

let initialized = false; //Была ли инициализация модуля
let started = false; //Было ли запущено распознавание

//Передача событий и данных из модуля
exports.on = (event, action) => emitter.on(event, data => action(data));

//Инициализация модуля распознавания лиц
exports.init = (descriptors = [], quality = 0.5, weightsPath = path.join(currentPath, 'weights')) => {
    emitter.emit('data', `Инициализация модуля распознавания лиц`);

    if (!document) {
        emitter.emit('error', `Модуль распознавания лиц работает только в браузере. Работа в NodeJS невозможна.`);
        return;
    };

    started = false;
    clearTimeout(awaitDetectTimer);
    awaitDetectTimer = null;
    clearTimeout(recognizeDelayTimer);
    recognizeDelayTimer = null;

    faceApi.env.monkeyPatch({
        Canvas: HTMLCanvasElement,
        Image: HTMLImageElement,
        ImageData: ImageData,
        Video: HTMLVideoElement,
        createCanvasElement: () => document.createElement('canvas'),
        createImageElement: () => document.createElement('img')
    });


    //Ожидание загрузки всех заданных моделей детекторов лиц
    const detectorsLoader = [];
    initStartTime = new Date();
    detectorsLoader.push(faceApi.nets.ssdMobilenetv1.loadFromDisk(weightsPath));
    detectorsLoader.push(faceApi.nets.tinyFaceDetector.loadFromDisk(weightsPath)); 
    detectorsLoader.push(faceApi.nets.faceLandmark68Net.loadFromDisk(weightsPath));
    detectorsLoader.push(faceApi.nets.faceRecognitionNet.loadFromDisk(weightsPath)); 
    Promise.all(detectorsLoader).then(() => {
        //Применяем дескрипторы для поиска пользователей
        if (Array.isArray(descriptors) && descriptors.length > 0) usersDescriptors = descriptors;
        recognizeQuality = quality;
        applyDescriptors(recognizeQuality);

        //Подставляем тестовое лицо для правильной инициализации
        testFace.onload = () => { startDetect(testFace, detector, camOptions, 0, 0); };
        testFace.src = path.join(currentPath, 'test-photo.png');
        testFace.id = 'test-face';
        document.body.appendChild(testFace);
    }).catch(error => {
        emitter.emit('error', `Не удалось загрузить модели модуля распознавания лиц. Ошибка: ${error.message}`);
    });
};

//Рассчитывает дескрипторы лица по фото
exports.calculateDescriptor = async (input) => { //, dom , Canvas, Image, ImageData
    if (!initialized) {
        emitter.emit('error', `Расчёт дескрипторов по фото невозможен. Необходимо инициализировать модуль`);
        return;
    };

    if (typeof HTMLElement === "object" ? 
        input instanceof HTMLElement : 
        input && typeof input === "object" && input !== null && input.nodeType === 1 && typeof input.nodeName==="string") {
        if (input.tagName === 'IMG' || input.tagName === 'CANVAS') {
            //Расчитываем дескрипторы по фото
            const startTime = new Date();
            try {
                let option = null;
                let optiionName = '';
                if (input.width <= 200 || input.height <= 200) {
                    option = new faceApi.TinyFaceDetectorOptions({ inputSize: 160 });
                    optiionName = 'TinyFaceDetector';
                } else {
                    option = new faceApi.SsdMobilenetv1Options();
                    optiionName = 'SsdMobilenetv1';
                };
                const faces = await faceApi.detectAllFaces(input, option).withFaceLandmarks().withFaceDescriptors();
                if (Array.isArray(faces) && faces.length > 0) {
                    emitter.emit('data', `Дескрипторы лица успешно расчитаны за ${new Date - startTime} мс с помощью "${optiionName}"`);
                    const maxWidth = Math.max.apply(Math, faces.map((face) => face.detection.relativeBox.width));
                    const descriptor = faces.find(face => face.detection.relativeBox.width === maxWidth).descriptor;
                    emitter.emit('calculated', descriptor);
                    return descriptor;
                } else {
                    emitter.emit('error', 'Не удалось расчитать дескрипторы лица');
                }
            } catch (error) {
                emitter.emit('error', 'Не удалось расчитать дескрипторы лица. Ошибка: ' + error.message);
            };

        } else {
            emitter.emit('error', `Модуль распознавания лиц принимает только HTML элементы с тегами "IMG" или "CANVAS". Распознавание лиц невозможено.`);
        };
    } else {
        emitter.emit('error', `Модуль распознавания лиц принимает только HTML элементы с тегами "IMG" или "CANVAS". Распознавание лиц невозможено.`);
    };
};

//Добавляет дескрипторы пользователю
exports.addDescriptor = (descriptor, name) => {
    try {
        if (!Array.isArray(usersDescriptors)) usersDescriptors = [];
        if (!name) name = 'Тестовый пользователь';
        //Если такой пользователь уже есть, добавляем дескрипторы ему, иначе добавляем нового пользователя
        const user = usersDescriptors.find(elem => elem.name === name);
        let descriptorsCount = 1;
        if (user) {
            user.descriptors.push(Array.from(descriptor));
            descriptorsCount = user.descriptors.length;
        } else {
            usersDescriptors.push({
                name: name,
                descriptors: [Array.from(descriptor)]
            });
        };
        emitter.emit('data', `Дескрипторы пользователя "${name}" успешно добавлены. Количество наборов: ${descriptorsCount}`);
        //Применяем дескрипторы для поиска пользователей
        applyDescriptors(recognizeQuality);
    } catch (error) {
        emitter.emit('error', `Не удалось добавить дескриптор пользователю "${name}". Ошибка: ${error.message}`);
    };
};

//Обновляет набор дескриптров пользователей
exports.updateDescriptors = (descriptors = []) => {
    if (Array.isArray(descriptors)) usersDescriptors = descriptors;
    applyDescriptors(recognizeQuality);
};

//Применяет дескрипторы для распознавания лиц
const applyDescriptors = (maxDescriptorDistance = 0.5) => {
    if (Array.isArray(usersDescriptors) && usersDescriptors.length > 0) {
        try {
            let labeledDescriptors = [];
            usersDescriptors.forEach(user => {
                if (user.name && Array.isArray(user.descriptors) && user.descriptors.length > 0) {
                    const userDescriptors = [];
                    user.descriptors.forEach(descriptor => {
                        if (Array.isArray(descriptor) && descriptor.length > 0) userDescriptors.push(descriptor);
                    });
                    if (userDescriptors.length > 0) labeledDescriptors.push(new faceApi.LabeledFaceDescriptors(user.name, userDescriptors.map((descriptor) => new Float32Array(descriptor))))
                } else {
                    emitter.emit('error', `не удалось применить дескрипторы пользователя "${user.name}". Данные не полны`);
                };
            });
            faceMatcher = new faceApi.FaceMatcher(labeledDescriptors, maxDescriptorDistance);
            emitter.emit('data', `Дескрипторы успешно применены`);
        } catch (error) {
            emitter.emit('error', `Не удалось применить дескрипторы. Ошибка: ${error.message}`);
        };
    } else {
        faceMatcher = null;
        emitter.emit('data', `Распознавание лиц невозможно. Не заданы дескрипторы лиц`);
    };
};

//Возвращает пользователя по дескриптору
exports.findUser = (descriptor) => {
    if (faceMatcher) {
        // const startTime = new Date();
        const user = faceMatcher.findBestMatch(descriptor);
        if (user._label && user._label !== 'unknown') {
            // emitter.emit('data', `Пользователь ${user} найден за ${new Date() - startTime} мс`);
            emitter.emit('user-found', user._label);
            return user._label;
        } else {
            return '';
        };
    };
};

const startDetect = async (input, detector, camOptions, i = 0, ms = 0) => {
    if (initialized && !started) return;

    const startTime = new Date();

    //Делаем снимок видеопотока и поворачиваем согласно настроек камеры
    const height = input.clientHeight;
    const width = input.clientWidth;

    if (initialized) {
        clearTimeout(awaitDetectTimer);
        awaitDetectTimer = setTimeout(() => {
            startDetect(input, detector, camOptions, ++i, ms);
        }, 5000);

        if (camOptions.rotate === 0 || camOptions.rotate === 180) {
            canvas.width = width;
            canvas.height = height;
        };
        if (camOptions.rotate === 90 || camOptions.rotate === 270) {
            canvas.width = height;
            canvas.height = width;
        };

        context.save();
        //Поварачиваем изображение на тот же угол, что и камера
        context.rotate(camOptions.rotate * Math.PI / 180);

        switch (camOptions.rotate) {
            case 0:
                if (camOptions.horizontalInvert) {
                    context.scale(-1, 1);
                    context.translate(-width, 0);
                };
                if (camOptions.verticalInvert) {
                    context.scale(1, -1);
                    context.translate(0, -height);
                };
                break;
            case 90:
                context.translate(0, -height);
                if (camOptions.horizontalInvert) {
                    context.scale(1, -1);
                    context.translate(0, -height);
                };
                if (camOptions.verticalInvert) {
                    context.scale(-1, 1);
                    context.translate(-width, 0);
                };
                break;
            case 180:
                context.translate(-width, -height);
                if (camOptions.horizontalInvert) {
                    context.scale(-1, 1);
                    context.translate(-width, 0);
                };
                if (camOptions.verticalInvert) {
                    context.scale(1, -1);
                    context.translate(0, -height);
                };
                break;
            case 270:
                context.translate(-width, 0);
                if (camOptions.horizontalInvert) {
                    context.scale(1, -1);
                    context.translate(0, -height);
                };
                if (camOptions.verticalInvert) {
                    context.scale(-1, 1);
                    context.translate(-width, 0);
                };
                break;    
            break;    
                break;    
            default:
                break;
        };

        context.drawImage(input, 0, 0, input.videoWidth, input.videoHeight, 0, 0, width, height);
    } else {
        canvas.width = width;
        canvas.height = height;
        context.drawImage(input, 0, 0, width, height, 0, 0, width, height);
    };

    let detectFaces = await faceApi.detectAllFaces(canvas, detector.options).withFaceLandmarks().withFaceDescriptors();
    const detectTime = new Date - startTime;
    clearTimeout(awaitDetectTimer);
    awaitDetectTimer = null;
    context.restore();

    if (initialized === true) {
        ms += detectTime;
        const faces = [];
        let face = null;
        if (Array.isArray(detectFaces) && detectFaces.length > 0) {
            //Находим самое ближнее лицо
            const maxWidth = Math.max.apply(Math, detectFaces.map((f) => f.detection.relativeBox.width));
            detectFaces.forEach(df => {
                if (df.detection.relativeBox.width === maxWidth) {
                    face = {
                        x: df.detection.relativeBox.x,
                        y: df.detection.relativeBox.y,
                        width: df.detection.relativeBox.width,
                        height: df.detection.relativeBox.height,
                        descriptor: df.descriptor,
                        name: df.descriptor ? this.findUser(df.descriptor) : ''
                    };
                } else {
                    faces.push({
                        x: df.detection.relativeBox.x,
                        y: df.detection.relativeBox.y,
                        width: df.detection.relativeBox.width,
                        height: df.detection.relativeBox.height,
                        descriptor: df.descriptor,
                        name: df.descriptor ? this.findUser(df.descriptor) : ''
                    });
                };
            });
        };
        if (started) emitter.emit('detected', {
            time: Math.round(ms / i),
            faces: faces,
            face: face
        });
    } else {
        initialized = true;
        const message = '\nssdMobilenetv1 \u2714\ntinyFaceDetector \u2714\nfaceLandmark68Net \u2714\nfaceRecognitionNet \u2714';
        emitter.emit('ready', `Модуль распознавания лиц успешно инициализирован. Все модели успешно загружены за ${new Date - initStartTime} мс: ${message}`);
        if (testFace) testFace.remove();
        return;
    }

    clearTimeout(recognizeDelayTimer);
    recognizeDelayTimer = setTimeout(() => {
        startDetect(input, detector, camOptions, ++i, ms);
    }, recognizeTimeout);
};

//Запускет обнаружение лиц из видео источника
exports.start = (video, options, timer = 0) => {
    if (!initialized) {
        emitter.emit('error', `Обнаружение лиц невозможно. Необходимо инициализировать модуль`);
        return;
    };

    if (started) {
        emitter.emit('data', `Обнаружение лиц уже запущено`);
        return;
    };

    if (video instanceof HTMLVideoElement) {
        if (typeof options !== 'object' || typeof options.rotate !== 'number' || typeof options.verticalInvert !== 'boolean' || typeof options.verticalInvert !== 'boolean') {
            emitter.emit('error', 'Неверно заданы настройки камеры. Взяты настройки камеры по умолчанию: ' + JSON.stringify(camOptions, null, '\t'));
        } else {
            camOptions = options;
            emitter.emit('data', `Настроки камеры: ${JSON.stringify(camOptions, null, '\t')}`);
        };

        if (timer || timer === 0) recognizeTimeout = timer * 1000;

        //Запуск определения лиц
        started = true;
        startDetect(video, detector, camOptions, 0, 0);
    } else {
        emitter.emit('error', `Обнаружение лиц невозможно. Необходимо инициализировать модуль`);
    };
};

//Осанавливает обнаружение лиц из видео источника
exports.stop = () => {
    if (started) {
        emitter.emit('data', `Обнаружение лиц успешно остановлено`);
    } else {
        emitter.emit('data', `Обнаружение лиц не запущено`);
    };
    started = false;
    clearTimeout(awaitDetectTimer);
    awaitDetectTimer = null;
    clearTimeout(recognizeDelayTimer);
    recognizeDelayTimer = null;
};