Source: Parser/ParserPerformanceData.js

/**
 * Gets the part names of the given data
 * @param {ParsedOutputPackage} pData The parsed data
 * @returns {string[]} The names of the parts (i.e. tracks)
 */
const getPartNames = (pData) => {
  let partNames = [];
  if (Array.isArray(pData['tracks'])) {
    pData['tracks'].every( (track) => partNames.push(track['name']));
  } else {
    let track = pData['tracks'];
    partNames.push(track['name']);
  }
  return partNames;
}

/**
 * Gets the expected note stream of the part with the provided part name
 * @param {ParsedOutputPackage} pData The parsed data
 * @param {string} partName The part (i.e. track) name to be accessed
 * @returns {number[]} A 1D array of even size with the ith index as the midi value and the i+1 index as the duration of that note for i%2==0
 */
const getNoteStream = (pData, partName) => {
  let noteStream = [];
  if (Array.isArray(pData['tracks'])) {
    pData['tracks'].forEach((track) => {
      getNoteStreamTrack(noteStream, track, partName);
    });
  } else {
    getNoteStreamTrack(noteStream, pData['tracks'], partName);
  }

  return noteStream;
}

/**
 * Gets the expected note stream of the given track if its name matches the given part name
 * @param {number[]} noteStream The expected expected note stream to be filled
 * @param {ParsedOutputTrackPackage} track The Track object to be accessed
 * @param {string} partName The part (i.e. track) name to be accessed
 * @private
 */
const getNoteStreamTrack = (noteStream, track, partName) => {
  if (track['name'] === partName) {
    getNoteStreamRecursive(noteStream, track, ['staffs','measures','chords', 'pitches'], 0);
  }
}

/**
 * Utility function for recursing on the structure of the ParsedOutputTrackPackage creating the expected note stream
 * @param {number[]} noteStream The expected expected note stream
 * @param {ParsedOutputStaffPackage|ParsedOutputMeasurePackage|ParsedOutputChordPackage|ParsedOutputPitchPackage} currentLevel The current level accessed
 * @param {string[]} levelNames The set of nested levels within the ParsedOutputTrackPackage
 * @param {number} levelIndex The current index into the levelNames
 * @private
 */
const getNoteStreamRecursive = (noteStream, currentLevel, levelNames, levelIndex) => {
  if (levelIndex >= levelNames.length) {
    if (Array.isArray(currentLevel)) {
      currentLevel.forEach((note) => {
        noteStream.push(note['midiValue']);
        noteStream.push(note['duration']);
      });
    } else {
      noteStream.push(currentLevel['midiValue']);
      noteStream.push(currentLevel['duration']);
    }
  } else {
    let nextLevelName = levelNames[levelIndex];
    if (Array.isArray(currentLevel[nextLevelName])) {
      currentLevel[nextLevelName].forEach((nextLevel) => getNoteStreamRecursive(noteStream, nextLevel, levelNames, levelIndex + 1));
    } else {
      getNoteStreamRecursive(noteStream, currentLevel[nextLevelName], levelNames, levelIndex + 1);
    }
  }
}

/**
 * @typedef {object} ExpectedNotePackage
 * @property {number} duration The duration in seconds of the note
 * @property {number} measureNumber The measure number that this note belongs to 
 * @property {number} midiValue The midi value of the note
 */

/**
 * Utility function for addNotesToExpectedPerformance to add the notes in the given Chord to the expected performance
 * @param {ParsedOutputChordPackage} chord The Chord object to be accessed
 * @param {ExpectedNotePackage[]} expectedPerformance A pointer to the expected performance array to be filled
 * @param {number} measureNumber The current measure number
 * @param {boolean} isDurationExercise If true then this is a duration exercise otherwise if false then it's not
 * @private
 */
const addNotesToExpectedPerformanceChords = (chord, expectedPerformance, measureNumber, isDurationExercise) => {
  if (Array.isArray(chord["pitches"])) {
    chord["pitches"].forEach((note) => {
      const newNote = {
        duration: note["duration"],
        measureNumber: measureNumber
      }
      if (isDurationExercise) {
        newNote.midiValue = 60;
      } else {
        newNote.midiValue = note["midiValue"];
      }
      expectedPerformance.push(JSON.parse(JSON.stringify(newNote)));
    });
  } else {
    let note = chord["pitches"];
    const newNote = {
      duration: note["duration"],
      measureNumber: measureNumber
    }
    if (isDurationExercise) {
      newNote.midiValue = 60;
    } else {
      newNote.midiValue = note["midiValue"];
    }
    expectedPerformance.push(JSON.parse(JSON.stringify(newNote)));
  }
}

/**
 * Utility function for getExpectedPerformance to add the notes in the given Measure to the expected performance
 * @param {ParsedOutputMeasurePackage} measure The Measure object to be accessed
 * @param {ExpectedNotePackage[]} expectedPerformance A pointer to the expected performance array to be filled
 * @param {number} measureNumber The current measure number
 * @param {boolean} isDurationExercise If true then this is a duration exercise otherwise if false then it's not
 * @private
 */
const addNotesToExpectedPerformance = (measure, expectedPerformance, measureNumber, isDurationExercise) => {
  if (Array.isArray(measure["chords"])) {
    measure["chords"].forEach((chord) => {
      addNotesToExpectedPerformanceChords(chord, expectedPerformance, measureNumber, isDurationExercise);
    });
  } else {
    addNotesToExpectedPerformanceChords(measure["chords"], expectedPerformance, measureNumber, isDurationExercise);
  }
}

/**
 * @typedef ExpectedPerformanceAndSizePackage
 * @property {ExpectedNotePackage[]} expectedPerformance The expected performance of the Track and Staff within the given bounds of the given type
 * @property {number} measureSize The number of measures included in expected performance
 */

/**
 * Gets the expected performance of within the given bounds of the Staff at the specified index of the Track at the specified index
 * @param {ParsedOutputPackage} pData The parsed data
 * @param {number} trackIndex The index of the Track to be accessed
 * @param {number} staffIndex The index of the Staff to be accessed
 * @param {number[]} measureBounds The lower and upper bounds of the measures to be accessed. If null, then will retrieve all measures
 * @param {boolean} isDurationExercise If true then this is a duration exercise otherwise if false then it's not
 * @returns {ExpectedPerformanceAndSizePackage} The expected performance and the number of measures returned
 */
const getExpectedPerformance = (pData, trackIndex, staffIndex, measureBounds, isDurationExercise) => {
  let expectedPerformance = null;
  let measureSize = 0;
  let track = null;
  if (Array.isArray(pData["tracks"])) {
    for (let i = 0; i < pData["tracks"].length; i++) {
      if (i === trackIndex) {
        track = pData["tracks"][i];
        break;
      }
    }
  } else if (trackIndex === 0) {
    track = pData["tracks"];
  }
  if (track !== null) {
    let staff = null;
    if (Array.isArray(track["staffs"])) {
      for (let i = 0; i < track["staffs"].length; i++) {
        if (i === staffIndex) {
          staff = track["staffs"][i];
          break;
        }
      }
    } else if (staffIndex === 0) {
      staff = track["staffs"];
    }

    if (staff !== null) {
      expectedPerformance = [];
      if (measureBounds !== null) {

      }

      if (Array.isArray(staff["measures"])) {
        for (let i = 0; i < staff["measures"].length; i++) {
          if (measureBounds === null || (measureBounds !== null && i+1 >= measureBounds[0])) {
            if (measureBounds!== null && i+1 > measureBounds[1]) {
              break;
            } else {
              addNotesToExpectedPerformance(staff["measures"][i], expectedPerformance, i+1, isDurationExercise);
            }
          }
        }

        if (measureBounds !== null) {
          measureSize = Math.abs(measureBounds[1] - measureBounds[0] + 1);
        } else {
          measureSize = staff["measures"].length;
        }
      } else {
        if (measureBounds === null || (measureBounds !== null && measureBounds[0] == 1)) {
          addNotesToExpectedPerformance(staff["measures"], expectedPerformance, 1, isDurationExercise);
        }

        if (measureBounds !== null) {
          measureSize = Math.abs(measureBounds[1] - measureBounds[0] + 1);
        } else {
          measureSize = 1;
        }
      }
    }
  }
  return {expectedPerformance, measureSize };
}

const LOWEST_MIDI = 21;
const HIGHEST_MIDI = 108;
const WIGGLE = 5;

/**
 * Gets the lower and upper bounds of the given note stream
 * @param {number[]} noteStream A stream of midi/duration numbers where the ith number is a midi value and the i+1th number is its duration for all i%2==0
 * @returns {number[]} A 1D two valued array with the lower and upper midi values
 */
const getLowerAndUpper = (noteStream) => {
  let lowerAndUpper = [HIGHEST_MIDI, LOWEST_MIDI];
  for (let i = 0; i < noteStream.length; i += 2) {
    if (noteStream[i] < 0) {
      continue;
    }

    if (noteStream[i] > lowerAndUpper[0]) {
      if (noteStream[i] > lowerAndUpper[1]) {
        lowerAndUpper[1] = noteStream[i];
      }
    } else if (noteStream[i] < lowerAndUpper[0]) {
      lowerAndUpper[0] = noteStream[i];
      if (lowerAndUpper[0] > lowerAndUpper[1]) {
        lowerAndUpper[1] = lowerAndUpper[0];
      }
    }
  }
  if (lowerAndUpper[0] === HIGHEST_MIDI && lowerAndUpper[1] === LOWEST_MIDI) {
    lowerAndUpper[0] = LOWEST_MIDI;
    lowerAndUpper[1] = HIGHEST_MIDI;
  } else {
    lowerAndUpper[0] -= WIGGLE;
    lowerAndUpper[1] += WIGGLE;
    if (lowerAndUpper[0] < LOWEST_MIDI) {
      lowerAndUpper[0] = LOWEST_MIDI;
    }
    if (lowerAndUpper[1] > HIGHEST_MIDI) {
      lowerAndUpper[1] = HIGHEST_MIDI;
    }
  }
  return lowerAndUpper;
}

/**
 * Utility function for getClefs which gets the clefs of the given track and adds them to the list of clefs
 * @param {ParsedOutputTrackPackage} track The Track object to be accessed
 * @param {string[]} clefs A pointer to the list of clefs to which to add the clefs of the staffs of the given track
 * @private
 */
const getClefsStaff = (track, clefs) => {
  let staffClef = [];
  if (Array.isArray(track["staffs"])) {
    track["staffs"].forEach((staff) => {
      if (staff["clef"]) {
        staffClef.push(staff["clef"]);
      } else {
        staffClef.push("treble");
      }
    });
  } else {
    let staff = track["staffs"];
    if (staff["clef"]) {
      staffClef.push(staff["clef"]);
    } else {
      staffClef.push("treble");
    }
  }
  clefs.push(staffClef);
}

/**
 * Gets the clefs of each Staff
 * @param {ParsedOutputPackage} pData The parsed data
 * @returns {string[][]|string[]} Clefs per Staff
 */
const getClefs = (pData) => {
  let clefs = [];
  if (Array.isArray(pData["tracks"])) {
    pData["tracks"].forEach((track) => {
      getClefsStaff(track, clefs);
    });
  } else {
    let track = pData["tracks"];
    getClefsStaff(track, clefs);
  }
  return clefs;
}

/**
 * @typedef {object} ProgressDataPackage
 * @property {number[]} progressData A score per measure representing how well they are doing with that measure (scale 0-100)
 * @property {number} trackIndex The index of the track for which the progress data applies
 */

/**
 * Creates an empty progress data array (a collection of scores per measure) and gets the index of the track matching the track name
 * @param {ParsedOutputPackage} pData The parsed data
 * @param {string} trackName The name of the track to be accessed
 * @returns {ProgressDataPackage} A new progress data and the track index for the track matching the requested track name
 */
const createEmptyProgressData = (pData, trackName) => {
  let progressData = null;
  let trackIndex = -1;
  let track = null;
  if (Array.isArray(pData["tracks"])) {
    for (let i = 0; i < pData["tracks"].length; i++) {
      const nextTrack = pData["tracks"][i];
      if (nextTrack.name === trackName) {
        track = nextTrack;
        trackIndex = i;
        break;
      }
    }
  } else {
    const nextTrack = pData["tracks"];
    if (nextTrack.name === trackName) {
      track = nextTrack;
    }
  }
  if (track !== null) {
    progressData = [];
    let staff = null;
    if (Array.isArray(track["staffs"])) {
      staff = track["staffs"][0];
    } else {
      staff = track["staffs"];
    }

    if (Array.isArray(staff["measures"])) {
      staff["measures"].forEach((measure) => {
        progressData.push(50);
      });
    } else {
      progressData.push(50);
    }
  }

  return {progressData, trackIndex};
}


module.exports = {getPartNames : getPartNames, getNoteStream: getNoteStream, getLowerAndUpper: getLowerAndUpper, getClefs: getClefs, createEmptyProgressData: createEmptyProgressData, getExpectedPerformance: getExpectedPerformance};