/**
 * Created by Gifer on 07.08.2016.
 */

module.exports = function (electron, app, mainWindow, DbWork, Profile, shutdownPlugins) {
    "use strict";

    const mcconnect         = require('./mcconnect');
    const CMD               = mcconnect.CMD;
    const GetFirstSID       = mcconnect.GetFirstSID;
    const clientInformation = mcconnect.clientInformation;

    const remoteWnd         = electron.BrowserWindow;

    const console           = require('gifer-console');
    const Tray              = require('./tray');
    const os                = require('os');
    const fs                = require('fs');
    const urlParse          = require('url');
    const Service           = require("./service");
    const tokenStore        = require('./tokenStore');

    const child             = require('child_process').execFile;
    const _exec             = require('child_process').exec;
    const sendDataToServer  = require('./web_client_connect.js').UniversalParser;
    // const serverHWID        = require('./serverCMD').getServerHWID;
    const serverHWID        = mcconnect.getServerHWID;
    const McFtpClient       = require('./mcFtpClient');
    const directReceiveFile = require('./mcFtpServer');
    const downloadFile      = require('./tools/downloadFile')    (electron.dialog, mainWindow);
    const uploadFile        = require('./tools/uploadFile')      (electron.dialog, mainWindow);
    const directUploadFile  = require('./tools/directUploadFile')(electron.dialog, mainWindow);
    const imageCache        = require('./tools/imageCache');
    const parsePath         = require('path');
    const HOME_DIR          = Service.HOME_DIR;

    const ALLOK             = 'all ok';
    const CR                = '\r';
    const TERMINATOR        = '\u2022';
    const oneDay            = 1000*60*60*24;

    let getFileFromClipboard= null;
    let ftpConnect          = null;
    let LMSG                = null;
    let ErrorsText          = null;
    let ipList              = {};
    let commands            = {};
    let ClientSettings      = {};
    let fileWriter          = {};
    let idxInc              = 0;
    let __                  = '***';
    let previewWin          = null;
    let idleTimer           = null;
    let idleTime            = 0;
    let systemLocked        = false;
    let eList               = electron.powerMonitor .eventNames();
    var timerBadgeCount     = null;
    var msgCount            = 0;
    var notify              = null;
    var _FTP_Server_Test_   = null;

    /*try {
        getFileFromClipboard = require('./addons/clipboard/getClipboardFileList_' + os.platform() + '.node').GetFileFromClipboard;
        
        console.log("GetFileFromClipboard loaded!");
    } catch (e){
        console.log(e.message);
    }*/

    mcconnect.directSendElectronClient(function (data) {
        if (mainWindow && !mainWindow.isDestroyed()){
            mainWindow.webContents.send('sc_CMD', data);
        }
    });

    Tray.init(electron);

    Tray.setEvent('click', () => {
        showProgram();
    });

    // === PREPARE =============================================

    function _prepare() {
        new Promise(function (resolve) {
            resolve();
        })

        // === Mac top menu ===
        .then(function () {
            if (process.platform === 'darwin') {
                let _menu   = electron.Menu;
                let osxMenu = _menu.buildFromTemplate([
                    {
                        label: "Application",
                        submenu: [
                            { label: "About MyChat Client", selector: "orderFrontStandardAboutPanel:" },
                            { type: "separator" },
                            { label: "Quit", accelerator: "Command+Q", click: function() {
                                closeProgram();
                            }}
                        ]
                    },

                    {
                        label: "Edit",
                        submenu: [
                            { label: "Undo", accelerator: "CmdOrCtrl+Z", selector: "undo:" },
                            { label: "Redo", accelerator: "Shift+CmdOrCtrl+Z", selector: "redo:" },
                            { type: "separator" },
                            { label: "Cut", accelerator: "CmdOrCtrl+X", selector: "cut:" },
                            { label: "Copy", accelerator: "CmdOrCtrl+C", selector: "copy:" },
                            { label: "Paste", accelerator: "CmdOrCtrl+V", selector: "paste:" },
                            { label: "Select All", accelerator: "CmdOrCtrl+A", selector: "selectAll:" }
                        ]
                    }
                ]);

                _menu.setApplicationMenu(osxMenu);
            }
        })

        // === Get Settings ===
        .then(function () {
            return DbWork.do.system.getAllSettings();
        })
        .then(function ([settings]) {
            ClientSettings                 = Service.MargeObj(ClientSettings, settings);
            ClientSettings.client_settings = JSON.parse(ClientSettings.client_settings);
            ClientSettings.ClientVersion   = MCServer.ClientVersion;

            uploadFile.setMode(ClientSettings.client_settings.SendFilesUploadMode);

            if (!ClientSettings.client_settings.SendFilesInputFilesDir || ClientSettings.client_settings.SendFilesInputFilesDir.indexOf("%s") !== -1){
                ClientSettings.client_settings.SendFilesInputFilesDir = "";
            }

            if (!ClientSettings.download_folder || ClientSettings.download_folder.indexOf("%s") !== -1){
                ClientSettings.download_folder = "";
            }

            ClientSettings.webPath = {
                AliasAdmin    : "admin",  // папка-псевдоним, путь к админке
                AliasAPI      : "API",    // папка-псевдоним, путь к Integration API
                AliasChat     : "chat",   // папка-псевдоним, путь к WEB-чату
                AliasFiles    : "files",  // папка-псевдоним, путь к файлам, залитым на сервер
                AliasForum    : "forum",  // папка-псевдоним, путь ко встроенному форуму
                AliasKanban   : "kanban", // папка-псевдоним, путь к канбан-доске

                LobbyEnable   : true, // включен доступ к списку сервисов MyChat
                ForumEnable   : true, // включить доступ ко встроенному форуму
                KanbanEnable  : true, // включить доступ к канбан-доске
                ChatEnable    : true, // включить доступ к WEB-чату
                AdminEnable   : true  // включить доступ к WEB-админке
            };

            __ = "&N*t^TN978N*7m&0a_8my87N))^*trr3eQZ2#CDVb79nm<JK>_Pl.,09UMynhn7BVd";
        })

        // === Get HardwareID ===
        // .then(function () {
        //     return DbWork.do.system.hardwareID.get();
        // })
        .then(function (/*[hwid]*/) {
            // if (!hwid || !hwid.hardware_id){
                // return new Promise(function (resolve) {
                //     Service.getHardwareID(function (newHwid) {
                        ClientSettings.hardware_id = global.myHWND; // newHwid;

                        console.log('Hardware ID:' + ClientSettings.hardware_id);

                        mcconnect.setHardwareID(ClientSettings.hardware_id);

                        // resolve(ClientSettings.hardware_id);
                    // });
                // });
            // } else {
            //     console.log('Hardware ID:' + hwid.hardware_id);
            //
            //     mcconnect.setHardwareID(hwid.hardware_id);
            // }
        })
        // .then(function (hardware_id) {
        //     hardware_id ? DbWork.do.system.hardwareID.set(hardware_id) : null;
        // })

        // === Set Download Folder ===
        .then(function () {
            return DbWork.do.system.downloadFolder.get();
        })
        .then(function ([path]) {
            ClientSettings.client_settings.SendFilesInputFilesDir =
            ClientSettings.download_folder = (
                ClientSettings.client_settings.SendFilesInputFilesDir ||
                path.download_folder ||
                (os.homedir() + HOME_DIR)
            ).replace(/\\/g, "/");
        })
        .then(function () {
            return createDefaultDownloadFolder();
        })

        // === Start FTP Server ===
        // .then(startFtpServer)     // вернул ----- FTP запускается при сохранении ID сервера после подключения, в этот момент сохраняется весь шаблон настроек

        // === GeneralMainWindow ===
        .then(function () {
            console.log("Set default folder: " + ClientSettings.download_folder);
                        
            if (ClientSettings.autoconnect){
                switch (ClientSettings.client_settings.GeneralMainWindow){
                    case "1": case 1:
                        hideProgram();
                    break;

                    case "2": case 2:
                        maximizeMinimize([true]);
                    break;
                }
            }
        })

        // ========================
        // .then(function () {
        //     modalWindow = new remoteWnd({
        //         autoHideMenuBar: true,
        //         alwaysOnTop    : false,
        //         frame : false,
        //         height: 200,
        //         width : 500,
        //         show  : true,
        //         icon  : `${global.__dirname}/client/modules/images/256x256.png`
        //     });
        //
        //     modalWindow.loadURL(`file://${global.__dirname}/client/modalWindow/modalWindow.html`);
        //     modalWindow.show();
        //     modalWindow.webContents.openDevTools();
        // })

        // === Catch Any Errors ===
        .catch(function (err) {
            if (err) {
                console.error(err.stack || err.message || err);
            }
        });
    }

    // Native контекстне меню (заміна electron-context-menu).
    // Підказки спелчекера, Cut/Copy/Paste, Copy для виділеного тексту,
    // Copy link для посилань, Save image для картинок (через downloadImage).
    function attachContextMenu(win) {
        win.webContents.on('context-menu', function (e, params) {
            const template = [];

            // Підказки спелчекера + "Add to dictionary" для невірного слова.
            if (params.misspelledWord) {
                params.dictionarySuggestions.forEach(function (suggestion) {
                    template.push({ label: suggestion, click: function () { win.webContents.replaceMisspelling(suggestion); } });
                });
                if (params.dictionarySuggestions.length) template.push({ type: 'separator' });
                template.push({
                    label: 'Add to dictionary',
                    click: function () { win.webContents.session.addWordToSpellCheckerDictionary(params.misspelledWord); }
                });
                template.push({ type: 'separator' });
            }

            if (params.isEditable) {
                template.push(
                    { role: 'cut',   enabled: params.editFlags.canCut },
                    { role: 'copy',  enabled: params.editFlags.canCopy },
                    { role: 'paste', enabled: params.editFlags.canPaste }
                );
            } else if (params.selectionText) {
                template.push({ role: 'copy' });
            }

            if (params.linkURL) {
                template.push({ label: 'Copy link', click: function () { electron.clipboard.writeText(params.linkURL); } });
            }

            // Превʼю-вікно: srcURL після переходу на blob — blob:/порожній, тому умова
            // привʼязана до win.__mcImage (server-URL там є завжди). Головне вікно — стара умова.
            if (win.__mcImage || (params.mediaType === 'image' && (params.srcURL || params.linkURL))) {
                // У чаті картинка — data:-мініатюра в <a href="реальний_url">,
                // тому для data: беремо linkURL (там справжня назва файла).
                const imgUrl = (/^data:/i.test(params.srcURL || '') && params.linkURL) ? params.linkURL : params.srcURL;

                if (template.length) template.push({ type: 'separator' });

                template.push({ label: 'Save image', click: function () {
                    if (win.__mcImage && win.__mcImage.cachePath){
                        saveImageFromCache(win); // файл уже в кеші — копія без мережі
                    } else
                    if (win.__mcImage){
                        downloadImage(win.__mcImage.url); // ще качається / fallback — старий шлях
                    } else {
                        downloadImage(imgUrl); // головне вікно — як раніше
                    }
                }});
            }

            if (template.length) {
                electron.Menu.buildFromTemplate(template).popup({ window: win });
            }
        });
    }

    function _prepareContext() {
        attachContextMenu(mainWindow);
    }

    _prepare();

    _prepareContext();

    // =========================================================

    function startFtpServer() {
        return new Promise(function () {
            if (_FTP_Server_Test_){
                return
            }

            let port  = 10000;
            let range = 100;

            if (ClientSettings.client_settings.SendFilesRandomPorts !== undefined){
                if (ClientSettings.client_settings.SendFilesRandomPorts){
                    try{
                        port  = parseInt(ClientSettings.client_settings.SendFilesRandomPortStart);
                    } catch (e) {}
                    try{
                        range = parseInt(ClientSettings.client_settings.SendFilesRandomPortEnd) - parseInt(ClientSettings.client_settings.SendFilesRandomPortStart);
                    } catch (e) {}
                } else {
                    try {
                        port  = parseInt(ClientSettings.client_settings.SendFilesBasePort);
                    } catch (e) {}
                    try {
                        range = parseInt(ClientSettings.client_settings.SendFilesDataPort) - parseInt(ClientSettings.client_settings.SendFilesBasePort);
                    } catch (e) {}
                }
            }

            stopFtp();

            _FTP_Server_Test_ = Service.isPortBusy(port, function success (newPort) {
                port = newPort;

                console.log('Start FTP server on ' + (ClientSettings.client_settings.SendFilesRandomPorts == 1 ? "dynamics" : "fixed") + " ports: " + port + '-' + (port + range));

                directReceiveFile.start({
                    startPort   : port,
                    endPort     : port + range,
                    onerror     : function () {
                        console.info("directReceiveFile onerror restart FTP");

                        setTimeout(startFtpServer, 1000);
                    }
                });

                _FTP_Server_Test_ = null;
            }, function fail (port, err) {
                console.err("Testing failed on port: " + port + "  Error: " + err.message);

                stopFtp();
            }, 500);
        });
    }

    function stopFtp() {
        if (directReceiveFile.isStarted()){
            console.info("Try stopFTP server");

            directReceiveFile.stop();
        }
    }

    function createDefaultDownloadFolder() {
        return new Promise(function (resolve, reject) {
            if (!ClientSettings.download_folder) {
                ClientSettings.client_settings.SendFilesInputFilesDir
                    = ClientSettings.download_folder
                    = (ClientSettings.client_settings.SendFilesInputFilesDir || (os.homedir() + HOME_DIR)).replace(/\\/g, "/");
            }
            
            fs.stat(ClientSettings.download_folder || "", function (err) {
                if (err){
                    switch (err.code){
                        case "EEXIST":
                            DbWork.do.system.downloadFolder.set(ClientSettings.download_folder);

                            resolve(ClientSettings.download_folder);
                        break;

                        case "ENOENT":
                            fs.mkdir(os.homedir() + "/Documents/", function (err) {
                                if (err && err.code !== "EEXIST"){
                                    // reject(err.message);
                                    console.err(err.message);
                                } else {
                                    console.log('Try to create folder: ' + ClientSettings.download_folder);

                                    try {
                                        fs.mkdirSync(ClientSettings.download_folder);

                                        DbWork.do.system.downloadFolder.set(ClientSettings.download_folder);
                                    } catch (e){
                                        // reject(e.message);
                                        console.err(e.message);
                                    }
                                }
                            });
                        break;
                    }
                } else {
                    DbWork.do.system.downloadFolder.set(ClientSettings.download_folder);

                    resolve(ClientSettings.download_folder);
                }
            });
        });
    }

    function mcLang(id, items, term){
        var res = LMSG[id.toString()];

        return (items) ? Service.myReplace(res, items, term) : res;
    }

    function mcError(id, items, term){
        let res = ErrorsText[id.toString()];

        return (items) ? Service.myReplace(res, items, term) : res;
    }

    function findIdx(ipOrId, port) {
        let ID = null;

        for (let id in ipList){
            if ((ipList[id].ServHost == ipOrId && ipList[id].Port == port) || (!port && ipList[id].ServerID == ipOrId)) {
                ID = id;

                break;
            }
        }

        return ID;
    }

    function getServerID(ID) {
        if (ID) for (let id in ipList){
            if (ipList[id].ID === ID) {
                return ipList[id].ServerID || null;
            }
        }

        return null;
    }

    let pluginsStopped = false;

    async function closeProgram() {
        // console.immediately("closeProgram()\n" + __stack.get().toString().replace(/,/g, '\n'));

        global.isBrodcast = false;

        app.closeNow = true;

        directReceiveFile && directReceiveFile.stop();

        if (!pluginsStopped && shutdownPlugins){
            pluginsStopped = true;

            try {
                await shutdownPlugins();
            } catch (e) {
                console.error("shutdownPlugins: " + e.message);
            }
        }

        mainWindow.close();
    }

    function relaunch() {
        console.immediately("=============== RESTART FOR UPDATE ===================");

        let args = [];

        for (let i = 1; i < process.argv.length; i++){
            if (process.argv[i] !== CMD.updateConsoleCMD
                && process.argv[i] !== CMD.normalStart
                && process.argv[i] !== CMD.afterCopyArg
                && process.argv[i] !== ""){
                args.push(process.argv[i]);
            }
        }

        args.push(CMD.updateConsoleCMD);

        app.relaunch({args: args});

        setTimeout(closeProgram, 500);
    }

    function hideProgram() {
        // console.immediately("hideProgram()");

        app.closeNow = false;

        mainWindow.close();
    }

    function showProgram(noFocus) {
        if (mainWindow && !mainWindow.isDestroyed()){
            // console.log("Tray click show");

            if (noFocus) {
                mainWindow.showInactive();
            } else {
                mainWindow.show();
            }

            if (mainWindow.isMinimized()){
                mainWindow.restore();
            }
            
            // if (!noFocus) {
            //     mainWindow.focus();
            // }
        }
    }

    function isWayland() {
        return os.platform() === 'linux'
            && (process.env.XDG_SESSION_TYPE === 'wayland' || !!process.env.WAYLAND_DISPLAY);
    }

    function ec_send_error(cmd, params) {
        if (mainWindow && !mainWindow.isDestroyed()){
            mainWindow.webContents.send('ec_CMD', [
                CMD.ec_error,
                {
                    code  : cmd,
                    msg   : mcError(cmd, params || []),
                    params: params
                }
            ]);
        }
    }

    function sendComplete(cmd) {
        mainWindow.webContents.send('ec_CMD', [
            CMD.ec_complete_command,
            {
                Cmd: cmd
            }
        ]);
    }

    function checkIsServerURL(url) {
        return Service.MCServer.HTTPS && Service.MCServer.PortNode == 443
            ? (url.toLowerCase().indexOf('https://' + Service.MCServer.ipNode /*+ ':' + Service.MCServer.PortNode*/) === 0) ||
              (url.toLowerCase().indexOf('https://' + Service.MCServer.ipNode) === 0)
            : (!Service.MCServer.HTTPS && Service.MCServer.PortNode == 80
                ? (url.toLowerCase().indexOf('http://' + Service.MCServer.ipNode /*+ ':' + Service.MCServer.PortNode*/) === 0) ||
                  (url.toLowerCase().indexOf('http://' + Service.MCServer.ipNode) === 0)
                : url.toLowerCase().indexOf('http' + (Service.MCServer.HTTPS ? 's' : '') + '://' + Service.MCServer.ipNode /*+ ':' + Service.MCServer.PortNode*/) === 0
              )
        ;
    }

    // Чисте ім'я файла картинки для діалогу збереження:
    // прибирає query (?sid=...&uin=...&cid=...) і fragment, обробляє data:-URL.
    function imageFileName(url) {
        if (/^data:/i.test(url)) {
            const m = /^data:image\/([a-z0-9.+-]+)/i.exec(url);
            return 'image.' + (m ? m[1].toLowerCase().replace('jpeg', 'jpg') : 'png');
        }

        let name = Service.ExtractFileName(url.split('?')[0].split('#')[0]);

        try { name = decodeURI(name); } catch (e) { /* битий URI — лишаємо як є */ }

        return name || 'image.png';
    }

    function saveImageFromCache(win){
        let dwnld = require('./tools/downloadFile')(electron.dialog, win);

        dwnld.openDialogDownload("", win.__mcImage.fileName)
            .then(function (savePath) {
                fs.copyFile(win.__mcImage.cachePath, savePath, function (copyErr) {
                    if (copyErr){
                        console.error(copyErr.message);
                    }
                });
            })
            .catch(function (dialogErr) {
                if (dialogErr && dialogErr !== ALLOK) {
                    console.error(dialogErr.stack || dialogErr.message || dialogErr);
                }
            });
    }

    function escapeHTML(text){
        const entities = {
            '&': '&amp;',
            '<': '&lt;',
            '>': '&gt;',
            '"': '&quot;',
            "'": '&#39;'
        };

        return String(text).replace(/[&<>"']/g, function (ch) {
            return entities[ch];
        });
    }

    function downloadImage (url) {
        url = refreshFilesSid(url); // context-menu бере baked src — освіжаємо перед main-process GET

        if (!previewWin){
            // _prepareContext();

            previewWin = mainWindow;
        }

        let dwnld = require('./tools/downloadFile')(electron.dialog, previewWin);

        dwnld.openDialogDownload("", imageFileName(url))
            .then(function (path) {
                return downloadFile.download(url, path, ec_send_error);
            })
            .catch(function (err) {
                if (err && err !== ALLOK) {
                    console.error(err.stack || err.message || err);
                }
            })
        ;
    }

    function displayImage(url) {
        url = refreshFilesSid(url); // клік міг прийти зі старого лінка в історії — свіжий sid для download у кеш

        const fileName        = imageFileName(url); // справжнє імʼя з URL, без query і percent-encoding
        const escapedFileName = escapeHTML(fileName);

        // Локальне посилання на саме це вікно. Спільний previewWin перезаписується
        // при відкритті наступної картинки, тому обробники мають тримати власне вікно,
        // інакше кнопки/ESC старих вікон діятимуть на найновіше.
        const win = previewWin = new remoteWnd({
            toolbar        : false,
            autoHideMenuBar: true,
            frame          : false,
            transparent    : true,
            backgroundColor: '#00000000',
            title          : fileName,
            icon           : Profile.getProfilePath() + '/images/256x256.png',
            webPreferences : {
                nodeIntegration : true,
                contextIsolation: false
            }
        });

        win.setMenuBarVisibility(false);
        win.setMenu(null);
        win.maximize();

        // Стан зображення для IPC і контекстного меню «Save image»:
        // url є завжди, cachePath зʼявляється після успішного кешування.
        win.__mcImage = {
            url      : url,
            fileName : fileName,
            cachePath: null
        };

        const imageReady = imageCache.ensureCached(url)
            .then(function (localPath) {
                win.__mcImage.cachePath = localPath;

                return { path: localPath, url: url }; // url — для fallback приймача, якщо файл не прочитається
            })
            .catch(function (cacheErr) {
                console.log('imageCache fallback: ' + (cacheErr.message || cacheErr));

                return { url: url }; // показ із сервера, як раніше
            });

        // Шлемо після did-finish-load (приймач у скрипті вікна вже підписаний)
        // і по готовності кешу — обидві умови незалежні.
        win.webContents.once('did-finish-load', function () {
            imageReady.then(function (imageInfo) {
                if (!win.isDestroyed()){
                    win.webContents.send('previewImg', imageInfo);
                }
            });
        });

        // IPC канал для кнопок керування власного title bar.
        win.webContents.on('ipc-message', function (e, ch, cmd) {
            if (ch !== 'previewCtrl' || win.isDestroyed()) return;

            if      (cmd === 'close') win.close();
            else if (cmd === 'min')   win.minimize();
            else if (cmd === 'max')   win.isMaximized() ? win.unmaximize() : win.maximize();
        });

        attachContextMenu(win);

        // HTML-обгортка замість прямого завантаження картинки.
        // Власний title bar (frame:false). Картинка 100% native, wheel = zoom від центру. ESC = закрити.
        const html = `<!DOCTYPE html><html><head><meta charset="utf-8"><title>${escapedFileName}</title>
<style>
    html, body { margin:0; padding:0; width:100%; height:100%; background:rgba(0,0,0,0.5); overflow:hidden;
        font-family:Segoe UI, sans-serif; color:#eee; }
    .titlebar { position:fixed; top:0; left:0; right:0; height:32px; display:flex; align-items:center;
        background:rgba(20,20,20,0.85); -webkit-app-region:drag; z-index:10; user-select:none; }
    .titlebar .name { flex:1; padding:0 12px; font-size:13px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
    .titlebar .btns { display:flex; -webkit-app-region:no-drag; }
    .titlebar .btn { width:46px; height:32px; display:flex; align-items:center; justify-content:center;
        cursor:pointer; font-size:14px; transition:background 0.1s; }
    .titlebar .btn:hover { background:#333; }
    .titlebar .btn.close:hover { background:#e81123; }
    .scroller { position:absolute; top:32px; left:0; right:0; bottom:0; overflow:auto;
        display:flex; align-items:center; justify-content:center; }
    img { display:block; user-select:none; -webkit-user-drag:none; cursor:zoom-in; }
    .spinner { width:48px; height:48px; border:4px solid rgba(255,255,255,0.25); border-top-color:#eee;
        border-radius:50%; animation:previewSpin 0.9s linear infinite; }
    @keyframes previewSpin { to { transform:rotate(360deg); } }
</style></head><body>
<div class="titlebar">
    <div class="name">${escapedFileName}</div>
    <div class="btns">
        <div class="btn"       id="btnMin"   title="Згорнути">&#x2014;</div>
        <div class="btn"       id="btnMax"   title="Розгорнути">&#x25A1;</div>
        <div class="btn close" id="btnClose" title="Закрити">&#x2715;</div>
    </div>
</div>
<div class="scroller" id="scroller">
    <div class="spinner" id="spinner"></div>
    <img id="pic" alt="" style="display:none">
</div>
<script>
    var ipc   = require('electron').ipcRenderer;
    var img   = document.getElementById('pic');
    var sc    = document.getElementById('scroller');
    var scale = 1;
    var nw    = 0;
    var nh    = 0;
    var min   = 0.20;
    var max   = 20;

    ipc.on('previewImg', function (e, imageInfo) {
        var spinner = document.getElementById('spinner');

        if (spinner){
            spinner.parentElement.removeChild(spinner);
        }

        if (imageInfo.path){
            // локальний файл кешу: fs доступний (nodeIntegration), blob обходить file://-обмеження data:-origin
            try {
                var fileBuffer = require('fs').readFileSync(imageInfo.path);

                img.src = URL.createObjectURL(new Blob([fileBuffer]));
            } catch (readErr) {
                img.src = imageInfo.url; // файл зник/битий між ensureCached і читанням — показ із сервера
            }
        } else {
            img.src = imageInfo.url; // fallback: показ із сервера
        }

        img.style.display = 'block';
    });

    function apply() {
        img.style.width  = (nw * scale) + 'px';
        img.style.height = (nh * scale) + 'px';

        sc.scrollLeft = (img.offsetWidth  - sc.clientWidth)  / 2;
        sc.scrollTop  = (img.offsetHeight - sc.clientHeight) / 2;
    }

    img.addEventListener('load', function () {
        nw = img.naturalWidth;
        nh = img.naturalHeight;
        var sw = sc.clientWidth;
        var sh = sc.clientHeight;
        if (nw > sw || nh > sh) {
            scale = Math.min(sw / nw, sh / nh);
        }
        apply();
    });

    document.addEventListener('wheel', function (e) {
        if (!nw) return;
        e.preventDefault();

        var stepCount = 1.20;
        var step = e.deltaY < 0 ? stepCount : 1 / stepCount;
        var ns   = Math.max(min, Math.min(max, scale * step));

        if (ns === scale) return;

        scale = ns;
        apply();
    }, { passive: false });

    document.addEventListener('keydown', function (e) { if (e.keyCode === 27) ipc.send('previewCtrl', 'close'); });

    document.getElementById('btnMin')  .addEventListener('click', function () { ipc.send('previewCtrl', 'min');   });
    document.getElementById('btnMax')  .addEventListener('click', function () { ipc.send('previewCtrl', 'max');   });
    document.getElementById('btnClose').addEventListener('click', function () { ipc.send('previewCtrl', 'close'); });
</script>
</body></html>`;

        win.loadURL('data:text/html;charset=utf-8,' + encodeURIComponent(html));
        win.show();
        win.focus();

        // Чистимо спільне посилання лише якщо воно ще вказує на це вікно.
        win.on('closed', () => { if (previewWin === win) previewWin = null; });
    }

    // Підставляє актуальні sid/uin/cid поточного підключення замість запечених у /files/-лінк.
    // Історія чату зберігає старі значення; після релогіну вони протухають → 409. Strip + re-add ідемпотентний.
    function refreshFilesSid (url) {
        if (!url || url.indexOf("sid=") === -1 || !Service.MCServer.SID) {
            return url;
        }

        var base = url.replace(/[?&]sid=[^&]*&uin=[^&]*&cid=[^&]*$/, "");

        if (base === url) {
            return url; // не наш формат — не чіпаємо
        }

        var glue = base.indexOf('?') === -1 ? '?' : '&';

        return base + glue +
               "sid=" + Service.MCServer.SID +
               "&uin=" + ClientSettings.uin +
               "&cid=" + Service.MCServer.CID;
    }

    async function handleRedirect (ev, url) {
        // if (url !== (this || mainWindow).webContents.getURL()) {
        if (url !== (this || mainWindow).getURL()) {
            if (ev){
                ev.preventDefault();
            }

            let myServer = Service.mcServerURL(url);

            if (url.indexOf("darwin://") === 0){
                let type = url.split("://")[1];

                if (type){
                    switch (type) {
                        case "mic":
                            _exec('open x-apple.systempreferences:com.apple.preference.security?Privacy_Microphone')
                        break;

                        case "cam":
                            await electron.shell.openExternal('x-apple.systempreferences:com.apple.preference.security?Privacy_Camera')
                        break;
                    }
                }

                return;
            } else
            // === MyChat FTP folders ============================
            if (url.indexOf("ftp://public") === 0) {
                sendDataToServer(CMD.cs_create_token, [
                    GetFirstSID(),
                    "ftp_public_token_" + ClientSettings.uin,
                    ""
                ].join(CR));

                return;
            } else
            if (url.indexOf("ftp://personal_") === 0) {
                let uin = url.replace("ftp://personal_", "").replace("/", "");

                sendDataToServer(CMD.cs_create_token, [
                    GetFirstSID(),
                    uin == ClientSettings.uin ? "ftp_personal_token_" + uin : "ftp_user_folder_read_only_token_" + uin + "_" + ClientSettings.uin,
                    ""
                ].join(CR));
                
                return;
            } else

            // ===================================================

            if (checkIsServerURL(url)){
                url = myServer + urlParse.parse(url).pathname + (urlParse.parse(url).search || urlParse.parse(url).hash || "");

                if (url.indexOf(myServer + '/' + ClientSettings.webPath.AliasKanban + '/') === 0){
                    mainWindow.webContents.send('ec_CMD', [
                        CMD.ec_open_local_kanban,
                        url
                    ]);

                    return;
                } else

                if (url.indexOf(myServer + '/' + ClientSettings.webPath.AliasForum + '/') === 0){
                    mainWindow.webContents.send('ec_CMD', [
                        CMD.ec_open_local_forum,
                        url
                    ]);

                    return;
                } else

                if (url.indexOf("/loadimg.gif") !== -1  || (url.indexOf(myServer + '/' + ClientSettings.webPath.AliasFiles + '/') === 0 && [".html", ".htm", ".css", ".php"].indexOf(parsePath.extname(url.split('?')[0]).toLowerCase()) !== -1)) {
                    // --- Lock a Download Link Event ---
                    // --- For Downloading Use "ce_file_open_or_download" or "ce_file_download_url" Events ---
                    if (url.indexOf(myServer + '/' + ClientSettings.webPath.AliasFiles + '/forum/') === 0 ||
                        url.indexOf(myServer + '/' + ClientSettings.webPath.AliasFiles + '/kanban/') === 0 ||
                        url.indexOf(myServer + '/' + ClientSettings.webPath.AliasFiles + '/public/') === 0){ // презентації форуму — відкриваємо в системному браузері
                        // open link
                    } else {
                        return;
                    }
                } else
                if (url.indexOf('/nofile.gif') === -1 && [".png", ".gif", ".jpeg", ".jpg", ".bmp", ".psd"].indexOf(parsePath.extname(url.toLowerCase().split('?')[0])) !== -1){
                    displayImage(url);

                    return;
                }
            }

            if ((url.toLowerCase().indexOf("ftp") === 0)){
                switch (os.platform()) {
                    case "win32":
                        setTimeout(() => child("explorer.exe", [url], (err) => {
                            if (err && err.code !== 1){
                                console.log(err);
                            }
                        }), 10);
                    break;

                    case "darwin":
                        child("open", [url], (err) => {
                            if (err && err.code !== 1){
                                console.log(err);
                            }
                        });
                    break;

                    case "linux":
                        child("xdg-open", [url], (err) => {
                            if (err && err.code !== 1){
                                console.log(err);
                            }
                        });
                    break;
                }
            } else {
                if (url.indexOf("local://") === 0 || url.indexOf("folder:") === 0){
                    if (os.platform() === "win32") {
                        url = url.replace("local://", "\\\\").replace(/\//g, "\\").replace("folder:", "");
                    } else {
                        url = url.replace("local://", "smb://");
                    }

                    var _path = Service.ExtractPath(url);

                    try {
                        electron.shell.openExternal(_path).catch(function (e) {
                            console.error("Error open url: " + _path);
                            console.error(e.message);

                            electron.shell.openPath(_path).catch(function (ee) {
                                console.error("Error open: " + _path);
                                console.error(ee.message);
                            });
                        });
                    } catch (e) {}
                } else
                if (url.indexOf("file:///") !== 0) {
                    try {
                        electron.shell.openExternal(url).catch(function (e) {
                            console.error("Error open url: " + url);
                            console.error(e.message);

                            electron.shell.openPath(url).catch(function (ee) {
                                console.error("Error open: " + url);
                                console.error(ee.message);
                            });
                        });
                    } catch (e) {}
                }
            }
        }
    }

    function getDownloadFolder(servId, fileInfo) {
        // === Get Download Folder For User ===
        return  fileInfo.downloadUpdate
            ? new Promise(function (resolve) {
                resolve(Profile.getProfilePath() + Service.getUpdateFilePath());
            })
            : DbWork.do.userPath.get(servId, ClientSettings.uin, fileInfo.uin)
                .then(function ([info]) {
                    return new Promise(function (resolve) {
                        let pathInfo = null;

                        if (info) {
                            try {
                                pathInfo = fs.statSync(info.path);
                            } catch (err) {
                                console.err(err.message);
                            }
                        }

                        if (pathInfo) {
                            resolve(info.path);
                        } else {
                            resolve();
                        }
                    });
                })

        .then(function (userFolder) {
            return userFolder
                ? userFolder
                : createDefaultDownloadFolder()
                    .then(function () {
                        return new Promise(function (resolve, reject) {
                            let userFolder = ClientSettings.download_folder + fileInfo.uin + " - " + (fileInfo.displayName || "") + "/";

                            fs.mkdir(userFolder, function (err) {
                                if (err && err.code !== "EEXIST"){
                                    reject(err.message);
                                } else {
                                    resolve(userFolder);
                                }
                            });
                        });
                    })
                ;
        })
        .catch(function (err) {
            if (err && err !== ALLOK) {
                console.error(err.stack || err.message || err);
            }
        });
    }

    function getServerHttpPath() {
        return (MCServer.HTTPS ? "https://" : "http://") + MCServer.ipNode + ":" + MCServer.PortNode;
    }

    function download(url, fileInfo, auto) { // --- If "auto=true" Then Save Dialog Wasn't Show ---
        let servId = getMyServerID();

        return getDownloadFolder(servId, fileInfo)

            // === Open Download Dialog ===
            .then(function (path) {
                let fileName = Service.ExtractFileName(fileInfo && fileInfo.filePath ? fileInfo.filePath : url);
                
                console.log("Start download file: " + fileName);

                return auto ? path : downloadFile.openDialogDownload(path || "", fileName);
            })

            // === Save a New Download Folder For User ===
            .then(function (path, cancel) {
                return new Promise(function (resolve, reject) {
                    if (path && !cancel){
                        if (fileInfo && !fileInfo.downloadUpdate) {
                            DbWork.do.userPath.set(servId, ClientSettings.uin, fileInfo.uin, Service.ExtractPath(path));
                        }

                        resolve(path);
                    } else {
                        reject(ALLOK); // -- Close Save Dialog Without Saving --
                    }
                });
            })

            // === Start Download ===
            .then(function (path) {
                if (fileInfo.downloadUpdate){
                    url = getServerHttpPath() + Service.getUpdateFilePath();
                    
                    console.log("Now download: " + os.platform() + " " + url);
                }

                return downloadFile.download(url, path, ec_send_error);
            })

            // === Save a File Download Info ===
            .then(function ([path, finalHash, totalLength]) {
                if (!fileInfo.downloadUpdate && fileInfo.hash){
                    DbWork.do.userFiles.set(servId, ClientSettings.uin, fileInfo.hash, fileInfo.fileDT, path);

                    if (finalHash !== fileInfo.hash){
                        console.error('Hash not match, file size: ' + totalLength);
                    }
                }
            })
            .catch(function (err) {
                if (err && err !== ALLOK) {
                    console.error(err.stack || err.message || err);
                }
            })
        ;
    }

    // Завантажити bundle files з відтворенням дерева каталогів у вибраній користувачем теці.
    function downloadBundleFiles(bundle) {
        let rayID = null; // ставиться перед стартом циклу; лишається null, якщо користувач скасував діалог

        if (!bundle || !bundle.Files) {
            return;
        }

        // діалог вибору каталога призначення
        return electron.dialog.showOpenDialog(mainWindow, {
            properties: ['openDirectory', 'createDirectory']
        }).then(function (result) {
            if (!result || result.canceled || !result.filePaths || !result.filePaths.length) {
                return; // користувач скасував
            }

            let base     = result.filePaths[0];
            let rootName = (bundle.Caption || ("bundle_" + bundle.BundleID)).replace(/[<>:"|?*\\/]+/g, "_"); // санітизуємо ім'я кореня
            let root     = parsePath.join(base, rootName);
            let rootAbs  = parsePath.resolve(root); // абсолютний корінь — еталон для перевірки виходу за межі

            fs.mkdirSync(root, { recursive: true });

            // спершу створюємо всі каталоги (зокрема порожні — їх не покриють файли)
            (bundle.Folders || []).forEach(function (folder) {
                let dir = parsePath.resolve(root, folder.Path || "");

                if (dir.indexOf(rootAbs) === 0) {
                    try { fs.mkdirSync(dir, { recursive: true }); } catch (err) {}
                }
            });

            let totalBytes = (bundle.Files || []).reduce(function (sum, file) { return sum + (file.Size || 0); }, 0);
            let doneBytes  = 0;

            // прогрес рахуємо на весь bundle: завершені файли + байти поточного
            function sendBundleProgress(name, current) {
                if (!mainWindow || mainWindow.isDestroyed()) {
                    return;
                }

                try {
                    mainWindow.webContents.send('ec_CMD', [
                        CMD.ec_file_download_progress,
                        {
                            size    : Service.format_size(totalBytes),
                            text    : Service.format_size(doneBytes + current),
                            percent : totalBytes ? ((doneBytes + current) * 100 / totalBytes).toFixed() : "100",
                            name    : name,
                            url     : "",
                            rayID   : rayID
                        }
                    ]);
                } catch (e) {}
            }

            rayID = Service.RandomHash(10);

            // один старт на весь bundle: renderer тримає один прогрес і покаже один "файл завантажено" в кінці
            mainWindow.webContents.send('ec_CMD', [
                CMD.ec_file_download_start,
                [ rayID, 'http' ]
            ]);

            // послідовно (promise-ланцюг) качаємо файли: 0-байтні створюємо локально, решту — через downloadFile
            return (bundle.Files || []).reduce(function (chain, file) {
                return chain.then(function () {
                    let dir    = parsePath.resolve(root, file.Path || "");
                    let target = parsePath.resolve(dir, file.Name);

                    if (target.indexOf(rootAbs) !== 0) { // захист від виходу за корінь
                        return;
                    }

                    fs.mkdirSync(dir, { recursive: true });

                    if (!file.Size) {
                        fs.writeFileSync(target, "");
                        return;
                    }

                    return downloadFile.download(file.Url, target, ec_send_error, {
                        silent    : true, // per-file події дали б окремий прогрес і toast на кожен файл
                        onProgress: function (received) {
                            sendBundleProgress(file.Name, received);
                        }
                    }).then(function () {
                        doneBytes += file.Size || 0;

                        sendBundleProgress(file.Name, 0);
                    });
                });
            }, Promise.resolve()).then(function () {
                mainWindow.webContents.send('ec_CMD', [ // єдине завершення на весь bundle
                    CMD.ec_file_download_complete,
                    rayID
                ]);

                electron.shell.openPath(root); // авто-reveal теки призначення після завантаження
                mainWindow.webContents.send('ec_CMD', [
                    CMD.ec_bundlefiles_downloaded,
                    { BundleID: bundle.BundleID, Root: root }
                ]); // renderer покаже кнопку "показати в каталозі"
            });
        }).catch(function (err) {
            // ланцюг обірвався — закриваємо прогрес, інакше renderer лишиться в стані DOWNLOAD
            if (rayID && mainWindow && !mainWindow.isDestroyed()) {
                try {
                    mainWindow.webContents.send('ec_CMD', [ CMD.ec_file_download_aborted, rayID ]);
                } catch (e) {}
            }

            console.error(err && (err.stack || err.message || err));
        });
    }

    function getMyServerID() {
        let id = ClientSettings.client_settings ? Service.StringToObj(ClientSettings.client_settings).ServID : null;

        // if (!id){
        //     console.error('Server ID is null!');
        // }

        return id;
    }

    function sortAB (a,b){
        return (a > b) ? 1 : ((a < b) ? -1 : 0);
    }

    function checkLogFile(dtFrom, dtTo, cb, list) {
        let currentDT = new Date(dtFrom);
        let filePath  = "";

        try {
            filePath = Profile.getProfilePath() + "/logs/" + Service.formatDate(currentDT, "yyyy/mm/dd/") + "node.log";
        } catch (e){}

        if (!list){
            list = {};
        }

        if (filePath){
            fs.stat(filePath, function (err) {
                if (!err){
                    list[dtFrom] = Service.loadFileSync(filePath).toString();
                }

                if (dtFrom < dtTo){
                    checkLogFile(dtFrom + oneDay, dtTo, cb, list);
                } else {
                    cb(list);
                }
            });
        }
    }

    function maximizeMinimize([maximize]) {
        if (maximize === true || !mainWindow.isMaximized()){
            mainWindow.maximize();
        } else {
            mainWindow.unmaximize();
        }
    }

    function kioskMode([value]) {
        mainWindow.setKiosk(value);
    }

    function idleHandler(autoAwayTime){
        idleTime = 0;

        clearInterval(idleTimer);

        idleTimer = setInterval(function () {
            if (!systemLocked) {
                let newTime = electron.powerMonitor.getSystemIdleTime();

                if (newTime >= ((autoAwayTime || 15) * 60)) { // 15 minutes
                    if (!idleTime) {
                        mainWindow.webContents.send('ec_CMD', [
                            CMD.ec_enter_to_idle
                        ]);
                    }

                    idleTime = newTime;
                } else {
                    if (idleTime) {
                        mainWindow.webContents.send('ec_CMD', [
                            CMD.ec_quit_from_idle
                        ]);
                    }

                    idleTime = 0;
                }
            }
        }, 2000);
    }

    // =========================================================

    function setMsgCount (count){
        msgCount = count;
    }

    function getMsgCount (str){
        return str ? (msgCount === 0 ? "" : msgCount.toString()) : msgCount;
    }
    
    // =========================================================

    // if (emList.indexOf('will-navigate') === -1 && emList.indexOf('new-window') === -1) {
        mainWindow.webContents.on('will-navigate', handleRedirect);

    if (typeof mainWindow.webContents.setWindowOpenHandler === "function"){
        mainWindow.webContents.setWindowOpenHandler(({url}) => {
            handleRedirect(null, url);

            return {action: 'deny'};
        });
    } else {
        mainWindow.webContents.on('new-window', handleRedirect); // deprecated, для старих Electron
    }

    // Chokepoint: усі renderer-GET на /files/ (inline-картинки, прев'ю, навігації) дістають
    // свіжі sid/uin/cid поточного підключення — лінк в історії може нести протухлі після релогіну.
    mainWindow.webContents.session.webRequest.onBeforeRequest(function (details, callback) {
        var fresh = refreshFilesSid(details.url);

        if (fresh !== details.url) {
            callback({ redirectURL: fresh });
        } else {
            callback({});
        }
    });

    if (eList.indexOf('lock-screen') === -1    && eList.indexOf('unlock-screen') === -1) {
        electron.powerMonitor.on('lock-screen', () => {
            systemLocked = true;

            mainWindow.webContents.send('ec_CMD', [
                CMD.ec_enter_to_idle
            ]);
        });

        electron.powerMonitor.on('unlock-screen', () => {
            systemLocked = false;

            mainWindow.webContents.send('ec_CMD', [
                CMD.ec_quit_from_idle
            ]);
        });
    }

    // commands.clearAllNotify = function (){
    //     eNotify.closeAll();
    // };

    // =========================================================

    commands[CMD.ce_restart_client] = relaunch;

    commands[CMD.ce_login_finish]   = function ([autoAwayTime]) {
        startFtpServer();

        let currentConnect = mcconnect.GetConnection();
        
        if (currentConnect){
            currentConnect.onDisconnect = function () {
                stopFtp();

                // розрив зʼєднання → прибираємо блимаючий конверт із трею
                if (Tray.isBlinking()) Tray.clearBlink();

                if (os.platform() === 'win32') mainWindow.flashFrame(false);
            };
        }

        idleHandler(autoAwayTime);
    };

    commands[CMD.ce_set_language]   = function ([lang]) { // data, res
        if (ftpConnect) {
            ftpConnect.close(true);
            ftpConnect = null;
        }

        Service.LoadJSONFile(`${MCPathes.Modules}electrontextsource/${lang}.errors.json`, function (data) {
            ErrorsText = Service.StringToObj(data);

            global._ErrorsText = ErrorsText;
        });

        Service.LoadJSONFile(`${MCPathes.Modules}electrontextsource/${lang}.system.json`, function (data) {
            LMSG = Service.StringToObj(data);

            if (LMSG) {
                let menu = [
                    { label: mcLang(3), click: function () { //  "3":"Открыть MyChat"
                            showProgram();
                        }},
                    { label: mcLang(2), click: function () { // "2":"Сменить пользователя",
                            mainWindow.webContents.send('ec_CMD', [
                                CMD.ec_logout
                            ]);

                            // mainWindow.reload(); //todo: remake
                        }},
                    { label: mcLang(1), click: function () { // "1":"Выйти из MyChat",
                            // let sID = GetFirstSID();
                            //
                            // if (sID){
                            //     sendDataToServer(CMD.cs_quit, sID);
                            // }
                            //
                            // closeProgram();

                            showProgram();

                            mainWindow.webContents.send('ec_CMD', [
                                CMD.ec_close_program
                            ]);
                        }}
                    /*,{ label: "DevTools", click: function () {
                     mainWindow.webContents.openDevTools();
                     }}*/
                ];

                Tray.trayMenu(menu);
            } else {
                console.error(`Fail load ${MCPathes.Modules}electrontextsource/${lang}.system.json`);
            }
        });
    };

    commands[CMD.ce_show_notify]    = function ([title, text, itemID, noBlink]) { // data, res
        if (!mainWindow.isFocused() || !mainWindow.isVisible() || mainWindow.isMinimized()){
            if (ClientSettings.client_settings.EventsShowPopupTrayWindow == 1){
                if (notify && notify.close)  {
                    notify.close();
                }

                notify = new electron.Notification({
                    title   : title,
                    body    : text,
                    silent  : true,
                    sound   : null,
                    icon    : global.__dirname + '/client/source/images/75x75.png'
                });

                notify.on("click",  function (){
                    if (!mainWindow.isVisible()){
                        mainWindow.show();
                    }

                    mainWindow.focus();

                    mainWindow.webContents.send('ec_CMD', [
                        CMD.ec_open_dialog,
                        { itemID }
                    ]);

                    notify.close();
                });

                notify.show();
            }

            if (!noBlink && !Tray.isBlinking() && ClientSettings.client_settings.EventsTrayBlinkOnTaskBar == 1) {
                Tray.blinkPost();
                
                if (os.platform() === 'win32') mainWindow.flashFrame(true);
            }
        }
    };

    commands[CMD.ce_hide_notify]    = function () { // data, res
        if (notify && notify.close) {
            notify.close();
            notify = null;
        }
    };

    commands[CMD.ce_quit_from_program] = function () { // data, res
        closeProgram();
    };

    commands[CMD.ce_hide_program] = function () { // data, res
        hideProgram();
    };

    commands[CMD.ce_hide_or_close_by_x] = function ([what]) { // data, res
        app.closeNow = what;
    };

    commands[CMD.ce_get_client_settings] = function () {
        mainWindow.webContents.send('ec_CMD', [
            CMD.ec_get_client_settings,
            ClientSettings
        ]);
    };

    // Обидва guard'и — ЗА ЗНАЧЕННЯМ, не за місцем виклику: renderer шле setAutoConnectInfo із
    // дев'яти різних точок, і жодну з них перелічувати в коді не можна
    commands[CMD.ce_save_autoconnect_server] = function ([pwd, rm, srvpwd, uin, nick]) {
        let chain;

        if (pwd === tokenStore.getSentinel()){
            // Повернули нашу ж заглушку — пароль на диску не чіпаємо
            chain = DbWork.do.system.saveAutoconnect(rm, uin, nick);
        } else
        if (pwd === "" && rm === false){
            // Галку знято: це тригер 3 Інвалідації повністю. Порядок жорсткий — синхронний cancel()
            // до будь-якого await, потім дочекатись зняття токена, і аж потім чистити пароль:
            // очищення поперед dropAll() піде через guard Ф6, побачить живий токен і зачепить нуль рядків
            tokenStore.cancel();

            chain = DbWork.do.reconnectToken.dropAll()
                .then(function () {
                    return DbWork.do.system.clearPwd();
                })
                .then(function () {
                    return DbWork.do.system.saveAutoconnect(rm, uin, nick);
                });
        } else {
            // Guard усередині методу: запис проходить лише якщо свого токена ще немає.
            // serv_id — саме MCServer.ServerID (серверна ідентичність), не getMyServerID()
            chain = DbWork.do.system.savePwd(Service.Encrypt(__, pwd || ""), Service.Encrypt(__, srvpwd || ""), MCServer.ServerID, uin)
                .then(function () {
                    return DbWork.do.system.saveAutoconnect(rm, uin, nick);
                });
        }

        chain
            .then(function () {
                ClientSettings.uin = uin;

                sendComplete(CMD.ce_save_autoconnect_server);
            })
            .catch(function (err) {
                if (err) console.err(err.stack || err.message || err);
            });
    };

    commands[CMD.ce_load_autoconnect_server] = function () { // data, res
        DbWork.do.system.loadPrvPwd()
            .then(function ([info]) {
                // Власний catch навколо читання: зовнішній catch обробника лише пише в лог і НЕ шле
                // ec_load_autoconnect_server, тож будь-який reject тут лишив би renderer чекати
                // назавжди — хоча пароль у system цілий і вхід можливий
                return DbWork.do.reconnectToken.get()
                    .catch(function (err) {
                        console.error("(token) can't read reconnect_token, falling back to password: " + (err && err.message ? err.message : err));

                        return [];
                    })
                    .then(function (rows) {
                        // Сире значення, ДО розшифровки. Акаунт без пароля відрізняється тут від
                        // стертого нами: перший зберігався через Service.Encrypt(__, "") і дає
                        // непорожній шифротекст, другий — літеральний "" від clearPwd (Ф6)
                        let hadStoredPwd = !!info.pwd;

                        try{
                            // Порожнє значення лишається порожнім і в Decrypt не йде: він AES-256-CBC
                            // і на порожньому вході кидає, а catch нижче кличе обробник позиційно
                            // проти масивної деструктуризації і тре uin/nick. Після Ф6 порожній
                            // пароль — норма для всіх
                            info.pwd    = info.pwd    ? Service.Decrypt(__, info.pwd)    : "";
                            info.srvpwd = info.srvpwd ? Service.Decrypt(__, info.srvpwd) : "";
                        } catch (e){
                            commands[CMD.ce_save_autoconnect_server](info.pwd, info.autoconnect, info.srvpwd, info.uin, info.nick);
                        }

                        // Умова саме «рядок є», а не «рядок для цього сервера»: у system немає
                        // серверної ідентичності взагалі. Живий рядок завжди один, а чужий serv_id
                        // відсіє пізній резолв у sc_hello
                        if (rows && rows.length){
                            info.pwd = tokenStore.getSentinel();
                        } else
                        if (!hadStoredPwd){
                            // Ні токена, ні збереженого пароля — автоконект тут шкідливий: renderer
                            // обирає стиль входу за наявністю пароля (Style = NO_PASS на порожньому,
                            // mcCmdParser.js), сервер відповідає 180 «неправильный тип аутентификации»,
                            // і користувач бачить модалку помилки замість форми логіна
                            info.autoconnect = 0;

                            console.log("(token) no token and no stored password - autoconnect disabled, login form expected", console.logLevel.INFO);
                        }

                        mainWindow.webContents.send('ec_CMD', [
                            CMD.ec_load_autoconnect_server,
                            info
                        ]);
                    });
            })
            .catch(function (err) {
                if (err) console.err(err.stack || err.message || err);
            });
    };

    commands[CMD.ce_set_client_settings] = function ([client_settings]) { // todo: добавить дополнительные проверки
        let data = Service.isString(client_settings) ? Service.convertToEntities(client_settings) : Service.convertToEntities(JSON.stringify(client_settings));

        ClientSettings.client_settings = Service.MargeObj(ClientSettings.client_settings, client_settings);
        ClientSettings.settings_crc32  = ClientSettings.client_settings.CRC32;
        
        ClientSettings.download_folder =
            ClientSettings.client_settings.SendFilesInputFilesDir =
            ClientSettings.client_settings.SendFilesInputFilesDir.replace(/\\/g, "/");

        uploadFile.setMode(ClientSettings.client_settings.SendFilesUploadMode);

        console.log('Save settings');

        DbWork.do.system.setClientSettings(data)
            .then(function () {
                return DbWork.do.system.downloadFolder.set(ClientSettings.download_folder);
            })
            .then(function () {
                return sendComplete(CMD.ce_set_client_settings);
            })
            .then(function () {
                return DbWork.do.system.setSettingsCRC32(ClientSettings.settings_crc32);
            })
            .then(function () {
                if (directReceiveFile && directReceiveFile.getPort() != ClientSettings.client_settings.SendFilesBasePort) {
                    console.log("Restart internal FTP");

                    directReceiveFile.stop();

                    startFtpServer();
                }
            })
            .catch(function (err) {
                if (err) console.err(err.stack || err.message || err);
            });
    };

    commands[CMD.ce_get_server_list] = function () { // data, res
        Promise.all([

            // 1 load ip's from db
            DbWork.do.servers.getList().then(function (dbServers) {
                dbServers.forEach(function (srv) {
                    let ID = findIdx(srv.IP, srv.port) || (Service.RandomHash(5) + idxInc++);

                    ipList[ID] = {
                        ID       : ID,            // уникальный идентификатор для списка в интерфейсе
                        ServerID : srv.rowid,
                        ServHost : srv.IP,
                        Port     : srv.port,
                        ServName : srv.name,
                        ServDesc : srv.description,
                        ServPass : srv.pwd,
                        AltHost  : srv.alternate_ip,
                        AltPort  : srv.alternate_port,
                        Secured  : srv.secure
                    }
                });
            }),

            // 2 get ip's list
            Service.getNetworkInterfacesList().map(function (info) {
                console.log(`Local IP: ${info.ip}, MAC: ${info.mac}`);

                return new Promise(resolve => {
                    Service.findMyChatServer(info.ip, function (serverInfo, serverHost) {
                        console.log("Find MyChat server: " + serverHost.address);

                        let ID = findIdx(serverHost.address, serverInfo.Port) || (Service.RandomHash(5) + idxInc++);

                        ipList[ID] = Service.MargeObj(ipList[ID] || {}, {
                            ID       : ID,
                            ServHost : serverHost.address
                        }, serverInfo);
                    }, resolve);
                })
            })
        ])
        .then(function () {
            mainWindow.webContents.send('ec_CMD', [
                CMD.ec_get_server_list,
                ipList
            ]);
        })
        .catch(function (err) {
            if (err) console.err(err.stack || err.message || err);
        });
    };

    commands[CMD.ce_test_server_ip] = function (data) {
        // data[0] == host, data[1] == port
        if (Service.isArray(data)){
            let testSocket = Service.testServerConnect.apply(null, data.concat([function (err, res) {
                mainWindow.webContents.send('ec_CMD', [
                    CMD.ec_test_server_ip,
                    [err, res]
                ]);

                try {
                    testSocket.destroy();
                } catch (e) {}

                testSocket = null;
            }, function () {
                try {
                    testSocket.destroy();
                } catch (e) {}

                testSocket = null;
            }]));
        } else {
            mainWindow.webContents.send('ec_CMD', [
                CMD.ec_test_server_ip,
                [{ code: "Incorrect data" }]
            ]);
        }
    };

    commands[CMD.ce_add_server] = function (srv) {
        let ip   = srv[0];
        let port = srv[1];

        if (srv.length === 9) { // remove 9-th element
            srv.pop();
        }

        DbWork.do.servers.serverExists(ip, port)
            .then(function ([res]) {
                if (res && res.chkID) {
                    let servID = findIdx(ip, port);

                    ec_send_error(CMD.errElectron.eDuplicateServer, [ip, port, servID]);

                    return Promise.reject();
                } else {
                    return DbWork.do.servers.addServer(srv);
                }
            })
            .then(function ([res]) { // if addServer
                let id = findIdx(ip, port) || (Service.RandomHash(5) + idxInc++);

                ipList[id] = {
                    ID       : id,
                    ServHost : srv[0], // sInfo.ServHost,
                    Port     : srv[1], // sInfo.Port,
                    ServName : srv[3], // sInfo.ServName,
                    ServDesc : srv[4], // "",
                    ServPass : srv[5], // sInfo.ServPwd,
                    ServerID : res.servID, // servID,
                    Secured  : srv[7]
                };

                mainWindow.webContents.send('ec_CMD', [
                    CMD.ec_server_added,
                    {
                        ServerID : res.servID,
                        ID       : id
                    }

                ]);
            })
            .catch(function (err) {
                if (err) console.err(err.stack || err.message || err);
            })
        ;
    };

    commands[CMD.ce_del_server] = function (data) {
        let srvID = data[0];

        DbWork.do.servers.delServer(srvID)
            .then(function () {
                let id = findIdx(srvID);

                if (id){
                    delete ipList[id];
                }

                sendComplete(CMD.ce_del_server);
            })
            .catch(function (err) {
                console.err(err.stack || err.message || err);
            });
    };

    commands[CMD.ce_modify_server] = function (srv) {
        let [ip, port, srvID] = [srv[0], srv[1], srv[srv.length - 1]];

        DbWork.do.servers.serverExists(ip, port, srvID)
            .then(function ([res]) {
                if (res && res.chkID){
                    ec_send_error(CMD.errElectron.eDuplicateServer, [ip, port]);

                    return Promise.reject();
                } else {
                    return DbWork.do.servers.saveServer(srv);
                }
            })
            .then(function () { // if saveServer
                let id = findIdx(srvID);

                if (id) {
                    ipList[id] = {
                        ID       : id,
                        ServHost : srv[0], // sInfo.ServHost,
                        Port     : srv[1], // sInfo.Port,
                        ServName : srv[3], // sInfo.ServName,
                        ServDesc : srv[4], // "",
                        ServPass : srv[5], // sInfo.ServPwd,
                        Secured  : srv[7],
                        ServerID : srv[8]  // servID,
                    };

                    mainWindow.webContents.send('ec_CMD', [
                        CMD.ec_server_saved
                    ]);
                } else {
                    ec_send_error(CMD.errElectron.eUnknownServerID, [srvID]);
                }
            })
            .catch(function (err) {
                if (err) console.err(err.stack || err.message || err);
            });
    };

    commands[CMD.ce_statistics_save] = function (data) {
        let [srvID, uin, name, stat] = data;
        let idSrv =  getServerID(srvID);

        DbWork.do.statistics.get(idSrv, uin)
            .then(function ([res]) {
                stat.last_update = stat.last_update || (new Date()).myFormat('yyyy-mm-dd hh:nn:ss');

                if (res && res.stat){
                    return DbWork.do.statistics.update(idSrv, uin, JSON.stringify(stat));
                } else {
                    return DbWork.do.statistics.add(idSrv, uin, JSON.stringify(stat));
                }
            })
            .then(function () {
                sendComplete(CMD.ce_statistics_save);
            })
            .catch(function (err) {
                if (err) console.err(err.stack || err.message || err);
            })
        ;
    };

    commands[CMD.ce_statistics_get] = function (data) {
        let [srvID, uin] = data;
        let idSrv =  getServerID(srvID);

        DbWork.do.statistics.get(idSrv, uin)
            .then(function ([res]) {
                let data = null;

                if (res && res.stat) {
                    try {
                        data = JSON.parse(res.stat);
                    } catch (e){
                        DbWork.do.userStorage.remove(idSrv, uin);
                    }
                }

                mainWindow.webContents.send('ec_CMD', [
                    CMD.ec_statistics_get,
                    data
                ]);
            })
            .catch(function (err) {
                if (err) console.err(err.stack || err.message || err);
            });
    };

    commands[CMD.ce_get_mc_client_info] = function () {
        let data = clientInformation();
        
        data.serverHWID = serverHWID();
        
        mainWindow.webContents.send('ec_CMD', [
            CMD.ec_get_mc_client_info,
            data
        ]);
    };

    commands[CMD.ce_break_blink] = function () {
        if (Tray.isBlinking()){
            Tray.clearBlink();
        }

        if (os.platform() === 'win32') mainWindow.flashFrame(false);
    };

    commands[CMD.ce_show_on_top] = function ([stay, isBroadcast]) {
        setTimeout(function () {
            // Wayland (Linux) забороняє застосунку піднімати себе:
            // setAlwaysOnTop/show/moveTop — no-op. Лишається тільки привернути увагу.
            if (isWayland()){
                mainWindow.flashFrame(true);
            } else {
                mainWindow.setAlwaysOnTop(true);
                mainWindow.show();

                if (!stay){
                    mainWindow.setAlwaysOnTop(false);
                }

                if (os.platform() === 'win32') mainWindow.flashFrame(true);
            }

            if (isBroadcast){
                global.isBrodcast = true;
            }
        }, 20);
    };

    commands[CMD.ce_disable_always_on_top] = function () {
        mainWindow.setAlwaysOnTop(false);
        global.isBrodcast = false;
    };

    commands[CMD.ce_hide_window] = function () {
        mainWindow.setAlwaysOnTop(false);
        mainWindow.hide();
        
        Tray.hide();
    };

    commands[CMD.ce_special_link_user_info] = function([userInfo]) {
        // Service.getLinkInfoByXY
    };

    commands[CMD.ce_console_log] = function([log, msg]) {
        console.important('[console.' + log + ']: ' + msg);
    };

    commands[CMD.ce_get_clipboard_files_list] = function() {
        if (getFileFromClipboard){
            let files = getFileFromClipboard();

            mainWindow.webContents.send('ec_CMD', [
                CMD.ec_get_clipboard_files_list,
                files
            ]);

            sendComplete(CMD.ce_get_clipboard_files_list);
        } else {
            console.warn('GetFileFromClipboard not loaded!');
        }
    };

    commands[CMD.ce_web_services_info] = function([list]) {
        if (list){
            ClientSettings.webPath = list;
        }
    };

    commands[CMD.ce_client_disconnected] = function() {
        Tray.clearBlink();

        ClientSettings.webPath = {
            AliasAdmin    : "admin",  // папка-псевдоним, путь к админке
            AliasAPI      : "API",    // папка-псевдоним, путь к Integration API
            AliasChat     : "chat",   // папка-псевдоним, путь к WEB-чату
            AliasFiles    : "files",  // папка-псевдоним, путь к файлам, залитым на сервер
            AliasForum    : "forum",  // папка-псевдоним, путь ко встроенному форуму
            AliasKanban   : "kanban", // папка-псевдоним, путь к канбан-доске

            LobbyEnable   : true, // включен доступ к списку сервисов MyChat
            ForumEnable   : true, // включить доступ ко встроенному форуму
            KanbanEnable  : true, // включить доступ к канбан-доске
            ChatEnable    : true, // включить доступ к WEB-чату
            AdminEnable   : true  // включить доступ к WEB-админке
        };
    };

    commands[CMD.ce_server_ports] = function([info]) {
        Service.MCServer = Service.MargeObj(Service.MCServer, info);
    };

    commands[CMD.ce_grant_access_media] = function([type]) {
        if (process.platform === 'darwin'){
            console.log("try ce_grant_access_media: " + type);

            switch (type){
                case "microphone":
                    electron.systemPreferences.askForMediaAccess('microphone').then(function (res) {
                        mainWindow.webContents.send('ec_CMD', [
                            CMD.ec_grant_access_media,
                            {
                                type,
                                res
                            }
                        ]);

                        console.log("result ce_grant_access_media: " + res);
                    });
                break;

                case "camera":
                    electron.systemPreferences.askForMediaAccess('camera').then(function (res) {
                        mainWindow.webContents.send('ec_CMD', [
                            CMD.ec_grant_access_media,
                            {
                                type,
                                res
                            }
                        ]);

                        console.log("result ce_grant_access_media: " + res);
                    });
                break;
            }
        } else {
            mainWindow.webContents.send('ec_CMD', [
                CMD.ec_grant_access_media,
                {
                    type,
                    res: true
                }
            ]);
        }
    };

    commands[CMD.ce_content_protection] = function([val]) {
        mainWindow.setContentProtection(!val);
    };

    // commands[CMD.ce_client_connected] = function([userInfo]) {
    //
    // };

    commands[CMD.ce_open_ftp_url] = function([url]) {
        handleRedirect(null, url);
    };

    commands[CMD.ce_set_title] = function([title]) {
        mainWindow.setTitle(title);
    };

    commands[CMD.ce_drop_connect] = function() {
        mcconnect.KILL_ALL_CONNECTION(true);
    };

    commands[CMD.ce_set_badge_count] = function([count]) {
        setMsgCount(count);

        if (!timerBadgeCount){
            timerBadgeCount = setTimeout(function () {
                if (process.platform === 'darwin' || (process.platform === 'linux' && app.isUnityRunning())) {
                    app.setBadgeCount(getMsgCount());

                    if (app.dock) {
                        try {
                            app.dock.setBadge(getMsgCount(true));
                        } catch (e) {
                            console.warn("app.dock.setBadge: " + e.message);
                        }
                    }
                }

                timerBadgeCount = null;
            }, 500);
        }
    };

    commands[CMD.ce_encode_qrcode_json] = function([data]) {
        let nt = Service.Base64.decode(mcconnect.getNT());

        try {
            data.Pwd       = data.rm                   ? Service.Decrypt(nt, data.Pwd || "")       : (data.Pwd || "");
            data.ServerPwd = data.ServerPwd && data.rm ? Service.Decrypt(nt, data.ServerPwd || "") : (data.ServerPwd || "");
        } catch (e){
            console.log(e.message);
            console.log(e.stack);

            if ((e.message.indexOf("Cipher functions:OPENSSL_internal:WRONG_FINAL_BLOCK_LENGTH") >= 0) ||
                (e.message.indexOf("The argument 'encoding' is invalid for data of length") >= 0)){
                console.log("Note: string maybe not encrypted");
            }
        }

        mainWindow.webContents.send('ec_CMD', [
            CMD.ec_encode_qrcode_json,

            [
                Service.EncryptBase64("3d9LKIJF$EGyncoi@uw5#ty9phsdfbrw560p", JSON.stringify(data)),
                MCServer.ServerID
            ]
        ]);
    };

    commands[CMD.ce_set_window_state] = function([state]) {
        if (!state.visible){
            hideProgram();
        } else
        if (state.minimize){
            mainWindow.minimize();
        } else 
        if (!state.focus) {
            mainWindow.blur();
        }
    };

    commands[CMD.ce_get_window_state] = function([state]) {
        if (!state.visible){
            hideProgram();
        } else
        if (state.minimize){
            mainWindow.minimize();
        } else
        if (!state.focus) {
            mainWindow.blur();
        }
    };

    commands[CMD.ce_toggle_min_max] = maximizeMinimize;
    
    commands[CMD.ce_toggle_kiosk]   = kioskMode;

    commands[CMD.ce_open_dev_tool]   = function (){
        mainWindow.webContents.openDevTools();
    };

    // --- Storage ------------------------

    commands[CMD.ce_clear_local_storage] = function (data) {
        /*let name = "dialog_list-";
        let [srvID, uin] = data;
        let idSrv =  srvID ? getServerID(srvID) : -1;

        uin = !uin ? -1 : uin;

        DbWork.do.userStorage.get(idSrv, uin, name)
        .then(function ([res]) {
            if (res && res.value) {
                return DbWork.do.userStorage.remove(idSrv, uin, name);
            } else {
                sendComplete(CMD.ce_clear_local_storage);
            }
        })
        .then(function () {
            sendComplete(CMD.ce_clear_local_storage);
        })
        .catch(function (err) {
            if (err) console.err(err.stack || err.message || err);
        });*/
    };

    commands[CMD.ce_storage_save]   = function (data) {
        let [srvID, uin, name, val] = data;
        let idSrv =  srvID ? getServerID(srvID) : -1;

        uin = !uin ? -1 : uin;

        DbWork.do.userStorage.get(idSrv, uin, name)
        .then(function ([res]) {
            if (res && res.value){
                return DbWork.do.userStorage.update(idSrv, uin, name, JSON.stringify(val));
            } else {
                return DbWork.do.userStorage.add(idSrv, uin, name, JSON.stringify(val));
            }
        })
        .then(function () { // if saveServer
            sendComplete(CMD.ce_storage_save);
        })
        .catch(function (err) {
            if (err) console.err(err.stack || err.message || err);
        });
    };

    commands[CMD.ce_storage_get]    = function (data) {
        let [srvID, uin, name] = data;
        let idSrv =  srvID ? getServerID(srvID) : -1;

        uin = !uin ? -1 : uin;

        DbWork.do.userStorage.get(idSrv, uin, name)
        .then(function ([res]) {
            let data = null;

            if (res && res.value) {
                try {
                    data = JSON.parse(res.value);
                } catch (e){
                    DbWork.do.userStorage.remove(idSrv, uin, name);
                }
            }

            mainWindow.webContents.send('ec_CMD', [
                CMD.ec_storage_get,
                {
                    data: data,
                    name: name
                }
            ]);
        })
        .catch(function (err) {
            if (err) console.err(err.stack || err.message || err);
        });
    };

    commands[CMD.ce_storage_remove] = function (data) {
        let [srvID, uin, name] = data;
        let idSrv =  srvID ? getServerID(srvID) : -1;

        uin = !uin ? -1 : uin;

        DbWork.do.userStorage.get(idSrv, uin, name)
        .then(function ([res]) {
            if (res && res.value) {
                return DbWork.do.userStorage.remove(idSrv, uin, name);
            } else {
                sendComplete(CMD.ce_storage_remove);
            }
        })
        .then(function () {
            sendComplete(CMD.ce_storage_remove);
        })
        .catch(function (err) {
            if (err) console.err(err.stack || err.message || err);
        });
    };

    // --- Profile ------------------------

    commands[CMD.ce_profile_save]   = function (data) {
        let [srvID, uin, change_id, profile] = data;
        let idSrv =  getServerID(srvID);

        uin = !uin ? -1 : uin;

        if (idSrv) {
            DbWork.do.profiles.get(idSrv, uin)
            .then(function ([res]) {
                if (res && res.profile){
                    return DbWork.do.profiles.update(idSrv, uin, change_id, JSON.stringify(profile));
                } else {
                    return DbWork.do.profiles.add(idSrv, uin, change_id, JSON.stringify(profile));
                }
            })
            .then(function () {
                sendComplete(CMD.ce_profile_save);
            })
            .catch(function (err) {
                if (err) console.err(err.stack || err.message || err);
            });
        } else {
            sendComplete(CMD.ce_profile_save);
        }
    };

    commands[CMD.ce_profile_get]    = function (data) {
        let [srvID, uin] = data;
        let idSrv        = getServerID(srvID);

        uin = !uin ? -1 : uin;

        DbWork.do.profiles.get(idSrv, uin)
        .then(function ([res]) {
            let profile   = null;
            let change_id = null;

            if (res && res.profile) {
                try {
                    profile   = JSON.parse(res.profile);
                    change_id = res.change_id;
                } catch (e){
                    DbWork.do.profiles.remove(idSrv, uin);
                }
            }

            mainWindow.webContents.send('ec_CMD', [
                CMD.ec_profile_get,
                {
                    profile  : profile,
                    change_id: change_id
                }
            ]);
        })
        .catch(function (err) {
            if (err) console.err(err.stack || err.message || err);
        });
    };

    commands[CMD.ce_profile_remove] = function (data) {
        let [srvID, uin] = data;
        let idSrv        = getServerID(srvID);

        uin = uin == void(0) ? -1 : uin;

        DbWork.do.profiles.get(idSrv, uin)
        .then(function ([res]) {
            if (res && res.profile) {
                return DbWork.do.profiles.remove(idSrv, uin);
            } else {
                sendComplete(CMD.ce_profile_remove);
            }
        })
        .then(function () {
            sendComplete(CMD.ce_profile_remove);
        })
        .catch(function (err) {
            if (err) console.err(err.stack || err.message || err);
        });
    };

    // --- Client Logs --------------------

    commands[CMD.ce_get_logs_list]  = function() {
        Service.FileWalker(Profile.getProfilePath() + "/logs/", function (_filesList) {
            let minDate   = "";
            let maxDate   = "";
            let sortData1 = "";
            let sortData2 = "";
            let sortData3 = "";
            let sortData4 = "";
            let filesList = [];

            _filesList.forEach(function (item) {
                if (item.data && item.data.length){
                    filesList.push(item);
                }
            });

            if (filesList && filesList.length){
                sortData1 = filesList[0].data;
                sortData2 = sortData1[0].data;
                sortData3 = filesList[filesList.length - 1].data;
                sortData4 = sortData3[sortData3.length - 1].data;

                if (sortData2 && sortData4) {
                    try {
                        filesList.sort(function (a, b) {
                            return sortAB(a.value, b.value);
                        });
                        sortData1.sort(function (a, b) {
                            return sortAB(a.value, b.value);
                        });
                        sortData2.sort(function (a, b) {
                            return sortAB(a.value, b.value);
                        });

                        sortData3.sort(function (a, b) {
                            return sortAB(a.value, b.value);
                        });
                        sortData4.sort(function (a, b) {
                            return sortAB(a.value, b.value);
                        });

                        minDate += filesList[0].value + ".";
                        minDate += sortData1[0].value + ".";
                        minDate += sortData2[0].value;

                        maxDate += filesList[filesList.length - 1].value + ".";
                        maxDate += sortData3[sortData3.length - 1].value + ".";
                        maxDate += sortData4[sortData4.length - 1].value;
                    } catch (e) {
                        console.error(e.message);
                    }
                }
            }

            mainWindow.webContents.send('ec_CMD', [
                CMD.ec_get_logs_list,
                {
                    filesList,
                    minDate,
                    maxDate
                },
            ]);
        });
    };

    commands[CMD.ce_get_logs_files] = function ([from, to]) {
        checkLogFile((new Date(from)).getTime(), (new Date(to)).getTime(), function (list) {
            mainWindow.webContents.send('ec_CMD', [
                CMD.ec_get_logs_files,
                list,
            ]);
        });
    };

    // =============================================
    // --- FTP File List In Client -----------------
    // =============================================

    // ce_ftp_upload              : '701C', //

    function closeFtpConnect(){
        if (ftpConnect){
            ftpConnect.close();
            ftpConnect = null;
        }
    }

    commands[CMD.ce_ftp_login]    = function ([login, pwd]) {
        let sendTimer = null;

        closeFtpConnect();

        ftpConnect = new McFtpClient(mainWindow, electron.dialog);

        ftpConnect.connect({
            host : Service.MCServer.ipNode,
            port : Service.MCServer.PortFTP,
            login: login,
            pwd  : pwd
        });

        ftpConnect.on(ftpConnect.const.LOGGED_IN, function () {
            console.log(" Login to FTP server");

            ftpConnect.PWD();
            ftpConnect.keepAlive();

            setTimeout(function () {
                sendComplete(CMD.ce_ftp_login);
            }, 10);
        });

        ftpConnect.on(ftpConnect.const.DEBUG, function (type, raw) {
            console.log((type === "SEND" ? ">" : "<") + raw, console.logLevel.L_Extended);
        });

        ftpConnect.on(ftpConnect.const.ERROR, function (err) {
            ec_send_error(CMD.errElectron.eFtpError, [err]);

            closeFtpConnect(true);

            console.log(err.stack || err.message || err)
        });

        ftpConnect.on(ftpConnect.const.PROGRESS, function () {
            if (!sendTimer) {
                sendTimer = setTimeout(function () {
                    sendTimer = null;

                    if (ftpConnect) {
                        mainWindow.webContents.send('ec_CMD', [
                            CMD.ec_file_download_progress,
                            ftpConnect.getProgress()
                        ]);
                    }
                }, 200);
            }
        });

    };

    commands[CMD.ce_ftp_list]     = function ([path]) {
        if (ftpConnect && ftpConnect.checkLogin()){
            ftpConnect.CWD(path)
            .then(function () {
                ftpConnect.fileList("/", function (err, list) {
                    if (err){
                        ec_send_error(CMD.errElectron.eFtpError, [err]);

                        console.err(err);
                    } else {
                        mainWindow.webContents.send('ec_CMD', [
                            CMD.ec_ftp_list,
                            list
                        ]);
                    }
                });
            })
            .catch(function (code, err) {
                ec_send_error(CMD.errElectron.eFtpError, [err, code]);
            });
        } else {
            ec_send_error(CMD.errElectron.eFtpNotConnected);
        }
    };

    commands[CMD.ce_ftp_download] = function ([path, fileName, size]) {
        if (ftpConnect && ftpConnect.checkLogin()){
            let rayID     = Service.RandomHash(10);

            ftpConnect.openDialog(path, fileName)
            .then(function ([savePath, canceled]) {
                if (savePath) {
                    mainWindow.webContents.send('ec_CMD', [
                        CMD.ec_file_download_start,
                        [ rayID, 'ftp', savePath ]
                    ]);
                }
                
                return !savePath || canceled ? "__skip__" : ftpConnect.download(path, savePath, size);
            })
            .then(function (skip) {
                if (skip !== "__skip__"){
                    mainWindow.webContents.send('ec_CMD', [
                        CMD.ec_file_download_progress,
                        ftpConnect.getProgress()
                    ]);

                    mainWindow.webContents.send('ec_CMD', [
                        CMD.ec_file_download_complete
                    ]);
                } else {
                    mainWindow.webContents.send('ec_CMD', [
                        CMD.ec_file_download_aborted
                    ]);
                }
            })
            .catch(function (err) {
                ec_send_error(CMD.errElectron.eFtpError, [err]);
                
                console.log(err.stack || err.message || err)
            });
        }
    };

    commands[CMD.ce_ftp_quit]     = function () {
        if (ftpConnect && ftpConnect.checkLogin()){
            console.log(" Close FTP connect");

            closeFtpConnect(true);

            sendComplete(CMD.ce_ftp_quit);
        }
    };

    commands[CMD.ce_ftp_abort]    = function () {
        if (ftpConnect && ftpConnect.checkLogin()){
            ftpConnect.abort();
        }
    };

    // =============================================

    commands[CMD.ce_get_user_folder] = function([uin, displayName]) {
        let servId = getMyServerID();

        getDownloadFolder(servId, { uin, displayName })
            .then(function (path) {
                if (path){
                    path = os.platform() === 'win32' ? path.replace(/\//g, '\\') : path.replace(/\\/g, '/');
                    
                    mainWindow.webContents.send('ec_CMD', [
                        CMD.ec_get_user_folder,
                        path
                    ]);
                }
            })
            .catch(function (err) {
                if (err && err !== ALLOK) {
                    console.error(err.stack || err.message || err);
                }
            })
        ;
    };

    commands[CMD.ce_file_set_new_folder_for_user] = function ([path]) {
        downloadFile.openDialogDownload(path || "", "", true)
            .then(function (data) {
                let newPath = data ? data[0] : null;
                
                if (newPath){
                    mainWindow.webContents.send('ec_CMD', [
                        CMD.ec_file_set_new_folder_for_user,
                        newPath
                    ]);
                }
            })
            .catch(function (err) {
                if (err && err !== ALLOK) {
                    console.error(err.stack || err.message || err);
                }
            })
        ;
    };

    commands[CMD.ce_file_download_abort] = function () {
        if (downloadFile){
            downloadFile.abortDownload();
        }
    };

    commands[CMD.ce_file_upload_start]   = function([filePath, okOptions]) {
        uploadFile.check(filePath, okOptions);//, ec_send_error);
    };

    commands[CMD.ce_file_upload_abort]   = function() {
        uploadFile.abortUpload();
    };

    commands[CMD.ce_file_upload_prepare_abort] = function() {
        uploadFile.abortUpload();
    };

    commands[CMD.ce_file_check_exist]    = function([exist, okOptions]) {
        uploadFile.Upload(exist, okOptions);
    };

    commands[CMD.ce_file_open_in_folder] = function([hash, uin, inFilePath]) {
        let servId = getMyServerID();

        DbWork.do.userFiles
            .get(servId, ClientSettings.uin, hash)
            .then(function ([info]) {
                return new Promise(function (resolve, reject) {
                    if (info && info.FullPathWithName){
                        electron.shell.showItemInFolder(info.FullPathWithName);

                        reject(ALLOK);
                    } else {
                        resolve();
                    }
                })
            })
            .then(function () {
                return DbWork.do.userPath.get(servId, ClientSettings.uin, uin);
            })
            .then(function ([info]) {
                if (inFilePath){
                    electron.shell.showItemInFolder(inFilePath);
                } else
                if (info && info.path){
                    electron.shell.showItemInFolder(info.path);
                }
            })
            .catch(function (err) {
                if (err && err !== ALLOK) {
                    console.error(err.stack || err.message || err);
                }
            })
        ;
    };

    commands[CMD.ce_open_logs_folder]    = function() {
        let now      = new Date();
        let dayPath  = Profile.getProfilePath() + "/logs/" + now.myFormat('yyyy') + "/" + now.myFormat('mm') + "/" + now.myFormat('dd');
        let rootLogs = Profile.getProfilePath() + "/logs/";
        let target   = fs.existsSync(dayPath) ? dayPath : rootLogs;

        // Windows ShellExecute не відкриває шлях зі слешами "/" — приводимо до нативних роздільників
        target = parsePath.normalize(target);

        // shell.openPath НЕ реджектить: при невдачі резолвиться непорожнім рядком-помилкою
        electron.shell.openPath(target).then(function (err) {
            if (err) console.error("ce_open_logs_folder.Error open: " + target + " :: " + err);
        });
    };

    commands[CMD.ce_file_open_folder]    = function([pathToFolder]) {
        try {
            let stat = fs.statSync(pathToFolder);

            if (stat.isDirectory()) {
                electron.shell.openPath(pathToFolder).catch(function (ee) {
                    console.error("Error open: " + pathToFolder);
                    console.error(ee.message);
                });
            } else {
                let ppp = Service.ExtractPath(pathToFolder);

                electron.shell.openPath(ppp).catch(function (ee) {
                    console.error("Error open: " + ppp);
                    console.error(ee.message);
                });
            }
        } catch (e) {
            if (!electron.shell.showItemInFolder(pathToFolder)){
                // let path = Service.ExtractPath(pathToFolder);
            }

            console.error("ce_file_open_folder.Error open: " + pathToFolder);
            console.error(e.message);
        }
    };

    commands[CMD.ce_file_download_url]   = function ([url, fileInfo, auto]) {
        download(url, fileInfo, auto);
    };

    commands[CMD.ce_bundlefiles_download] = function ([bundle]) {
        downloadBundleFiles(bundle);
    };

    commands[CMD.ce_bundlefiles_show_in_folder] = function ([path]) {
        if (path) {
            electron.shell.openPath(path); // відкрити теку призначення у провіднику ОС
        }
    };

    commands[CMD.ce_file_open_or_download] = function([url, fileInfo]) {
        let servId = getMyServerID();

        DbWork.do.userFiles
            // === Is File Exists? === Try To Run If Exists ===
            .get(servId, ClientSettings.uin, fileInfo.hash)
            .then(function ([info]) {
                return new Promise(function (resolve, reject) {
                    let fileStat   = null;
                    let pathToFile = info && info.FullPathWithName
                        ? info.FullPathWithName
                        : (fileInfo.uin == ClientSettings.uin
                                ? fileInfo.filePath
                                : ""
                        );

                    try{
                        fileStat = fs.statSync(pathToFile);
                    } catch (e){}

                    if (fileStat){
                        /*let ext = Service.ExtractFileExtension(info.FullPathWithName);
                         //todo: Добавить проверку на ехе файлы и выдать предупреждение
                         (Ext = '.EXE') or (Ext='.BAT') or (Ext='.REG') or (Ext='.CMD') or (Ext='.COM') or (Ext='.MSI')
                         or (Ext='.JS') or (Ext='.VBS') or (Ext='.SCR') or (Ext='.JSE') or (Ext='.WSH') or (Ext='.WSF')
                         or (Ext='.LNK')
                         if (['exe', 'cmd', 'com'].indexOf(ext) >=0 ){
                         }*/
                        
                        try {
                            electron.shell.openExternal(pathToFile).catch(function (e) {
                                electron.shell.openPath(pathToFile).catch(function (ee) {
                                    console.error("Error open: " + pathToFile);
                                    console.error(ee.message);
                                });
                            });
                        } catch (e) {
                            console.log(e.message);
                        }

                        reject(ALLOK);
                    } else {
                        resolve();
                    }
                });
            })
            
            .then(function () {
                return download(url, fileInfo);
            })
            .catch(function (err) {
                if (err && err !== ALLOK) {
                    console.error(err.stack || err.message || err);
                }
            })
        ;
    };

    // --- Direct Send ---

    commands[CMD.ce_file_direct_upload_prepare]  = function ([uin, getFromCB]) {  // todo: replace clipboard buffer
        directUploadFile
            .checkDirectUpload()
            .then(function (path) {
                return new Promise(function (resolve, reject) {
                    // let list = getFromCB ? getFileFromClipboard() : [];
                    //
                    // if (getFromCB){
                    //     if (list.length && list[0]){
                    //         resolve([list, Service.ExtractPath(list)]);
                    //     } else {
                    //         mainWindow.webContents.send('ec_CMD', [
                    //             CMD.ec_file_direct_upload_no_files
                    //         ]);
                    //
                    //         reject(ALLOK);
                    //     }
                    // } else {
                        resolve(directUploadFile.openDialogUpload(path));
                    // }
                });
            })
            .then(function ([list, openPath]) {
                //return new Promise(function (resolve) {
                    let totalSize  = 0;
                    let fileCount  = 0;
                    let filesList  = ""; // "1•eslintignore•temp\•26•128•" +
                    let rootDir    = "";
                    let lustDirIdx = 0;
                    let resList    = [];
                    let dirs       = [];

                    list.forEach(function (_path, idx) {
                        let fileName = Service.ExtractFileName(_path);
                        let filePath = Service.ExtractPath(_path);
                        let stat     = {};
                        
                        const attr   = "128";

                        try {
                            stat = fs.statSync(_path);
                        } catch (e) {}

                        if (!rootDir) {
                            rootDir = filePath.replace(new RegExp(fileName + "$"), "");
                        }

                        if (stat.isDirectory()) {
                            (function(_localRoot) {
                                dirs.push(new Promise(function (resolve) {
                                    Service.FileWalker(_localRoot, function (localList) {
                                        resList = resList.concat(localList);

                                        resolve();
                                    }, function (filePath, stat, root) { // template
                                        let isDir = stat.isDirectory();

                                        ++ lustDirIdx;

                                        if (!isDir) {
                                            fileCount ++;
                                            totalSize += stat.size;
                                            filesList += lustDirIdx + TERMINATOR + filePath + TERMINATOR + root.replace(rootDir, "") + TERMINATOR + stat.size + TERMINATOR + attr + TERMINATOR;
                                        }

                                        return {
                                            fileName: root + filePath,                      // реальный путь к файлу или папке на компьютере
                                            filePath: root.replace(rootDir, "") + filePath, // относительный путь, который будет создан на стороне получателя
                                            idx     : lustDirIdx,
                                            size    : isDir ? 0 : stat.size,
                                            dir     : isDir
                                        }
                                    }, true);
                                }));
                            }(_path));

                            ++ lustDirIdx;

                            fileCount ++;
                            totalSize += stat.size;
                            filesList += lustDirIdx + TERMINATOR + filePath + TERMINATOR + _path.replace(rootDir, "") + TERMINATOR + stat.size + TERMINATOR + attr + TERMINATOR;

                            resList.push({
                                fileName: _path,       // реальный путь к файлу или папке на компьютере
                                filePath: fileName,    // относительный путь, который будет создан на стороне получателя
                                idx     : lustDirIdx,
                                size    : 0,
                                dir     : true
                            });
                        } else {
                            let size = stat.size;

                            fileCount ++;
                            totalSize += size;
                            filesList += (idx + 1) + TERMINATOR + fileName + TERMINATOR + "" + TERMINATOR + size + TERMINATOR + attr + TERMINATOR;

                            resList.push({
                                fileName: _path,    // реальный путь к файлу или папке на компьютере
                                filePath: fileName, // относительный путь, который будет создан на стороне получателя
                                idx     : idx + 1,
                                size    : size,
                                dir     : false
                            });
                        }
                    });

                    lustDirIdx = resList[resList.length - 1].idx;

                    if (dirs.length) {
                        return Promise.all(dirs).then(function () {
                            return new Promise(function (resolve) {
                                resolve([fileCount, totalSize, filesList, resList, openPath]);
                            });
                        });
                    } else {
                        return new Promise(function (resolve) {
                            resolve([fileCount, totalSize, filesList, resList, openPath]);
                        });
                    }
            })
            .then(function([fileCount, totalSize, filesList, list, openPath]){
                let sID  = GetFirstSID();
                let data = {
                    "UIN"        : uin,       // идентификатор пользователя-получателя
                    "FilesCount" : fileCount, // общее количество файлов без учёта папок
                    "FilesSize"  : totalSize, // общий объём передаваемых файлов в байтах
                    "Desc"       : "",        // текстовый комментарий к отправляемому блоку файлов
                    "List"       : filesList  // список самих файлов. Доработать
                };

                sendDataToServer(CMD.cs_files_request_transfer, [sID, JSON.stringify(data)].join(CR));

                delete data.List;

                data.FilesList = list;
                data.myUIN     = ClientSettings.uin;
                data.filesPath = openPath;

                return data;
            })
            .then(directUploadFile.filesToSend)
            .then(function (hasFiles) {
                if (hasFiles){
                    mainWindow.webContents.send('ec_CMD', [
                        CMD.ec_file_direct_upload_start,
                        directUploadFile.getFilesInfo()
                    ]);
                }
            })
            .catch(function (err) {
                if (err && err !== ALLOK) {
                    console.error(err.stack || err.message || err);
                }
            })
        ;
    };

    commands[CMD.ce_file_direct_upload_start]    = function ([info]) {
        // "UIN"          : 456,                           // идентификатор пользователя-получателя файлов
        // "Port"         : 10000,                         // порт для приёма файлов "напрямую", минуя сервер
        // "BufSize"      : 65520,                         // размер буфера для приёма файлов (если файлы будут передаваться не напрямую, а через сервер)
        // "Interfaces"   : "192.168.10.1,213.130.24.149", // список сетевых интерфейсов получателя. Доработать
        // "UncheckFiles" : "1,17,21,25"                   // индексы файлов, которые получатель принимать отказался. Доработать

        directUploadFile
            .checkInterfaces(info.Interfaces.split(","), info.Port, ClientSettings.uin)
            .then(function ([host, port]) {
                let _uploadInfo = directUploadFile.getFilesInfo();
                
                return directUploadFile.doUpload(host, port, {
                    onProgress     : function (data, sendToClient) {
                        if (sendToClient){
                            sendDataToServer(CMD.cs_files_transfer_progress, [GetFirstSID()].concat(data).join(CR));
                        }

                        mainWindow.webContents.send('ec_CMD', [
                            CMD.ec_file_direct_upload_progress,
                            {
                                percent : data[1],
                                size    : Service.format_size(data[2]),
                                text    : Service.format_size(data[6]),
                                name    : ""

                            }
                        ]);
                    },
                    onFileUploaded : function (data) {
                        let idx = data[1];
                        let _idx = Service.findItemInArrayOfObj(_uploadInfo.FilesList, idx, 'idx');

                        _uploadInfo.FilesList[_idx].sended = true;

                        sendDataToServer(CMD.cs_files_internal_sended_ok_idx, [GetFirstSID()].concat(data).join(CR));
                    },
                    onComplete     : function () {
                        sendDataToServer(CMD.cs_files_success_recieved, [GetFirstSID(),info.UIN].join(CR));

                        mainWindow.webContents.send('ec_CMD', [
                            CMD.ec_file_direct_upload_complete,
                            _uploadInfo
                        ]);
                    },
                    onDebug        : function (type, data) {
                        console.log(type + ": " + data);
                    }
                });
            })
            .catch(function (err) {
                if (err && err !== ALLOK) {
                    console.error("Direct upload error");
                    console.error(err.stack || err.message || err);
                }
            })
        ;
    };

    commands[CMD.ce_file_direct_upload_abort]    = function ([]) {
        directUploadFile.abortUploading();
    };

    commands[CMD.ce_file_direct_upload_complete] = function ([]) {

    };

    // ---- Direct Receive ---

    commands[CMD.ce_stop_ftp_server]  = function () {
        directReceiveFile.stop();
    };

    commands[CMD.ce_start_ftp_server] = function () {
        startFtpServer();
    };

    commands[CMD.ce_remove_ftp_user]  = function ([uin]) {
        directReceiveFile.delUser(uin);
    };

    commands[CMD.ce_file_direct_receive_start] = function ([uin, path]) {
        let servId = getMyServerID();
        let _path  = path.toString();

        DbWork.do.userPath
            .set(servId, ClientSettings.uin, uin, _path)
            .catch(function (err) {
                if (err && err !== ALLOK) {
                    console.error(err.stack || err.message || err);
                }
            })
        ;

        directReceiveFile.addUser(uin, {
            uin      : uin,
            pwd      : uin,
            path     : _path,
            closeConnection: function (allOK) {
                mainWindow.webContents.send('ec_CMD', [
                    CMD.ec_file_direct_receive_client_disconnected,
                    {
                        UIN   : uin,
                        allOK : allOK
                    }
                ]);
            }
        });

        mainWindow.webContents.send('ec_CMD', [
            CMD.ec_file_direct_receive_start,
            {
                port      : directReceiveFile.getPort(),
                interfaces: directReceiveFile.getInterfaces()
            }
        ]);
    };

    // ==============================================

    commands[CMD.ce_history_get_dialogs] = function () {
        let serverId = getMyServerID();

        DbWork.do.dialogsHistory.get(serverId, ClientSettings.uin)
            .then(function ([data]) {
                let list = {};
                
                if (data && data.history){
                    list = data.history;
                }

                try{
                    JSON.parse(list);
                } catch (e){
                    list = "";
                }

                mainWindow.webContents.send('ec_CMD', [
                    CMD.ec_history_get_dialogs,
                    list
                ]);
            })
            .catch(function (err) {
                if (err && err !== ALLOK) {
                    console.error(err.stack || err.message || err);
                }
            })
        ;
    };

    commands[CMD.ce_history_set_dialogs] = function ([serverID, uin, name, history]) {
        let serverId = getMyServerID();
        let _data = Service.isString(history) ? JSON.parse(history) : history;

        DbWork.do.dialogsHistory
            .get(serverId, ClientSettings.uin)
            .then(function ([data]) {
                let list = {};

                if (data && data.history){
                    list = JSON.parse(data.history);
                }

                list = Service.MargeObj(list, _data);

                return Service.ObjToString(list);
            })

            .then(function (data) {
                return DbWork.do.dialogsHistory.set(serverId, ClientSettings.uin, data);
            })
            
            .then(function () {
                sendComplete(CMD.ce_history_set_dialogs);
            })

            .catch(function (err) {
                if (err && err !== ALLOK) {
                    console.error(err.stack || err.message || err);
                }
            })
        ;
    };

    commands[CMD.ce_history_remove_dialogs] = function () {
        let serverId = getMyServerID();

        DbWork.do.dialogsHistory.remove(serverId, ClientSettings.uin)
            .then(function () {
                sendComplete(CMD.ce_history_remove_dialogs);
            })
            .catch(function (err) {
                if (err && err !== ALLOK) {
                    console.error(err.stack || err.message || err);
                }
            })
        ;
    };

    // --- Media Recording ------------------

    // EBML/WebM парсер для виправлення Duration після запису.
    // Chrome MediaRecorder не записує Duration в Info секцію при чанковому записі.
    // Тривалість передається як durationMs (Date.now() - startTime) з ce_finish_media.

    function readVINT(buf, pos) {
        if (pos >= buf.length) return null;
        var first = buf[pos];
        var len = 0;
        var mask = 0x80;

        for (var i = 0; i < 8; i++) {
            if (first & mask) {
                len = i + 1;
                break;
            }
            mask >>= 1;
        }
        if (len === 0 || pos + len > buf.length) return null;

        var value = first & (mask - 1);
        for (var j = 1; j < len; j++) {
            value = value * 256 + buf[pos + j];
        }
        return { length: len, value: value };
    }

    function readEBMLId(buf, pos) {
        if (pos >= buf.length) return null;
        var first = buf[pos];
        var len = 0;

        if      (first & 0x80) len = 1;
        else if (first & 0x40) len = 2;
        else if (first & 0x20) len = 3;
        else if (first & 0x10) len = 4;
        else return null;

        if (pos + len > buf.length) return null;

        var id = 0;
        for (var i = 0; i < len; i++) {
            id = id * 256 + buf[pos + i];
        }
        return { length: len, id: id };
    }

    function readUInt(buf, pos, len) {
        var val = 0;
        for (var i = 0; i < len; i++) {
            val = val * 256 + buf[pos + i];
        }
        return val;
    }

    function encodeVINT(value, numBytes) {
        // Кодує value як VINT заданої довжини numBytes
        var maxValue = Math.pow(2, 7 * numBytes) - 2; // -1 зарезервовано для "unknown size"
        if (value > maxValue) return null;

        var result = Buffer.alloc(numBytes);
        var remaining = value;
        for (var i = numBytes - 1; i >= 1; i--) {
            result[i] = remaining & 0xFF;
            remaining = Math.floor(remaining / 256);
        }
        result[0] = remaining;
        // маркер довжини — біт у позиції (8 - numBytes)
        result[0] |= (0x80 >> (numBytes - 1));

        return result;
    }

    function isUnknownSize(value, vintLen) {
        // "Unknown size" — всі data-біти = 1
        return value === Math.pow(2, 7 * vintLen) - 1;
    }

    // EBML Element IDs
    var EBML_SEGMENT        = 0x18538067;
    var EBML_INFO           = 0x1549A966;
    var EBML_DURATION       = 0x4489;
    var EBML_TIMECODE_SCALE = 0x2AD7B1;

    // Duration element: ID 0x4489 (2 байти) + VINT size 8 (1 байт 0x88) + float64 (8 байт) = 11 байт
    var DURATION_ELEMENT_SIZE = 11;

    function fixWebmDuration(filePath, durationMs, callback) {
        fs.readFile(filePath, function (err, buf) {
            if (err) {
                console.err("fixWebmDuration read error: " + err.message);
                if (callback) callback();
                return;
            }

            try {
                var pos = 0;

                // Позиції Segment
                var segmentSizePos   = -1;
                var segmentSizeLen   = -1;
                var segmentSizeValue = -1;
                var segmentDataStart = -1;
                var segmentDataEnd   = -1;

                // Позиції Info
                var infoSizePos   = -1;
                var infoSizeLen   = -1;
                var infoSizeValue = -1;
                var infoDataStart = -1;
                var infoDataEnd   = -1;

                // Duration (якщо існує)
                var durationOffset = -1;
                var durationSize   = -1;

                var timecodeScale = 1000000;

                // Крок 1: знайти Segment
                while (pos < buf.length) {
                    var eid = readEBMLId(buf, pos);
                    if (!eid) break;
                    var esize = readVINT(buf, pos + eid.length);
                    if (!esize) break;

                    if (eid.id === EBML_SEGMENT) {
                        segmentSizePos   = pos + eid.length;
                        segmentSizeLen   = esize.length;
                        segmentSizeValue = esize.value;
                        segmentDataStart = pos + eid.length + esize.length;
                        segmentDataEnd   = segmentDataStart + esize.value;
                        if (segmentDataEnd > buf.length) segmentDataEnd = buf.length;
                        break;
                    }
                    pos += eid.length + esize.length + esize.value;
                }

                if (segmentDataStart === -1) {
                    console.err("fixWebmDuration: Segment not found");
                    if (callback) callback();
                    return;
                }

                // Крок 2: знайти Info всередині Segment
                pos = segmentDataStart;
                while (pos < segmentDataEnd) {
                    var eid2 = readEBMLId(buf, pos);
                    if (!eid2) break;
                    var esize2 = readVINT(buf, pos + eid2.length);
                    if (!esize2) break;

                    var dataStart = pos + eid2.length + esize2.length;
                    var dataEnd   = dataStart + esize2.value;

                    if (eid2.id === EBML_INFO) {
                        infoSizePos   = pos + eid2.length;
                        infoSizeLen   = esize2.length;
                        infoSizeValue = esize2.value;
                        infoDataStart = dataStart;
                        infoDataEnd   = dataEnd;

                        // шукаємо Duration і TimecodeScale всередині Info
                        var ipos = dataStart;
                        while (ipos < dataEnd) {
                            var iid = readEBMLId(buf, ipos);
                            if (!iid) break;
                            var isize = readVINT(buf, ipos + iid.length);
                            if (!isize) break;
                            var iDataStart = ipos + iid.length + isize.length;

                            if (iid.id === EBML_DURATION) {
                                durationOffset = iDataStart;
                                durationSize   = isize.value;
                            } else if (iid.id === EBML_TIMECODE_SCALE) {
                                timecodeScale = readUInt(buf, iDataStart, isize.value);
                            }

                            ipos = iDataStart + isize.value;
                        }
                        break; // Info знайдено, далі не скануємо
                    }

                    pos = dataEnd;
                }

                if (infoDataEnd === -1) {
                    console.err("fixWebmDuration: Info section not found");
                    if (callback) callback();
                    return;
                }

                // Конвертуємо durationMs у одиниці timecodeScale (за замовч. 1ms = 1 unit)
                var durationUnits = durationMs * (1000000 / timecodeScale);

                if (durationOffset !== -1) {
                    // === Випадок A: Duration вже є — просто патчимо значення ===
                    if (durationSize === 8) {
                        buf.writeDoubleBE(durationUnits, durationOffset);
                    } else if (durationSize === 4) {
                        buf.writeFloatBE(durationUnits, durationOffset);
                    }

                    fs.writeFile(filePath, buf, function (werr) {
                        if (werr) console.err("fixWebmDuration write error: " + werr.message);
                        if (callback) callback();
                    });
                } else {
                    // === Випадок B: Duration відсутній — вставляємо в кінець Info ===

                    // Створюємо Duration element: 0x4489 + 0x88 (VINT=8) + float64
                    var durEl = Buffer.alloc(DURATION_ELEMENT_SIZE);
                    durEl[0] = 0x44;  // Duration Element ID byte 1
                    durEl[1] = 0x89;  // Duration Element ID byte 2
                    durEl[2] = 0x88;  // VINT: data size = 8 bytes
                    durEl.writeDoubleBE(durationUnits, 3);

                    // Оновити розмір Info: old + 11
                    var newInfoSize = infoSizeValue + DURATION_ELEMENT_SIZE;
                    var newInfoVint = encodeVINT(newInfoSize, infoSizeLen);
                    // Якщо не поміщається в ту саму довжину VINT — спробувати +1
                    if (!newInfoVint) {
                        newInfoVint = encodeVINT(newInfoSize, infoSizeLen + 1);
                    }
                    if (!newInfoVint) {
                        console.err("fixWebmDuration: cannot encode new Info size");
                        if (callback) callback();
                        return;
                    }

                    var vintDiff = newInfoVint.length - infoSizeLen;

                    // Збираємо новий буфер:
                    // [до Info size VINT] + [новий Info size VINT] + [Info data] + [Duration el] + [решта файлу]
                    var part1 = buf.slice(0, infoSizePos);                          // все до Info size
                    var part2 = buf.slice(infoSizePos + infoSizeLen, infoDataEnd);  // Info data (зі старим VINT пропущеним)
                    var part3 = buf.slice(infoDataEnd);                             // все після Info

                    var newBuf = Buffer.concat([part1, newInfoVint, part2, durEl, part3]);

                    // Оновити Segment size (якщо він не "unknown")
                    if (!isUnknownSize(segmentSizeValue, segmentSizeLen)) {
                        var newSegSize = segmentSizeValue + DURATION_ELEMENT_SIZE + vintDiff;
                        var newSegVint = encodeVINT(newSegSize, segmentSizeLen);
                        if (newSegVint) {
                            newSegVint.copy(newBuf, segmentSizePos);
                        }
                    }

                    fs.writeFile(filePath, newBuf, function (werr) {
                        if (werr) console.err("fixWebmDuration write error: " + werr.message);
                        if (callback) callback();
                    });
                }

            } catch (ex) {
                console.err("fixWebmDuration exception: " + ex.message);
                if (callback) callback();
            }
        });
    }

    commands[CMD.ce_finish_media] = function ([name]) {
        if (!fileWriter) return;

        // з name — закрити лише цей файл; без name — legacy «закрити всі»
        var files = name
            ? (fileWriter[name] ? [name] : [])
            : Object.keys(fileWriter);

        files.forEach(function (n) {
            var entry = fileWriter[n];
            if (!entry || entry.finished) return;

            entry.finished = true;   // тромбстоун: пізні чанки ігноруються
            entry.active   = false;

            var durationMs = Date.now() - entry.startTime;

            try {
                entry.stream.end(function () {
                    var isWebm = entry.path && (
                        (entry.type && String(entry.type).indexOf("webm") !== -1) ||
                        /\.webm$/i.test(entry.path)
                    );

                    if (isWebm) {
                        fixWebmDuration(entry.path, durationMs, function () {
                            delete fileWriter[n];
                        });
                    } else {
                        delete fileWriter[n];
                    }
                });
            } catch (e) {
                console.err(e.message);
                delete fileWriter[n];
            }
        });
    };

    commands[CMD.ce_chunk_media] = function ([data, name, type, recordDir]) {
        if (!fileWriter) fileWriter = {};

        // файл уже завершується/завершено — ігноруємо пізні асинхронні чанки
        if (fileWriter[name] && fileWriter[name].finished) return;

        var targetDir = recordDir || (os.homedir() + parsePath.sep + 'Documents' + parsePath.sep);
        if (targetDir[targetDir.length - 1] !== parsePath.sep) targetDir += parsePath.sep;

        try {
            if (!fileWriter[name] || !fileWriter[name].active) {
                fileWriter[name] = {
                    stream   : fs.createWriteStream(targetDir + name),
                    type     : type,
                    path     : targetDir + name,
                    startTime: Date.now(),
                    active   : true,
                    finished : false
                };
            }
            fileWriter[name].stream.write(data, function (err) {
                if (err) console.err(err.message);
            });
        } catch (e) {
            console.err(e.message);
        }
    };

    // ==============================================

    return commands;
};
