 "use strict";

 const CRLF = "\r\n";
 const CR   = "\r";

 const fs         = require('fs');
 const console    = require('gifer-console');
 const crypto     = require("crypto");
 const iconv      = require('iconv-lite');
 const _path      = require('path');
 const HDWID      = require("machine-uuid");
 const moment     = require('moment');
 const HOME_DIR   = "/Documents/MyChat/";
 const os         = require('os');

 let Service = {};
 let iv2     = Buffer.alloc(16);

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

 Service.MCPathes = global.MCPathes = {};

 Service.MCServer = global.MCServer = {
     legacyList      : [],
     ProtocolVersion : "2.79",
     ClientVersion   : "2026.6.9",
     MinimumVersion  : "2026.3.0",
     ElectronVersion : "39.8.10",
     ElectronVersionShort: "39.8",
     ElectronChromeVersion: "142.0.7444.265",
     ElectronNodeVersion : "22.22.1",
     ElectronABIVersion  : "140",
     MaximumHTTPConnections: 1000,
     sendBufferSize: 10240,
     maxUploadData : 600000000,
     CompanyName   : "",
     ServerName    : "",
     Host          : '127.0.0.1',
     Port          : '2004',
     PWD           : "",
     ServPass      : "",
     MCPort        : 2004,
     STUN          : {
         enable : false,
         addr1  : "127.0.0.1",
         addr2  : "127.0.0.1",
         port1  : "3478",
         port2  : "3479"
     },
     GZIP          : false,
     Lang          : "ru",
     externalIceServers : [
         {url: "stun:stun.sipgate.net"},
         {url: "stun:217.10.68.152"},
         {url: "stun:stun.sipgate.net:10000"},
         {url: "stun:217.10.68.152:10000"}
     ],
     iceServers    : []
 };

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

 function dec2hex(i){
     let result = "0000";

     if      (i >= 0    && i <= 15)    { result = "000" + i.toString(16); }
     else if (i >= 16   && i <= 255)   { result = "00"  + i.toString(16); }
     else if (i >= 256  && i <= 4095)  { result = "0"   + i.toString(16); }
     else if (i >= 4096 && i <= 65535) { result =         i.toString(16); }

     return result
 }

 function isPlainObject(obj) {
     if (!obj || toString.call(obj) !== '[object Object]' || obj.nodeType || obj.setInterval)
         return false;

     let has_own_constructor = hasOwnProperty.call(obj, 'constructor');
     let has_is_property_of_method = hasOwnProperty.call(obj.constructor.prototype, 'isPrototypeOf');
     // Not own constructor property must be Object
     if (obj.constructor && !has_own_constructor && !has_is_property_of_method)
         return false;

     // Own properties are enumerated firstly, so to speed up,
     // if last one is own, then all properties are own.
     let key;
     for ( key in obj ) {}

     return key === undefined || hasOwn.call( obj, key );
 }

 function makeCRCTable(){
     let c;
     let crcTable = [];

     for(let n =0; n < 256; n++){
         c = n;

         for(let k =0; k < 8; k++){
             c = ((c&1) ? (0xEDB88320 ^ (c >>> 1)) : (c >>> 1));
         }

         crcTable[n] = c;
     }

     return crcTable;
 }

 function isObjectEmpty(obj){
     let flag = true;

     if (obj){
         for (let i in obj){
             flag = false;
         }
     }

     return flag;
 }

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

 Service.Const = {
     PrimaryHost : 'localhost',
     PrimaryPort : "80"
 };
 
 Service.FileWalker = function(rootDir, done, temp, plainList, parent){
    let results = [];
 
    fs.readdir(rootDir, function(err, list){
        if (err) {
            //console.error(err.message);
            
			if (results.length) {
			    done(results);
            }
		} else {
            let _root = rootDir + '/';

            (function next(i){
                let filePath = list[i];

                if (filePath) {
                    fs.stat(_root + filePath, function(err, stat){
                        if (stat && stat.isDirectory()) {
                            let fld = temp && Service.isFunction(temp)
                                ? temp(filePath, stat, _root)
                                : { id: filePath + (parent ? "_" + parent : ""), value: filePath, data: [] };

                            results.push(fld);

                            Service.FileWalker(_root + filePath, function(folderList){
                                if (plainList) {
                                    results = results.concat(folderList);
                                } else {
                                    fld.data = folderList;
                                }

                                next(++i);
                            }, temp, plainList, filePath + (parent ? "_" + parent : ""));
                        } else {
                            results.push(temp && Service.isFunction(temp)
                                ? temp(filePath, stat, _root)
                                : { id: filePath + (parent ? "_" + parent : ""), value: filePath }
                            );

                            next(++i);
                        }
                    });
                } else {
                    done(results);
                }
            })(0);
        }
    });
 };
 
 Service.JSON_to_INI = function(/* array of objects */_options, GlobalCallBack){
 	if (_options.length > 0){
		let Option = Service.MargeObj({
			JSON	 : {},
			FileName : 'file.ini',
			FilePath : './',
			Section	 : '',
			LocalCallBack : undefined
		}, _options[0] || {});
		
		let OutText = '';
		
		if (Option.Section !== ''){
			OutText += '[' + Option.Section + ']\r\n';
		}
		 
		for (let item in Option.JSON){
			if (Option.Section !== ''){
				OutText += item + '=' + Option.JSON[item] + '\r\n';
			} else
			if (typeof Option.JSON[item] === 'object'){
				OutText += '[' + item + ']\r\n';
				
				for (let subItem in Option.JSON[item]) {
					OutText += subItem + '=' + Option.JSON[item][subItem] + '\r\n';
				}
			}
		}
		
		Service.AddTextToFile(Option.FilePath, Option.FileName, OutText, function(){
			if (Option.LocalCallBack) {
				Option.LocalCallBack(Option.FilePath + Option.FileName);
			}
			
			_options.shift();
			
			if (_options.length > 0){
				Service.JSON_to_INI(_options, GlobalCallBack);
			} else {
				GlobalCallBack.apply();
			}
		});
	}
 };
 
 Service.AddTextToFile = function(Path, FileName, Text, CallBack){
 	if (Path !== '') {
		Path += (Path[Path.length] === '/') ? '' : '/';
	}
	 
 	fs.writeFile(Path + FileName, Text, CallBack);
 };

 Service.WriteFile = function(path, data, cb, opt){
     fs.writeFile(path, data, opt, function (err) {
         if (err) {
             console.err("Service.WriteFile " + err);
         } else {
             if (cb) cb();
         }
     });
 };

 Service.convertToEntities = function(tstr) {
     let bstr = "";

     if (tstr){
         for(let i = 0; i < tstr.length; ++i)
         {
             if(tstr.charCodeAt(i) > 127)
             {
                 bstr += "\\u" + dec2hex(tstr.charCodeAt(i));
             } else {
                 bstr += tstr.charAt(i);
             }
         }
     } else {
         throw new Error('convertToEntities: incoming data is empty or undefined: ' + tstr);
         //console.err('convertToEntities: incoming data is empty or undefined', true);
     }

     return bstr;
 };

 Service.UTF8 = {
     encode: function(s){
         for(let c, i = -1, l = (s = s.split("")).length, o = String.fromCharCode; ++i < l;
             s[i] = (c = s[i].charCodeAt(0)) >= 127 ? o(0xc0 | (c >>> 6)) + o(0x80 | (c & 0x3f)) : s[i]);

         return s.join("");
     },
     decode: function(s){
         for(let a, b, i = -1, l = (s = s.split("")).length, o = String.fromCharCode, c = "charCodeAt"; ++i < l;
             ((a = s[i][c](0)) & 0x80) &&
                 (s[i] = (a & 0xfc) == 0xc0 && ((b = s[i + 1][c](0)) & 0xc0) == 0x80 ?
                     o(((a & 0x03) << 6) + (b & 0x3f)) : o(128), s[++i] = "")
             );

         return s.join("");
     }
 };

 Service.Extend = Service.extend = Service.MargeObj = function () {
     if (!Object.assign) {
         Object.defineProperty(Object, 'assign', {
             enumerable  : false,
             configurable: true,
             writable    : true,
             value       : function(target, firstSource) {
                 'use strict';

                 if (target === undefined || target === null) {
                     throw new TypeError('Cannot convert first argument to object');
                 }

                 let to = Object(target);

                 for (let i = 1; i < arguments.length; i++) {
                     let nextSource = arguments[i];

                     if (nextSource === undefined || nextSource === null) {
                         continue;
                     }

                     let keysArray = Object.keys(Object(nextSource));

                     for (let nextIndex = 0, len = keysArray.length; nextIndex < len; nextIndex++) {
                         let nextKey = keysArray[nextIndex];
                         let desc = Object.getOwnPropertyDescriptor(nextSource, nextKey);

                         if (desc !== undefined && desc.enumerable) {
                             to[nextKey] = nextSource[nextKey];
                         }
                     }
                 }

                 return to;
             }
         });
     }

     return Object.assign.apply(null, arguments);

     // let options, name, src, copy, copyIsArray, clone,
	//     target = arguments[0] || {},
	//     i = 1,
	//     length = arguments.length,
	//     deep = false;
    //
	// // Handle a deep copy situation
	// if ( typeof target === "boolean" ) {
	// 	deep = target;
	// 	target = arguments[1] || {};
	// 	// skip the boolean and the target
	// 	i = 2;
	// }
    //
	// // Handle case when target is a string or something (possible in deep copy)
	// if ( typeof target !== "object" && typeof target !== "function") {
	// 	target = {};
	// }
    //
	// // extend jQuery itself if only one argument is passed
	// if ( length === i ) {
	// 	target = this;
	// 	--i;
	// }
    //
	// for ( ; i < length; i++ ) {
	// 	// Only deal with non-null/undefined values
	// 	if ( (options = arguments[ i ]) != null ) {
	// 		// Extend the base object
	// 		for ( name in options ) {
	// 			src = target[ name ];
	// 			copy = options[ name ];
    //
	// 			// Prevent never-ending loop
	// 			if ( target === copy ) {
	// 				continue;
	// 			}
    //
	// 			// Recurse if we're merging plain objects or arrays
	// 			if ( deep && copy && ( isPlainObject(copy) || (copyIsArray = Array.isArray(copy)) ) ) {
	// 				if ( copyIsArray ) {
	// 					copyIsArray = false;
	// 					clone = src && Array.isArray(src) ? src : [];
    //
	// 				} else {
	// 					clone = src && isPlainObject(src) ? src : {};
	// 				}
    //
	// 				// Never move original objects, clone them
	// 				target[ name ] = Service.MargeObj( deep, clone, copy );
    //
	// 			// Don't bring in undefined values
	// 			} else if ( copy !== undefined ) {
	// 				target[ name ] = copy;
	// 			}
	// 		}
	// 	}
	// }
    //
	// // Return the modified object
	// return target;
 };
 
 Service.Get_CMD = function(msg){
	let WEB_CR_IDX = msg.indexOf(CR);
	let res = {};
	
	if (WEB_CR_IDX === -1) WEB_CR_IDX = msg.length;
	
	let request = msg.slice(0, WEB_CR_IDX);
	let _msg    = msg.slice(WEB_CR_IDX + 1, msg.length + 1);
	
	res.CMD  = request;
	res.DATA = Service.DropEndCRLF(_msg);
	
	return res;
 };

 /**
  * @return {number}
  */
 Service.CRC32 = function(str) {
     let crcTable = makeCRCTable();
     let crc = 0 ^ (-1);

     for (let i = 0; i < str.length; i++ ) {
         crc = (crc >>> 8) ^ crcTable[(crc ^ str.charCodeAt(i)) & 0xFF];
     }

     return (crc ^ (-1)) >>> 0;
 };

 Service.GetSubstringFrom = function(str, term){
	let idx = str.indexOf(term);
	let res = (idx != -1)? str.slice(idx + term.length, str.length + 1) : str.slice(0, str.length + 1);
	return (res === '') ? '' : res;
 };

 Service.Delete_File = function(FileName, CallBack){
     /*let cmd = 'del "' + FileName + '" /q';

     console.log("File: " + cmd + " deleted", console.logLevel.L_Normal);

     exec(cmd, _execOptions, CallBack);*/

     fs.unlink(FileName, CallBack);
 };

 Service.DropEndCRLF = function(str){
 	if ((str[str.length - 2] + str[str.length - 1]) === '\r\n') {
		return str.slice(0, str.length - 2);
	} else {
		return str;
	}
 };

 /**
 * @return {string}
 */
 Service.RandomHash = function (len) {
    let letters = 'JjKAEeaMpsTtQqRxBcDdFfGgHhI23ibC-kLmNnOoPV8yrSUuZ1WwXlY456790._vz_';
	let res = '';

    for (let i = 0; i < len; i++) {
        res += letters[Math.floor(Math.random() * letters.length)];
    }
    
	return res;
 };

 Service.ExtractFileName = function(Path){
     let count = 0;

     if (Path)
     for (let i = Path.length - 1; i >= 0; i--){
         count ++;

         if ((Path[i] === '\\') || (Path[i] === '/')) {
             return Path.slice(i + 1, i + count);
         }
     }

     return Path || "";
 };

 Service.ExtractFileExtension = function (Path){
     let res = (Path || "").split('.');

     res = res.length > 1 ? res[res.length - 1] : "";

     return res;
 };

 Service.ExtractPath = function(Path){
     if (Path){
         for (let i = Path.length - 1; i >= 0; i--){
             if ((Path[i] === '\\') || (Path[i] === '/')) {
                 return Path.slice(0, i);
             }
         }
     }

     return Path || "";
 };

 Service.LoadFileSync = function (path){
     var res = null;

     try {
         res = fs.readFileSync(path);
     } catch (e) {
         console.err(e.message);
     }

     return res;
 };

 Service.LoadMail = Service.LoadJSONFile = Service.LoadFile = function(Path, callback, opt) {
     let _opt = opt || 'win1251';

     fs.stat(Path, function (err, stats) {
         if (err){
             if (Path.indexOf('first.login') === -1) {
                 console.err('Failed Load ' + Path + "\n" + err, true);
             }

             if (callback){
                 callback(null);
             }
         } else
         if (stats.size > 50000000){
             console.err('Log file ' + Path + ' too large: ' + stats.size);

             if (callback){
                 callback('FFFF|Log file ' + Path + ' is too large for view, size: ' + stats.size + " bite|||");
             }
         } else {
             let str    = fs.createReadStream(Path);
             let inData = [];

             str.on('data', function(chunk){
                 inData.push(chunk);
             });

             str.on('error', function(err){
                 console.err('Failed Load ' + Path + "\n" + err, true);

                 if (callback){
                     callback(null);
                 }
             });

             str.on('end', function(){
                 let res = "";

                 try {
                     if (opt){
                         res = iconv.decode(Buffer.concat(inData), _opt);
                     } else {
                         res = Buffer.concat(inData).toString();
                     }

                     console.info('Loaded file: "' + Path + '"', console.logLevel.L_Full);
                 } catch (e){
                     console.err('Failed Load ' + Path + "\n" + e.message + "\n" + e.stack + "\n", true);

                     if (callback){
                         callback(null);
                     }

                     return;
                 }

                 if (callback){
                    callback(res);
                 }
             });
         }
     });
 };

 Service.Replace = function(str, cond, args){
     if (Service.isArray(args)){
         for (let i = 0; i < args.length; i++){
             str = str.replace(cond, args[i]);
         }
     } else {
         str = str.replace(cond, args);
     }

     return str;
 };

 Service.myReplace = function(source, _items, term){
     let res = source;
     let items = [];

     if (_items !== undefined && source !== undefined){
         let _term = term || "%s";

         if (!Service.isArray(_items)) {
             items.push(_items);
         } else {
             items = _items;
         }

         if (items.length > 0){
             res = source.replace(_term, items.shift());
             res = Service.myReplace(res, items, _term);
         }
     }

     return res;
 };

 /**
 * @return {string}
 */
 Service.CapitaliseFirstLetter = function(string){
     return string.charAt(0).toUpperCase() + string.slice(1);
 };
 
 Service.minimumOfTwo = function(a, b){
     let res = [];

     if ((parseInt(a) - parseInt(b)) > 0){
         res = [b,  a];
     } else {
         res = [a, b];
     }

     return res;
 };

 Service.PrintObjectItems = function(_obj){
   for (let i in _obj){
       console.log(i + ": " + ((typeof(_obj[i]) === 'object') ? JSON.stringify(_obj[i]) : _obj[i]));
   }
 };
 
 Service.isValidEmailAddress = function(emailAddress) {
    let pattern = new RegExp(/^(("[\w-\s]+")|([\w-]+(?:\.[\w-]+)*)|("[\w-\s]+")([\w-]+(?:\.[\w-]+)*))(@((?:[\w-]+\.)*\w[\w-]{0,66})\.([a-z]{2,6}(?:\.[a-z]{2})?)$)|(@\[?((25[0-5]\.|2[0-4][0-9]\.|1[0-9]{2}\.|[0-9]{1,2}\.))((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[0-9]{1,2})\.){2}(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[0-9]{1,2})\]?$)/i);

     return pattern.test(emailAddress);
 };

 Service.isValidIpAddress = function (ip) {
    let reg = /(^((25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(25[0-5]|2[0-4]\d|[01]?\d\d?$))/; // |(((^|:)([0-9a-fA-F]{0,4})){1,8})

    return reg.test(ip);
 };

 Service.StringToObj = function(_data, debug){
     let res = {};
     
     if (debug){
         console.info("StringToObj: " + _data);
     }

     if (Service.isObject(_data)){
         if (debug) Service.PrintObjectItems(_data);
         
         res = _data;
     } else {
         try {
             let new_obj = JSON.parse(_data);

             if (debug) {
                 Service.PrintObjectItems(new_obj);
             }

             res = new_obj;
         } catch(e){
             console.err("StringToObj: " + e.message);
             //console.err("StringToObj: " + e.stack);

             res = _data;
         }
     }
    
    return res;
 };

 Service.inArrayNoStrict = function( elem, arr, i ) {
    let len;

    if ( arr ) {
        len = arr.length;
        i = i ? i < 0 ? Math.max( 0, len + i ) : i : 0;

        for ( ; i < len; i++ ) {
            if ( i in arr && arr[ i ] == elem ) {
                return i;
            }
        }
    }

    return -1;
 };

 /**
  * @return {string}
  */
 Service.ArrayToString = function(_array, _separator){
     let res = "";
     let separator = (!_separator) ? CR : _separator;

     for (let i = 0; i < _array.length; i++){
         res += Service.ObjToString(_array[i]) + separator;
     }

     return res;
 };

 /**
  * @return {string}
  */
 Service.ObjToString = function(_obj){
     let res = "";

     if (Service.isString(_obj) || Service.isNumber(_obj)){
         res = _obj.toString() + "";
     } else
     try {
         res = JSON.stringify(_obj);
     } catch (e){
         res = _obj.toString() + "";
     }

     return res;
 };

 /**
  * @return {boolean}
  */
 Service.InArray = function(item, arr){
   let res = false;
   
   for(let i = 0; i < arr.length; i++) {
        if (item == arr[i]) {
            res = true;
            break;
        }
    }
    
    return res;
 };

 /**
 * @return {boolean}
 */
 Service.InObject = function(item, obj){
    let res = false;

    for(let i in obj) {
        if (item == obj[i]) {
            res = true;
            break;
        }
    }

    return res;
 };

 Service.isArray = function isArray(obj) {
     return Object.prototype.toString.call( obj ) === '[object Array]';
 };

 Service.isObject = function isObject(obj) {
     return Object.prototype.toString.call( obj ) === '[object Object]';
 };

 Service.isString = function isString(obj) {
     return Object.prototype.toString.call( obj ) === '[object String]';
 };

 Service.isNumber = function isNumber(obj) {
     return Object.prototype.toString.call( obj ) === '[object Number]';
 };

 Service.isFunction = function (obj) {
    return Object.prototype.toString.call( obj ) === '[object Function]';
 };

 /**
 * @return {string}
 */
 Service.GetType = function GetType(obj){
     return Object.prototype.toString.call( obj );
 };

 /**
 * @return {string}
 */
 Service.GetSubstring = function (str, term){
     let res = "";

     if (str) {
         let idx = str.indexOf(term);

         res = (idx !== -1)? str.slice(0, idx) : str.slice(0, str.length + 1);
     }

     return res;
 };


 function testPort(PORT, passedCallBack, failedCallBack, loop, host) {
     let net = require('net');
     let tester = net.createServer();

     console.info("Testing port: " + PORT);

     tester.on('error', function (err) {
         if (failedCallBack && failedCallBack(PORT, err) === true){
             return;
         }

         if (loop){
             process.nextTick(function () {
                 testPort(++PORT, passedCallBack, failedCallBack, --loop);
             });
         }
     });

     tester.on('close', function() {
         if (passedCallBack) {
             setTimeout(function () {
                 passedCallBack(PORT);
             }, 100);
         }
     });

     tester.listen(PORT, host || "0.0.0.0", function () {
         tester.maxConnections = 0;

         tester.close();
     });

     return tester;
 }

 Service.isPortBusy = testPort;

 /**
 * @return {undefined}
 */
 Service.Read_JSON_File = function(_file, invertSlesh){
     let res = undefined;

     try {
         res = fs.readFileSync(_file);

         if (invertSlesh) res = res.toString().replace(/\\/g, '/');

         let txt = res;
     } catch (e){
         console.err("Can't load JSON file: " + _file + "\n" + e);

         return;
     }

     try {
         res = JSON.parse(res);
     } catch (e){
         console.err("Wrong JSON format or syntax error: " + _file + "\n" + e);

         return;
     }

     console.log("JSON file successfully loaded: " + _file + "\n" + txt);

     return res;
 };

 Service.InitMCPathes = function(params){
     MCPathes.Profile    = params + (params[params.length - 1] !== "/" ? "/" : "");
     MCPathes.ProfilNode = MCPathes.Profile    + "node/";
     MCPathes.Events     = MCPathes.ProfilNode + "events/";
     MCPathes.History    = MCPathes.Profile    + "logs/";
     MCPathes.Uploads    = MCPathes.ProfilNode + "uploads/";
     MCPathes.Downloads  = MCPathes.ProfilNode + "downloads/";
     MCPathes.Updates    = MCPathes.Profile    + "updates/";
     // MCPathes.Smileys    = MCPathes.Profile    + "smileys/";
 };

 Service.Base64 = {

     // private property
     _keyStr : "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",

     // public method for encoding
     encode : function (input) {
         let output = "";
         let chr1, chr2, chr3, enc1, enc2, enc3, enc4;
         let i = 0;

         input = Service.Base64._utf8_encode(input);

         while (i < input.length) {

             chr1 = input.charCodeAt(i++);
             chr2 = input.charCodeAt(i++);
             chr3 = input.charCodeAt(i++);

             enc1 = chr1 >> 2;
             enc2 = ((chr1 & 3) << 4) | (chr2 >> 4);
             enc3 = ((chr2 & 15) << 2) | (chr3 >> 6);
             enc4 = chr3 & 63;

             if (isNaN(chr2)) {
                 enc3 = enc4 = 64;
             } else if (isNaN(chr3)) {
                 enc4 = 64;
             }

             output = output +
                 this._keyStr.charAt(enc1) + this._keyStr.charAt(enc2) +
                 this._keyStr.charAt(enc3) + this._keyStr.charAt(enc4);

         }

         return output;
     },

     // public method for decoding
     decode : function (input) {
         let output = "";
         let chr1, chr2, chr3;
         let enc1, enc2, enc3, enc4;
         let i = 0;

         input = input.replace(/[^A-Za-z0-9\+\/\=]/g, "");

         while (i < input.length) {

             enc1 = this._keyStr.indexOf(input.charAt(i++));
             enc2 = this._keyStr.indexOf(input.charAt(i++));
             enc3 = this._keyStr.indexOf(input.charAt(i++));
             enc4 = this._keyStr.indexOf(input.charAt(i++));

             chr1 = (enc1 << 2) | (enc2 >> 4);
             chr2 = ((enc2 & 15) << 4) | (enc3 >> 2);
             chr3 = ((enc3 & 3) << 6) | enc4;

             output = output + String.fromCharCode(chr1);

             if (enc3 != 64) {
                 output = output + String.fromCharCode(chr2);
             }

             if (enc4 != 64) {
                 output = output + String.fromCharCode(chr3);
             }

         }

         output = Service.Base64._utf8_decode(output);

         return output;

     },

     // private method for UTF-8 encoding
     _utf8_encode : function (string) {
         string = string.replace(/\r\n/g,"\n");
         let utftext = "";

         for (let n = 0; n < string.length; n++) {

             let c = string.charCodeAt(n);

             if (c < 128) {
                 utftext += String.fromCharCode(c);
             } else
             if((c > 127) && (c < 2048)) {
                 utftext += String.fromCharCode((c >> 6) | 192);
                 utftext += String.fromCharCode((c & 63) | 128);
             } else {
                 utftext += String.fromCharCode((c >> 12) | 224);
                 utftext += String.fromCharCode(((c >> 6) & 63) | 128);
                 utftext += String.fromCharCode((c & 63) | 128);
             }

         }

         return utftext;
     },

     // private method for UTF-8 decoding
     _utf8_decode : function (utftext) {
         let string = "";
         let i  = 0;
         let c  = 0;
         let c1 = 0;
         let c2 = 0;
         let c3 = 0;

         while ( i < utftext.length ) {

             c = utftext.charCodeAt(i);

             if (c < 128) {
                 string += String.fromCharCode(c);
                 i++;
             } else
             if((c > 191) && (c < 224)) {
                 c2 = utftext.charCodeAt(i+1);
                 string += String.fromCharCode(((c & 31) << 6) | (c2 & 63));
                 i += 2;
             } else {
                 c2 = utftext.charCodeAt(i+1);
                 c3 = utftext.charCodeAt(i+2);
                 string += String.fromCharCode(((c & 15) << 12) | ((c2 & 63) << 6) | (c3 & 63));
                 i += 3;
             }

         }

         return string;
     }
 };

 // Ключі, значення яких не мають потрапляти в лог. Список ширший за пароль входу:
 // pass (з малої) — пароль у UserInfo (web_client_connect.js), Password — cs_adm_add_auto_conf,
 // UserPassword — LDAP-акаунт, OldPass/NewPass — зміна пароля акаунта, ServPass — пароль сервера.
 Service.SECRET_KEYS = ['Token', 'pass', 'Pass', 'Password', 'UserPassword', 'OldPass', 'NewPass', 'ServPass', 'pwd', 'srvpwd', 'SecretAnswer'];

 // Повертає рядок для логу, НЕ змінюючи джерело: serverCMD.js логує socket.UserInfo і одразу
 // віддає ТОЙ САМИЙ об'єкт у білдер команди входу — мутація заради логу відправила б серверу маркер.
 Service.redactSecrets = function (source) {
     let obj = source;

     if (Service.isString(source)){
         try {
             obj = JSON.parse(source);
         } catch (e){
             return source;
         }
     }

     if (!obj || typeof obj !== 'object') return String(source);

     let copy = Array.isArray(obj) ? obj.slice() : Object.assign({}, obj);

     Service.SECRET_KEYS.forEach(function (key) {
         if (copy[key] !== void(0) && copy[key] !== null && copy[key] !== "") copy[key] = "<hidden>";
     });

     return JSON.stringify(copy);
 };

 Service.formatDate = function formatDate(formatDate, formatString, utc){

     if (!formatDate) return formatDate;

     let mcDate = Service.isString(formatDate);

     if (mcDate) formatDate = formatDate.split('.');

     // utc=true → читаємо UTC-компоненти Date (getUTC*), інакше локальні. Рядок розбирається як є.
     let yyyy = mcDate ? parseInt(formatDate[2]) : (utc ? formatDate.getUTCFullYear()     : formatDate.getFullYear());
     let yy = yyyy.toString().substring(2);
     let m = mcDate ? parseInt(formatDate[1]) : (utc ? formatDate.getUTCMonth()         : formatDate.getMonth()) + 1;
     let mm = m < 10 ? "0" + m : m;
     let d = mcDate ? parseInt(formatDate[0]) : (utc ? formatDate.getUTCDate()          : formatDate.getDate());
     let dd = d < 10 ? "0" + d : d;

     let  h = mcDate ? parseInt(formatDate[3]) : (utc ? formatDate.getUTCHours()         : formatDate.getHours());
     let  hh = h < 10 ? "0" + h : h;
     let  n = mcDate ? parseInt(formatDate[4]) : (utc ? formatDate.getUTCMinutes()       : formatDate.getMinutes());
     let  nn = n < 10 ? "0" + n : n;
     let  s = mcDate ? parseInt(formatDate[5]) : (utc ? formatDate.getUTCSeconds()       : formatDate.getSeconds());
     let  ss = s < 10 ? "0" + s : s;
     let  zzz = mcDate ? parseInt(formatDate[6] || "0") : (utc ? formatDate.getUTCMilliseconds() : formatDate.getMilliseconds());

     if (zzz < 10) {
         zzz = "00" + zzz;
     } else
     if (zzz >= 10 && zzz < 100){А
         zzz = "0" + zzz;
     }

     // MMM/MM рахуємо лише коли формат їх містить — moment() дорогий, не викликаємо безумовно
     let MMM = "", MM = "";
     if (formatString.indexOf('M') !== -1) {
         MMM = moment().month(parseInt(m) - 1).format('MMMM');
         MM  = moment().month(parseInt(m) - 1).format('MMM');
     }

     formatString = formatString.replace(/yyyy/, yyyy);
     formatString = formatString.replace(/yy/, yy);
     formatString = formatString.replace(/mm/, mm);
     formatString = formatString.replace(/m/, m);
     formatString = formatString.replace(/dd/, dd);
     formatString = formatString.replace(/d/, d);
     formatString = formatString.replace(/hh/, hh);
     formatString = formatString.replace(/h/, h);
     formatString = formatString.replace(/nn/, nn);
     formatString = formatString.replace(/n/, n);
     formatString = formatString.replace(/ss/, ss);
     formatString = formatString.replace(/s/, s);
     formatString = formatString.replace(/zzz/, zzz);
     formatString = formatString.replace(/MMM/, MMM);
     formatString = formatString.replace(/MM/, MM);

     return formatString;

 };

 Service.Encrypt = function encrypt(key, data) {
     let _key = crypto.createHash('sha256').update(key).digest('base64').substring(0, 32);
     let cipher = crypto.createCipheriv('aes-256-cbc',  _key, iv);
     let crypted = cipher.update(data, 'utf-8', 'hex');
     crypted += cipher.final('hex');

     return crypted;
 };

 Service.Decrypt = function decrypt(key, data) {
     let _key = crypto.createHash('sha256').update(key).digest('base64').substring(0, 32);
     let decipher = crypto.createDecipheriv('aes-256-cbc',  _key, iv);
     let decrypted = decipher.update(data, 'hex', 'utf-8');
     decrypted += decipher.final('utf-8');

     return decrypted;
 };

 Service.EncryptBase64 = function encrypt(key, data) {
     let _key    = crypto.createHash('sha256').update(key).digest();
     let cipher  = crypto.createCipheriv('aes-256-cbc',  _key, iv2);
     let crypted = cipher.update(data, 'utf-8', 'base64');
     crypted    += cipher.final('base64');

     return crypted;
 };

 Service.DecryptBase64 = function decrypt(key, data) {
     let _key      = crypto.createHash('sha256').update(key).digest();
     let decipher  = crypto.createDecipheriv('aes-256-cbc',  _key, iv2);
     let decrypted = decipher.update(data, 'base64', 'utf-8');
     decrypted    += decipher.final('utf-8');

     return decrypted;
 };

 Service.Queue = function(lookupTime, loop){
     let self      = this;
     let needRun   = [];
     let canNext   = loop || false;
     let paramList = [];
     let nowRunned = false;
     let time      = lookupTime || 1000;

     let lookUp    = null;

     function start(){
         return setInterval(function(){
             if (!nowRunned && needRun.length > 0){
                 nowRunned = true;

                 let arg = (Service.isArray(paramList[0])) ? paramList[0] : [paramList[0]];

                 needRun[0].apply(self, arg);
             }
         }, time);
     }

     this.next = function(){
         if (needRun.length > 0){
             needRun.shift();
             paramList.shift();

             nowRunned = false;
         } else
         if (!canNext){
             clearInterval(lookUp);

             self = null;
         }
     };

     this.addTask = function (qItem, params){
         if (needRun.length === 0 && !nowRunned) {
             lookUp = start();
         }

         needRun.push(qItem);
         paramList.push(params);

         return needRun.length;
     };
 };

 Service.TaskList = function(_scope){
     let self  = this;
     let list  = [];
     let scope = {};

     this.AddTask = function(task){
         list.push(task);
     };

     this.Next = function(){
         if (list.length){
             list.shift().apply(scope, arguments);
         } else {
             self = null;
         }
     };

     this.Run = function(){
         self.Next();
     };

     scope = Service.MargeObj({
         next : self.Next
     }, _scope || {});
 };

 /**
 * @return {boolean}
 */
 Service.NTLM = function(request, response) {
    let auth = request.headers.authorization;
    let res = false;

    if (!auth) {
        console.log('Start NTLM authentication', console.logLevel.L_Normal);

        response.statusCode = 401;
        response.setHeader('WWW-Authenticate', 'NTLM');
        response.end();
    } else {
        console.log('Authentication-Header ' + auth, console.logLevel.L_Normal);

        // let msg = new Buffer(auth.substr(5), 'base64');
        let msg = Buffer.from(auth.substr(5), 'base64');

        if (msg.toString('utf8', 0, 8) != 'NTLMSSP\0') {
            console.error('Invalid NTLM response header.');
        }

        if (msg.readUInt8(8) == 1) {
            console.log('NTML Message Type 1', console.logLevel.L_Normal);

            // let msg2 = new Buffer(40);
            let msg2 = Buffer.alloc(40);
            let offset = 0;

            let header = 'NTLMSSP\0';
            for (let i = 0; i < header.length; i++) {
                msg2.writeUInt8(header.charCodeAt(i), offset++)
            }

            msg2.writeUInt8(0x02, offset++);
            msg2.writeUInt8(0x00, offset++);
            msg2.writeUInt8(0x00, offset++);
            msg2.writeUInt8(0x00, offset++);
            msg2.writeUInt8(0x00, offset++);
            msg2.writeUInt8(0x00, offset++);
            msg2.writeUInt8(0x00, offset++);
            msg2.writeUInt8(0x00, offset++);
            msg2.writeUInt8(0x00, offset++);
            msg2.writeUInt8(0x28, offset++);
            msg2.writeUInt8(0x00, offset++);
            msg2.writeUInt8(0x00, offset++);
            msg2.writeUInt8(0x01, offset++);
            msg2.writeUInt8(0x82, offset++);
            msg2.writeUInt8(0x00, offset++);
            msg2.writeUInt8(0x00, offset++);
            msg2.writeUInt8(0x01, offset++);
            msg2.writeUInt8(0x23, offset++);
            msg2.writeUInt8(0x45, offset++);
            msg2.writeUInt8(0x67, offset++);
            msg2.writeUInt8(0x89, offset++);
            msg2.writeUInt8(0xab, offset++);
            msg2.writeUInt8(0xcd, offset++);
            msg2.writeUInt8(0xef, offset++);
            msg2.writeUInt8(0x00, offset++);
            msg2.writeUInt8(0x00, offset++);
            msg2.writeUInt8(0x00, offset++);
            msg2.writeUInt8(0x00, offset++);
            msg2.writeUInt8(0x00, offset++);
            msg2.writeUInt8(0x00, offset++);
            msg2.writeUInt8(0x00, offset++);
            msg2.writeUInt8(0x00, offset++);

            console.log('NTML Message Type 2', 'NTLM ' + msg2.toString(), console.logLevel.L_Normal);

            response.statusCode = 401;
            response.setHeader('WWW-Authenticate', 'NTLM ' + msg2.toString('base64'));
            response.end();
        } else if (msg.readUInt8(8) == 3) {
            console.log('NTML Message Type 3', console.logLevel.L_Normal);

            let ntlmdata = {
                target: msg.toString('utf16le', msg.readUInt16LE(32), msg.readUInt16LE(32) + msg.readUInt16LE(28)),
                userid: msg.toString('utf16le', msg.readUInt16LE(40), msg.readUInt16LE(40) + msg.readUInt16LE(36)),
                workstation: msg.toString('utf16le', msg.readUInt16LE(48), msg.readUInt16LE(48) + msg.readUInt16LE(44))
            };

            request.ntlm = ntlmdata;
            response.ntlm = ntlmdata;

            res = true;

            console.log(ntlmdata);
        } else {
            console.log('NTML Message Type not known', console.logLevel.L_Normal);

            response.end();
        }
    }

    return res;
};

 Service.SendDataInternal = null;

 Service.decodeBase64Image = function (dataString) {
    let matches = dataString.match(/^data:([A-Za-z-+\/]+);base64,(.+)$/);
    let response = {};

    if (!matches || matches.length !== 3) {
        console.err('Invalid input string');

        return false;
    }

    response.type = matches[1];
    // response.data = new Buffer(matches[2], 'base64');
    response.data = Buffer.from(matches[2], 'base64');

    return response;
 };

 Service.findMyChatServer = function (host, cb, done) {
     // let message = new Buffer('FindMyChat');
     let message = Buffer.from('FindMyChat');
     let client  = require('dgram').createSocket('udp4');
     let broadcastMask = host.split('.');

     broadcastMask[3] = '255';
     broadcastMask = broadcastMask.join('.');

     const finish = function(){
         if (client){
             client.close();
             client = null;

             if (done) done();
         }
     };

     client.bind(2005, host, function () {
         setTimeout(finish, 2000);

         client.setBroadcast(true);

         client.on('message', function(msg, info) {
             cb(Service.StringToObj(msg), info); // msg info.address info.port
         });

         client.send(message, 0, message.length, 2004, broadcastMask);
     });
     
     client.on("error", (err) => {
         console.log(`findMyChatServer: ${err.message}\n${err.stack}`);

         finish();
     });
 };

 Service.getNetworkInterfacesList = function () {
     let netList = os.networkInterfaces();
     let list    = [];

     Object.keys(netList).forEach(function (ifname) {
         netList[ifname].forEach(function (currentInterface) {
             if (!('IPv4' !== currentInterface.family || currentInterface.internal !== false)) { // skip over internal (i.e. 127.0.0.1) and non-ipv4 addresses
                 list.push({
                     ip : currentInterface.address,
                     mac: currentInterface.mac
                 });
             }
         });
     });

     return list;
 };

 Service.testServerConnect = function (host, port, callback, errHandle) {
     try {
         let inData   = "";
         let __socket = require('net').createConnection(
             port,
             host,
             function () {
                 __socket.setTimeout(10000);
                 
                 __socket.write("mctest" + CRLF);
             }
         );
         var noSendNoData = false;

         __socket.setTimeout(10000);

         __socket.on('data', function (data) {
             inData += data.toString();
         });

         __socket.on('close', function () {
             if (inData){
                 if (inData.indexOf("mctest") === 0){
                     callback(null, inData);
                 } else
                 if (inData.indexOf('\u0017\u0006' + '8001') === 0) {
                     errHandle(inData);
                 } else {
                     callback({
                         message: "it is not a MyChat Server or this server version is too old"
                     });
                 }
             } else {
                 !noSendNoData && callback({
                     message: "testServerConnect no data"
                 });
             }
         });

         __socket.on('error', function (err) {
             inData = "";

             __socket.destroy();
             __socket = null;

             noSendNoData = true;

             callback(err);
         });

         __socket.on('timeout', function () {
             noSendNoData = true;
             
             callback({
                 code   : "connection timeout",
                 message: "testServerConnect: connection timeout"
             });
         });

         return __socket;
     } catch (e) {
         callback(e);
     }
 };

 Service.getHardwareID = function (cb, path) {
     HDWID(cb, path);
 };

 Service.getLinkInfoByXY = function(win, x, y) {
     function script() {
         let x = "XXX";
         let y = "YYY";
         let link = document.elementFromPoint(x, y);
         let uin  = link.getAttribute('uin');

         window.mcComponents._broadcast(window._messages_.dialogsList.getQuickUserInfo, [uin, function (userInfo) {
             let _electronHandler = require("electron").ipcRenderer;
             let data = {
                 id      : link.id,
                 href    : link.href,

                 filePath: link.getAttribute("filepath"),
                 fileDT  : link.getAttribute("filedt"),
                 fileSize: link.getAttribute("filesize"),

                 userInfo: userInfo
             };
             
             _electronHandler.send("e_CMD", [
                 '7027', // ce_special_link_user_info
                 data
             ]);
         }]);
     }
     
     win.webContents.executeJavaScript(("(" + script.toString().replace("XXX", x).replace('YYY', y) + ")()"));
 };

 Service.loadFileSync = fs.readFileSync;

 Service.format_size = function(size) {
     let index = 0;

     while (size > 1024){
         index ++;

         size = size / 1024;
     }

     return Math.round(size * 100)/100 + " " + ["B","KB","MB","GB","TB","PB","EB"][index];
 };

 Service.findItemInArrayOfObj = function( arr, val, id, lowerCase ){
     for (let i = 0; i < arr.length; i++){
         if (!id){
             for (let j in arr[i]){
                 if (lowerCase){
                     if (arr[i][j].toLowerCase() == val.toLowerCase()){
                         return i;
                     }
                 } else
                 if (arr[i][j] == val){
                     return i;
                 }
             }
         } else
         if (lowerCase){
             if (arr[i][id].toLowerCase() == val.toLowerCase()){
                 return i;
             }
         } else
         if (arr[i][id] == val){
             return i;
         }
     }

     return -1;
 };

 Service.checkDir = function (checkPath) {
     let statLogDir = null;
     let createPath = Service.ExtractPath(checkPath);

     try {
         statLogDir = fs.statSync(createPath) ? createPath : "./";
     } catch (e){
         statLogDir = './';
     }

     try {
         if (statLogDir === './' && createPath !== './') {
             let sep = '/';
             let initDir = _path.isAbsolute(createPath) ? sep : '';

             createPath.split(sep).reduce((parentDir, childDir) => {
                 let curDir = _path.resolve(parentDir, childDir);

                 if (!fs.existsSync(curDir)) {
                     fs.mkdirSync(curDir);
                 }

                 return curDir;
             }, initDir);

             statLogDir = createPath;
         }
     } catch (e){
         process.stdout.write("\x1B[31m >> [err]: Path not found: " + createPath + CRLF);

         statLogDir = './';
     }

     return statLogDir;
 };

 Service.deleteFolderRecursive = function(path) {
     if( fs.existsSync(path) ) {
         fs.readdirSync(path).forEach(function(file){
             let curPath = path + (path[path.length - 1] === "/" ? "" : "/") + file;
             
             if(fs.lstatSync(curPath).isDirectory()) { // recurse
                 Service.deleteFolderRecursive(curPath);
             } else { // delete file
                 fs.unlinkSync(curPath);
             }
         });

         fs.rmdirSync(path);
     }
 };

 Service.versionToInt = function(_ver, all){
     let res = 0;

     if (typeof _ver !== "string" && typeof _ver !== "number") {
         // skip return 0
     } else {
         let parts = String(_ver).split(".");
         let major = parseInt(parts[0], 10) || 0;
         let minor = parseInt(parts[1], 10) || 0;
         let patch = all ? (parseInt(parts[2], 10) || 0) : 0;

         res = major * 1000000 + minor * 1000 + patch;
     }

     return res;
 };

 Service.convertIntToBool = function (val) {
     return (Object.prototype.toString.call( val ) === '[object Boolean]') ? val : val == 1;
 };

 Service.myReplaceFormated = function(source, items){
     if (Service.isObject(items) && Service.isString(source)){
         for (let i in items){
             source = source.replace(new RegExp("#{" + i + "}", "g"), items[i]);
         }
     }

     return source;
 };

 Service.myReplaceUni = function(source, items){
     if (Service.isObject(items) && Service.isString(source)){
         for (let i in items){
             source = source.replace(new RegExp("\%" + i + "\%", "g"), items[i]);
         }
     }

     return source;
 };

 Service.HOME_DIR = HOME_DIR;

 Service.mcServerURL = function (url, sInfo) {
     let serverInfo = Service.MargeObj(Service.MCServer, sInfo);

     return 'http' + (serverInfo.HTTPS ? 's' : '') + '://' + serverInfo.ipNode +
         ( serverInfo.HTTPS
                 ? (serverInfo.PortNode != 443 || (url && url.indexOf(':443/')) > 0 ? ':' + serverInfo.PortNode : '')
                 : (serverInfo.PortNode != 80  || (url && url.indexOf(':80/'))  > 0 ? ':' + serverInfo.PortNode : '')
         );
 };

 Service.getUpdateFilePath = function () {
     let updatePath = "";

     switch (os.platform()) {
         case "darwin":
             updatePath += "/updates/updatemac.zip";
         break;

         case "linux":
             updatePath += "/updates/updatelinux" + (os.arch() === "ia32" ? "32" : "") + ".zip";
         break;

         case "win32":
             updatePath += "/updates/updatewin32next.zip";
         break;
     }

     return updatePath;
 };

 Service.forvardZero = function (st, strLength, symbol) {
     st = st.toString();

     return (new Array(strLength + 1 - st.length)).join(symbol || '0') + st;
 };

 Service.convertBin = {
     bin2dec : function(s) { return parseInt(s, 2).toString(10)},
     bin2hex : function(s) { return parseInt(s, 2).toString(16)},
     dec2bin : function(s) { return parseInt(s, 10).toString(2)},
     dec2hex : function(s) { return parseInt(s, 10).toString(16)},
     hex2bin : function(s) { return Service.forvardZero(parseInt(s, 16).toString(2), s.length * 4)},
     hex2dec : function(s) { return parseInt(s, 16).toString(10)}
 };

 Service.bitOperation = {
     getBit: function (number, bitPosition) {
     return (number & (1 << bitPosition)) === 0 ? 0 : 1;
 },

     setBit: function (number, bitPosition) {
     return number | (1 << bitPosition);
 },

     clearBit: function (number, bitPosition) {
     const mask = ~(1 << bitPosition);
     return number & mask;
 },

     updateBit: function (number, bitPosition, bitValue) {
     const bitValueNormalized = bitValue ? 1 : 0;
     const clearMask = ~(1 << bitPosition);
     return (number & clearMask) | (bitValueNormalized << bitPosition);
 }
 };

 let iv = "c.randomByte(16)";

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

 function getExecPath() {
     // Electron
     try {
         const electron = require('electron');
         if (electron && electron.app.getExecutablePath) {
             return electron.app.getExecutablePath();
         }
     } catch (e) {}

     // Node.js - основні варіанти
     if (process.execPath) return process.execPath;
     if (process.argv[0]) return process.argv[0];
     if (process.argv0) return process.argv0;

     // Fallback
     return process.platform === 'win32' ? 'node.exe' : '/usr/bin/node';
 }

 MCPathes.NodeEXE    = Service.ExtractPath(getExecPath()).replace(/\\/g, '/');
 MCPathes.NodeModules= MCPathes.NodeEXE + '/node_modules/';
 MCPathes.Index      = global.__dirname; //ii + ((ii[ii.length - 1] != '/')? '/' : '');
 MCPathes.Service    = MCPathes.Index   + '/client/modules/';
 MCPathes.Modules    = MCPathes.Index   + '/client/modules/';
 MCPathes.Images     = MCPathes.Index   + '/client/modules/images/';
 MCPathes.WWW        = MCPathes.Index   + '/client/';

 MCPathes.Profile    = "./";
 MCPathes.History    = "./";
 MCPathes.ProfilNode = "./";
 MCPathes.Events     = "./";
 MCPathes.Updates    = "./";

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

 module.exports = Service.MargeObj(module.exports, Service);
