let spriteData = null;

window.addEventListener('load', () => {
  const gameSettings = document.getElementById('weighted-settings');
  Promise.all([fetchWeightedSettingsYaml(), fetchWeightedSettingsJson(), fetchSpriteData()]).then((results) => {
    // Load YAML into object
    const sourceData = jsyaml.safeLoad(results[0], { json: true });
    const wsVersion = sourceData.ws_version;
    delete sourceData.ws_version; // Do not include the settings version number in the export

    // Check if settings exist in localStorage. If no settings are present, this is a first load (or reset to default)
    // and the version number should be silently updated
    if (!localStorage.getItem('weightedSettings1')) {
      localStorage.setItem('wsVersion', wsVersion);
    }

    // Update localStorage with three settings objects. Preserve original objects if present.
    for (let i=1; i<=3; i++) {
      const localSettings = JSON.parse(localStorage.getItem(`weightedSettings${i}`));
      const updatedObj = localSettings ? Object.assign(sourceData, localSettings) : sourceData;
      localStorage.setItem(`weightedSettings${i}`, JSON.stringify(updatedObj));
    }

    // Build the entire UI
    buildUI(JSON.parse(results[1]), JSON.parse(results[2]));

    // Populate the UI and add event listeners
    populateSettings();
    document.getElementById('preset-number').addEventListener('change', populateSettings);
    gameSettings.addEventListener('change', handleOptionChange);
    gameSettings.addEventListener('keyup', handleOptionChange);

    document.getElementById('export-button').addEventListener('click', exportSettings);
    document.getElementById('reset-to-default').addEventListener('click', resetToDefaults);
    adjustHeaderWidth();

    if (localStorage.getItem('wsVersion') !== wsVersion) {
      const userWarning = document.getElementById('user-warning');
      const messageSpan = document.createElement('span');
      messageSpan.innerHTML = "A new version of the weighted settings file is available. Click here to update!" +
        "<br />Be aware this will also reset your presets, so you should export them now if you want to save them.";
      userWarning.appendChild(messageSpan);
      userWarning.style.display = 'block';
      userWarning.addEventListener('click', resetToDefaults);
    }
  }).catch((error) => {
    console.error(error);
    gameSettings.innerHTML = `
            <h2>Something went wrong while loading your game settings page.</h2>
            <h2>${error}</h2>
            <h2><a href="${window.location.origin}">Click here to return to safety!</a></h2>
            `
  });
  document.getElementById('generate-game').addEventListener('click', () => generateGame());
  document.getElementById('generate-race').addEventListener('click', () => generateGame(true));
});

const fetchWeightedSettingsYaml = () => new Promise((resolve, reject) => {
  const ajax = new XMLHttpRequest();
  ajax.onreadystatechange = () => {
    if (ajax.readyState !== 4) { return; }
    if (ajax.status !== 200) {
      reject("Unable to fetch source yaml file.");
      return;
    }
    resolve(ajax.responseText);
  };
  ajax.open('GET', `${window.location.origin}/static/static/weightedSettings.yaml` ,true);
  ajax.send();
});

const fetchWeightedSettingsJson = () => new Promise((resolve, reject) => {
  const ajax = new XMLHttpRequest();
  ajax.onreadystatechange = () => {
    if (ajax.readyState !== 4) { return; }
    if (ajax.status !== 200) {
      reject('Unable to fetch JSON schema file');
      return;
    }
    resolve(ajax.responseText);
  };
  ajax.open('GET', `${window.location.origin}/static/static/weightedSettings.json`, true);
  ajax.send();
});

const fetchSpriteData = () => new Promise((resolve, reject) => {
  const ajax = new XMLHttpRequest();
  ajax.onreadystatechange = () => {
    if (ajax.readyState !== 4) { return; }
    if (ajax.status !== 200) {
      reject('Unable to fetch sprite data.');
      return;
    }
    resolve(ajax.responseText);
  };
  ajax.open('GET', `${window.location.origin}/static/generated/spriteData.json`, true);
  ajax.send();
});

const handleOptionChange = (event) => {
  if(!event.target.matches('.setting')) { return; }
  const presetNumber = document.getElementById('preset-number').value;
  const settings = JSON.parse(localStorage.getItem(`weightedSettings${presetNumber}`))
  const settingString = event.target.getAttribute('data-setting');
  document.getElementById(settingString).innerText = event.target.value;
  if(getSettingValue(settings, settingString) !== false){
    const keys = settingString.split('.');
    switch (keys.length) {
      case 1:
        settings[keys[0]] = isNaN(event.target.value) ?
          event.target.value : parseInt(event.target.value, 10);
        break;
      case 2:
        settings[keys[0]][keys[1]] = isNaN(event.target.value) ?
          event.target.value : parseInt(event.target.value, 10);
        break;
      case 3:
        settings[keys[0]][keys[1]][keys[2]] = isNaN(event.target.value) ?
          event.target.value : parseInt(event.target.value, 10);
        break;
      default:
        console.warn(`Unknown setting string received: ${settingString}`)
        return;
    }

    // Save the updated settings object bask to localStorage
    localStorage.setItem(`weightedSettings${presetNumber}`, JSON.stringify(settings));
  }else{
    console.warn(`Unknown setting string received: ${settingString}`)
  }
};

const populateSettings = () => {
  buildSpriteOptions();
  const presetNumber = document.getElementById('preset-number').value;
  const settings = JSON.parse(localStorage.getItem(`weightedSettings${presetNumber}`))
  const settingsInputs = Array.from(document.querySelectorAll('.setting'));
  settingsInputs.forEach((input) => {
    const settingString = input.getAttribute('data-setting');
    const settingValue = getSettingValue(settings, settingString);
    if(settingValue !== false){
      input.value = settingValue;
      document.getElementById(settingString).innerText = settingValue;
    }
  });
};

/**
 * Returns the value of the settings object, or false if the settings object does not exist
 * @param settings
 * @param keyString
 * @returns {string} | bool
 */
const getSettingValue = (settings, keyString) => {
  const keys = keyString.split('.');
  let currentVal = settings;
  keys.forEach((key) => {
    if(typeof(key) === 'string' && currentVal.hasOwnProperty(key)){
      currentVal = currentVal[key];
    }else{
      currentVal = false;
    }
  });
  return currentVal;
};

const exportSettings = () => {
  const presetNumber = document.getElementById('preset-number').value;
  const settings = JSON.parse(localStorage.getItem(`weightedSettings${presetNumber}`));
  const yamlText = jsyaml.safeDump(settings, { noCompatMode: true }).replaceAll(/'(\d+)':/g, (x, y) => `${y}:`);
  download(`${settings.description}.yaml`, yamlText);
};

const resetToDefaults = () => {
  [1, 2, 3].forEach((presetNumber) => localStorage.removeItem(`weightedSettings${presetNumber}`));
  location.reload();
};

/** Create an anchor and trigger a download of a text file. */
const download = (filename, text) => {
  const downloadLink = document.createElement('a');
  downloadLink.setAttribute('href','data:text/yaml;charset=utf-8,'+ encodeURIComponent(text))
  downloadLink.setAttribute('download', filename);
  downloadLink.style.display = 'none';
  document.body.appendChild(downloadLink);
  downloadLink.click();
  document.body.removeChild(downloadLink);
};

const buildUI = (settings, spriteData) => {
  const settingsWrapper = document.getElementById('settings-wrapper');
  const settingTypes = {
    gameOptions: 'Game Options',
    romOptions: 'ROM Options',
  }

  Object.keys(settingTypes).forEach((settingTypeKey) => {
    const sectionHeader = document.createElement('h2');
    sectionHeader.innerText = settingTypes[settingTypeKey];
    settingsWrapper.appendChild(sectionHeader);

    Object.values(settings[settingTypeKey]).forEach((setting) => {
      if (typeof(setting.inputType) === 'undefined' || !setting.inputType){
        console.error(setting);
        throw new Error('Setting with no inputType specified.');
      }

      switch(setting.inputType){
        case 'text':
          // Currently, all text input is handled manually because there is very little of it
          return;
        case 'range':
          buildRangeSettings(settingsWrapper, setting);
          return;
        default:
          console.error(setting);
          throw new Error('Unhandled inputType specified.');
      }
    });
  });

  // Build sprite options
  const spriteOptionsHeader = document.createElement('h2');
  spriteOptionsHeader.innerText = 'Sprite Options';
  settingsWrapper.appendChild(spriteOptionsHeader);

  const spriteOptionsWrapper = document.createElement('div');
  spriteOptionsWrapper.setAttribute('id', 'sprite-options-wrapper');
  spriteOptionsWrapper.className = 'setting-wrapper';
  settingsWrapper.appendChild(spriteOptionsWrapper);

  // Append sprite picker
  settingsWrapper.appendChild(buildSpritePicker(spriteData));
};

const buildSpriteOptions = () => {
  const spriteOptionsWrapper = document.getElementById('sprite-options-wrapper');

  // Clear the contents of the wrapper div
  while(spriteOptionsWrapper.firstChild){
    spriteOptionsWrapper.removeChild(spriteOptionsWrapper.lastChild);
  }

  const spriteOptionsTitle = document.createElement('span');
  spriteOptionsTitle.className = 'title-span';
  spriteOptionsTitle.innerText = 'Alternate Sprites';
  spriteOptionsWrapper.appendChild(spriteOptionsTitle);

  const spriteOptionsDescription = document.createElement('span');
  spriteOptionsDescription.className = 'description-span';
  spriteOptionsDescription.innerHTML = 'Choose an alternate sprite to play the game with. Additional randomization ' +
    'options are documented in the ' +
    '<a href="https://github.com/Berserker66/MultiWorld-Utilities/blob/main/playerSettings.yaml#L374">settings file</a>.';
  spriteOptionsWrapper.appendChild(spriteOptionsDescription);

  const spriteOptionsTable = document.createElement('table');
  spriteOptionsTable.setAttribute('id', 'sprite-options-table');
  spriteOptionsTable.className = 'option-set';
  const tbody = document.createElement('tbody');
  tbody.setAttribute('id', 'sprites-tbody');

  const currentPreset = document.getElementById('preset-number').value;
  const playerSettings = JSON.parse(localStorage.getItem(`weightedSettings${currentPreset}`));

  // Manually add a row for random sprites
  addSpriteRow(tbody, playerSettings, 'random');

  // Add a row for each sprite currently present in the player's settings
  Object.keys(playerSettings.rom.sprite).forEach((spriteName) => {
    if(['random'].indexOf(spriteName) > -1) return;
    addSpriteRow(tbody, playerSettings, spriteName)
  });

  spriteOptionsTable.appendChild(tbody);
  spriteOptionsWrapper.appendChild(spriteOptionsTable);
};

const buildRangeSettings = (parentElement, settings) => {
  // Ensure we are operating on a range-specific setting
  if(typeof(settings.inputType) === 'undefined' || settings.inputType !== 'range'){
    throw new Error('Invalid input type provided to buildRangeSettings func.');
  }

  const settingWrapper = document.createElement('div');
  settingWrapper.className = 'setting-wrapper';

  if(typeof(settings.friendlyName) !== 'undefined' && settings.friendlyName){
    const sectionTitle = document.createElement('span');
    sectionTitle.className = 'title-span';
    sectionTitle.innerText = settings.friendlyName;
    settingWrapper.appendChild(sectionTitle);
  }

  if(settings.description){
    const description = document.createElement('span');
    description.className = 'description-span';
    description.innerText = settings.description;
    settingWrapper.appendChild(description);
  }

  // Create table
  const optionSetTable = document.createElement('table');
  optionSetTable.className = 'option-set';

  // Create table body
  const tbody = document.createElement('tbody');
  Object.keys(settings.subOptions).forEach((setting) => {
    // Overwrite setting key name with real object
    setting = settings.subOptions[setting];
    const settingId = (Math.random() * 1000000).toString();

    // Create rows for each option
    const optionRow = document.createElement('tr');

    // Option name td
    const optionName = document.createElement('td');
    optionName.className = 'option-name';
    const optionLabel = document.createElement('label');
    optionLabel.setAttribute('for', settingId);
    optionLabel.setAttribute('data-tooltip', setting.description);
    optionLabel.innerText = setting.friendlyName;
    optionName.appendChild(optionLabel);
    optionRow.appendChild(optionName);

    // Option value td
    const optionValue = document.createElement('td');
    optionValue.className = 'option-value';
    const input = document.createElement('input');
    input.className = 'setting';
    input.setAttribute('id', settingId);
    input.setAttribute('type', 'range');
    input.setAttribute('min', '0');
    input.setAttribute('max', '100');
    input.setAttribute('data-setting', setting.keyString);
    input.value = setting.defaultValue;
    optionValue.appendChild(input);
    const valueDisplay = document.createElement('span');
    valueDisplay.setAttribute('id', setting.keyString);
    valueDisplay.innerText = setting.defaultValue;
    optionValue.appendChild(valueDisplay);
    optionRow.appendChild(optionValue);
    tbody.appendChild(optionRow);
  });

  optionSetTable.appendChild(tbody);
  settingWrapper.appendChild(optionSetTable);
  parentElement.appendChild(settingWrapper);
};

const addSpriteRow = (tbody, playerSettings, spriteName) => {
  const rowId = (Math.random() * 1000000).toString();
  const optionId = (Math.random() * 1000000).toString();
  const tr = document.createElement('tr');
  tr.setAttribute('id', rowId);

  // Option Name
  const optionName = document.createElement('td');
  optionName.className = 'option-name';
  const label = document.createElement('label');
  label.htmlFor = optionId;
  label.innerText = spriteName;
  optionName.appendChild(label);

  if(['random', 'random_sprite_on_event'].indexOf(spriteName) === -1) {
    const deleteButton = document.createElement('span');
    deleteButton.setAttribute('data-sprite', spriteName);
    deleteButton.setAttribute('data-row-id', rowId);
    deleteButton.innerText = ' (❌)';
    deleteButton.className = 'delete-button';
    optionName.appendChild(deleteButton);
    deleteButton.addEventListener('click', removeSpriteOption);
  }

  tr.appendChild(optionName);

  // Option Value
  const optionValue = document.createElement('td');
  optionValue.className = 'option-value';
  const input = document.createElement('input');
  input.className = 'setting';
  input.setAttribute('id', optionId);
  input.setAttribute('type', 'range');
  input.setAttribute('min', '0');
  input.setAttribute('max', '100');
  input.setAttribute('data-setting', `rom.sprite.${spriteName}`);
  input.value = "50";
  optionValue.appendChild(input);

  // Value display
  const valueDisplay = document.createElement('span');
  valueDisplay.setAttribute('id', `rom.sprite.${spriteName}`);
  valueDisplay.innerText = playerSettings.rom.sprite.hasOwnProperty(spriteName) ?
    playerSettings.rom.sprite[spriteName] : '0';
  optionValue.appendChild(valueDisplay);

  tr.appendChild(optionValue);
  tbody.appendChild(tr);
};

const addSpriteOption = (event) => {
  const presetNumber = document.getElementById('preset-number').value;
  const playerSettings = JSON.parse(localStorage.getItem(`weightedSettings${presetNumber}`));
  const spriteName = event.target.getAttribute('data-sprite');

  if (Object.keys(playerSettings.rom.sprite).indexOf(spriteName) !== -1) {
    // Do not add the same sprite twice
    return;
  }

  // Add option to playerSettings object
  playerSettings.rom.sprite[event.target.getAttribute('data-sprite')] = 50;
  localStorage.setItem(`weightedSettings${presetNumber}`, JSON.stringify(playerSettings));

  // Add <tr> to #sprite-options-table
  const tbody = document.getElementById('sprites-tbody');
  addSpriteRow(tbody, playerSettings, spriteName);
};

const removeSpriteOption = (event) => {
  const presetNumber = document.getElementById('preset-number').value;
  const playerSettings = JSON.parse(localStorage.getItem(`weightedSettings${presetNumber}`));
  const spriteName = event.target.getAttribute('data-sprite');

  // Remove option from playerSettings object
  delete playerSettings.rom.sprite[spriteName];
  localStorage.setItem(`weightedSettings${presetNumber}`, JSON.stringify(playerSettings));

  // Remove <tr> from #sprite-options-table
  const tr = document.getElementById(event.target.getAttribute('data-row-id'));
  tr.parentNode.removeChild(tr);
};

const buildSpritePicker = (spriteData) => {
  const spritePicker = document.createElement('div');
  spritePicker.setAttribute('id', 'sprite-picker');

  // Build description
  const description = document.createElement('span');
  description.innerText = 'To add a sprite to your playable list, click the one you want below.';
  spritePicker.appendChild(description);

  const sprites = document.createElement('div');
  sprites.setAttribute('id', 'sprite-picker-sprites');
  spriteData.sprites.forEach((sprite) => {
    const spriteImg = document.createElement('img');
    let spriteGifFile = sprite.file.split('.');
    spriteGifFile.pop();
    spriteGifFile = spriteGifFile.join('.') + '.gif';
    spriteImg.setAttribute('src', `static/generated/sprites/${spriteGifFile}`);
    spriteImg.setAttribute('data-sprite', sprite.file.split('.')[0]);
    spriteImg.setAttribute('alt', sprite.name);

    // Wrap the image in a span to allow for tooltip presence
    const imgWrapper = document.createElement('span');
    imgWrapper.className = 'sprite-img-wrapper';
    imgWrapper.setAttribute('data-tooltip', `${sprite.name}${sprite.author ? `, by ${sprite.author}` : ''}`);
    imgWrapper.appendChild(spriteImg);
    imgWrapper.setAttribute('data-sprite', sprite.name);
    sprites.appendChild(imgWrapper);
    imgWrapper.addEventListener('click', addSpriteOption);
  });

  spritePicker.appendChild(sprites);
  return spritePicker;
};

const generateGame = (raceMode = false) => {
  const presetNumber = document.getElementById('preset-number').value;
  axios.post('/api/generate', {
    weights: { player: localStorage.getItem(`weightedSettings${presetNumber}`) },
    presetData: { player: localStorage.getItem(`weightedSettings${presetNumber}`) },
    playerCount: 1,
    race: raceMode ? '1' : '0',
  }).then((response) => {
    window.location.href = response.data.url;
  }).catch((error) => {
    const userMessage = document.getElementById('user-message');
    userMessage.innerText = 'Something went wrong and your game could not be generated.';
    if (error.response.data.text) {
      userMessage.innerText += ' ' + error.response.data.text;
    }
    userMessage.classList.add('visible');
    window.scrollTo(0, 0);
    console.error(error);
  });
};