import {type JSX, render} from "preact";
import {
  extractGroups,
  initializeGlobals,
  log,
  userIsLoggedIn,
} from "../utilities/exports.js";
import {
  Data,
  Feature,
  MiscellaneousFeature,
  fromStorage,
} from "../storage/exports.js";
import {
  AutocompleteFeature,
  BackToTopFeature,
  JumpToNewCommentFeature,
  UserLabelsFeature,
  runAnonymizeUsernamesFeature,
  runCommentAnchorFixFeature,
  runGroupListSubscribeButtonFeature,
  runHideTopicsFeature,
  runHideVotesFeature,
  runMarkdownToolbarFeature,
  runThemedLogoFeature,
  runTopicInfoIgnore,
  runUnignoreAllButtonFeature,
  runUsernameColorsFeature,
} from "./features/exports.js";

async function initialize() {
  const start = window.performance.now();
  initializeGlobals();
  const enabledFeatures = await fromStorage(Data.EnabledFeatures);
  const miscEnabled = await fromStorage(Data.MiscellaneousEnabledFeatures);
  window.TildesReExtended.debug = enabledFeatures.value.has(Feature.Debug);

  // Any features that will use the knownGroups data should be added to this
  // array so that when groups are changed on Tildes, TRX can still update
  // them without having to change the hardcoded values.
  const usesKnownGroups = new Set<Feature>([Feature.Autocomplete]);
  const knownGroups = await fromStorage(Data.KnownGroups);
  const userLabels = await fromStorage(Feature.UserLabels);

  const isLoggedIn = userIsLoggedIn();
  if (!isLoggedIn) {
    log("User is not logged in, running with limited features enabled.", true);
  }

  // Only when any of the features that uses this data are enabled, try to save
  // the groups.
  if (
    Array.from(usesKnownGroups).some((feature) =>
      enabledFeatures.value.has(feature),
    )
  ) {
    const extractedGroups = extractGroups();
    if (extractedGroups !== undefined) {
      knownGroups.value = new Set(extractedGroups);
      await knownGroups.save();
    }
  }

  const anonymizeUsernamesEnabled = enabledFeatures.value.has(
    Feature.AnonymizeUsernames,
  );

  const observerFeatures: Array<() => void | Promise<void>> = [];
  const observer = new window.MutationObserver(async () => {
    log("Page mutation detected, rerunning features.");
    observer.disconnect();
    await Promise.all(observerFeatures.map(async (feature) => feature()));
    startObserver();
  });

  function startObserver() {
    observer.observe(document.body, {
      attributes: true,
      childList: true,
      subtree: true,
    });
  }

  if (anonymizeUsernamesEnabled) {
    observerFeatures.push(() => {
      runAnonymizeUsernamesFeature();
    });
  }

  if (enabledFeatures.value.has(Feature.HideTopics)) {
    observerFeatures.push(async () => {
      await runHideTopicsFeature(userLabels);
    });
  }

  if (enabledFeatures.value.has(Feature.HideVotes)) {
    observerFeatures.push(async () => {
      const data = await fromStorage(Feature.HideVotes);
      runHideVotesFeature(data.value);
    });
  }

  if (enabledFeatures.value.has(Feature.MarkdownToolbar) && isLoggedIn) {
    observerFeatures.push(() => {
      runMarkdownToolbarFeature();
    });
  }

  if (enabledFeatures.value.has(Feature.ThemedLogo)) {
    observerFeatures.push(() => {
      runThemedLogoFeature();
    });
  }

  if (enabledFeatures.value.has(Feature.UsernameColors)) {
    observerFeatures.push(async () => {
      const data = await fromStorage(Feature.UsernameColors);
      const randomizeUsernameColors = await fromStorage(
        Data.RandomizeUsernameColors,
      );
      await runUsernameColorsFeature(
        data,
        anonymizeUsernamesEnabled,
        randomizeUsernameColors.value,
      );
    });
  }

  // Initialize all the observer-dependent features first.
  await Promise.all(observerFeatures.map(async (feature) => feature()));

  // Object to hold the active components we are going to render.
  const components: Record<string, JSX.Element | undefined> = {};

  if (enabledFeatures.value.has(Feature.Autocomplete) && isLoggedIn) {
    components.autocomplete = (
      <AutocompleteFeature
        anonymizeUsernamesEnabled={anonymizeUsernamesEnabled}
        knownGroups={knownGroups.value}
        userLabels={userLabels}
      />
    );
  }

  if (enabledFeatures.value.has(Feature.BackToTop)) {
    components.backToTop = <BackToTopFeature />;
  }

  if (enabledFeatures.value.has(Feature.JumpToNewComment) && isLoggedIn) {
    components.jumpToNewComment = <JumpToNewCommentFeature />;
  }

  if (enabledFeatures.value.has(Feature.UserLabels)) {
    components.userLabels = (
      <UserLabelsFeature
        anonymizeUsernamesEnabled={anonymizeUsernamesEnabled}
        userLabels={userLabels}
      />
    );
  }

  if (miscEnabled.value.has(MiscellaneousFeature.CommentAnchorFix)) {
    runCommentAnchorFixFeature();
  }

  if (
    miscEnabled.value.has(MiscellaneousFeature.GroupListSubscribeButtons) &&
    isLoggedIn
  ) {
    runGroupListSubscribeButtonFeature();
  }

  if (
    miscEnabled.value.has(MiscellaneousFeature.TopicInfoIgnore) &&
    isLoggedIn
  ) {
    runTopicInfoIgnore();
  }

  if (
    miscEnabled.value.has(MiscellaneousFeature.UnignoreAllButton) &&
    isLoggedIn
  ) {
    runUnignoreAllButtonFeature();
  }

  // Insert a placeholder at the end of the body first, then render the rest
  // and use that as the replacement element. Otherwise render() would put it
  // at the beginning of the body which causes a bunch of different issues.
  const replacement = document.createElement("div");
  document.body.append(replacement);

  // The jump to new comment button must come right before
  // the back to top button. The CSS depends on them being in this order.
  render(
    <div id="trx-container">
      {components.jumpToNewComment} {components.backToTop}
      {components.autocomplete} {components.userLabels}
    </div>,
    document.body,
    replacement,
  );

  // Start the mutation observer only when some features depend on it are enabled.
  if (observerFeatures.length > 0) {
    startObserver();
  }

  const initializedIn = window.performance.now() - start;
  log(`Initialized in approximately ${initializedIn} milliseconds.`);
}

document.addEventListener("DOMContentLoaded", initialize);
