Source: routers/performance.js

const express = require('express');
const auth = require('../middleware/authenticator.js');
const verify = require("../middleware/verifier.js");
const mysql = require("../db/mysql.js");
const PerformanceAnalyser = require("../Parser/PerformanceAnalyser.js");
const query = require("../util/query.js");
const clean = require('../Parser/cleaner.js');
const router = express.Router();

/**
 * Route for getting all performance information about the given member
 * @name member/get/all
 * @function
 * @inner
 * @param {*} req
 * @param {string} req.body.sheetMusicId The sheet music ID to be accessed.
 * @param {*} res
 * @returns {string} An error message if an error occured
 * @async
 */
router.get('/all', auth.token, async (req,res) => {
    verify.memberOfSheetMusic(req, res).then((choirId) => {
        if (choirId === null) {
            res.status(403).send('Unauthorized');
        } else {
            let sheetMusicId = req.query.sheetMusicId;
            const errorMessage = "Unable to get exercises";
            if (sheetMusicId) {
                mysql.getClient().then((client) => {
                    client.query("SELECT hex(member_id) FROM tma.member WHERE person_id=? AND choir_id=unhex(?)", [res.locals.uid, choirId], function(error, results, fields) {
                        if (error) {
                            res.status(503).send(errorMessage);
                        } else {
                            let memberId = results[0]["hex(member_id)"];
                            client.query("SELECT performance_data FROM tma.performance WHERE member_id=? AND sheet_music_id=?", [memberId, sheetMusicId], function(error, results, fields) {
                                if (error) {
                                    res.status(503).send(errorMessage);
                                } else {
                                    res.status(200).send();
                                }
                            });
                        }
                    });
                });
            } else {
                res.status(400).send(errorMessage);
            }
        }
    }).catch((error) => {
        res.status(403).send('Unauthorized');
    })
});

/**
 * Route for updating performance information without analyzing the results
 * @name member/put/no-analysis
 * @function
 * @inner
 * @param {*} req
 * @param {string} req.body.sheetMusicId The sheet music ID to be accessed.
 * @param {string} req.body.performanceData The stringified performance data to be added
 * @param {string} req.body.performanceId The ID of the performance to which to append the latest performance data
 * @param {*} res
 * @returns {string} An error message if an error occured
 * @async
 */
router.put('/no-analysis', auth.token, async(req,res) => {
    verify.memberOfSheetMusic(req, res).then((choirId) => {
        if (choirId === null) {
          res.status(403).send('Unauthorized');
        } else {
            let performanceData = req.body.performanceData;
            if (performanceData && typeof(performanceData) === 'string') {
                performanceData = JSON.parse(performanceData);
            }
            let performanceId = req.body.performanceId
            const errorMessage = "Unable to update performance";
            if (performanceData && performanceId && performanceData.pitches.length > 0) {
                mysql.getClient().then((client) => {
                    client.query("SELECT hex(member_id) FROM tma.member WHERE person_id=? AND choir_id=unhex(?)", [res.locals.uid, choirId], function(error, results, fields) {
                        if (error) {
                            res.status(503).send(errorMessage);
                        } else {
                            let memberId = results[0]["hex(member_id)"];
                            client.query("SELECT performance_data FROM tma.performance WHERE performance_id=unhex(?) AND member_id=unhex(?)", [performanceId, memberId], function(error, results, fields) {
                                if (error) {
                                    res.status(503).send(errorMessage);
                                } else if (results.length === 0) {
                                    res.status(204).send(errorMessage);
                                } else {
                                    let currentPerformance = JSON.parse(results[0]["performance_data"]);
                                    currentPerformance["pitches"] = currentPerformance["pitches"].concat(performanceData["pitches"]);
                                    client.query("UPDATE tma.performance SET performance_data=? WHERE performance_id=unhex(?) AND member_id=unhex(?)", [JSON.stringify(currentPerformance), performanceId, memberId], function(error, results, fields) {
                                        if (error) {
                                            res.status(503).send(errorMessage);
                                        } else {
                                            res.status(200).send();
                                        }
                                    });
                                }
                            });
                        }
                    });
                });
            } else {
                res.status(400).send(errorMessage);
            }
        }
    }).catch((error) => {
        res.status(403).send('Unauthorized');
    });
});

/**
 * Route for updating performance information and analyzing the results
 * @name member/put/analysis
 * @function
 * @inner
 * @param {*} req
 * @param {string} req.body.sheetMusicId The sheet music ID to be accessed
 * @param {string} req.body.performanceData The stringified performance data to be added
 * @param {string} req.body.performanceId The ID of the performance to which to append the latest performance data
 * @param {string} req.body.sheetMusicId The sheet music that the performance is attached to
 * @param {string} req.body.exerciseId If not null, gives the ID of the exercise that this performance was based on. Otherwise null
 * @param {boolean} req.body.isDurationExercise Whether the performance was for a duration exercise
 * @param {number} req.body.measureStart The start measure number of the performance
 * @param {number} req.body.measureEnd The end measure number of the performance
 * @param {*} res
 * @returns {string} An error message if an error occured
 * @async
 */
router.put("/analysis", auth.token, async(req,res) => {
    verify.memberOfSheetMusic(req, res).then((choirId) => {
        if (choirId === null) {
          res.status(403).send('Unauthorized');
        } else {
            let performanceData = req.body.performanceData;
            if (performanceData && typeof(performanceData) === 'string') {
                performanceData = JSON.parse(performanceData);
            }
            let performanceId = req.body.performanceId;
            let sheetMusicId = req.body.sheetMusicId;
            let exerciseId = req.body.exerciseId;
            let isDurationExercise = req.body.isDurationExercise;
            let measureStart = req.body.measureStart;
            let measureEnd = req.body.measureEnd;
            const errorMessage = "Unable to update performance";
            if (performanceId && performanceData && sheetMusicId && (exerciseId !== null || exerciseId === null) && performanceData.pitches.length > 0 && measureStart && measureEnd && (isDurationExercise || !isDurationExercise)) {
                mysql.getClient().then((client) => {
                    client.query("SELECT hex(member_id) FROM tma.member WHERE person_id=? AND choir_id=unhex(?)", [res.locals.uid, choirId], function(error, results, fields) {
                        if (error) {
                            res.status(503).send(errorMessage);
                        } else {
                            let memberId = results[0]["hex(member_id)"];
                            client.query("SELECT performance_data FROM tma.performance WHERE performance_id=unhex(?) AND member_id=unhex(?)", [performanceId, memberId], function(error, results, fields) {
                                if (error) {
                                    res.status(503).send(errorMessage);
                                } else if (results.length === 0) {
                                    res.status(204).send(errorMessage);
                                } else {
                                    let currentPerformance = JSON.parse(results[0]["performance_data"]);
                                    currentPerformance["pitches"] = currentPerformance["pitches"].concat(performanceData["pitches"]);
    
                                    client.query("UPDATE tma.performance SET performance_data=? WHERE performance_id=unhex(?) AND member_id=unhex(?)", [JSON.stringify(currentPerformance), performanceId, memberId], function(error, results, fields) {
                                        if (error) {
                                            res.status(503).send(errorMessage);
                                        } else {
                                            client.query("SELECT part FROM tma.member_sheet_music_part WHERE sheet_music_id = unhex(?) AND member_id=unhex(?)", [sheetMusicId, memberId], function(error, results, fields) {
                                                if (error) {
                                                    res.status(503).send(errorMessage);
                                                } else if (results.length === 0) {
                                                    res.status(204).send(errorMessage);
                                                } else {
                                                    let partName = results[0]["part"];
    
                                                    client.query("SELECT performance_data FROM tma.sheet_music WHERE sheet_music_id = unhex(?)", [sheetMusicId], function(error, results, fields) {
                                                        if (error) {
                                                            res.status(503).send(errorMessage);
                                                        } else if (results.length === 0) {
                                                            res.status(204).send(errorMessage);
                                                        } else {
                                                            let pData = JSON.parse(results[0]['performance_data']);
                
                                                            let measureBounds = null;
                                                            if (measureStart && measureEnd && measureStart !== null && measureEnd !== null) {
                                                                measureBounds= [measureStart, measureEnd];
                                                            }
                
                                                            let measureErrorExercise = exerciseId !== null ? isDurationExercise : false;
                                                            let measureErrors = cleanAndAnalyzePerformance(performanceData, pData, partName, 0, measureBounds, measureErrorExercise);
                                                            saveMeasureErrors(measureErrors, memberId, sheetMusicId).then((didSave) => {
                                                                if (!didSave) {
                                                                    res.status(503).send(errorMessage);
                                                                } else {  
                                                                    res.status(200).send();
                                                                }
                                                            }).catch((error) => {
                                                                client.rollback(function() {
                                                                    res.status(503).send(errorMessage);
                                                                });
                                                            });
                                                        }
                                                    });
                                                } 
                                            });
                                        }
                                    });
                                }
                            });
                        }
                    });
                });
            } else {
                res.status(400).send(errorMessage);
            }
        }
    }).catch((error) => {
        res.status(403).send('Unauthorized');
    });
});

/**
 * @typedef {object} MemberPostNewNoAnalysisPackage
 * @property {string} performance_id The ID of the newly generated performance data
 */

/**
 * Route for creating a new record of performance information without analyzing the results
 * @name member/post/new/no-analysis
 * @function
 * @inner
 * @param {*} req
 * @param {string} req.body.sheetMusicId The sheet music ID to be accessed
 * @param {string} req.body.performanceData The stringified performance data to be added
 * @param {string} req.body.sheetMusicId The sheet music that the performance is attached to
 * @param {string} req.body.exerciseId If not null, gives the ID of the exercise that this performance was based on. Otherwise null
 * @param {boolean} req.body.isDurationExercise Whether the performance was for a duration exercise
 * @param {number} req.body.measureStart The start measure number of the performance
 * @param {number} req.body.measureEnd The end measure number of the performance
 * @param {*} res
 * @returns {MemberPostNewNoAnalysisPackage|string} On success the ID of the newly generated performance generated. Otherwise, an error message.
 * @async
 */
router.post('/new/no-analysis', auth.token, async(req,res) => {
    verify.memberOfSheetMusic(req, res).then((choirId) => {
        if (choirId === null) {
          res.status(403).send('Unauthorized');
        } else {
            let performanceData = req.body.performanceData;
            if (performanceData && typeof(performanceData) === 'string') {
                performanceData = JSON.parse(performanceData);
            }
            let sheetMusicId = req.body.sheetMusicId;
            let exerciseId = req.body.exerciseId;
            let isDurationExercise = req.body.isDurationExercise;
            let measureStart = req.body.measureStart;
            let measureEnd = req.body.measureEnd;
            const errorMessage = "Unable to add performance";
            if (performanceData && sheetMusicId && (exerciseId !== null || exerciseId === null) && performanceData.pitches.length > 0 && measureStart && measureEnd && (isDurationExercise || !isDurationExercise)) {
                mysql.getClient().then((client) => {
                    client.query("SELECT hex(member_id) FROM tma.member WHERE person_id=? AND choir_id=unhex(?)", [res.locals.uid, choirId], function(error, results, fields) {
                        if (error) {
                            res.status(503).send(errorMessage);
                        } else {
                            let memberId = results[0]["hex(member_id)"];
    
                            let lastValue;
                            let args = [JSON.stringify(performanceData), memberId];
                            if (exerciseId === null) {
                                lastValue = "sheet_music_id";
                                args.push(sheetMusicId);
                            } else {
                                lastValue = "exercise_id";
                                args.push(exerciseId);
                            }
    
                            client.query("SELECT replace(uuid(),\"-\",\"\") AS uuid", function(error, results, fields) {
                                if (error) {
                                    res.status(503).send(errorMessage);
                                    } else {
                                    let generated_uuid = results[0]["uuid"];
                                    args.unshift(generated_uuid);
                                    let sql = "insert into tma.performance (performance_id, performance_data, member_id, " + lastValue + ") values(unhex(?), ?, unhex(?), unhex(?))"; 
    
                                    client.query(sql, args, function(error, results, fields) {
                                        if (error) {
                                            res.status(503).send(errorMessage);
                                        } else {
                                            res.status(200).json({ performance_id: generated_uuid });
                                        }
                                    });
                                    }
                            });
                        }
                    });
                });
            } else {
                res.status(400).send(errorMessage);
            }
        }
    }).catch((error) => {
        res.status(403).send('Unauthorized');
    });
});

/**
 * Route for creating a new record of performance information and analyzing the results
 * @name member/post/new/analysis
 * @function
 * @inner
 * @param {*} req
 * @param {string} req.body.sheetMusicId The sheet music ID to be accessed
 * @param {string} req.body.performanceData The stringified performance data to be added
 * @param {string} req.body.sheetMusicId The sheet music that the performance is attached to
 * @param {string} req.body.exerciseId If not null, gives the ID of the exercise that this performance was based on. Otherwise null
 * @param {boolean} req.body.isDurationExercise Whether the performance was for a duration exercise
 * @param {number} req.body.measureStart The start measure number of the performance
 * @param {number} req.body.measureEnd The end measure number of the performance
 * @param {*} res
 * @returns {string} An error message if an error occured
 * @async
 */
router.post('/new/analysis', auth.token, async (req,res) => {
    verify.memberOfSheetMusic(req, res).then((choirId) => {
        if (choirId === null) {
          res.status(403).send('Unauthorized');
        } else {
            let performanceData = req.body.performanceData;
            if (performanceData && typeof(performanceData) === 'string') {
                performanceData = JSON.parse(performanceData);
            }
            let sheetMusicId = req.body.sheetMusicId;
            let exerciseId = req.body.exerciseId;
            let isDurationExercise = req.body.isDurationExercise;
            let measureStart = req.body.measureStart;
            let measureEnd = req.body.measureEnd;
            const errorMessage = "Unable to add performance";
            if (performanceData && sheetMusicId && (exerciseId !== null || exerciseId === null) && performanceData.pitches.length > 0 && measureStart && measureEnd && (isDurationExercise || !isDurationExercise)) {
                mysql.getClient().then((client) => {
                    client.beginTransaction(function(error) {
                        client.query("SELECT hex(member_id) FROM tma.member WHERE person_id=? AND choir_id=unhex(?)", [res.locals.uid, choirId], function(error, results, fields) {
                            if (error) {
                                client.rollback(function() {
                                    res.status(503).send(errorMessage);
                                });
                            } else {
                                let memberId = results[0]["hex(member_id)"];
    
                                client.query("SELECT part FROM tma.member_sheet_music_part WHERE sheet_music_id = unhex(?) AND member_id=unhex(?)", [sheetMusicId, memberId], function(error, results, fields) {
                                    if (error) {
                                        client.rollback(function() {
                                            res.status(503).send(errorMessage);
                                        });
                                    } else if (results.length === 0) {
                                        client.rollback(function() {
                                            res.status(204).send(errorMessage);
                                        });
                                    } else {
                                        let partName = results[0]["part"];
    
                                        client.query("SELECT performance_data FROM tma.sheet_music WHERE sheet_music_id = unhex(?)", [sheetMusicId], function(error, results, fields) {
                                            if (error) {
                                                client.rollback(function() {
                                                    res.status(503).send(errorMessage);
                                                });
                                            } else if (results.length === 0) {
                                                client.rollback(function() {
                                                    res.status(204).send(errorMessage);
                                                });
                                            } else {
                                                let pData = JSON.parse(results[0]['performance_data']);
    
                                                let measureBounds = null;
                                                if (measureStart && measureEnd && measureStart !== null && measureEnd !== null) {
                                                    measureBounds= [measureStart, measureEnd];
                                                }
    
                                                let measureErrorExercise = exerciseId !== null ? isDurationExercise : false;
                                                let measureErrors = cleanAndAnalyzePerformance(performanceData, pData, partName, 0, measureBounds, measureErrorExercise);
                                                saveMeasureErrors(measureErrors, memberId, sheetMusicId).then((didSave) => {
                                                    if (!didSave) {
                                                        client.rollback(function() {
                                                            res.status(503).send(errorMessage);
                                                        });
                                                    } else {
                                                        let lastValue;
                                                        let args = [JSON.stringify(performanceData), memberId];
                                                        if (exerciseId === null) {
                                                            lastValue = "sheet_music_id";
                                                            args.push(sheetMusicId);
                                                        } else {
                                                            lastValue = "exercise_id";
                                                            args.push(exerciseId);
                                                        }
    
                                                        let sql = "insert into tma.performance (performance_id, performance_data, member_id, " + lastValue + ") values(unhex(replace(uuid(),\"-\",\"\")), ?, unhex(?), unhex(?))"; 
    
                                                        client.query(sql, args, function(error, results, fields) {
                                                            if (error) {
                                                                client.rollback(function() {
                                                                    res.status(503).send(errorMessage);
                                                                });
                                                            } else {
                                                                client.commit(function(err) {
                                                                    if (err) {
                                                                        client.rollback(function() {
                                                                            res.status(503).send(errorMessage);
                                                                        });
                                                                    } else {
                                                                        res.status(200).send();
                                                                    }
                                                                });
                                                            }
                                                        });
                                                    }
                                                }).catch((error) => {
                                                    client.rollback(function() {
                                                        res.status(503).send(errorMessage);
                                                    });
                                                });
                                            }
                                        });
                                    } 
                                });
                            }
                        });
                    });
                });
            } else {
                res.status(400).send(errorMessage);
            }
        }
    }).catch((error) => {
        res.status(403).send('Unauthorized');
    });
});

/**
 * Saves the new result of measure errors into the list for the given member of the given sheet music
 * @param {AnalyzedPerformancePackage[]} measureErrors An array of analyzed performance objects with information about each measure analyzed
 * @param {string} memberId The ID of the member to be updated
 * @param {string} sheetMusicId The ID of the sheet music that the performance was for
 * @returns {boolean} If the saving was successful
 * @private
 */
const saveMeasureErrors = async(measureErrors, memberId, sheetMusicId) => {
    try {
        let obj_1 = await query("SELECT progress_data FROM tma.performance_progress WHERE member_id=unhex(?) AND sheet_music_id=unhex(?)", [memberId, sheetMusicId]);
        if (obj_1.results.length === 0) {
            return false;
        } else {
            let progress = JSON.parse(obj_1.results[0]["progress_data"]);
            let currentIndex = progress.index;
            let newIndex = (progress.index + 1) % 5;

            if (progress.data.length === 5) {
                for (let i = 0; i < measureErrors.length; i++) {
                    progress.data[newIndex][measureErrors[i]["measureNumber"] - 1] = measureErrors[i]["overallScore"];
                }
            } else {
                let newProgressData = JSON.parse(JSON.stringify(progress.data[currentIndex]));
                for (let i = 0; i < measureErrors.length; i++) {
                    newProgressData[measureErrors[i]["measureNumber"] - 1] = measureErrors[i]["overallScore"];
                }
                progress.data.push(newProgressData);
            }

            progress.index = newIndex;

            await query("UPDATE tma.performance_progress SET progress_data=? WHERE member_id=unhex(?) AND sheet_music_id=unhex(?)", [JSON.stringify(progress), memberId, sheetMusicId]);
            return true;
        }
    } catch(error) {
        return false;
    }
}

/**
 * Cleans provided performance data and analyzes it comparing to the provided sheet music data of the specified track and staff number within the measure bounds provided
 * @param {PerformancePackage} performanceData - The performance object derivied from user performance
 * @param {ParsedOutputPackage} sheetMusicData - Internalized object based on alphaTex for sheet music
 * @param {string} partName - The name of the part to be analyzed in the sheet music
 * @param {number} staffIndex - The number of the staff to be analyzed
 * @param {int[]} measureBounds - The bounds of the measures or null for all
 * @param {Boolean} isDurationExercise - Whether this was a duration exercise performance
 * @return {AnalyzedPerformancePackage[]} An array of analyzed performance objects with information about each measure analyzed
 * TODO: Make overall score based on duration as well
 */
const cleanAndAnalyzePerformance = (performanceData, sheetMusicData, partName, staffIndex, measureBounds, isDurationExercise) => {
    let pitches = clean(performanceData);
    let results = PerformanceAnalyser.analyzePerformance({pitches}, sheetMusicData, partName, staffIndex, measureBounds, isDurationExercise);
    return results;
}


module.exports = router;