const fs = require('fs');
const https = require('https');
const seedrandom = require('seedrandom');

const express = require('express');
const app = express();

const options = {
    key: fs.readFileSync("ssl/private.key"),
    cert: fs.readFileSync("ssl/certificate.crt"),
    ca: [fs.readFileSync('ssl/ca_bundle.crt')]
};

const server = https.createServer(options, app);
const io = require('socket.io')(server);

const PORT = 443;

const admins = {
    'André': 'passwort'
}

var users = [];

var bannedIps = [];

fs.readFile("config/ban.json", "utf8", (err, data) => {
    if (!err) {
        bannedIps = JSON.parse(data);
    }
})

const randomColors = [
    '#c42020',
    '#de801e',
    '#f6c60b',
    '#19ce19',
    '#129696',
    '#2c53e3',
    '#6f23e3',
    '#883f88',
];

const HISTORY_SIZE = 50;
var history = [];

app.use(express.static(__dirname + '/www'));
app.use('/node_modules', express.static(__dirname + '/node_modules'));
app.get('/img/ava/:username', (req, res) => {
    var file = '';
    if (fs.existsSync(__dirname + '/www/img/ava/' + req.params.username + '.png')) {
        file = __dirname + '/www/img/ava/' + req.params.username + '.png';
    } else {
        const files = fs.readdirSync(__dirname + '/www/img/ava/random/').filter((f) => f.toLowerCase().endsWith('.png'));

        const seededRandom = seedrandom(req.params.username)();
        const randomFile = files[Math.floor(seededRandom * files.length)];
        file = __dirname + '/www/img/ava/random/' + randomFile;
    }

    res.sendFile(file);
});

io.on('connection', (socket) => {
    const ip = socket.handshake.address;

    log('- New user joined the server:', ip);

    for (const bannedItem of bannedIps) {
        if (bannedItem.ip == ip) {
            log('- User blacklisted, kicking:', ip)
            socket.emit('serverKick');
            socket.disconnect();
            break;
        }
    }

    socket.emit('serverHandshake');

    const user = {ip: ip, socket: socket};
    users.push(user);

    socket.on('login', (username, password) => {
        if (!username) {
            socket.emit('usernameInvalid');
            return;
        }

        for (let user of users) {
            if (user.name == username) {
                socket.emit('usernameTaken');
                return;
            }
        }


        if (admins && admins[username]) {
            if (!password) {
                log('- Attempted login as admin without password.', username, '(' + ip + ')');
                socket.emit('passwordRequired');
                return;
            } else if (admins[username] != password) {
                log('- Attempted login as admin with wrong password.', username, '(' + ip + ')');
                socket.emit('passwordWrong');
                return;
            }

            log('- Admin "' + username + '" login successful');
            user.admin = true;

            socket.on('requestKick', (userToBeKicked) => {
                log('- Admin "' + username + '" requested kick of User "' + userToBeKicked.name + '"');
                kickUser(userToBeKicked);
            });

            socket.on('requestLiftBan', (ip) => {
                log('- Admin "' + username + '" requested to lift ban for ip "' + ip + '"');
                liftBan(ip);
            });
        }

        log('- New user logged in:', username, '(' + ip + ')');

        user.name = username;
        user.color = getRandomColor(username);

        socket.emit('serverLogin', getCleanUser(user), history);
        io.emit('userJoined', username);
        updateUsers();
        updateBanned();

        socket.on('disconnect', () => {
            log('- User joined the server:', ip);

            users = users.filter((listUser) => listUser !== user);
            io.emit('userLeft', username);
            updateUsers();
        });

        socket.on('message', (msg) => {
            if (msg) {
                if (!user.admin) {
                    const regex = /(&nbsp;|<([^>]+)>)/ig;
                    msg = msg.replace(regex, "");
                }

                msg = msg.replace(/(?<!(href|src)=")(\b[\w]+:\/\/[\w-?&;#~=\.\/\@%]+[\w\/])/g, (url) => {
                    try {
                        const urlObj = new URL(url);
                        const urlWithoutParams = urlObj.origin + urlObj.pathname;
                        if (urlWithoutParams.toLowerCase().endsWith('.jpg')
                            || urlWithoutParams.toLowerCase().endsWith('.gif')
                            || urlWithoutParams.toLowerCase().endsWith('.png')) {
                            return '<a href="' + url + '"><img src="' + url + '" alt="" /></a>';
                        }
                    } catch (e) {}

                    var faviconUrl;
                    try {
                        faviconUrl = new URL(url).origin + '/favicon.ico';
                    } catch (e) {
                        log('Error getting favicon for "' + url + '"');
                    }

                    return '<a href="' + url + '"><img class="favicon" src="' + faviconUrl + '" alt="" />' + url + '</a>';
                });

                log(user.name + ':', msg, '(' + user.ip + ')');
                if (history.length >= HISTORY_SIZE) {
                    history = history.slice(1);
                }
                history.push({msg: msg, user: getCleanUser(user), timestamp: Date.now()});
                io.emit('message', msg, getCleanUser(user), Date.now());
            }
        });
    });
});

function updateUsers() {
    io.emit('usersUpdated', users.filter((user) => user.name != null).map(getCleanUser));
}

function getCleanUser(user) {
    return {
        name: user.name,
        admin: user.admin,
        color: user.color
    }
}

function kickUser(user) {
    let ip;

    for (const exUser of users) {
        if (exUser.name == user.name) {
            ip = exUser.ip;
            break;
        }
    }

    bannedIps.push({ip: ip, user: user});
    updateBanned();

    for (const exUser of users) {
        if (exUser.ip == ip) {
            exUser.socket.emit('serverKick');
            exUser.socket.disconnect();
        }
    }
}

function liftBan(ip) {
    bannedIps = bannedIps.filter((listItem) => listItem.ip != ip);
    updateBanned();
}

function updateBanned() {
    fs.writeFile('config/ban.json', JSON.stringify(bannedIps), (e) => {
    });
    io.emit('bannedUpdated', bannedIps);
}

function getRandomColor(seed) {
    return randomColors[Math.floor(seedrandom(seed)() * randomColors.length)];
}

function log(...inputs) {
    var output = '[' + new Date().toISOString().substr(0, 19).replace('T', ', ') + '] ';

    for (const i of inputs) {
        output += i + ' ';
    }

    console.log(output);

    fs.appendFile('log.txt', '' + output + '\n', (e) => {
    });
}

server.listen(PORT, () => {
    log('- Server up and running at port ' + PORT);
});
