// Constants
const lang = navigator.language.substring(0,2);

const lowestNotePosition = 13;
const notePosition0Midi = 62; // midi number of the note at position 0

const minLastFret = 4;
const maxLastFret = 12;

const msgTimeout = 1000; // milliseconds
const noteTimeout = 1000;
const gameDuration = 300; // seconds (5 minutes)

const triesForLevelUpdate = 5; // number of tries till level may change
const levelBound = 0.9; // performace ratio bound for level change
// hits/tries min ratio for level assessment
const minProficiencyVeryGood = 0.95; 
const minProficiencyGood = 0.85;
const minProficiencyNormal = 0.70;
// min tries per minute (gameDuration / 60) for level assessment 
const minTriesVeryGood = (gameDuration / 60) * 19; 
const minTriesGood = (gameDuration / 60) * 17;
const minTriesNormal = (gameDuration / 60) * 16;


const diatonicTones = 7;
const octaveSemitones = 12;

const awardEmote = "&#x1F425";

// Info about guitar frets containing the highest staff positions 
// (higher are smaller) and the valid accidentals for the highest
// note in a fret.
// No accidental (0) is repeated for weighting probabilites. In case
// the highest possible note is Cb or Fb, the preceding position
// (for B or E, respectively) is chosen to avoid B# or E# out of bounds
// notes. So without complicating the implementation for these cases.
const defaultAccidentalPool = [1, -1].concat(Array(8).fill(0));
const lowestAccidentalPool = [1].concat(Array(9).fill(0));

// lastFret0 to lastFret3 (included) given for completness but unused
// in the current implementation
const lastFret0 = {
  highestNotePosition: -1, //E (Fb discarded)
  highestAccidentalPool: [-1].concat(Array(9).fill(0)) //Eb, E
}

const lastFret1 = {
  highestNotePosition: -2, //F
  highestAccidentalPool: [-1].concat(Array(9).fill(0)) //Fb, F
}

const lastFret2 = {
  highestNotePosition: -3, //Gb
  highestAccidentalPool: [-1] //Gb
}

const lastFret3 = {
  highestNotePosition: -3, //G
  highestAccidentalPool: [-1].concat(Array(9).fill(0)) //Gb, G
}

const lastFret4 = {
  highestNotePosition: -4, //Ab
  highestAccidentalPool: [-1] //Ab
};

const lastFret5 = {
  highestNotePosition: -4, //A
  highestAccidentalPool: [-1].concat(Array(9).fill(0)) //A, Ab
};

const lastFret6 = {
  highestNotePosition: -5, //Bb
  highestAccidentalPool: [-1] //Bb
};

const lastFret7 = {
  highestNotePosition: -5, //B (Cb discarded)
  highestAccidentalPool: [-1].concat(Array(9).fill(0)) //Bb, B
};

const lastFret8 = {
  highestNotePosition: -6, //C
  highestAccidentalPool: [-1].concat(Array(9).fill(0)) //Cb, C
};

const lastFret9 = {
  highestNotePosition: -7, //Db
  highestAccidentalPool: [-1] //Db
};

const lastFret10 = {
  highestNotePosition: -7, //D
  highestAccidentalPool: [-1].concat(Array(9).fill(0)) //Db, D
};

const lastFret11 = {
  highestNotePosition: -8, //Eb
  highestAccidentalPool: [-1] //Eb
};

const lastFret12 = {
  highestNotePosition: -8, //E (Fb discarded)
  highestAccidentalPool: [-1].concat(Array(9).fill(0)) //Eb, E
};

const lastFrets = [
  lastFret0, lastFret1, lastFret2, lastFret3, 
  lastFret4, lastFret5, lastFret6, lastFret7, 
  lastFret8, lastFret9, lastFret10, lastFret11, lastFret12
];

const hitMsgI18n = {
  "en": "Great!",
  "es": "Genial"
}

const failedMsgI18n = {
  "en": "Oops!",
  "es": "uy"
}

const moreMsgI18n = {
  "en": "Find one more fret...",
  "es": "Busca en otro traste..."
}

const hitMsg = hitMsgI18n[lang];
const failedMsg = failedMsgI18n[lang];
const moreMsg = moreMsgI18n[lang];

const settingsTitleI18n = {
  "en": "Choose your level",
  "es": "Elige tu nivel"
}

const settingsLabelBeginnerI18n = {
  "en": "Beginner",
  "es": "1EE - 2EE"
}

const settingsLabelIntermediateI18n = {
  "en": "Intermediate",
  "es": "3EE - 4EE"
}

const settingsLabelAdvancedI18n = {
  "en": "Advanced",
  "es": "1EP - 6EP"
}

const settingsTitle = settingsTitleI18n[lang];
const settingsLabelBeginner = settingsLabelBeginnerI18n[lang];
const settingsLabelIntermediate = settingsLabelIntermediateI18n[lang];
const settingsLabelAdvanced = settingsLabelAdvancedI18n[lang];

const resultTitleI18n = {
  "en": "Mark",
  "es": "Marcador"
}

const resultTriesLabelI18n = {
  "en": "Tries",
  "es": "Intentos"
}

const resultHitsLabelI18n = {
  "en": "Hits",
  "es": "Aciertos"
}

const resultFailuresLabelI18n = {
  "en": "Failures",
  "es": "Fallos"
}

const resultAssessmentVeryGoodI18n = {
  "en": "You rock!",
  "es": "Dominas la materia"
}

const resultAssessmentGoodI18n = {
  "en": "Good job!",
  "es": "Buen trabajo"
}

const resultAssessmentNormalI18n = {
  "en": "Fine, keep practising",
  "es": "Bien, sigue practicando"
}

const resultAssessmentBadI18n = {
  "en": "You need more practice",
  "es": "Necesitas practicar más"
}

const resultTitle = resultTitleI18n[lang];
const triesLabel = resultTriesLabelI18n[lang];
const hitsLabel = resultHitsLabelI18n[lang];
const failuresLabel = resultFailuresLabelI18n[lang];
const assessmentVeryGood = resultAssessmentVeryGoodI18n[lang];
const assessmentGood = resultAssessmentVeryGoodI18n[lang];
const assessmentNormal = resultAssessmentNormalI18n[lang];
const assessmentBad = resultAssessmentBadI18n[lang];


// -----------------------------------------------------------
// Globals w/ initial values
var intervalID; //for internal use of timer functions
var startTime = 0; 
var elapsedTime = 0; 
var initialLastFret = 4;
var lastFret = 0;
var currentNotePosition = 0;
var currentAccidental = 0;
var currentLegerPositions = [];
var tries = 0;
var hits = 0;
var score = 0;

// -----------------------------------------------------------
// Utils: Element Getters
function getFrets() {
  return document.getElementsByClassName("fret");
}

function getAvailableFrets() {
  var availFrets = []
  for (let fret of getFrets()) {
    if (fret.className.includes("available")) {
      availFrets.push(fret);
    }
  }
  return availFrets;
}

function getFretSymbols() {
  return document.getElementsByClassName("fretsymbol");
}

function getLegers() {
  return document.getElementsByClassName("leger");
}

function getLegerPosition(leger) {
  return parseInt(leger.getAttribute("data-sp"));
}

function getNoteAccidental() {
  return document.getElementById("accidental");
}

function getFretNum(elt) {
  return elt.id.substring(2,);
}

function getFretString(fret) {
  // for debugging
  return fret.id.substring(1,2);
}

function getMidi(fret) {
  return parseInt(fret.getAttribute("data-midi"));
}

function getMessage() {
  return document.getElementById("message");
}

function getMinuteHand() {
  return document.getElementById("minutes");
}

function getSecondHand() {
  return document.getElementById("seconds");
}

function getProgressBar() {
  return document.getElementById("progress");
}

function getLevelBox() {
  return document.getElementById("level");
}

function getGame() {
  return document.getElementById("game");
}

function getLevelSettings() {
  return document.getElementById("level-settings");
}

function getLevelButtons() {
  return document.getElementsByClassName("level-button");
}

function getLevelButtonId(button) {
  return button.id;
}

function getSettingsTitle() {
  return document.getElementById("level-title");
}

function getSettingsBeginnerLabel() {
  return document.getElementById("beginner");
}

function getSettingsIntermediateLabel() {
  return document.getElementById("intermediate");
}

function getSettingsAdvancedLabel() {
  return document.getElementById("advanced");
}

function getResult() {
  return document.getElementById("result");
}

function getResultTitle() {
  return document.getElementById("result-title");
}

function getResultTriesLabel() {
  return document.getElementById("tries-label");
}

function getResultTries() {
  return document.getElementById("tries-result");
}

function getResultHitsLabel() {
  return document.getElementById("hits-label");
}

function getResultHits() {
  return document.getElementById("hits-result");
}

function getResultFailuresLabel() {
  return document.getElementById("failures-label");
}

function getResultFailures() {
  return document.getElementById("failures-result");
}

function getResultAssessment() {
  return document.getElementById("assessment");
}

// -----------------------------------------------------------
// Utils: Element Updaters
function markAsAvailable(elt) {
  elt.classList.add("available");
}

function markAsUnavailable(elt) {
  elt.classList.remove("available");
}

function markAsFound(fret) {
  fret.classList.add("found");
}

function markAsUnfound(fret) {
  fret.classList.remove("found");
}

function markAsFailed(fret) {
  fret.classList.add("failed");
}

function markAsUnchecked(fret) {
  fret.classList.remove("failed");
}

function showLeger(leger) {
  leger.classList.add("visible");
}

function hideLeger(leger) {
  leger.classList.remove("visible");
}

function markAsFired(msg) {
  msg.classList.add("fired");
}

function markAsMute(msg) {
  msg.classList.remove("fired");
}

function markAsSharp(noteAccidental) {
  noteAccidental.className = "sharp";
}

function markAsFlat(noteAccidental) {
  noteAccidental.className = "flat";
}

function markAsNatural(noteAccidental) {
  noteAccidental.className = "no-accidental";
}

function setNotePosition() {
  document.documentElement.style.setProperty(
    "--current-staff-position",
    currentNotePosition
  ) 
}

function setMinuteHand(v) {
  const minuteHand = getMinuteHand();
  minuteHand.innerHTML = v;
}

function setSecondHand(v) {
  const secondHand = getSecondHand();
  secondHand.innerHTML = v;
}

function setProgressBar(v) {
  const progressBar = getProgressBar();
  progressBar.value = v;
}

function setLevelBox(v) {
  const levelBox = getLevelBox();
  levelBox.innerHTML = v;
}

function showGame() {
  const game = getGame();
  game.style.opacity = 1;
}

function hideGame() {
  const game = getGame();
  game.style.opacity = 0.1;
}

function hideSettings() {
  const settings = getLevelSettings();
  settings.style.display = "none";
}

function showResult() {
  const result = getResult();
  result.style.display = "block";
  result.style.border = "1px solid white";
}

function setSettingsTitle() {
  const title = getSettingsTitle();
  title.innerHTML = settingsTitle;
}

function setSettingsBeginnerLabel() {
  const beginner = getSettingsBeginnerLabel();
  beginner.innerHTML = settingsLabelBeginner;
}

function setSettingsIntermediateLabel() {
  const intermediate = getSettingsIntermediateLabel();
  intermediate.innerHTML = settingsLabelIntermediate;
}

function setSettingsAdvancedLabel() {
  const advanced = getSettingsAdvancedLabel();
  advanced.innerHTML = settingsLabelAdvanced;
}

function setResultTitle() {
  const title = getResultTitle();
  title.innerHTML = resultTitle;
}

function setResultTriesLabel() {
  const resultTriesLabel = getResultTriesLabel();
  resultTriesLabel.innerHTML = triesLabel;
}

function setResultHitsLabel() {
  const resultHitsLabel = getResultHitsLabel();
  resultHitsLabel.innerHTML = hitsLabel;
}

function setResultFailuresLabel() {
  const resultFailuresLabel = getResultFailuresLabel();
  resultFailuresLabel.innerHTML = failuresLabel;
}

function setResultTries() {
  const resultTries = getResultTries();
  resultTries.innerHTML = tries;
}

function setResultHits() {
  const resultHits = getResultHits();
  resultHits.innerHTML = hits;
}

function setResultFailures() {
  const resultFailures = getResultFailures();
  resultFailures.innerHTML = tries - hits;
}

function setResultAssessment(msg) {
  const resultAssessment = getResultAssessment();
  resultAssessment.innerHTML = msg;
}

// -----------------------------------------------------------
// Events 
function activate(fret) {
  fret.addEventListener("click", handleGuess);
}

function deactivate(fret) {
  fret.removeEventListener("click", handleGuess);
}

function enableLevelButton(button) {
  button.addEventListener("click", start)
}

// =============================================================
function init() {
  hideGame();
  fillLevelSettings();
  enableLevelButtons();
}

// -------------------------------------------------------------
function fillLevelSettings() {
  setSettingsTitle();
  setSettingsBeginnerLabel();
  setSettingsIntermediateLabel();
  setSettingsAdvancedLabel();
}

// -------------------------------------------------------------
function enableLevelButtons() {
  const levelButtons = getLevelButtons();
  for (button of levelButtons) {
    enableLevelButton(button);
  }
}

// -------------------------------------------------------------
function start() {
  setInitialLevel(this);
  hideSettings();
  showGame();
  initTimer();
  nextNote();
}

// -------------------------------------------------------------
function setInitialLevel(button) {
  const buttonId = getLevelButtonId(button);
  if (buttonId === "beginner") {
    lastFret = 4;
  }
  if (buttonId === "intermediate") {
    lastFret = 7;
  }
  if (buttonId === "advanced") {
    lastFret = 12;
  }
  initialLastFret = lastFret;
}

// -------------------------------------------------------------
function initTimer() {
  startTime = Date.now();
  intervalID = window.setInterval(showTime, 1000);
}

// =============================================================
function nextNote() {
  updateLevel();
  updateGuitar();
  updateNote();
  showLevel();
  showNote(); 
}

// ------------------------------------------------------------
function updateLevel() {
  if (tries < triesForLevelUpdate) {
    // do nothing 
  }
  if (tries % triesForLevelUpdate == 0 && 
      hits / tries > levelBound) {
    lastFret = Math.min(lastFret + 1, maxLastFret);
  }
  if (tries % triesForLevelUpdate == 0 && 
      hits / tries < levelBound) {
    lastFret = Math.max(lastFret - 1, minLastFret);
  }
}

// ------------------------------------------------------------
function updateGuitar() {
  clearFrets();
  updateFrets();
  updateFretSymbols();
}

// ------------------------------------------------------------
function clearFrets() {
  for (let fret of getFrets()) {
    markAsUnfound(fret); 
    markAsUnchecked(fret);
  }
}

// ------------------------------------------------------------
function updateFrets() {
  const frets = getFrets();
  for (let fret of frets) {
    if (getFretNum(fret) <= lastFret) {
      markAsAvailable(fret);
      activate(fret);
    } else {
      markAsUnavailable(fret);
      deactivate(fret);
    }
  }
}

// ------------------------------------------------------------
function updateFretSymbols() {
  const fretSymbols = getFretSymbols();
  for (let fretsymbol of fretSymbols) {
    if (getFretNum(fretsymbol) <= lastFret) {
      markAsAvailable(fretsymbol);
    } else {
      markAsUnavailable(fretsymbol);
    }
  }
}

// -------------------------------------------------------------
function updateNote() {
  updatePitch();
  updateLegers();
}

// ------------------------------------------------------------
function updatePitch() {
  const highestNotePosition = lastFrets[lastFret].highestNotePosition;
  const randomNotePosition = chooseNotePosition(highestNotePosition);
  const randomAccidental = chooseAccidental(randomNotePosition, highestNotePosition);
  currentNotePosition = randomNotePosition;
  currentAccidental = randomAccidental;
}

// ------------------------------------------------------------
function chooseNotePosition(highestNotePosition) {
  var candidate = genRandomNotePosition(lowestNotePosition + 1, highestNotePosition); 
  if (candidate == currentNotePosition) {
    return chooseNotePosition(highestNotePosition); 
  } else {
    return candidate;
  }
}

// ------------------------------------------------------------
function genRandomNotePosition(min, max) {
  return Math.floor(Math.random() * (max - min) + min);
}

// ------------------------------------------------------------
function chooseAccidental(notePosition, highestNotePosition) {
  var pool = [];
  if (notePosition == highestNotePosition) {
    pool = lastFrets[lastFret].highestAccidentalPool;
  } else if (notePosition == lowestNotePosition) {
    pool = lowestAccidentalPool;
  } else {
    pool = defaultAccidentalPool;
  }
  const randomIndex = Math.floor(Math.random() * pool.length)
  return pool[randomIndex];
}

// ------------------------------------------------------------
function updateLegers() {
  currentLegerPositions = legerPositions(currentNotePosition);
} 

// ------------------------------------------------------------
function legerPositions(notePosition) {
  if (notePosition == -12) {
    return [-12, -10, -8, -6, -4];
  } else if (notePosition == -11 || notePosition == -10) {
    return [-10, -8, -6, -4];
  } else if (notePosition == -9 || notePosition == -8) {
    return [-8, -6, -4];
  } else if (notePosition == -7 || notePosition == -6) {
    return [-6, -4];
  } else if (notePosition == -5 || notePosition == -4) {
    return [-4];
  } else if (notePosition == 13 || notePosition == 12) {
    return [12, 10, 8];
  } else if (notePosition == 11 || notePosition == 10) {
    return [10, 8];
  } else if (notePosition == 9 || notePosition == 8) {
    return [8];
  } else {
    return [];
  }
}

// -------------------------------------------------------------
function showLevel() {
  setLevelBox(awardEmote.repeat(lastFret));
}

// -------------------------------------------------------------
function showNote() {
  showNotehead();
  showAccidental();
  showLegers();
}

// -------------------------------------------------------------
function showNotehead() {
  setNotePosition();
}

// -------------------------------------------------------------
function showAccidental() {
  const noteAccidental = getNoteAccidental();
  if (currentAccidental == 1) {
    markAsSharp(noteAccidental);
  } else if (currentAccidental == -1) {
    markAsFlat(noteAccidental);
  } else {
    markAsNatural(noteAccidental);
  }
}

// -------------------------------------------------------------
function showLegers() {
  const legers = getLegers();
  for (let leger of legers) {
    const legerPosition = getLegerPosition(leger);
    if (currentLegerPositions.includes(legerPosition)) {
      showLeger(leger);
    } else {
      hideLeger(leger);
    }
  }
}

// ============================================================
function handleGuess() {
  checkGuess(this);
  showPower();
}

// ------------------------------------------------------------
function checkGuess(fret) {
  const fretMidi = getMidi(fret);
  const noteMidi = toMidi(currentNotePosition, currentAccidental);
  if (fretMidi == noteMidi) {
    markAsFound(fret);
    if (stillMoreFrets(noteMidi)) {
      updateScore("hit");
      showMessage(moreMsg);
      deactivate(fret);
    } else {
      updateScore("hitAll");
      showMessage(hitMsg);
      window.setTimeout(nextNote, noteTimeout);
    }
  } else { 
    markAsFailed(fret);
    updateScore("failed");
    showMessage(failedMsg);
  }
} 

// ------------------------------------------------------------
function toMidi(notePosition, accidental) {
  const midi = notePositionToMidi(notePosition) + accidental;
  return midi;
}

// ------------------------------------------------------------
function notePositionToMidi(notePosition) {
  return notePosition0Midi - Math.sign(notePosition) * midiDistance(Math.abs(notePosition));
}

// ------------------------------------------------------------
function midiDistance(notePosition) {
  //absolute distance to position 0 
  const baseMidi = notePosition % diatonicTones;
  const octave = Math.floor(notePosition / diatonicTones);
  return baseMidiDistance(baseMidi) + octaveSemitones * octave;  
}

// ------------------------------------------------------------
function baseMidiDistance(notePosition) {
  // midi distance of pitch classes
  if (notePosition == 1) {
    return 2;
  } else if (notePosition == 2) {
    return 3;
  } else if (notePosition == 3) {
    return 5;
  } else if (notePosition == 4) {
    return 7;
  } else if (notePosition == 5) {
    return 9;
  } else if (notePosition == 6) {
    return 10;
  } else {
    return 0;
  }
}

// ------------------------------------------------------------
function stillMoreFrets(midi) {
  for (let fret of getAvailableFrets()) {
    if (getMidi(fret) == midi && !isFound(fret)) {
      return true;
    }
  }
  return false;
}

// ------------------------------------------------------------
function isFound(fret) {
  return fret.className.includes("found");
}

// -------------------------------------------------------------
function updateScore(achievement) {
  var success = hits / tries;
  tries += 1;
  if (achievement === "hit") {
    hits += 1;
  } else if (achievement === "hitAll") {
    hits += 1;
    score += 1;
  } else {
    score -= 1;
  }
}

// ------------------------------------------------------------
function showMessage(str) {
  var msg = getMessage(); 
  markAsFired(msg); 
  msg.innerHTML = str;
  window.setTimeout(clearMessage, msgTimeout);
}

// -------------------------------------------------------------
function clearMessage() {
  const msg = getMessage();
  msg.innerHTML = "";
  markAsMute(msg);
}

// -------------------------------------------------------------
function showPower() {
  var currentPower;
  if (tries == 0) {
    currentPower = 0;
  } else {
    currentPower = Math.floor((hits / tries) * 100);
  }
  setProgressBar(currentPower);
}

// -------------------------------------------------------------
function showTime() {
  const time = new Date(Date.now() - startTime);
  const minutes = time.getMinutes();
  const seconds = time.getSeconds();
  const secondsElapsed = minutes * 60 + seconds;
  const minuteStr = minutes.toString().padStart(2, "0");
  const secondStr = seconds.toString().padStart(2, "0");
  setMinuteHand(minuteStr);
  setSecondHand(secondStr);
  endWhenTimeOver(secondsElapsed); 
}

// -------------------------------------------------------------
function endWhenTimeOver(elapsedTime) {
  if (elapsedTime >= gameDuration) {
    fillResult();
    showResult();
    hideGame();
    stopTimer();
  }
}

// -------------------------------------------------------------
function fillResult() {
  setResultTitle();
  setResultTriesLabel(); 
  setResultHitsLabel(); 
  setResultFailuresLabel(); 
  setResultTries();
  setResultHits();
  setResultFailures();
  setResultAssessment(assessResult());
}

// -------------------------------------------------------------
function stopTimer() {
  window.clearInterval(intervalID);
}

// -------------------------------------------------------------
function assessResult() {
  const hasWorseLevel = lastFret < initialLastFret;
  const proficiency = hits / tries;
  if (hasWorseLevel) {
    return assessmentBad;
  } else if (proficiency >= minProficiencyVeryGood && 
             tries >= minTriesVeryGood) {
    return assessmentVeryGood;
  } else if (proficiency > minProficiencyGood && 
             tries >= minTriesGood) {
    return assessmentGood;
  } else if (proficiency > minProficiencyNormal && 
             tries >= minTriesNormal) {
    return assessmentNormal;
  } else {
    return assessmentBad;
  }
}

// ============================================================= 
// Running ...
init();

