const mysql = require('mysql')
const Spawner = require('child_process');
const crypto = require('crypto');
const request = require('request');
const cheerio = require('cheerio');


class Child
{
    /**
     * 
     * @param {String} userID -User's Hash 
     */
    constructor(userID)
    {
        this.ID = userID;
        this.child = Spawner.fork('./worker.js',[this.ID]);
        this.birth = process.uptime();
        this.timeOut = setTimeout(() => {
            this.child.kill('SIGINT');
        },2*60*60*1000);
    }
    get EOL() 
    {
        const EOL = Math.trunc((this.timeOut._idleTimeout - ((process.uptime()*1000)-this.birth))/1000);
        return EOL>0?EOL:0;
    }
    getMapData()
    {
        return new Promise((resolve, reject)=>
        {
            this.child.send('MapData');
            this.child.on('message', m=>
            {
                if(typeof m == Object)
                {
                    resolve(m.name);
                }
            })
        })
    }
    waitStart()
    {
        return new Promise((resolve, reject)=>
        {
          var status =  setInterval(() => {
               if(this.isOpen)
               {
                    clearInterval(status);
                    resolve(true)
               }
           }, 100);
        })
    }
    createMapImage()
    {
        return new Promise((resolve, reject)=>
        {
            this.child.send('MapImage')
            this.child.on('message', m=>
            {
                if(typeof m == String && m === 'Done')
                {
                    resolve(m);
                }
            })
        })
    }
    getConsole()
    {
        return new Promise((resolve, reject)=>
        {
            this.child.send('Console')
            this.child.on('message', m=>
            {
                resolve(m);
            })
        })
    }
  
}module.exports.Child = Child;


class playerCache
{
    /**
     * 
     * @param {String} Name 
     * @param {String} SteamID 
     * @param {Number} Ping 
     * @param {String} Address IP
     * @param {Number} ConnectedSeconds 
     * @param {Number} Health 
     * @param {Object} Teammates ??'Not in a team'
     * @param {String} Position ??'Not spawned yet'
     */
    constructor(Name, SteamID, Ping, Address,ConnectedSeconds, Health, Teammates, Position)
    {
        this.Displayname = Name;
        this.SteamID = SteamID;
        this.Address = Address;
        this.Health = Health;
        this.Teammates =  Teammates??'NOT IN A TEAM';
        this.Position = Position??'NOT SPAWNED YET';
        this.Ping = Ping;
        this.ConnectedSeconds = ConnectedSeconds;
    }
    



}module.exports.playerCache = playerCache;


class ServerCache
{
    constructor(ID,Hostname,MaxPlayers, GameTime, Framerate, NetworkIn, NetworkOut, Seed, WorldSize)
    {
        this.ID = ID;
        this.Hostname=Hostname;
        this.MaxPlayers =MaxPlayers;
        this.GameTime = GameTime;
        this.Framerate = Framerate;
        this.NetworkIn = NetworkIn;
        this.NetworkOut = NetworkOut;
        this.Seed = Seed;
        this.WorldSize = WorldSize;
        this.Console = [];
    }

    get WorldData ()
    {
        return {
            Seed:this.Seed,
            Size:this.WorldSize,
            Time:this.GameTime
        }
    }
    get NetWorkData ()
    {
        return {
                Framerate:this.Framerate,
                In:this.NetworkIn,
                Out:this.NetworkIn
        }
    }
    get ConsoleData ()
    {
        return this.Console;
    }
    get BasicData ()
    {
        return {
            HostName:this.Hostname,
            MaxPlayers:this.MaxPlayers,
            GameTime:this.GameTime
        }
    }
}module.exports.ServerCache = ServerCache;


class pChild
{
    
    constructor(ip, port, pw)
    {
        this.ip = ip;
        this.port = port;
        this.pw = pw;
        this.child = Spawner.fork('./pWorker.js');
    }
    /**
     * @argument {*} - Anything 
     */
    sendData() 
    {
        for (var i = 0; i < arguments.length; i++)
        {
            
        }
    }  

    /**
     *   To be used with programmed commands
     * @param {String} command - Command to be sent
     */
    send(command)
    {
        return new Promise((resolve, reject)=>
        {
            this.child.send({'command':command, 'ip':this.ip, 'port':this.port, 'pw':this.pw});
            this.child.on('message', (m)=>
            {
                if(typeof m=='string')
                { 
                    if(m ==='end')
                    {
                        resolve();
                    }
                }
                else
                {
                    reject("Worker didn't respond to querry.")
                }
            })
        })
    }
    close ()
    {
        return new Promise((resolve, reject)=>
        {
            resolve(this.child.disconnect());
        })
    }

}module.exports.pChild = pChild;
class rustChild
{
    constructor(size, seed)
    {
        this.child = Spawner.exec(`cmd /c updateServer.bat ${size} ${seed}`)
    }
}module.exports.rustChild = rustChild;


class DBConnection
{
    constructor()
    {
        this.connection = mysql.createConnection(
            {
                host:'localhost',
                user:'root',
                password:'root',
                database:'legendary'
            })
        this.connection.connect();
        
    }
    close()
    {
        setTimeout(() => 
        {
            this.connection.destroy(); 
        }, 2000);        
    }
    //User
    get Users()
    {
        return new Promise((resolve, reject)=>
        {
            this.connection.query('Select * from administrator', (e, r, f)=>
            {
                if(!e)
                {
                    let aux = JSON.parse(JSON.stringify(r))
                    var payload =[];
                    
                    for( var i = 0; i<aux.length; i++)
                    {
                        payload.push(aux[i].Hash)
                    }
                    resolve(payload);
                    this.close();
                }
                else
                {
                    reject(e)
                    this.close();
                }
            });
        })
    }
    hasSVData(Hash)
    {
        return new Promise((resolve, reject)=>
        {
            this.UData(Hash).then(
                m=> 
                {

                    if(m.Server=='No server')
                    {
                        reject(false)
                    }
                    else
                    {
                        resolve(true)
                    }
                }
            )
        })
    }
    UData(HASH)
    {
        return new Promise((resolve, reject)=>
        {
            this.connection.query(`SELECT * FROM administrator WHERE Hash = '${HASH}' `, (e, r, f)=>  
            {
                if(e)reject(e);
                r = JSON.parse(JSON.stringify(r))[0];
                resolve({
                    id:r.A_ID,
                    Hash:r.Hash,
                    Email:r.EHash,
                    Server:r.IP || 'No server',
                    Port: JSON.stringify(r.Port) || 'No server',
                    SPW:r.sv_pw,
                    Epoch:r.Epoch
                })
                this.close();
            })
        })
    }

    get_ID(hash)
    {
        return new Promise((resolve, reject)=>
        { 
          
            //User
            this.Users.then((x)=>
            {
               for(var i =0; i<x.length; i++)
               {
                   if(x[i]==hash)
                   {   
                       
                       this.UData(hash).then((m)=>
                       {
                           resolve(m.id);
                       })
                       .catch(()=>reject('no ID for this hash'))
                   }
               }  
            }).catch(m=>
            {
                console.log(m);
                reject('no ID for this HASH');
            });
        })
    }

    getCommands(Hash)
    {
        return new Promise((resolve, reject)=>
        {
            this.get_ID(Hash).then(id=>
                {
                    this.connection.query(`Select * from legendary.timed_com where administrator_A_ID = '${id}'`, (e, r, f)=>
                    {
                        if(e)reject(e);
                        resolve(JSON.parse(JSON.stringify(r)));
                    })
                })
        })
    }

    addCommand(Hash, Command, Loop, Send, tz)
    {
        this.get_ID(Hash).then(id=>
            {
                
                this.connection.query(`INSERT INTO legendary.timed_com (\`Command\`, \`Loop\`, \`Send\`, \`administrator_A_ID\`) VALUES ('${Command}', '${Loop}', '${Send}', '${id}')`);
                this.close();
            })
    }
  
    removeCommand(id)
    {
        return new Promise((resolve, reject)=>
        {
            this.connection.query(`DELETE FROM legendary.timed_com WHERE (T_ID =  ${id});`, (e, r, f)=>
            {
                if(e)reject(e)
                if(r.affectedRows>0)resolve()
            })
            this.close()   
        })
     
    }
  
    editCommand(id, Command, Loop, Send, tz)
    {
        this.connection.query(`UPDATE legendary.timed_com SET \`Command\` = '${Command}', \`Loop\` = '${Loop}',\`Send\` = '${Send}' WHERE (T_ID = '${id}')`)
        this.close()
    }
    
    addUser(Hash, EHash, Epoch)
    {
        if(Hash||EHash||Epoch)
        {
            this.connection.query(`INSERT INTO legendary.administrator (Hash, EHash, Epoch) VALUES ('${Hash}', '${EHash}', '${Epoch}')`)
            this.close()
        }
    }
    
    updateUser(HASH, NewHASH)
    {
        return new Promise((resolve, reject)=>
        {
            this.get_ID(HASH).then((m)=>
            {
                this.connection.query(`UPDATE administrator  SET Hash='${NewHASH}' WHERE A_ID = ${m}`);
                this.close();  
                resolve(NewHASH);
            }).catch((err)=>reject(err));   
        })
    }
  
    addServer(HASH, IP, PW, PORT)
    {
        //needs to know what user should it be connected to
        this.connection.query(`INSERT INTO adminsitrator (ServerIP, ServerPW, ServerPort) VALUES ('${HASH}', '${IP}','${PW}', '${PORT}')`)
        this.close();
    }
     
    updateServer(Hash, IP, PW, PORT)
    {   
        this.get_ID(Hash).then((id)=>
        {
            this.connection.query(`UPDATE administrator SET IP = '${IP}', sv_pw = '${PW}', port='${PORT}' WHERE A_ID = ${id}`);
            this.close();
        }).catch(e=>console.log('error',e))
    }
    get_Players(Hash)
    {
        return new Promise((resolve, reject)=>
        {
            this.connection.query(`SELECT * FROM player INNER JOIN time ON player.p_id = time.player_p_id
            where administrator_a_id = (SELECT A_ID FROM administrator WHERE Hash = '${Hash}')
            order by player_p_id asc;`,(e,r,f)=>
            {
                if(e)reject(e);this.close;
                try 
                {
                    // console.log('Classes: DBConnection: Get_Players: r: ', r);
                    r = JSON.parse(JSON.stringify(r));
                    resolve(r)

                } catch (err)
                {
                    reject(err)
                }
                this.close;
            })
        })
    }
}module.exports.DBConnection = DBConnection;

class _Crypto
{
    constructor(User, Password)
    {
        this.salt = 'H$44Q3RVCd9X8Ef63tB4';
        this.secret = 'mYFUZX9NSx7K74r7Jh@O';
        this.pepper = String.fromCharCode(this.getRandomInt(65, 90));
        this.password = Password;
        this.user = User;
        this.algorithm = 'aes-192-cbc';

    }
    get Hash() // to use on register
    {
        return this.hash()
    }
    hash(p)
    {
        if(!p)
        {
            return crypto.createHmac('sha256', this.secret).update(this.user+this.password+this.salt+this.pepper).digest('hex');  
        }
        else
        {
            return crypto.createHmac('sha256', this.secret).update(this.user+this.password+this.salt+p).digest('hex');  
        }
    }
    eHash(Email)
    {
      return new Buffer.from(Email).toString('base64');
    }
  
    decrypt(eHash)
    {
      return new Buffer.from(eHash, 'base64').toString('utf8');
    }
    getRandomInt(min, max)
    {
        min = Math.ceil(min);
        max = Math.floor(max);
        return Math.floor(Math.random() * (max - min)) + min;
    }
    compare() // to use on login
    {
       return new Promise((resolve, reject)=>
       {
            new DBConnection().Users
            .then((Users)=>
            {
                for(var i=0; i<26;i++)
                {   
                    const nPepper = String.fromCharCode(65+i);
                    const hash = this.hash(nPepper);
                    for(var j = 0; j<Users.length; j++)
                    {
                            if(Users[j]==hash) resolve(hash);
                    }
                }
                reject(false);
            })
            .catch((m)=>
            {
                console.log('error'+m);
            })
           
       });
    }
}module.exports._Crypto = _Crypto;


class Epoch
{
    /**
     * 
     * @param {Number} Days - Must be and integer.
     */
    constructor(Days)
    {
        this.Days = (Days*(24*60*60));
        this.epoch = this.epocher();
    }
    epocher()
    {
        return Math.floor(this.Days + Date.now()/1000)
    }
    /**
     * @param {String} time - Must follow the format of '18:12'.
     */
    sEpoch(time)
    {
        let hours = time.split(':')[0];
        let minutes = time.split(':')[1];
        return Math.floor(this.next(hours, minutes)/1000);
    }
    /**
     * @param {Number} time - Must be and integer.
     */
    toDate(time)
    {
        //console.log('classes:toDate:', time)
        const d = new Date(time*1000);
        return d.getHours()+':'+ d.getMinutes();
    }
    /**
     * Returns the next possible time for the command to be sent. Meaning if the hours inputed by the user
     * have already gone past this day then the epoch will be set for the next. 
     * If not the epoch will be created like usual.
     * 
     * @param {Number} hours 
     * @param {Number} minutes 
     */
    next(hours, minutes)
    {
        var d= new Date();
        var payload = new Date(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate(), hours, minutes, 0, 0).getTime();    
        //console.log('Classes:Next:',hours, minutes, payload)
        return payload;
    }
}module.exports.Epoch = Epoch;

class IPTranslator
{
    constructor(IP)
    {
        this.IP = IP;
    }
    translate()
    {
        return new Promise((resolve, reject)=>
        {
            request(`http://ip-api.com/json/${this.IP}`, (e,r,b)=>
            {
                var body
                if(!e)
                {
                    try {
                        body = JSON.parse(b);
                    } catch (error) {
                        console.log('IP Transalator: Bad Parse.');
                        resolve('Err')
                    }
                    resolve(body);             
                }
                else
                {
                    resolve( {'status':'error'});
                }
            })
        })
    }
    CC()
    {
        return new Promise((resolve, reject)=>
        {
            this.translate().then(m=>resolve(m.countryCode)).catch(e=>
                {
                    console.log('IP Translator: CountryCode:'+e);
                    reject(e);
                })
        })
    }
}module.exports.IPTranslator = IPTranslator;

class VAC
{
    constructor(SteamID)
    {
        this.SteamID = SteamID;
    }
    get ban()
    {
        return new Promise((resolve,reject)=>
        {
            request(`http://vacbanned.com/engine/check?qsearch=${this.SteamID}`, (e,r,b)=>
            {
                if(e)reject('VAC: Request:'+e)
                const $ = cheerio.load(b);
                // console.log('Worker: VAC: body: ', $('td').text());
                $('.strong').remove()
                request(`http://api.steampowered.com/IPlayerService/GetOwnedGames/v0001/?key=DCA47FAA9F3D1485FA8C1F4FA15A5266&steamid=${this.SteamID}&format=json`, (e,r,steamb)=>
                {
                    if(e)reject('VAC: FS: Request:'+e)
                    var fs;
                    let aux 
                    //console.log('Vac class: SteamAPI', steamb)
                    try 
                    {
                        aux = JSON.parse(steamb)['response'].games;
                    } catch (error) 
                    {
                        console.log('JSON parse error: ',error);
                    }
                    if(aux)
                    {
                        for (let i = 0; i < aux.length; i++)
                        {
                            // console.log('VAC: FS: Request2: '+aux[i].appid, '252490');
                            if(aux[i].appid)
                            {
                                if(aux[i].appid=='252490')
                                {
                                    i = aux.length;
                                    fs = 'NO'
                                }
                                else
                                {
                                    fs='Yes'
                                }
                            } 
                            }
                            var vac;
                            try {
                                vac = $('td').text().replace(/ /g, '').split('Hex)')[1].replace(/\n/g, 'SPLIT').split('SPLIT')[1];
                            } catch (error) {
                                vac = 'Error';
                            }
                        let payload = {
                            'vac':vac,
                            'fs':fs
                        }
                        
                        // console.log('VAC: Payload: ', payload)                   
                        resolve(payload);
                    } 
                    else
                    {
                        var vac;
                            try {
                                vac = $('td').text().replace(/ /g, '').split('Hex)')[1].replace(/\n/g, 'SPLIT').split('SPLIT')[1];
                            } catch (error) {
                                vac = 'Error';
                            }
                        let payload = {
                            'vac':vac,
                            'fs':fs
                        }
                        resolve(payload);
                    }   
                })
            });
        })
    }
}module.exports.VAC = VAC;



/**
 * Use it to transform an hour value into miliseconds.
 * @param {Number} Hours - Hours to transform
 */
function hours(Hours)
{
    return Hours*60*60*1000;
}
/**
 *  This function writes a cookie on the client side.
 *  Primary use: Store users Hash for every other page.
 * @argument {Response} res - Response: express response object.
 * @argument {String} name - Name of the cookie.
 * @argument {*} value  - Value of the cookie.
 */
exports.baker = (res, name, value) =>
{
    return new Promise((resolve,reject)=>
    {
        try 
        {
            res.cookie(name, '',{maxAge:0});
            res.cookie(name, value, {maxAge:hours(2),path:'/',secure:false,httpOnly:false});
            resolve();
        }
        catch (e) 
        {
            reject(e)
        }
    })
}

/**
 * Hash used on register to check if there is already a user with the same credentials.
 * @param {String} user - User's email 
 * @param {String} password - User's Password
 * @param {String} hash - User's identifying hash
 */
exports.bouncer = (user, password, hash) =>
{
    return new Promise((resolve, reject)=>
    {
        new _Crypto(user, password).compare().then(m=>
        {
            if(hash)
            {
                if(m==hash)
                {
                    resolve(m)
                }
                else
                {
                    reject('You are on a different account.')
                }
            }
            else
            {
                resolve(m)
            }
        }).catch(m=>
        {
            reject(m)
        })
    })
}

/**
 * Reduces the number of decimal cases to the required value. 
 
 * @param {Number} num - The number you want to cut to size.
 * @param {Number} decimal - Ammount of decimal cases.
 */
exports.toFixed =  (num, decimal)=>
{
    decimal = decimal || 0;
    decimal = Math.pow(10, decimal);
    return Math.floor(num * decimal) / decimal;
}
/**
 * Transforms a value into a 0-1 value given the max value it could take.
 * @param {Number} val - Value to transform to percentage 
 * @param {Number} max  - Maximum the value could be
 */
exports.toPercent = (val, max)=>
{
    return val/max;
}

/**
 * Removes the timezone difference to a time. 
 * @param {String} time - Must follow 'HH:MM' format.
 * @param {Number} offSet - Number of minutes from gmt+0.
*/
exports.removeTZ  = (time, offSet)=>
{
    var h = parseInt(time.split(':')[0]);
    var maux = parseInt(time.split(':')[1]);
    var m = maux - offSet;
    var d = new Date();
    var payload = new Date(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate(), h, m, 0, 0).toString().split(' ')[4].split(' ')[0].slice(0,-3);
    return payload;
}


/**
 * Adds timezone difference to a time. 
 * @param {String} time - Must follow 'HH:MM' format.
 * @param {Number} offset - Number of minutes from gmt+0.
*/
exports.addTZ = (time, offset) =>
{
    offset = parseInt(offset);
    let h = parseInt(time.split(':')[0]);
    let m = parseInt(time.split(':')[1])+offset;
    var d= new Date();
    return payload = new Date(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDay(), h, m, 0, 0).toString().split(' ')[4].split(' ')[0].slice(0,-3);
}