const selectors = {
  get colorSelect() {
    return document.forms.colorSelect.elements;
  },
  set colorText(hex) {
    selectors.colorSelect.colorText.value = hex;
  },
  set colorPicker(hex) {
    selectors.colorSelect.colorPicker.value = hex;
  },

  get sortControl() {
    return document.forms.sortControl;
  },
  get resultsToDisplay() {
    return selectors.sortControl.elements.resultsToDisplay.value;
  },
  get colorSpace() {
    return selectors.sortControl.elements.colorSpace.value;
  },

  get prevColors() {
    return document.getElementById("prev-colors");
  },
  get pokemonTemplate() {
    return document.getElementById("pkmn-template").content;
  },
  get pokemonDataTemplate() {
    return document.getElementById("pkmn-data-template").content;
  },
  get colorSearchResults() {
    return document.getElementById("color-results");
  },
  get nameSearchResults() {
    return document.getElementById("name-results");
  },

  set background(hex) {
    document.querySelector(":root").style.setProperty("--background", hex);
  },
  set highlight(hex) {
    document.querySelector(":root").style.setProperty("--highlight", hex);
  },

  get metricSelectTemplate() {
    return document.getElementById("metric-select-template").content;
  },

  get sortFunction() {
    return document.forms.sortFunction;
  },
  get sortMetric() {
    return document.forms.sortMetric.elements.sortMetric.value;
  },
  get sortOrder() {
    return selectors.sortFunction.elements.sortOrder.checked ? "max" : "min";
  },
  get sortUseBestCluster() {
    return selectors.sortFunction.elements.useBestCluster.checked;
  },
  get sortUseWholeImage() {
    return selectors.sortFunction.elements.useWholeImage.checked;
  },
  get sortClusterSize() {
    return selectors.sortFunction.elements.clusterSize.checked;
  },
  get sortInverseClusterSize() {
    return selectors.sortFunction.elements.invClusterSize.checked;
  },
  get sortTotalSize() {
    return selectors.sortFunction.elements.totalSize.checked;
  },
  get sortInverseTotalSize() {
    return selectors.sortFunction.elements.invTotalSize.checked;
  },
  set sortMetricSymbol(sym) {
    selectors.sortFunction.elements.metricSymbolP.value = sym;
    selectors.sortFunction.elements.metricSymbolB.value = sym;
  },
  get sortIsUsingCluster() {
    return (
      selectors.sortUseBestCluster ||
      selectors.sortClusterSize ||
      selectors.sortInverseClusterSize
    );
  },

  get clusterFunction() {
    return document.forms.clusterFunction;
  },
  get clusterSortMetric() {
    return document.forms.clusterMetric.elements.sortMetric.value;
  },
  get clusterSortOrder() {
    return selectors.clusterFunction.elements.sortOrder.checked ? "max" : "min";
  },
  get clusterSortClusterSize() {
    return selectors.clusterFunction.elements.clusterSize.checked;
  },
  get clusterSortInverseClusterSize() {
    return selectors.clusterFunction.elements.invClusterSize.checked;
  },
  get clusterSortTotalSize() {
    return selectors.clusterFunction.elements.totalSize.checked;
  },
  get clusterSortInverseTotalSize() {
    return selectors.clusterFunction.elements.invTotalSize.checked;
  },
  set clusterMetricSymbol(sym) {
    selectors.clusterFunction.elements.metricSymbol.value = sym;
  },
};

const onMetricChange = (elements, skipUpdates = false) => {
  const kind = elements.metricKind.value;
  elements.whole.disabled = kind !== "whole";
  elements.mean.disabled = kind !== "mean";
  elements.statistic.disabled = kind !== "statistic";
  elements.sortMetric.value = elements[kind].value;

  if (!skipUpdates) {
    // terrible hack
    selectors.sortMetricSymbol = document
      .querySelector(`option[value=${selectors.sortMetric}]`)
      .textContent.at(-2);
    selectors.clusterMetricSymbol = document
      .querySelector(`option[value=${selectors.clusterSortMetric}]`)
      .textContent.at(-2);

    updateSort();
  }
};

const onColorChange = (inputValue, skipUpdates = false) => {
  const colorInput = "#" + (inputValue?.replace("#", "") ?? "FFFFFF");

  if (colorInput.length !== 7) {
    return;
  }

  const rgb = d3.color(colorInput);
  if (!rgb) {
    return;
  }

  const hex = rgb.formatHex();
  selectors.colorText = hex;
  selectors.colorPicker = hex;

  const contrast = getContrastingTextColor(hex);

  const newColor = document.createElement("div");
  newColor.innerHTML = hex;
  newColor.style = `
    color: ${contrast}; 
    background-color: ${hex};
  `;
  newColor.setAttribute("role", "button");
  newColor.addEventListener("click", () => onColorChange(hex));
  selectors.prevColors.prepend(newColor);

  selectors.background = hex;
  selectors.highlight = contrast;

  if (!skipUpdates) {
    updateScores(rgb);
    updateSort();
  }
};

const randomColor = () =>
  d3.hsl(Math.random() * 360, Math.random(), Math.random()).formatHex();

const sortOrders = {
  max: (a, b) => b - a,
  min: (a, b) => a - b,
};

const getCardinalityFactorExtractor = (
  clusterSize,
  invClusterSize,
  totalSize,
  invTotalSize
) => {
  const extractors = [];
  if (clusterSize) {
    extractors.push((scores) => scores.clusters.map(({ size }) => size));
  }
  if (invClusterSize) {
    extractors.push((scores) => scores.clusters.map(({ inverseSize }) => inverseSize));
  }
  if (totalSize) {
    extractors.push((scores) => scores.clusters.map(() => scores.total.size));
  }
  if (invTotalSize) {
    extractors.push((scores) => scores.clusters.map(() => scores.total.inverseSize));
  }
  return (scores) =>
    extractors
      .map((ext) => ext(scores))
      .reduce(
        (acc, xs) => acc.map((a, i) => a * xs[i]),
        scores.clusters.map(() => 1)
      );
};

const currentScores = {};
const currentBestClusterIndices = {};
const currentSortValues = {};
let sortedData = [];

const updateScores = (rgb) => {
  const { J, a, b } = d3.jab(rgb);
  const targetJab = buildVectorData([J, a, b], jab2hue, jab2lit, jab2chroma, jab2hex);
  const targetRgb = buildVectorData(
    [rgb.r, rgb.g, rgb.b],
    rgb2hue,
    rgb2lit,
    rgb2chroma,
    rgb2hex
  );
  pokemonData.forEach(({ name, jab, rgb }) => {
    currentScores[name] = {
      jab: {
        total: calcScores(jab.total, targetJab),
        clusters: jab.clusters.map((c) => calcScores(c, targetJab)),
      },
      rgb: {
        total: calcScores(rgb.total, targetRgb),
        clusters: rgb.clusters.map((c) => calcScores(c, targetRgb)),
      },
    };
  });
};

const updateSort = () => {
  // fade out cluster stuff if not in use
  selectors.clusterFunction.classList.toggle("fade", !selectors.sortIsUsingCluster);
  document.forms.clusterMetric.classList.toggle("fade", !selectors.sortIsUsingCluster);

  // update cluster rankings
  const clusterSortOrder = sortOrders[selectors.clusterSortOrder];
  const getClusterCardinalityFactors = getCardinalityFactorExtractor(
    selectors.clusterSortClusterSize,
    selectors.clusterSortInverseClusterSize,
    selectors.clusterSortTotalSize,
    selectors.clusterSortInverseTotalSize
  );
  pokemonData.forEach(({ name }) => {
    const { jab, rgb } = currentScores[name];
    // multiply scales with the intended metric, and find the index of the best value
    const forSpace = (clusters, factors) =>
      clusters
        .map((c, i) => [c[selectors.clusterSortMetric] * factors[i], i])
        .reduce((a, b) => (clusterSortOrder(a[0], b[0]) > 0 ? b : a))[1];
    currentBestClusterIndices[name] = {
      jab: forSpace(jab.clusters, getClusterCardinalityFactors(jab)),
      rgb: forSpace(rgb.clusters, getClusterCardinalityFactors(rgb)),
    };
  });

  // set up for actual sort
  const getCardinalityFactors = getCardinalityFactorExtractor(
    selectors.sortClusterSize,
    selectors.sortInverseClusterSize,
    selectors.sortTotalSize,
    selectors.sortInverseTotalSize
  );
  const factors = [
    (name) =>
      getCardinalityFactors(currentScores[name][selectors.colorSpace])[
        currentBestClusterIndices[name][selectors.colorSpace]
      ],
  ];
  if (selectors.sortUseWholeImage) {
    factors.push(
      (name) => currentScores[name][selectors.colorSpace].total[selectors.sortMetric]
    );
  }
  if (selectors.sortUseBestCluster) {
    factors.push(
      (name) =>
        currentScores[name][selectors.colorSpace].clusters[
          currentBestClusterIndices[name][selectors.colorSpace]
        ][selectors.sortMetric]
    );
  }
  pokemonData.forEach(({ name }) => {
    currentSortValues[name] = factors.map((fn) => fn(name)).reduce((x, y) => x * y);
  });

  // update actual sorted data
  const sortOrder = sortOrders[selectors.sortOrder];
  sortedData = pokemonData
    .map(({ name }) => name)
    .sort((a, b) => sortOrder(currentSortValues[a], currentSortValues[b]));

  // and desplay results
  showResults();
};

const getSprite = (() => {
  const stripForm = [
    "flabebe",
    "floette",
    "florges",
    "vivillon",
    "basculin",
    "furfrou",
    "magearna",
    "alcremie",
  ];
  return (pokemon) => {
    pokemon = pokemon
      .replace("-alola", "-alolan")
      .replace("-galar", "-galarian")
      .replace("-phony", "") // sinistea and polteageist
      .replace("darmanitan-galarian", "darmanitan-galarian-standard");
    if (stripForm.find((s) => pokemon.includes(s))) {
      pokemon = pokemon.replace(/-.*$/, "");
    }
    return `https://img.pokemondb.net/sprites/sword-shield/icon/${pokemon}.png`;
  };
})();

const populateDataDialog = (data, dialog) =>
  Object.entries(data).forEach(([metric, value]) => {
    const target = dialog.querySelector(`.pkmn-data-value--${metric}`);
    if (target) {
      target.innerText = value % 1 === 0 ? value : value.toFixed(3);
    }
  });

const makePokemonTile = (name) => {
  const clone = selectors.pokemonTemplate.cloneNode(true);

  const img = clone.querySelector("img");
  img.src = getSprite(name);
  img.alt = name;

  const formattedName = name
    .split("-")
    .map((part) => part.charAt(0).toUpperCase() + part.substr(1))
    .join(" ");
  clone.querySelector(".pkmn-name").innerText = formattedName;
  clone.querySelector(".pkmn-name").title = formattedName;

  clone.querySelector(".pkmn-score").innerText = currentSortValues[name].toFixed(3);

  const { total, clusters } = currentScores[name][selectors.colorSpace];

  const mu = clone.querySelector(".pkmn-total");
  mu.appendChild(document.createTextNode(total.muHex));
  mu.addEventListener("click", () => onColorChange(total.muHex));
  mu.style = `
    color: ${getContrastingTextColor(total.muHex)}; 
    background-color: ${total.muHex};
  `;

  const totalDataDialog = selectors.pokemonDataTemplate.cloneNode(true);
  populateDataDialog(total, totalDataDialog);
  clone.querySelector("dialog").appendChild(totalDataDialog);

  clusters.forEach((cls, i) => {
    const clsDiv = clone.querySelector(`.pkmn-cls${i + 1}`);
    clsDiv.addEventListener("click", () => onColorChange(cls.muHex));
    clsDiv.querySelector("span:first-child").innerText =
      (cls.proportion * 100).toFixed(2) + "%";
    clsDiv.querySelector("span:nth-child(2)").innerText = cls.muHex;
    clsDiv.style = `
      color: ${getContrastingTextColor(cls.muHex)}; 
      background-color: ${cls.muHex};
    `;
    const dialog = clsDiv.querySelector("dialog");
    dialog.removeAttribute("hidden");
    const clsDataDialog = selectors.pokemonDataTemplate.cloneNode(true);
    populateDataDialog(cls, clsDataDialog);
    dialog.appendChild(clsDataDialog);
  });

  if (selectors.sortUseWholeImage) {
    clone.querySelector(".pkmn").classList.add("pkmn-total-selected");
  }
  if (selectors.sortIsUsingCluster) {
    clone
      .querySelector(".pkmn")
      .classList.add(
        `pkmn-cls${currentBestClusterIndices[name][selectors.colorSpace] + 1}-selected`
      );
  }

  return clone;
};

const pokemonLookup = new Fuse(pokemonData, { keys: ["name"] });
let currentNameSearchResults = [];

const searchByName = (newSearch) => {
  currentNameSearchResults = pokemonLookup
    .search(newSearch, { limit: 100 })
    .map(({ item: { name } }) => name);
  showResults();
};

const randomPokemon = () => {
  currentNameSearchResults = Array.from(
    { length: 100 },
    () => pokemonData[Math.floor(Math.random() * pokemonData.length)].name
  );
  showResults();
};

const showResults = () => {
  selectors.colorSearchResults.innerHTML = "";
  sortedData.slice(0, selectors.resultsToDisplay).forEach((name) => {
    selectors.colorSearchResults.appendChild(makePokemonTile(name));
  });

  selectors.nameSearchResults.innerHTML = "";
  currentNameSearchResults.slice(0, selectors.resultsToDisplay).forEach((name) => {
    selectors.nameSearchResults.appendChild(makePokemonTile(name));
  });
};

const metricSelect = selectors.metricSelectTemplate;

document.forms.sortMetric.appendChild(metricSelect.cloneNode(true));
document.forms.sortMetric.querySelector("legend").textContent = "Sort Metric";
document.forms.sortMetric.elements.metricKind.value = "whole";
document.forms.sortMetric.elements.whole.value = "alpha";
onMetricChange(document.forms.sortMetric.elements, true);

document.forms.clusterMetric.appendChild(metricSelect.cloneNode(true));
document.forms.clusterMetric.querySelector("legend").textContent = "Cluster Metric";
document.forms.clusterMetric.elements.metricKind.value = "statistic";
document.forms.clusterMetric.elements.statistic.value = "importance";
onMetricChange(document.forms.clusterMetric.elements, true);

onColorChange(randomColor());
