Source: Parser/ParserMeasures.js

const ParsedOutputManipulator = require("./ParsedOutputManipulator.js");

const NOTE_STRING = ["c", "c#", "d", "d#", "e", "f", "f#", "g", "g#", "a", "a#", "b"];
/**
 * @class
 * @classdesc Encapsulates functions used to generate an exercise from a collection of measures
 */
class ParserMeasures {
    /**
     * Creates a new ParserMeasures
     * @param {ParsedOutputPackage} parsedOutputObj The main ParsedOutputPackage
     */
    constructor(parsedOutputObj) {
        this.manipulator = new ParsedOutputManipulator(parsedOutputObj);
    }

    /**
     * Converts the duration in seconds of a Note back to the duration in AlphaTex
     * @param {number} durationInSeconds The duration in seconds of the given Note
     * @param {number} currentTempoFactor The current factor based on the tempo
     * @param {Set} beatEffects A set of effects that are modifying the Note
     * @returns {number} The duration as would be noted in AlphaTex
     */
    getDurationFromDurationInSeconds(durationInSeconds, currentTempoFactor, beatEffects) {
        let duration = durationInSeconds;
        if (beatEffects.includes("dotted")) {
            duration /= 1.5;
        }
        duration /= currentTempoFactor;
        duration = 4 / duration;
        return duration;
    }

    /**
     * Creates AlphaTex from a given midi value
     * @param {number} midi The midi value of the Note with -1 as a special value for silence
     * @returns {string} The AlphaTex for that note either "r" for a rest (midi === -1) or the appended string representation of the note and its octave
     * @private
     */
    getTextFromMidi(midi) {
        let text = "";
        if (midi === -1) {
            text = "r";
        } else {
            let octave = Math.floor((midi / 12) - 1);
            let noteIndex = midi % 12;
            let note = NOTE_STRING[noteIndex];
            text = note + octave;
        }
        return text;
    }

    /**
     * @typedef {object} LyricPackage
     * @property {string} tempLyricText The lyric text for this note or empty string
     * @property {number} lyricArrayIndex The latest lyric index into lyricArray
     */

    /**
     * Gets the lyric text for the given Pitch object
     * @param {ParsedOutputPitchPackage} pitch The Pitch object to be analyzed
     * @param {boolean} isDurationExercise If true then this is a duration exercise otherwise if false then it's not
     * @param {string[]} lyricArray The collection of lyrics to pull from
     * @param {number} lyricArrayIndex The latest index into lyricArray
     * @returns {LyricPackage} The lyric text for the pitch if it's not silence and the latest lyric index
     * @private 
     */
    getLyricText(pitch, isDurationExercise, lyricArray, lyricArrayIndex) {
        let tempLyricText;
        let tempLyricIndex = lyricArrayIndex;
        if (isDurationExercise) {
            if (pitch["midiValue"] !== -1) {
                tempLyricText = "oo";
            } else {
                tempLyricText = "";
            }
        } else if (pitch["midiValue"] !== -1 && lyricArray !== null) {
            tempLyricText = lyricArray[tempLyricIndex];
            tempLyricIndex++;
        } else {
            tempLyricText = "";
        }
        return {tempLyricText, "lyricArrayIndex":tempLyricIndex};
    }

    /**
     * Creates AlphaTex for the given Pitch
     * @param {ParsedOutputPitchPackage} pitch The Pitch object to be analyzed
     * @param {number} tempoFactor The latest factor based on the tempo
     * @param {boolean} isDurationExercise If true then this is a duration exercise otherwise if false then it's not
     * @returns {string} The generated AlphaTex for the given Pitch
     * @private
     */
    getPitchText(pitch, tempoFactor, isDurationExercise) {
        let pitchText;
        let effectText = "";
        let beatEffectText = "";
        if (pitch["beatEffects"].length > 0) {
            pitch["beatEffects"].forEach((effect) => {
                if (effect === "tied") {
                    if (effectText.length > 0) {
                        effectText += " ";
                    }
                    effectText += "-";
                } else if (effect === "dotted") {
                    if (effectText.length > 0) {
                        effectText += " ";
                    }
                    effectText += "d";
                } else if (effect === "crescendo") {
                    if (beatEffectText.length > 0) {
                        beatEffectText += " ";
                    }
                    beatEffectText += "cre";
                } else if (effect === "decrescendo") {
                    if (beatEffectText.length > 0) {
                        beatEffectText += " ";
                    }
                    beatEffectText += "dec";
                }
            });
        }

        let duration = this.getDurationFromDurationInSeconds(pitch["duration"], tempoFactor, pitch["beatEffects"]);

        if (isDurationExercise) {
            if (pitch["midiValue"] === -1) {
                pitchText = "r";
            } else {
                pitchText = "c4";
            }
        } else {
            pitchText = this.getTextFromMidi(pitch["midiValue"]);
        }

        if (pitchText === "r") {
            pitchText += "." + duration;
            if (effectText.length > 0 || beatEffectText.length > 0) {
                pitchText += "{";
                pitchText += effectText + beatEffectText;
                pitchText += "}";
            }
        } else {
            if (effectText.length > 0) {
                pitchText += "{" + effectText + "}";
            }
            pitchText += "." + duration;
            if (beatEffectText.length > 0) {
                pitchText += "{" + beatEffectText + "}";
            }
        }
        return pitchText;
    }

    /**
     * @typedef {object} ChordTextPackage
     * @property {string} chordText The AlphaTex of the Chord
     * @property {string} lyricText The lyrics that go along with the Chord
     * @property {number} lyricArrayIndex The latest index in the lyricArray
     */

    /**
     * Creates AlphaTex for the given Chord
     * @param {ParsedOutputChordPackage} chord The Chord object to be analyzed
     * @param {number} tempoFactor The latest factor based on the tempo
     * @param {boolean} isDurationExercise If true then this is a duration exercise otherwise if false then it's not
     * @param {string[]} lyricArray The collection of lyrics to pull from
     * @param {number} lyricArrayIndex The latest index into lyricArray
     * @returns {ChordTextPackage} The generated AlphaTex for the Chord and some auxilary information
     * @private
     */
    getChordText(chord, tempoFactor, isDurationExercise, lyricArray, lyricArrayIndex) {
        let chordText = "";
        let lyricText = "";
        let tempLyricIndex = lyricArrayIndex;
        if (Array.isArray(chord["pitches"])) {
            chordText += "( ";
            let duration = null;
            chord["pitches"].forEach((pitch) => {
                let pitchText = this.getPitchText(pitch, tempoFactor, isDurationExercise);
                let dotIndex = pitchText.indexOf(".");
                chordText += pitchText.substring(0, dotIndex) + " ";
                if (duration === null) {
                    duration = parseInt(pitchText.substring(dotIndex+1, pitchText.length));
                }
                let {tempLyricText, lyricArrayIndex} = this.getLyricText(pitch, isDurationExercise, lyricArray, tempLyricIndex);
                tempLyricIndex = lyricArrayIndex;
                if (tempLyricText.length > 0) {
                    if (lyricText.length > 0) {
                        lyricText += " ";
                    }
                    lyricText += tempLyricText;
                }
            });
            chordText += ")";
            chordText += "." + duration;
        } else {
            chordText += this.getPitchText(chord["pitches"], tempoFactor, isDurationExercise) + " ";
            let {tempLyricText, lyricArrayIndex} = this.getLyricText(chord["pitches"], isDurationExercise, lyricArray, tempLyricIndex);
            tempLyricIndex = lyricArrayIndex;
            if (tempLyricText.length > 0) {
                if (lyricText.length > 0) {
                    lyricText += " ";
                }
                lyricText += tempLyricText;
            }
        }
        return {chordText, lyricText, "lyricArrayIndex":tempLyricIndex};
    }

    /**
     * @typedef {object} MeasureTextPackage
     * @property {string} measureText The AlphaTex for the Measure
     * @property {string} lyricsText The lyrics that go along with the Measure
     * @property {number} tempLyricIndex The latest index in the lyricArray
     */

    /**
     * Creates AlphaTex for the given Measure
     * @param {number} measureIndex The index of the Measure to be accessed
     * @param {ParsedOutputStaffPackage} staff The Staff object to be accessed
     * @param {number} currentTempoFactor The latest factor based on the tempo
     * @param {number} measureTempo The tempo of the Measure to be accessed
     * @param {boolean} isDurationExercise If true then this is a duration exercise otherwise if false then it's not
     * @param {string[]} lyricArray The collection of lyrics to pull from
     * @param {number} lyricArrayIndex The latest index into lyricArray
     * @param {number[]} startingTs A 1D two element array representing the current tempo
     * @param {boolean} isLastMeasure If true then this is the last measure otherwise false
     * @returns {MeasureTextPackage} The generated AlphaTex for the Measure and some auxillary data
     * @private
     */
    getMeasureText(measureIndex, staff, currentTempoFactor, measureTempo, isDurationExercise, lyricArray, lyricArrayIndex, startingTs, isLastMeasure) {
        let measureObj = this.manipulator.getMeasure(staff, measureIndex);
        let tempLyricIndex = lyricArrayIndex;
        let measureText = "";
        let lyricsText = "";
        let attributesInt = measureObj["attributesInt"];
        if (attributesInt["tempo"]) {
            measureText += "\\tempo " + attributesInt["tempo"] + "\n";
        }
        if (startingTs !== null) {
            measureText += "\\ts " + startingTs[0] + " " + startingTs[1] + "\n";
        } else if (attributesInt["tsTop"]) {
            measureText += "\\ts " + attributesInt["tsTop"] + " " + attributesInt["tsBottom"] + "\n";
        }
        let tempoFactor = measureTempo === -1 ? currentTempoFactor : 60 / measureTempo;

        if (Array.isArray(measureObj["chords"])) {
            measureObj["chords"].forEach((chord) => {
                let {chordText, lyricText, lyricArrayIndex} = this.getChordText(chord, tempoFactor, isDurationExercise, lyricArray, tempLyricIndex);
                tempLyricIndex = lyricArrayIndex;
                measureText += chordText + " ";
                if (lyricText.length > 0) {
                    if (lyricsText.length > 0) {
                        lyricsText += " ";
                    }
                }
                lyricsText += lyricText;
            });
        } else {
            let {chordText, lyricText, lyricArrayIndex} = this.getChordText(measureObj["chords"], tempoFactor, isDurationExercise, lyricArray, tempLyricIndex);
            tempLyricIndex = lyricArrayIndex;
            measureText += chordText;
            if (lyricText.length > 0) {
                if (lyricsText.length > 0) {
                    lyricsText += " ";
                }
            }
            lyricsText += lyricText;
        }
        if (!isLastMeasure) {
            measureText += "|";
        }
        measureText += "\n";

        return {measureText, lyricsText, tempLyricIndex};
    }

    /**
     * @typedef {object} MeasureAlphaTexPackage
     * @property {string} lyricText The lyrics of the provided AlphaTex
     * @property {string} alphaTex The generated AlphaTex
     */

    /**
     * Creates an exercise within the provided measure bounds of the track and staff number specified either a duration exercise or a normal one
     * @param {number[]} measures The start and end of the measure bounds. If null, then get all measures
     * @param {number} trackNumber The number of the Track to be retrieved.
     * @param {number} staffNumber The number of the Staff to be retrieved
     * @param {boolean} isDurationExercise If true then this will generate a duration exercise otherwise if false then it will just copy the Measures
     * @returns {MeasureAlphaTexPackage} If successful, provides the lyrics and generated alphaTex. Otherwise, returns null
     */
    measuresToAlphaTex(measures, trackNumber, staffNumber, isDurationExercise) {
        let trackIndex = trackNumber - 1;
        let staffIndex = staffNumber - 1;
        let track = this.manipulator.getTrack(trackIndex);
        let staff;
        if (track !== null) {
            staff = this.manipulator.getStaff(track, staffIndex);
        }
        if (staff && Array.isArray(measures) && measures.length > 0) {
            // assumes provided array measures is sorted
            let {lyricArray, lyricArrayIndex} = this.manipulator.getLyricArray(staff, measures[0]);
            let lyricText = "";
            let measuresText = "";
            let {measureToTempo, ts} = this.manipulator.visitMeasure(staff);

            // assumes provided array measures is sorted
            let firstMeasureTempo = measureToTempo[measures[0]];
            let startingTempo = firstMeasureTempo === -1 ? this.manipulator.mainObj["tempo"] : firstMeasureTempo;
            let currentTempoFactor = 60 / startingTempo;

            let alphaTex = "\\title \"Exercise t: " + trackNumber + " s: " + staffNumber + " startM: " + measures[0] + " endM: " + measures[1] + "\"\n" +
            "\\tempo " + startingTempo + "\n" +
            ".\n" +
            "\n";

            if (measures.length > 2 && measures[2] === "all") {
                alphaTex += "\\track \"" + this.manipulator.getTrack(trackIndex).name + "\"\n";
            } else {
                alphaTex += "\\track \"Exercise\"\n";
            }
            alphaTex += "\\staff {score} \\tuning piano \\instrument acousticgrandpiano \\ks " + staff.keySignature + (staff.clef ? " \\clef " + staff.clef : "") + "\n";



            for (let i = measures[0]; i <= measures[1]; i++) {
                let measureIndex = i - 1;
                let startingTs = i !== measures[0] ? null : ts;
                let isLastMeasure = i === measures[1];
                let {measureText, lyricsText, tempLyricIndex } = this.getMeasureText(measureIndex, staff, currentTempoFactor, measureToTempo[measureIndex], isDurationExercise, lyricArray, lyricArrayIndex, startingTs, isLastMeasure);
                lyricArrayIndex = tempLyricIndex;
                measuresText += measureText;
                if (lyricsText.length > 0) {
                    if (lyricText.length > 0) {
                        lyricText += " ";
                    }
                    lyricText += lyricsText;
                }
            }

            if (lyricText.length > 0) {
                alphaTex += "\\lyrics \"" + lyricText + "\"\n";
            }
            alphaTex += measuresText;

            return {lyricText, alphaTex};
        } else {
            return null;
        }
    }

}

module.exports = ParserMeasures;