Source: Parser/ParserOutput.js

const SECONDS_IN_MINUTE = 60;
/**
 * @class
 * @classdesc Encapsulates functions to convert am A;phaTexStructure into a ParsedOutputPackage for more efficient storage in the database
 */
class ParserOutput {

    /**
     * Creates a new ParserOutput
     * @param {AlphaTexStructure} alphaTexStructure The main AlphaTexStructure to be converted
     */
    constructor(alphaTexStructure) {
        this.alphaTexStructure = alphaTexStructure;
        this.mainObj = null;
    }

    /**
     * Pushes the provided value onto the attribute of the provided object
     * @param {object} obj The object to add the value to with the given attribute
     * @param {string} attribute The attribute of the object to be assigned
     * @param {object} newValue The value to be assigned
     * @private
     */
    push(obj, attribute, newValue) {
        if (obj[attribute] === undefined) {
            obj[attribute] = newValue;
        } else if (!Array.isArray(obj[attribute])) {
            let arr = [obj[attribute], newValue];
            obj[attribute] = arr;
        } else {
            obj[attribute].push(newValue);
        }
    }

    /**
     * @typedef {object} ParsedOutputPackage
     * @property {ParsedOutputTrackPackage[]|ParsedOutputTrackPackage} tracks The collection of tracks
     */
    
    /**
     * @typedef {object} ParsedOutputTrackPackage
     * @property {string} name The name of the Track
     * @property {ParsedOutputStaffPackage[]} staffs The collection of staffs
     */

    /**
     * Creates and adds a new ParsedOutputTrackPackage object to the main ParsedOutputPackage
     * @param {ParsedOutputPackage} obj Object that will get the new Track added to its list of Tracks
     * @param {string} trackName The name of the new Track to be created
     * @returns {ParsedOutputTrackPackage} The created Track object
     * @private
     */
    addTrack(obj, trackName) {
        let track = {"name" : trackName, "staffs":[]};
        this.push(obj, "tracks", track);
        return track;
    }

    /**
     * @typedef {object} ParsedOutputStaffPackage
     * @property {string} name The name of the Staff prepended by "staff_"
     * @property {string} keySignature The key signature of the Staff
     * @property {string[]} lyrics Optional. If present, contains the lyrics of the Staff
     * @property {string} clef Optional. If present, contains the clef of the Staff
     * @property {ParsedOutputMeasurePackage[]|ParsedOutputMeasurePackage} measures The collection of the Measures
     */

    /**
     * Creates and adds a new ParsedOutputStaffPackage object to a ParsedOutputTrackPackage
     * @param {ParsedOutputTrackPackage} obj The Track that will get the new Staff added to its collection of Staffs
     * @param {string} keySignature The key signature of the Staff to be created
     * @param {string} lyrics If defined, the lyrics of the Staff to be created
     * @param {string} clef If defined, the clef of the Staff to be created 
     * @returns {ParsedOutputStaffPackage} The newly created Staff object. Doesn't start with a measures array.
     * @private
     */
    addStaff(obj, keySignature, lyrics, clef) {
        let staff = {"name": "staff_" + obj.staffs.length.toString(), "keySignature":keySignature}
        if (lyrics) {
            staff.lyrics = lyrics;
        }
        if (clef) {
            staff.clef = clef;
        }
        this.push(obj, "staffs", staff);
        return staff;
    }

    /**
     * @typedef {object} ParsedOutputMeasurePackage
     * @property {Map} attributesStr Mapping from String attributes to String values
     * @property {Map} attributesInt Mapping from String attributes to number values
     * @property {ParsedOutputChordPackage[]|ParsedOutputChordPackage} chords The collection of the Chords
     */

    /**
     * Creates and adds a new ParsedOutputMeasurePackage object to a ParsedOutputStaffPackage
     * @param {ParsedOutputStaffPackage} obj The Staff that will get the new Measure added to its collection of Measures
     * @param {Map} measureAttributesString A mapping from String attributes to String values
     * @param {Map} measureAttributesInt A mapping from String attributes to number values
     * @returns {ParsedOutputMeasurePackage} The newly created Measure object. Doesn't start with a chords array.
     * @private
     */
    addMeasure(obj, measureAttributesString, measureAttributesInt) {
        let measure = {};
        let attributesStr = {};
        measureAttributesString.forEach((value, key, map) => {
            attributesStr[key] = value;
        });
        measure["attributesStr"] = attributesStr;
        let attributesInt = {};
        measureAttributesInt.forEach((value, key, map) => {
            attributesInt[key] = value;
        });
        measure["attributesInt"] = attributesInt;
        this.push(obj, "measures", measure);
        return measure;
    }

    /**
     * @typedef {object} ParsedOutputChordPackage
     * @property {ParsedOutputPitchPackage[]|ParsedOutputPitchPackage} pitches The collection of the Pitches
     */

    /**
     * Creates and adds a new ParsedOutputChordPackage object to a ParsedOutputMeasurePackage
     * @param {ParsedOutputMeasurePackage} obj The Measure that will get the new Chord added to its collection of Chords
     * @returns {ParsedOutputChordPackage} The newly created Chord object. Doesn't start with a pitches array.
     * @private
     */
    addChord(obj) {
        let chord = {};
        this.push(obj, "chords", chord);
        return chord;
    }

    /**
     * @typedef {object} ParsedOutputPitchPackage
     * @property {number} midiValue The midi value of the Pitch
     * @property {number} duration The duration in seconds of the Pitch
     * @property {Set} beatEffects A collection of String beat effects that affect the Pitch
     * @property {Set} noteEffects A collection of String note effects that affect the Pitch
     */

    /**
     * Creates and adds a new ParsedOutputPitchPackage object to a ParsedOutputChordPackage
     * @param {ParsedOutputChordPackage} obj The Chord that will get the new Pitch added to its collection of Pitches
     * @param {number} midi The midi value for the newly created Pitch
     * @param {number} time The duration of the newly created Pitch in seconds
     * @param {Set} noteBeatEffects A Set of beat effects for the Pitch
     * @param {Set} noteNoteEffects A Set of note effects for the Pitch
     * @returns {ParsedOutputPitchPackage} The newly created Pitch object.
     * @private
     */
    addPitch(obj, midi, time, noteBeatEffects, noteNoteEffects) {
        let pitch = {"midiValue" : midi, "duration" : time};
        let beatEffects = [];
        if (noteBeatEffects !== undefined) {
            noteBeatEffects.forEach((value1, value2, set) => {
                beatEffects.push(value1);
            });
        }
        pitch["beatEffects"] = beatEffects;
        let noteEffects = [];
        if (noteNoteEffects !== undefined) {
            noteNoteEffects.forEach((value1, value2, set) => {
                noteEffects.push(value1);
            });
        }
        pitch["noteEffects"] = noteEffects;
        this.push(obj, "pitches", pitch);
        return pitch;
    }

    /**
     * Parses the AlphaTexStructure into a ParsedOutputPackage
     * @param {object} lyricsPerStaff Relates Staff indexes to lyric strings. 
     */
    parse(lyricsPerStaff) {
        let mainObj = {};
        let currentTempoFactor;

        let startingCurrentTempoFactor = SECONDS_IN_MINUTE;
        if (this.alphaTexStructure.getInt("tempo") !== null) {
            mainObj["tempo"] = this.alphaTexStructure.getInt("tempo");
            startingCurrentTempoFactor /= this.alphaTexStructure.getInt("tempo");
        } else {
            mainObj["tempo"] = 80;
            startingCurrentTempoFactor /= 80;
        }

        let staffIndex = 0;
        this.alphaTexStructure.tracks.forEach((track) => {
            let trackObject = this.addTrack(mainObj, track.getTrackName());
            track.trackData.staffs.forEach((staff) => {
                currentTempoFactor = startingCurrentTempoFactor;
                let staffObject = this.addStaff(trackObject, staff.attributesStr.get("keySignature"), (lyricsPerStaff[staffIndex] ? lyricsPerStaff[staffIndex].split(" ") : undefined), staff.attributesStr.get("clef") );
                staffIndex++;
                staff.measures.forEach((measure) => {
                    let measureObject = this.addMeasure(staffObject, measure.attributesStr, measure.attributesInt);
                    if (measure.getInt("tempo") !== undefined) {
                        currentTempoFactor = SECONDS_IN_MINUTE / measure.getInt("tempo");
                    }
                    measure.chords.forEach((chord) => {
                        let chordObject = this.addChord(measureObject);
                        chord.notes.forEach((note) => {
                            this.addPitch(chordObject, note.getMidiValue(), note.getDurationInSeconds(currentTempoFactor),
                            note.beatEffects, note.noteEffects);
                        });
                    });
                });
            });
        });


        this.mainObj = mainObj;
    }

    /**
     * Creates the pretty string for the given ParsedOutputPitchPackage 
     * @param {ParsedOutputPitchPackage} pitch The Pitch object to be converted
     * @returns {string} A pretty String representation of the Pitch
     * @private
     */
    pitchToString(pitch) {
        let output = "";
        if (pitch["beatEffects"].length > 0) {
            output += "\t\t\t\tBeatEffects: {\n";
            for (let key in pitch["beatEffects"]) {
                output += "\t\t\t\t\t" + pitch["beatEffects"][key] + "\n";
            }
            output += "\t\t\t\t}\n";
        }
        if (pitch["noteEffects"].length > 0) {
            output += "\t\t\t\t\NoteEffects: {\n";
            for (let key in pitch["noteEffects"]) {
                output += "\t\t\t\t\t" + pitch["noteEffects"][key] + "\n";
            }
            output += "\t\t\t\t}\n";
        }

        output += "\t\t\t\t";
        output += pitch.midiValue.toString();
        output += " ";
        output += pitch.duration.toString();
        output += "\n";

        return output;
    }

    /**
     * Creates the pretty string for the notes in the given ParsedOutputChordPackage 
     * @param {ParsedOutputChordPackage} chord The Chord object to be converted
     * @returns {string} A pretty String representation of the Chord
     * @private
     */
    notesToString(chord) {
        let ret = {"count" : 0};
        let output = "";

        if (Array.isArray(chord.pitches)) {
            chord.pitches.forEach((pitch) => {
                output += this.pitchToString(pitch);
                ret.count = ret.count + 1;
            });
        } else {
            output += this.pitchToString(chord.pitches);
            ret.count = ret.count + 1;
        }

        ret.output = output;
        return ret;
    }

    /**
     * Creates the pretty string for the given ParsedOutputMeasurePackage
     * @param {ParsedOutputMeasurePackage} measure The Measure object to be converted
     * @returns {string} A pretty String representation of the Measure
     * @private
     */
    chordsToString(measure) {
        let output = "";
        if (Array.isArray(measure.chords)) {
            measure.chords.forEach((chord) => {
                let notesOutput = this.notesToString(chord);
                if (notesOutput.count > 1) {
                    output += "\t\t\t{\n";
                    output += notesOutput.output;
                    output += "\t\t\t}\n";
                } else {
                    output += notesOutput.output;
                }
            });
        } else {
            let notesOutput = this.notesToString(measure.chords);
            if (notesOutput.count > 1) {
                output += "\t\t\t{\n";
                output += notesOutput.output;
                output += "\t\t\t}\n";
            } else {
                output += notesOutput.output;
            }
        }
        return output;
    }

    /**
     * Creates the pretty string for the given ParsedOutputStaffPackage
     * @param {ParsedOutputStaffPackage} staff The Staff object to be converted
     * @returns {string} A pretty String representation of the given Staff object
     * @private
     */
    measuresToString(staff) {
        let output = "";
        if (Array.isArray(staff.measures)) {
            staff.measures.forEach((measure) => {
                output += "\t\tMeasure:\n";
                output += "\t\t\tAttributesString: {\n";
                for (let key in measure["attributesStr"]) {
                    output += "\t\t\t\t" + key + " : " + measure["attributesStr"][key] + "\n";
                }
                output += "\t\t\t}\n";
                output += "\t\t\tAttributesInt: {\n";
                for (let key in measure["attributesInt"]) {
                    output += "\t\t\t\t" + key + " : " + measure["attributesInt"][key] + "\n";
                }
                output += "\t\t\t}\n";
                output += this.chordsToString(measure);
            });
        } else {
            output += "\t\tMeasure:\n";
            output += this.chordsToString(staff.measures);
        }
        return output;
    }

    /**
     * Creates the pretty string for the given ParsedOutputTrackPackage
     * @param {ParsedOutputTrackPackage} track The Track object to be converted
     * @returns {string} A pretty String representation of the given Track object
     * @private
     */
    staffToString(track) {
        let output = "";

        if (Array.isArray(track.staffs)) {
            track.staffs.forEach((staff) => {
                output += "\tStaff: ";
                output += staff.name;
                output += "\n";
                output += this.measuresToString(staff);
            })
        } else {
            output += "\tStaff: ";
            output += tracks.staffs.name;
            output += "\n";
            output += this.measuresToString(tracks.staffs);
        }

        return output;
    }

    /**
     * Creates a pretty String representation of the ParsedOutputPackage
     * @returns {string} The pretty String representation of the ParsedOutputPackage
     */
    toPrettyString() {
        let output = "";
        if (this.mainObj === null) {
            return output;
        }
        if (this.mainObj["tempo"]) {
            output += "tempo: " + this.mainObj["tempo"] + "\n";
        }
        if (Array.isArray(this.mainObj.tracks)) {
            this.mainObj.tracks.forEach((track) => {
                output += "Track: ";
                output += track.name;
                output += "\n";
                output += this.staffToString(track);
            })
        } else {
            output += "Track: ";
            output += this.mainObj.tracks.name;
            output += "\n";
            output += this.staffToString(this.mainObj.tracks);
        }
        return output;
    }

    /**
     * Returns the result of stringifing the ParsedOutputPackage
     * @returns {string} The result of stringifing the ParsedOutputPackage
     */
    toString() {
        return JSON.stringify(this.mainObj);
    }
}

module.exports = ParserOutput