# Source: db.js

    /*
    Copyright (C) 2016
    Developed at University of Toronto

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.
    */

    const Db = require('mongodb').Db;
    const Server = require('mongodb').Server;
    const logger = require('./log.js');
    const common = require('./common.js');
    const bcrypt = require('bcryptjs');

    const DB_HOST = process.env.DB_HOST || 'localhost';
    const DB_PORT = process.env.DB_PORT || 27017;
    const DB_NAME = process.env.DB_NAME || 'quizzard';

    const db = new Db(DB_NAME, new Server(DB_HOST, DB_PORT));

    var usersCollection;
    var questionsCollection;
    var analyticsCollection;
    var feedbackCollection;
    var settingsCollection;
    var vfsCollection;

    var nextQuestionNumber = 0;

    /**
     * Open a connection to the database
     *
     * @param {function} callback
     */
    exports.initialize = function (callback) {
        db.open(function (err, db) {
            if (err) {
                logger.error(common.getError(1004).message);
                return callback(err, null);
            }

            logger.log('Connection to Quizzard database successful.');
            usersCollection = db.collection('users');
            questionsCollection = db.collection('questions');
            analyticsCollection = db.collection('analytics');
            feedbackCollection = db.collection('feedback');
            settingsCollection = db.collection('settings');
            vfsCollection = db.collection('virtualFileSystem');

            getNextQuestionNumber(function () {
                logger.log(common.formatString('next question number: {0}', [nextQuestionNumber]));
                return callback(err, null);
            });
        });
    }

    /**
     * add a student
     *
     * @param {object} student
     * @param {function} callback
     */
    exports.addStudent = function (student, callback) {
        addUser(student, callback);
    }

    /**
     * add a user
     *
     * @param {object} admin
     * @param {function} callback
     */
    exports.addAdmin = function (admin, callback) {
        addUser(admin, callback);
    }

    /**
     * add a user
     *
     * @param {object} user
     * @param {function} callback
     */
    var addUser = function (user, callback) {
        usersCollection.findOne({$or:[{_id: user._id}, {username: user.username}]}, function (err, obj) {
            if (err) {
                logger.error(JSON.stringify(err));
                return callback(common.getError(2014), null);
            }

            if (obj) {
                return callback(common.getError(2019), null);
            }

            usersCollection.insert(user, function (err, res) {
                return callback(err, user);
            });
        });
    }

    /**
     * get admin list
     *
     * @param {function} callback
     */
    exports.getAdminsList = function (callback) {
        getUsersList({type: common.userTypes.ADMIN}, {username: 1}, callback);
    }

    /**
     * get student list
     *
     * @param {function} callback
     */
    exports.getStudentsList = function (callback) {
        getUsersList({type: common.userTypes.STUDENT}, {username: 1}, callback);
    }

    /**
     * get users list
     *
     * @param {function} callback
     */
    exports.getUsersList = function (callback) {
        getUsersList({}, {username: 1}, callback);
    }

    /**
     * get students list with status
     *
     * @param {string} status
     * @param {function} callback
     */
    exports.getStudentsListWithStatus = function (status, callback) {
        getUsersList({type: common.userTypes.STUDENT, active: status}, {username: 1}, callback);
    }

    /**
     * Return an array of users in the database, sorted by rank
     *
     * @param {object} findQuery
     * @param {object} sortQuery
     * @param {function} callback
     */
    var getUsersList = function (findQuery, sortQuery, callback) {
        usersCollection.find(findQuery).sort(sortQuery).toArray(function (err, docs) {
            if (err) {
                return callback(common.getError(2013), []);
            }

            return callback(null, docs);
        });
    }

    /**
     * get students list sorted
     *
     * @param {int} lim
     * @param {function} callback
     */
    exports.getStudentsListSorted = function (lim, callback) {
        usersCollection.find({type: common.userTypes.STUDENT, active:{$ne: false}})
                .sort({points: -1})
                .limit(lim)
                .toArray(function (err, docs) {
            if (err) {
                return callback(common.getError(2015), null);
            }

            return callback(null, docs);
        });
    }

    /**
     * get user by id
     *
     * @param {string} userId
     * @param {function} callback
     */
    exports.getUserById = function (userId, callback) {
        getUserById(userId, callback);
    }

    /**
     * Check if the account given by user and pass is valid
     * user type of null
     *
     * @param {string} userId
     * @param {string} pass
     * @param {function} callback
     */
    exports.checkLogin = function (userId, pass, callback) {
        usersCollection.findOne({username : userId}, function (err, obj) {
            if (err) {
                logger.error(JSON.stringify(err));
                return callback(common.getError(2014), null);
            }

            if (!obj) {
                return callback(common.getError(2021), null);
            }

            if (!obj.active) {
                return callback(common.getError(2022), null);
            }

            validatePassword(obj, pass, function (err, valid) {
                if (err) {
                    return callback(common.getError(1005), null);
                }

                if (valid) {
                    delete obj.password;
                    return callback(null, obj);
                }

                return callback(common.getError(1006), null);
            });
        });
    }

    /**
     * Check the hash of pass against the password stored in userobj
     *
     * @param {object} userobj
     * @param {string} pass
     * @param {function} callback
     */
    var validatePassword = function (userobj, pass, callback) {
        bcrypt.compare(pass, userobj.password, function (err, obj) {
            callback(err, obj);
        });
    }

    /**
     * cleanup the users collection
     *
     * @param {function} callback
     */
    exports.removeAllUsers = function (callback) {
        usersCollection.remove({}, function (err, obj) {
            if (err) {
                logger.error(JSON.stringify(err));
                return callback(common.getError(1008), null);
            }

            logger.log('All users have been removed');
            return callback(null, obj);
        });
    }

    /**
     * get student by id
     *
     * @param {string} studentId
     * @param {function} callback
     */
    exports.getStudentById = function (studentId, callback) {
        getUserById(studentId, callback);
    }

    /**
     * get admin by id
     *
     * @param {string} adminId
     * @param {function} callback
     */
    exports.getAdminById = function (adminId, callback) {
        getUserById(adminId, callback);
    }

    /**
     * get user by id
     *
     * @param {string} userId
     * @param {function} callback
     */
    var getUserById = function (userId, callback) {
        getUserObject({_id : userId}, callback);
    }

    /**
     * get user by search query
     *
     * @param {object} findQuery
     * @param {function} callback
     */
    var getUserObject = function (findQuery, callback) {
        usersCollection.findOne(findQuery, function (err, obj) {
            if (err) {
                logger.error(JSON.stringify(err));
                return callback(common.getError(2017), null);
            }

            return callback(null, obj);
        });
    }
    exports.getUserObject = getUserObject;

    /**
     * update user by id
     *
     * @param {string} userId
     * @param {object} info
     * @param {function} callback
     */
    exports.updateUserById = function (userId, info, callback) {
        updateUserById(userId, info, callback);
    }

    /**
     * update student by id
     *
     * @param {string} userId
     * @param {object} info
     * @param {function} callback
     */
    exports.updateStudentById = function (userId, info, callback) {
        updateUserById(userId, info, callback);
    }

    /**
     * update admin by id
     *
     * @param {string} userId
     * @param {object} info
     * @param {function} callback
     */
    exports.updateAdminById = function (userId, info, callback) {
        updateUserById(userId, info, callback);
    }

    /**
     * update user by id
     *
     * @param {string} userId
     * @param {object} info
     * @param {function} callback
     */
    var updateUserById = function (userId, info, callback) {
        var currentDate = new Date().toString();
        var query = { _id : userId };
        var update = {};

        update.$addToSet = {};
        update.$inc = {};
        update.$pull = {};
        update.$set = { mtime : currentDate };
        update.$push = {};

        if ('username' in info) {
            update.$set.username = info.username;
        }

        if ('fname' in info) {
            update.$set.fname = info.fname;
        }

        if ('lname' in info) {
            update.$set.lname = info.lname;
        }

        if ('email' in info) {
            update.$set.email = info.email;
        }

        if ('points' in info && parseInt(info.points)) {
            update.$set.points = parseInt(info.points);
        }

        if ('rating' in info && parseInt(info.rating)) {
            update.$push.ratings = {
                question: info.questionId,
                date: currentDate,
                rating: info.rating
            }
        }

        if (typeof info.active !== 'undefined') {
            update.$set.active = info.active;
        }

        if (common.isEmptyObject(update.$addToSet)) {
            delete update.$addToSet;
        }

        if (common.isEmptyObject(update.$inc)) {
            delete update.$inc;
        }

        if (common.isEmptyObject(update.$set)) {
            delete update.$set;
        }

        if (common.isEmptyObject(update.$pull)) {
            delete update.$pull;
        }

        if (common.isEmptyObject(update.$push)) {
            delete update.$push;
        }

        if (typeof info.newPassword === 'undefined') {
            usersCollection.update(query, update, function (err, obj) {
                if (err) {
                    logger.error(JSON.stringify(err));
                    return callback(common.getError(2018), null);
                }

                return callback(null, 'success');
            });
        } else {
            update.$set.password = info.newPassword;
            updateUserPassword(query, update, info.newPassword, callback);
        }
    }

    /**
     * update user password
     *
     * @param {object} query
     * @param {object} update
     * @param {string} password
     * @param {function} callback
     */
    exports.updateUserPassword = function (query, update, password, callback) {
        updateUserPassword(query, update, password, callback);
    }

    /**
     * update user password
     *
     * @param {object} query
     * @param {object} update
     * @param {string} password
     * @param {function} callback
     */
    var updateUserPassword = function (query, update, password, callback) {
        bcrypt.hash(password, 11, function (err, hash) {
            if (err) {
                logger.error(JSON.stringify(err));
                return callback(common.getError(1009), null);
            }

            if (update.$set && !common.isEmptyObject(update.$set)) {
                update.$set.password = hash;
            } else {
                update.$set = {password: hash};
            }

            usersCollection.update(query, update, function (err, obj) {
                if (err) {
                    logger.error(JSON.stringify(err));
                    return callback(common.getError(2018), null);
                }

                return callback(null, 'success');
            });
        });
    }

    /**
     * update users collection directly by a query
     *
     * @param {object} query
     * @param {object} update
     * @param {function} callback
     */
    exports.updateUserByQuery = function (query, update, callback) {
        usersCollection.update(query, update, function (err, obj) {
            return callback(err, obj);
        });
    }

    /**
     * Questions functions
     * Add QUESTION to questionsCollection in the database
     *
     * @param {object} question
     * @param {function} callback
     */
    exports.addQuestion = function (question, callback) {
        question.number = ++nextQuestionNumber;
        questionsCollection.insert(question, function (err, res) {
            if (err) {
                logger.error(JSON.stringify(err));
                return callback(common.getError(3018), null);
            }

            return callback(null, question.number);
        });
    }

    /**
     * cleanup the users collection
     *
     * @param {function} callback
     */
    exports.removeAllQuestions = function (callback) {
        questionsCollection.remove({}, function (err, res) {
            if (err) {
                logger.error(JSON.stringify(err));
                return callback(common.getError(1008), null);
            }

            nextQuestionNumber = 0;
            logger.log('All questions have been removed');
            logger.log(common.formatString('next question number: {0}', [nextQuestionNumber]));
            return callback(null, res);
        });
    }

    /**
     * get next question number
     *
     * @param {function} callback
     */
    var getNextQuestionNumber = function (callback) {
          questionsCollection.find().sort({number: -1}).limit(1).toArray(function (err, docs) {
            if (err) {
                logger.error(JSON.stringify(err));
                process.exit(1);
            }

            nextQuestionNumber = docs[0] ? docs[0].number : 0;
            return callback(nextQuestionNumber);
        });
    }

    /**
     * get the list of Active questions sorted
     *
     * @param {object} findQuery
     * @param {object} sortQuery
     * @param {function} callback
     */
    exports.getQuestionsList = function (findQuery, sortQuery, callback) {
        var query = findQuery;
        query['deleted'] = {$ne: true};
        questionsCollection.find(query).sort(sortQuery).toArray(function (err, docs) {
            if (err) {
                return callback(common.getError(3017), null);
            }

            for (q in docs) {
                docs[q].firstAnswer = docs[q].correctAttempts[0] ? docs[q].correctAttempts[0].userId : 'No One';
            }

            return callback(null, docs);
        });
    }

    /**
     * get the list of questions sorted
     *
     * @param {object} findQuery
     * @param {object} sortQuery
     * @param {function} callback
     */
    exports.getQuestionsListforAdmin = function (findQuery, sortQuery, callback) {
        questionsCollection.find(findQuery).sort(sortQuery).toArray(function (err, docs) {
            if (err) {
                return callback(common.getError(3017), null);
            }

            for (q in docs) {
                docs[q].firstAnswer = docs[q].correctAttempts[0] ? docs[q].correctAttempts[0].userId : 'No One';
            }

            return callback(null, docs);
        });
    }

    /**
     * Extract a question object from the database using its ID
     *
     * @param {object} findQuery
     * @param {function} callback
     */
    exports.lookupQuestion = function (findQuery, callback) {
        var query = findQuery;
        query['deleted'] = {$ne: true};
        questionsCollection.findOne(query, function (err, question) {
            if (err) {
                return callback(common.getError(3019), null);
            }

            if (!question) {
                return callback(common.getError(3003), null);
            }

            /* necessary for later database update */
            question.firstAnswer = question.correctAttempts[0] ? question.correctAttempts[0].userId : 'No One';
            return callback(null, question);
        });
    }

    /**
     * update a question record based on its id
     *
     * @param {string} questionId
     * @param {object} request
     * @param {function} callback
     */
    exports.updateQuestionById = function (questionId, request, callback) {
        var currentDate = new Date().toString();
        var query = {_id: questionId};
        var update = {};

        update.$addToSet = {};
        update.$push = {};
        update.$pull = {};
        update.$set = {};
        update.$inc = {};

        if ('topic' in request) {
          update.$set.topic = request.topic;
        }

        if ('title' in request) {
          update.$set.title = request.title;
        }

        if ('text' in request) {
          update.$set.text = request.text;
        }

        if ('answer' in request) {
          update.$set.answer = request.answer;
        }

        if ('hint' in request) {
          update.$set.hint = request.hint;
        }

        if ('minpoints' in request) {
          update.$set.minpoints = request.minpoints;
        }

        if ('maxpoints' in request) {
          update.$set.maxpoints = request.maxpoints;
        }

        if ('choices' in request) {
          update.$set.choices = request.choices;
        }

        if ('leftSide' in request) {
          update.$set.leftSide = request.leftSide;
        }

        if ('rightSide' in request) {
          update.$set.rightSide = request.rightSide;
        }

        if ('visible' in request) {
            update.$set.visible = request.visible;
        }

        if (common.isEmptyObject(update.$addToSet)) {
            delete update.$addToSet;
        }

        if (common.isEmptyObject(update.$push)) {
            delete update.$push;
        }

        if (common.isEmptyObject(update.$set)) {
            delete update.$set;
        }

        if (common.isEmptyObject(update.$pull)) {
            delete update.$pull;
        }

        if (common.isEmptyObject(update.$inc)) {
            delete update.$inc;
        }

        questionsCollection.update(query, update, function (err, info) {
            if (err) {
                return callback(common.getError(3020), null);
            }

            return callback(null, 'success');
        });
    }

    /**
     * update users collection directly by a query
     *
     * @param {object} query
     * @param {object} update
     * @param {function} callback
     */
    exports.updateQuestionByQuery = function (query, update, callback) {
        questionsCollection.update(query, update, function (err, obj) {
            return callback(err, obj);
        });
    }

    // add feedback to feedback collections directly using a query
    exports.addFeedback = function (feedback, callback) {
        feedbackCollection.insert(feedback, function (err, res) {
            if (err) {
                logger.error('Failed to add feedback');
                return callback(common.getError(8000), null);
            }

            return callback(null, 'success');
        });
    }

    exports.getFeedback = function (callback) {
        feedbackCollection.find().sort({time: -1}).toArray(function (err, res) {
            if (err) {
                logger.error('Failed to get feedback');
                return callback(common.getError(8001), res);
            }

            return callback(null, res);
        });
    }

    exports.removeAllFeedback = function (callback) {
        feedbackCollection.remove({}, function (err, result) {
            if (err) {
                return callback(common.getError(8002), null);
            }

            return callback(null, 'ok');
        });
    }

    /**
     * reset all settings to default
     *
     * @param {function} callback
     */
    exports.resetAllSettings = function (callback) {
        resetAllSettings(callback);
    }

    /**
     * reset all settings to default
     *
     * @param {function} callback
     */
    var resetAllSettings = function (callback) {
        settingsCollection.remove({}, function (err, result) {
            if (err) {
                return callback(common.getError(7000), null);
            }

            var defaultSettings = {};
            defaultSettings._id = common.getUUID();

            defaultSettings['general'] = {};
            defaultSettings['student'] = {};
            defaultSettings['question'] = {};
            defaultSettings['discussionboard'] = {};

            defaultSettings.general['active'] = true;
            defaultSettings.general['leaderboardLimited'] = false;
            defaultSettings.general['leaderboardLimit'] = 3;

            defaultSettings.student['editNames'] = false;
            defaultSettings.student['editEmail'] = false;
            defaultSettings.student['editPassword'] = true;

            defaultSettings.question['defaultTopic'] = null;
            defaultSettings.question['defaultMinPoints'] = 10;
            defaultSettings.question['defaultMaxPoints'] = 100;
            defaultSettings.question['timeoutEnabled'] = true;
            defaultSettings.question['timeoutPeriod'] = 30*60000;

            defaultSettings.discussionboard['visibility'] = common.discussionboardVisibility.ALL;
            defaultSettings.discussionboard['dislikesEnabled'] = true;

            settingsCollection.insert(defaultSettings, function (err, obj) {
                if (err) {
                    return callback(common.getError(7002), null);
                }

                return callback(null, defaultSettings);
            });
        });
    }

    /**
     * get all settings objects from the collection
     *
     * @param {function} callback
     */
    exports.getAllSettings = function (callback) {
        getAllSettings(callback);
    }

    /**
     * get all settings objects from the collection
     *
     * @param {function} callback
     */
    var getAllSettings = function (callback) {
        settingsCollection.findOne({}, function (err, obj) {
            if (err) {
                return callback (common.getError(7003), null);
            }
            else if (!obj) {
                resetAllSettings(function (err, result) {
                    if (err) {
                        return callback(err, null)
                    }
                    return callback(null, result);
                });
            } else {
                return callback (null, obj);
            }
        });
    }

    /**
     * remove all previous analytics
     *
     * @param {function} callback
     */
    exports.removeAnalytics = function (callback) {
        analyticsCollection.remove({}, function (err, obj) {
            if (err) {
                logger.error(JSON.stringify(err));
                return callback(common.getError(1008), null);
            }

            logger.log('All analytics have been removed');
            return callback(null, obj);
        });
    }

    /**
     * add student analytics
     * if there are no records of the student, create a new record
     * if there are recards of the student, get the last recard and compute the deltas
     *
     * @param {string} studentId
     * @param {string} date
     * @param {object} info
     * @param {function} callback
     */
    exports.addStudentAnalyticsWithDate = function (studentId, date, info, callback) {
        var query = {_id: studentId};
        var update = {};

        analyticsCollection.findOne(query, function (err, student) {
            if (err) {
                return callback(err, null);
            }

            if (!student) {
                info.correctAttemptsDelta = 0;
                info.wrongAttemptsDelta = 0;
                info.totalAttemptsDelta = 0;
                info.pointsDelta = 0;
                info.accuracyDelta = 0;
                update._id = studentId;
                update.dates = [{date: date, info: info}];

                analyticsCollection.insert(update, function (err, obj) {
                    if (err) {
                        return callback(err, null);
                    }
                    return callback(null, obj);
                });
            }

            if (student) {
                info.correctAttemptsDelta = info.correctAttemptsCount - student.dates[student.dates.length-1].info.correctAttemptsCount;
                info.wrongAttemptsDelta = info.wrongAttemptsCount - student.dates[student.dates.length-1].info.wrongAttemptsCount;
                info.totalAttemptsDelta = info.totalAttemptsCount - student.dates[student.dates.length-1].info.totalAttemptsCount;
                info.pointsDelta = info.points - student.dates[student.dates.length-1].info.points;
                info.accuracyDelta = (info.accuracy - student.dates[student.dates.length-1].info.accuracy).toFixed(2);
                update.$push = {dates: {date: date, info: info}};

                analyticsCollection.update(query, update, function (err, info) {
                    if (err) {
                        return callback(err, null);
                    }
                    return callback(null, info);
                });
            }
        });
    }

    exports.getTimeBasedAnalytics = function (findQuery, callback) {
        analyticsCollection.findOne(findQuery, function (err, data) {
            if (err) {
                return callback(err, null);
            }

            if (!data) {
                return callback('invalid search query', null);
            }

            return callback(null, data);
        });
    }

    /**
     * update settings object
     *
     * @param {object} findQuery
     * @param {object} updateQuery
     * @param {function} callback
     */
    exports.updateSettings = function (findQuery, updateQuery, callback) {
        settingsCollection.update(findQuery, updateQuery, callback);
    }

    /**
     * clean up the virtual file system
     *
     * @param {function} callback
     */
    exports.removeVirtualFileSystem = function (callback) {
        vfsCollection.remove({}, function (err, result) {
            if (err) {
                logger.error(JSON.stringify(err));
                return callback(common.getError(9000), null);
            }

            return callback(null, 'ok');
        });
    }

    /**
     * add item to the virtual file system
     *
     * @param {function} callback
     */
    exports.addToVirtualFileSystem = function (object, callback) {
        vfsCollection.insert(object, function (err, obj) {
            if (err) {
                return callback(common.getError(9001), null);
            }

            return callback(null, 'ok');
        });
    }

    /**
     * find in the virtual file system
     *
     * @param {function} callback
     */
    exports.findInVirtualFileSystem = function (findQuery, callback) {
        vfsCollection.findOne(findQuery, function (err, obj) {
            if (err) {
                return callback(common.getError(9002), null);
            }

            if (!obj) {
                return callback(common.getError(9003), null);
            }

            return callback(null, obj);
        });
    }