Source: Player.js

// @flow

const Utils     = require('./Utils.js');
const path      = require('path');
const clPlayer  = require('./client/clPlayer.js');
const CircularJSON = require('./circularjson.js');

/** Class representing a player. */
class Player {

    constructor(id, participant, group, idInGroup) {

        /**
         * This player's participant.
         * @type {Participant}
         */
        this.participant = participant;

        /**
         * This player's group.
         * @type {Group}
         */
        this.group = group;

        /**
         * The player's ID.
         * @type {String}
         */
        this.id = id;

        /**
         * @type {Number}
         */
        this.idInGroup = idInGroup;

        /**
         * @type number
         * @default 0
         */
        this.points = 0; // points from the current period


        /**
         * The current status of this player.
         *
         * 1. 'ready': the player is ready to play their current stage, but is waiting (for their fellow group members).
         * 2. 'playing': the player is playing their current stage.
         * 3. 'done': the player is done playing their current stage, waiting to call 'Stage.playerEnd(player)'.
         * 4. 'finished': player is ready to move to next stage.
         * 
         * See Session Flow tutorial for more details.
         * 
         * @type string
         * @default 'ready'
         */
        this.status = 'ready';

        /**
         * @type number
         * @default 0
         */
        this.stageIndex = 0

        /**
         * @type {Stage}
         * @default null
         */
        this.stage = null;

        /**
         * 'outputHide' fields are not included in output.
         * @type Array
         * @default []
         * @private
         */
        this.outputHide = [];

        /**
         * 'outputHideAuto' fields are not included in output.
         * @type {String[]}
         * @private
         */
        this.outputHideAuto = [
            'appIndex',
            'group',
            'groupId',
            'outputHide',
            'outputHideAuto',
            'participant',
            'periodId',
            'stage',
            'stageTimer',
            'stageClientDuration',
            'this',
            'type'
        ];
    }

    timeInStage() {
        if (this.group.stageTimer != null) {
            return this.group.timeInStage();
        } else {
            let startTime = this['timeStart_' + this.stage.id];
            let curTime = this.timeStamp();
            let diff = Utils.diffDates(startTime, curTime);
            return diff;
        }
    }

    asClPlayer() {
        return new clPlayer.new(this);
    }

    outputFields() {
        var fields = [];
        for (var prop in this) {
            if (
                !Utils.isFunction(this[prop]) &&
                !this.outputHide.includes(prop) &&
                !this.outputHideAuto.includes(prop)
            )
            fields.push(prop);
        }
        return fields;
    }

    // /**
    //  * Move this player to their next stage.
    //  *
    //  * If this player is currently in a stage, call {@link Stage#playerEnd}.
    //  * Otherwise, call {@link App#playerMoveToNextStage}.
    //  *
    //  * @return {type}  description
    //  */
    // moveToNextStage() {
    //     console.log('player.moveToNextStage: ' + this.roomId());
    //     this.session().printStatuses();

    //     if (this.stage !== null) {
    //         this.stage.playerEnd(this);
    //     } else {
    //         // TODO: Delete.
    //         console.log('SHOULD NEVER HAPPEN??');
    //         debugger;
    //         this.app().playerMoveToNextStage(this);
    //     }
    // }

    recordStageEndTime(stage) {
        let timeStamp = this.timeStamp();
        this['timeEnd_' + stage.id] = timeStamp;
        if (this['timeStart_' + stage.id] == null) {
            console.log('Player ERROR, missing stage start time! Using end time.');
            this['timeStart_' + stage.id] = timeStamp;
        }
        this['msInStage_' + stage.id] = Utils.dateFromStr(timeStamp) - Utils.dateFromStr(this['timeStart_' + stage.id]);
    }

    recordStageStartTime(stage) {
        let timeStamp = this.timeStamp();
        console.log(timeStamp + ' START - PLAYER: ' + stage.id + ', ' + this.roomId());
        this['timeStart_' + stage.id] = timeStamp;
    }

    jt() {
        return this.session().jt;
    }

    canProcessMessage() {
        return (this.status === 'playing');
    }


    /**
     * roomId - description
     *
     * @return {type}  description
     */
    roomId() {
        return this.group.roomId() + '_player_' + this.id;
    }

    static genRoomId(player) {
        var sId     = player.group.period.app.session.id;
        var aId     = player.group.period.app.id;
        var prdId   = player.group.period.id;
        var gId     = player.group.id;
        var pId     = player.id;
        return 'session_' + sId + '_app_' + aId + '_period_' + prdId + '_group_' + gId + '_period_' + pId;
    }

    /**
     * appIndex - description
     *
     * @return {type}  description
     */
    appIndex() {
        return this.participant.appIndex;
    }

    /**
     * periodIndex - description
     *
     * @return {type}  description
     */
    periodIndex() {
        return this.period().id;
    }

    /**
     * period - description
     *
     * @return {type}  description
     */
    period() {
        return this.group.period;
    }

    /**
     * The next stage for this player.
     **/
    nextStage() {
        var stageInd = this.stageIndex;

        // If not in the last stage, return next stage.
        if (stageInd < this.app().stages.length-1) {
            return this.app().stages[stageInd+1];
        }

        // If in the last stage, but not the last period, return first stage (of next period).
        else if (this.period().id < this.app().numPeriods) {
            return this.app().stages[0];
        }

        // If not in the last app, return first stage of next app.
        else {
            var app = this.session().appFollowing(this.app());
            if (app !== null && app.stages.length > 0) {
                return app.stages[0];
            }
            // Otherwise, return null.
            else {
                return null;
            }
        }
    }


    /**
     * otherPlayersInGroup - description
     *
     * @return {type}  description
     */
    otherPlayersInGroup() {
        return this.group.playersExcept(this);
    }

    /**
     * addClient - description
     *
     * @param  {type} client description
     */
    addClient(client) {
        client.socket.join(this.roomId());
    }

    /**
     * compId - description
     *
     * @return {type}  description
     */
    compId() {
        var out = {};
        out.playerId = this.id;
        out.groupId = this.group.id;
        out.periodId = this.group.period.id;
        out.appId = this.group.period.app.id;
        out.sessionId = this.group.period.app.session.id;
        out.roomId = this.roomId();
        return out;
    }

    /**
     * app - description
     *
     * @return {@link App}  The app that this player is a member of.
     */
    app() {
        return this.period().app;
    }

    /**
     * matchesPlayer - description
     *
     * @param  {type} plyrShell description
     * @return {boolean}           whether or not the inputted player matches this one.
     */
    matchesPlayer(p) {
        return (
            p.id === this.id &&
            p.group.id === this.group.id &&
            p.group.period.id === this.group.period.id &&
            p.group.period.app.id === this.app().id && // TODO: update to match index instead of ID
            p.group.period.app.session.id === this.session().id
        )
    }

    /**
     * old - description
     *
     * @return {type}  description
     */
    old() {
        return this.app().previousPlayer(this);
    }

    /**
     * session - description
     *
     * @return {type}  description
     */
    session() {
        return this.app().session;
    }

    /**
     * emit - description
     *
     * @param  {type} name description
     * @param  {type} dta  description
     */
    emit(name, dta) {
        dta.participantId = this.participant.id;
        dta.sessionId = this.session().id;
        dta = CircularJSON.stringify(dta);
        this.io().to(this.roomId()).emit(name, dta);
        this.session().emitToAdmins(name, dta);
    }

    io() {
        return this.session().io();
    }

    /**
     * this - description
     *
     * @return {type}  description
     */
    shell() {
        var out = {};
        var fields = this.outputFields();
        for (var f in fields) {
            var field = fields[f];
            out[field] = this[field];
        }
        out.participantId = this.participant.id;
        out.groupId = this.group.id;
        out.periodId = this.period().id;
        out.appIndex = this.app().indexInSession();
        out.roomId = this.roomId();
        return out;
    }

    shellWithParticipant() {
        var out = this.shellWithParent();
        out.participant = this.participant.shell();
        var group = this.group;
        if (group.stageTimer !== undefined) {
            out.stageTimerStart = group.stageTimer.timeStarted;
            out.stageTimerDuration = group.stageTimer.duration;
            out.stageTimerTimeLeft = group.stageTimer.timeLeft;
            out.stageTimerRunning = group.stageTimer.running;
        }
        if (this.stage.clientDuration > 0) {
            out.stageClientDuration = this.stage.clientDuration;
        }
        return out;
    }

    shellWithParent() {
        var out = {};
        var fields = this.outputFields();
        for (var f in fields) {
            var field = fields[f];
            out[field] = this[field];
        }
        out.group = this.group.shellWithParent();
        if (this.stage !== null && this.stage !== undefined) {
            out.stage = this.stage.shellWithParent();
        }
        out.roomId = this.roomId();
        return out;
    }

    /**
    * CALLED FROM
    * - {@link Participant#shellAll}
     *
     * @return {type}  description
     */
     shellWithChildren() {
        var out = {};
        var fields = this.outputFields();
        for (var f in fields) {
            var field = fields[f];
            out[field] = this[field];
        }
        out.groupId = this.group.roomId();
        out.roomId = this.roomId();
        if (this.stage !== null && this.stage !== undefined) {
            out.stageId = this.stage.id;
        } else {
            out.stageId = null;
        }
        out.participantId = this.participant.id;
        if (this.group.stageTimer !== undefined) {
            out.stageTimerStart = this.group.stageTimer.timeStarted;
            out.stageTimerDuration = this.group.stageTimer.duration;
            out.stageTimerTimeLeft = this.group.stageTimer.timeLeft;
            out.stageTimerRunning = this.group.stageTimer.running;
        }

        if (this.stage != null && this.stage.clientDuration > 0) {
            out.stageClientDuration = this.stage.clientDuration;
        }

        return out;
    }

    /**
     * emitUpdate - description
     *
     * @return {type}  description
     */
    emitUpdate() {
        let data = this.shellWithChildren();
        data = CircularJSON.stringify(data);
        this.emit('playerUpdate', data);
    }

    emitUpdate2() {
        this.participant.emitUpdate();
    }

    /**
     * sendUpdate - description
     *
     * @param  {type} channel description
     */
    sendUpdate(channel) {
        // p: send this player's data
        // channel: channel to send this player's data to,
        // usually either the player themselves or an individual
        // client that is subscribed to the player.
        if (this.stage === null || this.stage.onPlaySendPlayer) {
            let data = new clPlayer.new(this);
            data = CircularJSON.stringify(data);
            this.io().to(channel).emit('playerUpdate', data);
        }
    }

    /**
     * isFinished - description
     *
     * @return {type}  description
     */
    isFinished() {
        var actualPlyr = this.participant.player;

        // No active player.
        if (actualPlyr === null) {
            return false;
        }

        // Already past this app.
        if (actualPlyr.group.period.app.indexInSession() > this.group.period.app.indexInSession()) {
            return true;
        }
        // Still in this app.
        else if (actualPlyr.group.period.app.indexInSession() === this.group.period.app.indexInSession()) {
            // Already past this period.
            if (actualPlyr.group.period.id > this.group.period.id) {
                return true;
            }
            // Still in this period.
            else if (actualPlyr.group.period.id === this.group.period.id) {
                // Not yet in the group's current stage.
                if (actualPlyr.group.stageIndex > this.stageIndex) {
                    return false;
                } 
                // Passed the group's current stage.
                else if (actualPlyr.group.stageIndex < this.stageIndex) {
                    return true;
                }
                // In the group's current stage. Check status.
                else {
                    if (['finished', 'done'].includes(actualPlyr.status)) {
                        return true;
                    } else {
                        return false;
                    }
                }
            }
            // Not yet in this period.
            else {
                return false;
            }
        }
        // Not yet in this app.
        else {
            return false;
        }
    }

    /**
     * isReady - description
     *
     * @return {type}  description
     */
    isReady(stageIndex) {
        var actualPlyr = this.participant.player;

        // No active player.
        if (actualPlyr == null || this.roomId() !== actualPlyr.roomId()) {
            return false;
        }

        if (this.stageIndex !== stageIndex) {
            return false;
        }

        if (this.status !== 'ready') {
            return false;
        }

        return true;
    }

    /**
     * save - description
     *
     */
    save() {
        try {
            this.session().jt.log('Player.save: ' + this.roomId());
            var toSave = this.shell();
            this.session().saveDataFS(toSave, 'PLAYER');
        } catch (err) {
            console.log('Error saving player ' + this.roomId() + ': ' + err + '\n' + err.stack);
        }
    }

    saveAndUpdate() {
        let data = this.asClPlayer();
        data = CircularJSON.stringify(data);
        this.io().to(this.roomId()).emit('playerUpdate', data);
        this.save();
    }

    /**
     * Is the player at least finished the given stage of the given period?
     *
     * Return false if any of the following are true:
     * - the player is in a previous app.
     * - the player is in the same app, but a previous period.
     * - the player is in the same period, but a previous stage.
     * - the player is in the same stage, but is still 'playing'.
     *
     * Otherwise return true.
     *
     * CALLED FROM:
     * - {@link Stage#playerCanGroupProceedToNextStage}.
     */

    atLeastFinishedStage(stage, period) {

        if (this.app().indexInSession() < period.app.indexInSession()) {
            return false;
        } else if (this.app().indexInSession() > period.app.indexInSession()) {
            return true;
        }

        if (this.period().id < period.id) {
            return false;
        } else if (this.period().id > period.id) {
            return true;
        }

        if (this.stageIndex < stage.indexInApp()) {
            return false;
        } else if (this.stageIndex > stage.indexInApp()) {
            return true;
        }

        if (this.status === 'playing') {
            return false;
        }

        return true;

    }

    /**
     * @static load - description
     *
     * CALLED FROM:
     * - {@link Session#load}
     *
     * @param  {type} json    description
     * @param  {type} session description
     * @return {type}         description
     */
    static load(json, session) {
        var playerId = json.id;
        var app = session.apps[json.appIndex-1];
        var period = app.periods[json.periodId-1];
        var group = period.groups[json.groupId-1];
        var participant = session.participants[playerId];
        var newPlayer = new Player(playerId, participant, group, json.idInGroup);
        for (var j in json) {
            newPlayer[j] = json[j];
        }
        group.players[json.idInGroup-1] = newPlayer;
    }













    startStage(stage) {
        this.group.startStage(this.stage);
        if (!this.participant.canStartStage(stage)) {
            return;
        }
        if (this.stageIndex !== stage.indexInApp()) {
            this.stage = stage;
            this.stageIndex = stage.indexInApp();
            this.status = 'ready';            
        }
        if (this.group.canPlayersStart(this.stage)) {
            if (this.stage.canPlayerParticipate(this)) {
                if (this.status === 'ready') {
                    this.status = 'playing';
                    this.recordStageStartTime(stage);
                    try {
                        stage.playerStart(this);
                    } catch(err) {
                        console.log(err + '\n' + err.stack);
                    }
                    this.save();
                }
            } else {
                this.finishStage(true);
            }
        }
        this.emitUpdate2();
    }

    endStage(endGroup) {
        if (endGroup == null) {
            endGroup = true;
        }

        if (this.status === 'playing') {
            this.recordStageEndTime(this.stage);
            this.status = 'done';
        }

        if (!this.group.canPlayersEnd(this.stage)) {
            this.emitUpdate2();
            return;
        }

        console.log(this.timeStamp() + ' END   - PLAYER: ' + this.stage.id + ', ' + this.roomId());
        this.stage.playerEnd(this);
        this.emitUpdate2();
        this.finishStage(endGroup);
    }

    timeStamp() {
        return this.jt().settings.getConsoleTimeStamp();
    }

    finishStage(endGroup) {
        this.status = 'finished';
        console.log(this.timeStamp() + ' FINISH- PLAYER: ' + this.stage.id + ', ' + this.roomId());
        let curRoomId = this.roomId();
        let curStageIndex = this.stageIndex;
        if (endGroup) {
            this.group.endStage(this.stage);
        }
        if (curRoomId == this.roomId() && curStageIndex === this.stageIndex) {
            this.moveToNextStage();
        }
    }

    // If there is a next stage, enter it.
    // Otherwise, if there is a next period, start it.
    // Otherwise, end the current app.
    moveToNextStage() {
        let stage = this.stage;
        let player = this;

        // If this player is no longer active, do nothing.
        if (player.participant.player == null || player.roomId() !== player.participant.player.roomId()) {
            return;
        }

        var nextStage = this.app().getNextStageForPlayer(player);
        var nextPeriod = this.app().getNextPeriod(player.participant);
        if (nextStage !== null) {
            player.stage = nextStage;
            player.stageIndex++;
            player.status = 'ready';
            console.log(this.timeStamp() + ' READY - PLAYER: ' + this.stage.id + ', ' + this.roomId());
            player.startStage(player.stage);
        } else if (nextPeriod !== null) {
            player.participant.startPeriod(nextPeriod);
        } else {
            this.emitUpdate2();
            player.participant.endCurrentApp();
        }

    } 











}

var exports = module.exports = {};
exports.new = Player;
exports.load = Player.load;
exports.genRoomId = Player.genRoomId;