Source: Participant.js

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

/**
 * A participant in the experiment.
 */
class Participant {

    /*
     * constructor - description
     *
     * @param  {type} id      description
     * @param  {type} session description
     * @return {type}         description
     */
    constructor(id, session) {
        /**
         * Unique identifier for this participant.
         * @type {String}
         */
        this.id = id;

        /**
         *  Session for this participant.
         * @type {Session}
         */
        this.session = session;

        this.indexInSession = Object.keys(session.participants).length;

        /**
         * @type array
         * @default []
         */
        this.clients = [];

        this.reset();

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

        /**
         * 'outputHideAuto' fields are not included in output.
         * @type {String[]}
         */
        this.outputHideAuto = [
            'this',
            'session',
            'players',
            'clients',
            'outputHide',
            'outputHideAuto',
            'player',
            'period',
            'autoplay',
            'appTimer',
            'indexInSession',
            'finishedApps',
            'updateScheduled'
        ];
    }

    /**
     * @static load - description
     *
     * CALLED FROM:
     * - {@link Session#load}
     *
     * @param  {type} json    description
     * @param  {type} session description
     * @return {type}         description
     */
    static load(json, session) {
        var id = json.id;
        var newParticipant = new Participant(id, session);
        for (var j in json) {
            if (j !== 'session') {
                newParticipant[j] = json[j];
            }
        }
        session.participants[id] = newParticipant;
    }

    /**
     * Move this participant to their next stage.
     *
     * If the participant is already playing in an app as a player, call that player's {@link Player#moveToNextStage} function. Otherwise, call {@link Session#participantMoveToNextApp}.
     */
    moveToNextStage() {
        if (this.player === null) {
            this.session.participantMoveToNextApp(this);
        } else {
            this.player.endStage(true);
        }
    }

    printStatus() {
        if (this.player == null) {
            console.log(this.id + ' - no player');
        } else {
            this.player.showStatus();
        }
    }

    reset() {
        /**
         * @type array
         * @default []
         */
        this.players = [];

        /**
         * The current player of this participant.
         * @type {Player}
         * @default null
         */
        this.player = null;

        /**
         * Indexed at 0.
         * @type number
         * @default -1
         */
        this.periodIndex = -1;
        /**
         * The current app index of this participant.
         * 0 indicates no current app.
         * @type number
         * @default 0
         */
        this.appIndex = 0;

        /**
         * List of app ids that this participant has completed.
         */
        this.finishedApps = [];

        /**
         * @type boolean
         * @default false
         */
        this.autoplay = false;
    }

    canStartStage(stage) {
        if (this.player === null) {
            return false;
        }

        // Ready in this stage.
        // TODO: Check whether correct period and app.
        if (
            this.player.stageIndex === stage.indexInApp() &&
            this.player.status === 'ready'
        ) {
            return true;
        }

        return false;
    }

    finishCurrentApp() {
        // ensure that they are in the last period and waiting
        if (this.player.group.period.id < this.getApp().numPeriods) {
            // this.player.group.
        }
    }

    getApp() {
        if (this.appIndex < 1) {
            return null;
        }
        return this.session.getApp(this);
    }

    endCurrentApp() {

        if (this.getApp() !== null) {
            this.getApp().participantEnd(this);
            this.finishedApps.push(this.getApp().getIdInSession());
        }

        this.player = null;

        this.appIndex++;

        var nextApp = this.session.getApp(this);
        if (nextApp != null) {
            //nextApp.participantBegin(this);
//            this.startApp(nextApp);
            this.session.participantBeginApp(this);
        } else {
            this.endSession();
        }
    }

    endSession() {
        this.session.participantEnd(this);
        this.session.tryToEnd();
    }

    refreshClients() {
        this.emit('reload');
    }

    startPeriod(period) {
        this.periodIndex = period.id - 1;
        this.getApp().participantBeginPeriod(this);
    }

    canProcessMessage() {
        return true;
    }



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

    /**
     * stageId - description
     *
     * @return {type}  description
     */
    stageId() {
        if (this.player == null) {
            return null;
        } else {
            return this.player.stage.id;
        }
    }

    stageIndex() {
        if (this.player === null) {
            return -1;
        } else {
            return this.player.stage.indexInApp();
        }
    }

    /**
     * isFinishedApp - checks whether Participant is finished the given App.
     *
     * CALLED FROM
     * - {@link App#tryToEndApp}
     *
     * @param  {App} app The given app.
     * @return {boolean} If the player is in a previous app, as given by the app's
     * indexInSession compared to the participant's appIndex, then return false.
     * If the player is in a previous app, return false.
     * If the player is in this app,
     *   + not yet in the last period, return false.
     *   + in the last period, but still 'playing', return false.
     *   + in the last period and not 'playing', return true.
     * If the player is past this app, true.
     *
     */
    isFinishedApp(app) {

        // Already finished this app.
        if (this.finishedApps.includes(app.getIdInSession())) {
            return true;
        }

        // In this app.
        if (this.getApp().getIdInSession() === app.getIdInSession()) {
            // Not yet in the last period.
            if (this.periodIndex < app.numPeriods - 1) {
                return false;
            }
            // In the last period, but still not 'finished'.
            if (this.player !== null && this.player.status !== 'finished') {
                return false;
            }
            // Finished all periods.
            return true;
        }

        // Not in this app, and not finished it already.
        return false;
    }

// TODO: Remove??
    isFinishedSession() {
        // In an app.
        if (this.appIndex <= this.session.apps.length) {
            return false;
        } else {
            return true;
        }
    }

    emit(name, dta) {
        if (dta === undefined) {
            dta = {};
        }
        dta.participantId = this.id;
        dta.sessionId = this.session.id;
        this.session.io().to(this.roomId()).emit(name, dta);
//        this.session.io().to(this.session.roomId()).emit(name, dta);
    }

    clientRemove(clientId) {
        Utils.deleteById(this.clients, clientId);
    }

    clientAdd(client) {
        this.clients.push(client);
        client.socket.join(this.roomId());
        if (this.player != null) {
            this.player.addClient(client);
            this.player.group.addClient(client);
            this.player.group.period.addClient(client);
            this.player.group.period.app.addClientDefault(client);
        }
    }

    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;
    }

    points() {
        return this.pointsForApp(null);
    }

    /**
     * Returns the points the participant earned in the given app.
     * If app is null, returns points earned across all apps.
     */
    pointsForApp(app) {
        let out = 0;
        for (var p in this.players) {
            let player = this.players[p];
            if (app != null && player.app().id !== app.id) {
                continue;
            }
            var points = player.points;
            if (points !== undefined && points !== null && !isNaN(points)) {
                out += parseFloat(points);
            }
        }
        return out;
    }

    setPlayer(player) {
        this.player = player;
        this.save();
        for (var i=0; i<this.clients.length; i++) {
            var client = this.clients[i];
            player.addClient(client);
            player.group.addClient(client);
            player.period().addClient(client);
        }
        this.emit('participantSetPlayer', {player: player.shellWithParent()});
    }

    actuallyEmitUpdate() {
        if (this.updateScheduled === true) {
            try {
                console.log('sending update for ' + this.id);
                if (this.player !== null) {
                    this.player.emit('playerUpdate', new clPlayer.new(this.player));
                } else {
                    this.emit('start-new-app'); // refresh clients.
                }
                this.updateScheduled = false;
            } catch (err) {
                debugger;
            }
        }
    }

    emitUpdate() {
        this.updateScheduled = true;
    }

    shell() {
        var out = {};
        var fields = this.outputFields();
        for (var f in fields) {
            var field = fields[f];
            out[field] = this[field];
        }
        out.numClients = this.clients.length;
        if (this.player !== null) {
            out.player = this.player.shellWithParent();
        } else {
            out.player = null;
        }
        out.numPoints = this.points();
        out.session = this.session.shell();
        return out;
    }

    shellAll() {
        var out = {};
        var fields = this.outputFields();
        for (var f in fields) {
            var field = fields[f];
            out[field] = this[field];
        }
        out.numClients = this.clients.length;
        if (this.player != null) {
            out.player = this.player.shellWithChildren();
        } else {
            out.player = null;
        }
        out.numPoints = this.points();
        out.playerIds = [];
        out.players = [];
        for (var i in this.players) {
            out.playerIds[i] = this.players[i].roomId();
            out.players.push(this.players[i].shell());
        }
        return out;
    }

    shellLocal() {
        var out = {};
        var fields = this.outputFields();
        for (var f in fields) {
            var field = fields[f];
            out[field] = this[field];
        }
        out.playerIds = [];
        for (var i in this.players) {
            out.playerIds.push(this.players[i].compId());
        }
        out.playerIds = JSON.stringify(out.playerIds);
        out.playerId = null;
        if (this.player !== null) {
            out.playerId = JSON.stringify(this.player.compId());
        }
        return out;
    }

    save() {
        try {
            this.session.jt.log('Participant.save: ' + this.id);
            var localData = this.shellLocal();
            this.session.saveDataFS(localData, 'PARTICIPANT');
        } catch (err) {
            console.log('Error saving participant ' + this.id + ': ' + err + '\n' + err.stack);
        }
    }

}

var exports = module.exports = {};
exports.new = Participant;
exports.load = Participant.load;