// ---- Observables ----
const targetColor = U.obs();
const resultsToDisplay = U.obs(6);
const colorSpace = U.obs("jab");

const sortOrder = U.obs("min");
const sortUseWholeImage = U.obs(true);
const sortUseBestCluster = U.obs(true);
const sortUseClusterSize = U.obs(false);
const sortUseInverseClusterSize = U.obs(true);
const sortUseTotalSize = U.obs(true);
const sortUseInverseTotalSize = U.obs(false);
const sortMetric = addMetricForm(
  "sortMetric",
  "Sort Metric",
  "whole",
  "alpha",
  "sort-metric-mount"
);

const clusterOrder = U.obs("max");
const clusterUseClusterSize = U.obs(false);
const clusterUseInverseClusterSize = U.obs(false);
const clusterUseTotalSize = U.obs(false);
const clusterUseInverseTotalSize = U.obs(false);
const clusterMetric = addMetricForm(
  "clsMetric",
  "Cluster Metric",
  "stat",
  "importance",
  "cls-metric-mount"
);

const sortIgnoresCluster = U.obs(() =>
  [
    // ensure dep on all three
    sortUseBestCluster.value,
    sortUseClusterSize.value,
    sortUseInverseClusterSize.value,
  ].every((v) => !v)
);

// ---- Color Controls ----
const randomizeTargetColor = () => {
  targetColor.value = d3
    .hsl(Math.random() * 360, Math.random(), Math.random())
    .formatHex();
};

U.element("targetSelect", ({ randomButton }, form) => {
  U.field(form.elements.colorText, { obs: targetColor });
  U.field(form.elements.colorPicker, { obs: targetColor });

  U.field(form.elements.resultsToDisplay, { obs: resultsToDisplay });
  U.reactive(() => {
    form.elements.resultsToDisplayOutput.value = resultsToDisplay.value;
  });

  // non-reactive since the form onchange updates it
  form.elements.colorSpace.value = colorSpace.value;
  U.form(form, {
    onChange: {
      colorSpace(value) {
        colorSpace.value = value;
      },
    },
  });

  randomButton.addEventListener("click", randomizeTargetColor);
});

const getColorButtonStyle = (hex) => {
  const { h, s, l } = d3.hsl(hex);
  return `
    color: ${getContrastingTextColor(hex)}; 
    --button-bg: ${hex};
    --button-bg-hover: ${d3.hsl(h, s, clamp(0.25, l * 1.25, 0.9)).formatHex()};
  `;
};

targetColor.subscribe((hex, { previous }) => {
  const style = document.querySelector(":root").style;
  style.setProperty("--background", hex);
  const highlight = getContrastingTextColor(hex);
  style.setProperty("--highlight", highlight);
  style.setProperty("--shadow-component", highlight.includes("light") ? "255" : "0");

  if (previous) {
    const prevButton = document.createElement("button");
    prevButton.innerText = previous;
    prevButton.classList = "color-select";
    prevButton.style = getColorButtonStyle(previous);
    prevButton.addEventListener("click", () => (targetColor.value = previous));
    document.getElementById("prevColors").prepend(prevButton);
  }
});

// ---- Metric Controls ----
function addMetricForm(formName, legendText, initialKind, initialMetric, mountPoint) {
  const metricKind = U.obs(initialKind);
  let metric;

  U.template("metric-select-template", ({ form, legend }) => {
    form.name = formName;
    legend.innerText = legendText;
    form.elements[initialKind].disabled = false;
    form.elements[initialKind].value = initialMetric;
    form.elements.metricKind.value = initialKind;

    U.reactive(() => {
      form.elements.whole.disabled = metricKind.value !== "whole";
      form.elements.mean.disabled = metricKind.value !== "mean";
      form.elements.stat.disabled = metricKind.value !== "stat";
    });

    const metrics = U.obs([
      U.field(form.elements.whole),
      U.field(form.elements.mean),
      U.field(form.elements.stat),
    ]);

    U.form(form, {
      onChange: {
        metricKind(kind) {
          metricKind.value = kind;
        },
      },
    });

    metric = U.obs(() => {
      const [whole, mean, stat] = metrics.value;
      return { whole, mean, stat }[metricKind.value];
    });

    return mountPoint;
  });

  return metric;
}

// ---- Sorting Function Controls ----
const getMetricSymbol = (metric) =>
  // terrible hack
  document.querySelector(`option[value=${metric}]`).textContent.at(-2);

U.template(
  "function-template",
  ({ form, minmaxLabel, metricSymbol, metricSymbolCls, clusterName, clusterNameInv }) => {
    form.name = "sortFunction";

    const toggle = U.field(form.elements.sortOrder);
    U.reactive(() => {
      toggle.value = sortOrder.value === "max";
    });
    U.reactive(() => {
      sortOrder.value = toggle.value ? "max" : "min";
    });
    U.reactive(() => {
      minmaxLabel.innerText = sortOrder.value;
    });

    U.field(form.elements.useWholeImage, { obs: sortUseWholeImage });
    U.field(form.elements.useBestCluster, { obs: sortUseBestCluster });
    U.field(form.elements.clusterSize, { obs: sortUseClusterSize });
    U.field(form.elements.invClusterSize, { obs: sortUseInverseClusterSize });
    U.field(form.elements.totalSize, { obs: sortUseTotalSize });
    U.field(form.elements.invTotalSize, { obs: sortUseInverseTotalSize });

    U.reactive(() => {
      const symbol = getMetricSymbol(sortMetric.value);
      metricSymbol.innerText = symbol;
      metricSymbolCls.innerText = `${symbol}(B)`;
    });

    clusterName.innerText = clusterNameInv.innerText = "B";

    U.form(form);
    return "sort-fn-mount";
  }
);

U.template(
  "function-template",
  ({
    form,
    minmaxLabel,
    useWholeImageLabel,
    metricSymbolCls,
    clusterName,
    clusterNameInv,
  }) => {
    form.name = "clsFunction";

    form.elements.sortOrder.checked = true;

    const toggle = U.field(form.elements.sortOrder);
    U.reactive(() => {
      toggle.value = clusterOrder.value === "max";
      minmaxLabel.innerText = `(Arg) ${clusterOrder.value}`;
    });
    U.reactive(() => {
      clusterOrder.value = toggle.value ? "max" : "min";
    });

    U.field(form.elements.clusterSize, { obs: clusterUseClusterSize });
    U.field(form.elements.invClusterSize, { obs: clusterUseInverseClusterSize });
    U.field(form.elements.totalSize, { obs: clusterUseTotalSize });
    U.field(form.elements.invTotalSize, { obs: clusterUseInverseTotalSize });

    // not needed for cluster function
    useWholeImageLabel.nextSibling.remove();
    useWholeImageLabel.remove();
    // and this can't be disabled so just replace the field with the span
    metricSymbolCls.parentElement.replaceWith(metricSymbolCls);

    U.reactive(() => {
      metricSymbolCls.innerText = `${getMetricSymbol(clusterMetric.value)}(K)`;
    });

    clusterName.innerText = clusterNameInv.innerText = "K";

    U.form(form);
    return "cls-fn-mount";
  }
);

U.reactive(() => {
  document.forms.clsMetric.dataset.faded = document.forms.clsFunction.dataset.faded =
    sortIgnoresCluster.value;
});

// ---- Score Calculation ----
const currentScores = U.obs(() => {
  const rgb = d3.rgb(targetColor.value);
  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
  );

  const scores = {};
  pokemonData.forEach(({ name, jab, rgb }) => {
    scores[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)),
      },
    };
  });
  return scores;
});

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

const getClusterRankingValue = U.obs(() => {
  const metric = clusterMetric.value;
  const factors = [(cluster) => cluster[metric]];
  if (clusterUseClusterSize.value) {
    factors.push((cluster) => cluster.size);
  }
  if (clusterUseInverseClusterSize.value) {
    factors.push((cluster) => cluster.inverseSize);
  }
  if (clusterUseTotalSize.value) {
    factors.push((_, total) => total.size);
  }
  if (clusterUseInverseTotalSize.value) {
    factors.push((_, total) => total.inverseSize);
  }
  return (cluster, total) =>
    factors.map((fn) => fn(cluster, total)).reduce((x, y) => x * y);
});

const getBestClusterIndex = U.obs(() => {
  const rank = getClusterRankingValue.value;
  const clsSort = sortOrders[clusterOrder.value];

  return (scores) => {
    return scores.clusters
      .map((c, i) => [rank(c, scores.total), i])
      .reduce((a, b) => (clsSort(a[0], b[0]) > 0 ? b : a))[1];
  };
});

const getRankingValue = U.obs(() => {
  const metric = sortMetric.value;
  const factors = [];
  if (sortUseWholeImage.value) {
    factors.push((scores) => scores.total[metric]);
  }
  if (sortUseBestCluster.value) {
    factors.push((scores, index) => scores.clusters[index][metric]);
  }
  if (sortUseClusterSize.value) {
    factors.push((scores, index) => scores.clusters[index].size);
  }
  if (sortUseInverseClusterSize.value) {
    factors.push((scores, index) => scores.clusters[index].inverseSize);
  }
  if (sortUseTotalSize.value) {
    factors.push((scores) => scores.total.size);
  }
  if (sortUseInverseTotalSize.value) {
    factors.push((scores) => scores.total.inverseSize);
  }
  return (scores, bestClusterIndex) =>
    factors.map((fn) => fn(scores, bestClusterIndex)).reduce((x, y) => x * y, 1);
});

const currentResults = U.obs(() => {
  console.log("Recalculating");

  const bestClusterIndices = {};
  const rankingValues = {};

  pokemonData.forEach(({ name }) => {
    const { jab, rgb } = currentScores.value[name];
    const bestJab = getBestClusterIndex.value(jab);
    const bestRgb = getBestClusterIndex.value(rgb);
    bestClusterIndices[name] = { jab: bestJab, rgb: bestRgb };
    rankingValues[name] = {
      jab: getRankingValue.value(jab, bestJab),
      rgb: getRankingValue.value(rgb, bestRgb),
    };
  });

  return { bestClusterIndices, rankingValues };
});

const sortedResults = U.obs(() => {
  console.log("Resorting");

  const { rankingValues } = currentResults.value;
  const sort = sortOrders[sortOrder.value];
  const space = colorSpace.value;
  return pokemonData
    .map(({ name }) => name)
    .sort(
      (a, b) =>
        sort(rankingValues[a][space], rankingValues[b][space]) || a.localeCompare(b)
    );
});

const colorSearchResults = U.obs(() =>
  sortedResults.value.slice(0, resultsToDisplay.value)
);

// ---- Pokemon Display ----
const getSpriteName = (() => {
  const stripForm = [
    "flabebe",
    "floette",
    "florges",
    "vivillon",
    "basculin",
    "furfrou",
    "magearna",
    "alcremie",
  ];
  return (pokemon) => {
    pokemon = pokemon
      .replace("-alola", "-alolan")
      .replace("-galar", "-galarian")
      .replace("-hisui", "-hisuian")
      .replace("-paldea", "-paldean")
      .replace("-paldeanfire", "-paldean-fire") // tauros
      .replace("-paldeanwater", "-paldean-water") // tauros
      .replace("-phony", "") // sinistea and polteageist
      .replace("darmanitan-galarian", "darmanitan-galarian-standard");
    if (stripForm.find((s) => pokemon.includes(s))) {
      pokemon = pokemon.replace(/-.*$/, "");
    }
    return pokemon;
  };
})();

const displayPokemon = (pkmnName, target) => {
  const spriteName = getSpriteName(pkmnName);
  const formattedName = pkmnName
    .split("-")
    .map((part) => part.charAt(0).toUpperCase() + part.substr(1))
    .join(" ");

  // TODO this will need to be reactive when we start actually pulling stats
  const { total, clusters } = currentScores.value[pkmnName][colorSpace.value];

  U.template(
    "pkmn-template",
    ({ image, name, score, totalBtn, cls1Btn, cls2Btn, cls3Btn, cls4Btn }) => {
      const imageErrHandler = () => {
        image.removeEventListener("error", imageErrHandler);
        image.src = `https://img.pokemondb.net/sprites/sword-shield/icon/${spriteName}.png`;
      };
      image.addEventListener("error", imageErrHandler);
      image.src = `https://img.pokemondb.net/sprites/scarlet-violet/icon/${spriteName}.png`;

      name.innerText = name.title = image.alt = formattedName;

      U.reactive(() => {
        const { rankingValues } = currentResults.value;
        score.innerText = rankingValues[pkmnName][colorSpace.value].toFixed(2);
      });

      totalBtn.innerText = total.muHex;
      totalBtn.style = getColorButtonStyle(total.muHex);
      totalBtn.addEventListener("click", () => {
        targetColor.value = total.muHex;
      });
      totalBtn.dataset.best =
        sortUseWholeImage.value ||
        sortUseTotalSize.value ||
        sortUseInverseTotalSize.value;

      [cls1Btn, cls2Btn, cls3Btn, cls4Btn].forEach((button, index) => {
        if (index >= clusters.length) {
          button.remove();
          return;
        }

        const { muHex } = clusters[index];
        button.innerText = muHex;
        button.style = getColorButtonStyle(muHex);
        button.addEventListener("click", () => {
          targetColor.value = muHex;
        });

        U.reactive(() => {
          const { bestClusterIndices } = currentResults.value;
          const bestCluster = bestClusterIndices[pkmnName][colorSpace.value];
          button.dataset.best = !sortIgnoresCluster.value && index === bestCluster;
        });
      });

      // TODO stat dialog
      // TODO click name to copy?

      return target;
    }
  );
};

// ---- Display Logic ----
const colorSearchResultsTarget = document.getElementById("color-results");
colorSearchResults.subscribe(() => {
  colorSearchResultsTarget.innerHTML = "";
  colorSearchResults.value.forEach((name) =>
    displayPokemon(name, colorSearchResultsTarget)
  );
});

// ---- Name Search ----
U.element("nameSearch", ({ input, button }) => {
  const nameSearchInput = U.field(input);

  const lookupLimit = 24;
  const pokemonLookup = new Fuse(pokemonData, { keys: ["name"] });
  const nameSearchResults = U.obs(() =>
    pokemonLookup
      .search(nameSearchInput.value, { limit: lookupLimit })
      .map(({ item: { name } }) => name)
  );

  button.addEventListener("click", () => {
    nameSearchResults.value = Array.from(
      { length: lookupLimit },
      () => pokemonData[Math.floor(Math.random() * pokemonData.length)].name
    );
  });

  const nameResultsTarget = document.getElementById("name-results");
  nameSearchResults.subscribe(() => {
    nameResultsTarget.innerHTML = "";
    nameSearchResults.value.forEach((name) => displayPokemon(name, nameResultsTarget));
  });
});

// ---- Initial Target ----
randomizeTargetColor();
