<script setup lang="ts">
import Fuse from 'fuse.js/dist/fuse.basic'
import RadioButtonIcon from '~/assets/images/utils/radio-button.svg'
import RadioButtonCheckedIcon from '~/assets/images/utils/radio-button-checked.svg'
import WarningIcon from '~/assets/images/utils/issues.svg'
import { isModifierKeyDown } from '~/helpers/events.ts'
import { commonSettingsMessages } from '~/utils/common-messages.ts'

const vintl = useVIntl()
const { formatMessage } = vintl

const messages = defineMessages({
  languagesDescription: {
    id: 'settings.language.description',
    defaultMessage:
      'Choose your preferred language for the site. Translations are contributed by volunteers <crowdin-link>on Crowdin</crowdin-link>.',
  },
  automaticLocale: {
    id: 'settings.language.languages.automatic',
    defaultMessage: 'Sync with the system language',
  },
  noResults: {
    id: 'settings.language.languages.search.no-results',
    defaultMessage: 'No languages match your search.',
  },
  searchFieldDescription: {
    id: 'settings.language.languages.search-field.description',
    defaultMessage: 'Submit to focus the first search result',
  },
  searchFieldPlaceholder: {
    id: 'settings.language.languages.search-field.placeholder',
    defaultMessage: 'Search for a language...',
  },
  searchResultsAnnouncement: {
    id: 'settings.language.languages.search-results-announcement',
    defaultMessage:
      '{matches, plural, =0 {No languages match} one {# language matches} other {# languages match}} your search.',
  },
  loadFailed: {
    id: 'settings.language.languages.load-failed',
    defaultMessage: 'Cannot load this language. Try again in a bit.',
  },
  languageLabelApplying: {
    id: 'settings.language.languages.language-label-applying',
    defaultMessage: '{label}. Applying...',
  },
  languageLabelError: {
    id: 'settings.language.languages.language-label-error',
    defaultMessage: '{label}. Error',
  },
})

const categoryNames = defineMessages({
  auto: {
    id: 'settings.language.categories.auto',
    defaultMessage: 'Automatic',
  },
  default: {
    id: 'settings.language.categories.default',
    defaultMessage: 'Standard languages',
  },
  fun: {
    id: 'settings.language.categories.fun',
    defaultMessage: 'Fun languages',
  },
  experimental: {
    id: 'settings.language.categories.experimental',
    defaultMessage: 'Experimental languages',
  },
  searchResult: {
    id: 'settings.language.categories.search-result',
    defaultMessage: 'Search results',
  },
})

type Category = keyof typeof categoryNames

const categoryOrder: Category[] = ['auto', 'default', 'fun', 'experimental']

function normalizeCategoryName(name?: string): keyof typeof categoryNames {
  switch (name) {
    case 'auto':
    case 'fun':
    case 'experimental':
      return name
    default:
      return 'default'
  }
}

type LocaleBase = {
  category: Category
  tag: string
  searchTerms?: string[]
}

type AutomaticLocale = LocaleBase & {
  auto: true
}

type CommonLocale = LocaleBase & {
  auto?: never
  displayName: string
  defaultName: string
  translatedName: string
}

type Locale = AutomaticLocale | CommonLocale

const $defaultNames = useDisplayNames(() => vintl.defaultLocale)

const $translatedNames = useDisplayNames(() => vintl.locale)

const $locales = computed(() => {
  const locales: Locale[] = []

  locales.push({
    auto: true,
    tag: 'auto',
    category: 'auto',
    searchTerms: [
      'automatic',
      'Sync with the system language',
      formatMessage(messages.automaticLocale),
    ],
  })

  for (const locale of vintl.availableLocales) {
    let displayName = locale.meta?.displayName

    if (displayName == null) {
      displayName = createDisplayNames(locale.tag).of(locale.tag) ?? locale.tag
    }

    let defaultName = vintl.defaultResources['languages.json']?.[locale.tag]

    if (defaultName == null) {
      defaultName = $defaultNames.value.of(locale.tag) ?? locale.tag
    }

    let translatedName = vintl.resources['languages.json']?.[locale.tag]

    if (translatedName == null) {
      translatedName = $translatedNames.value.of(locale.tag) ?? locale.tag
    }

    let searchTerms = locale.meta?.searchTerms
    if (searchTerms === '-') searchTerms = undefined

    locales.push({
      tag: locale.tag,
      category: normalizeCategoryName(locale.meta?.category),
      displayName,
      defaultName,
      translatedName,
      searchTerms: searchTerms?.split('\n'),
    })
  }

  return locales
})

const $query = ref('')

const isQueryEmpty = () => $query.value.trim().length === 0

const fuse = new Fuse<Locale>([], {
  keys: ['tag', 'displayName', 'translatedName', 'englishName', 'searchTerms'],
  threshold: 0.4,
  distance: 100,
})

watchSyncEffect(() => fuse.setCollection($locales.value))

const $categories = computed(() => {
  const categories = new Map<Category, Locale[]>()

  for (const category of categoryOrder) categories.set(category, [])

  for (const locale of $locales.value) {
    let categoryLocales = categories.get(locale.category)

    if (categoryLocales == null) {
      categoryLocales = []
      categories.set(locale.category, categoryLocales)
    }

    categoryLocales.push(locale)
  }

  for (const categoryKey of [...categories.keys()]) {
    if (categories.get(categoryKey)?.length === 0 ?? false) {
      categories.delete(categoryKey)
    }
  }

  return categories
})

const $searchResults = computed(() => {
  return new Map<Category, Locale[]>([
    ['searchResult', isQueryEmpty() ? [] : fuse.search($query.value).map(({ item }) => item)],
  ])
})

const $displayCategories = computed(() =>
  isQueryEmpty() ? $categories.value : $searchResults.value
)

const $changingTo = ref<string | undefined>()

const isChanging = () => $changingTo.value != null

const $failedLocale = ref<string>()

const $activeLocale = computed(() => {
  if ($changingTo.value != null) return $changingTo.value
  return vintl.automatic ? 'auto' : vintl.locale
})

async function changeLocale(value: string) {
  if ($activeLocale.value === value) return

  $changingTo.value = value

  try {
    await vintl.changeLocale(value)
    $failedLocale.value = undefined
  } catch (err) {
    $failedLocale.value = value
  } finally {
    $changingTo.value = undefined
  }
}

const $languagesList = ref<HTMLDivElement | undefined>()

function onSearchKeydown(e: KeyboardEvent) {
  if (e.key !== 'Enter' || isModifierKeyDown(e)) return

  const focusableTarget = $languagesList.value?.querySelector(
    'input, [tabindex]:not([tabindex="-1"])'
  ) as HTMLElement | undefined

  focusableTarget?.focus()
}

function onItemKeydown(e: KeyboardEvent, locale: Locale) {
  switch (e.key) {
    case 'Enter':
    case ' ':
      break
    default:
      return
  }

  if (isModifierKeyDown(e) || isChanging()) return

  changeLocale(locale.tag)
}

function onItemClick(e: MouseEvent, locale: Locale) {
  if (isModifierKeyDown(e) || isChanging()) return

  changeLocale(locale.tag)
}

function getItemLabel(locale: Locale) {
  const label = locale.auto
    ? formatMessage(messages.automaticLocale)
    : `${locale.translatedName}. ${locale.displayName}`

  if ($changingTo.value === locale.tag) {
    return formatMessage(messages.languageLabelApplying, { label })
  }

  if ($failedLocale.value === locale.tag) {
    return formatMessage(messages.languageLabelError, { label })
  }

  return label
}
</script>

<template>
  <div>
    <section class="universal-card">
      <h2>{{ formatMessage(commonSettingsMessages.language) }}</h2>

      <div class="card-description">
        <IntlFormatted :message-id="messages.languagesDescription">
          <template #crowdin-link="{ children }">
            <a href="https://crowdin.com/project/modrinth">
              <component :is="() => children" />
            </a>
          </template>
        </IntlFormatted>
      </div>

      <div class="search-container">
        <input
          id="language-search"
          v-model="$query"
          name="language"
          type="search"
          :placeholder="formatMessage(messages.searchFieldPlaceholder)"
          class="language-search"
          aria-describedby="language-search-description"
          :disabled="isChanging()"
          @keydown="onSearchKeydown"
        />

        <div id="language-search-description" class="visually-hidden">
          {{ formatMessage(messages.searchFieldDescription) }}
        </div>

        <div id="language-search-results-announcements" class="visually-hidden" aria-live="polite">
          {{
            isQueryEmpty()
              ? ''
              : formatMessage(messages.searchResultsAnnouncement, {
                  matches: $searchResults.get('searchResult')?.length ?? 0,
                })
          }}
        </div>
      </div>

      <div ref="$languagesList" class="languages-list">
        <template v-for="[category, locales] in $displayCategories" :key="category">
          <strong class="category-name">
            {{ formatMessage(categoryNames[category]) }}
          </strong>

          <div
            v-if="category === 'searchResult' && locales.length === 0"
            class="no-results"
            tabindex="0"
          >
            {{ formatMessage(messages.noResults) }}
          </div>

          <template v-for="locale in locales" :key="locale.tag">
            <div
              role="button"
              :aria-pressed="$activeLocale === locale.tag"
              :class="{
                'language-item': true,
                pending: $changingTo == locale.tag,
                errored: $failedLocale == locale.tag,
              }"
              :aria-describedby="
                $failedLocale == locale.tag ? `language__${locale.tag}__fail` : undefined
              "
              :aria-disabled="isChanging() && $changingTo !== locale.tag"
              :tabindex="0"
              :aria-label="getItemLabel(locale)"
              @click="(e) => onItemClick(e, locale)"
              @keydown="(e) => onItemKeydown(e, locale)"
            >
              <RadioButtonCheckedIcon v-if="$activeLocale === locale.tag" class="radio" />
              <RadioButtonIcon v-else class="radio" />

              <div class="language-names">
                <div class="language-name">
                  {{ locale.auto ? formatMessage(messages.automaticLocale) : locale.displayName }}
                </div>

                <div v-if="!locale.auto" class="language-translated-name">
                  {{ locale.translatedName }}
                </div>
              </div>
            </div>

            <div
              v-if="$failedLocale === locale.tag"
              :id="`language__${locale.tag}__fail`"
              class="language-load-error"
            >
              <WarningIcon /> {{ formatMessage(messages.loadFailed) }}
            </div>
          </template>
        </template>
      </div>
    </section>
  </div>
</template>

<style scoped lang="scss">
.languages-list {
  display: flex;
  flex-direction: column;
  gap: 0.6rem;
}

.language-item {
  display: flex;
  align-items: center;
  column-gap: 0.5rem;
  border: 0.15rem solid transparent;
  border-radius: var(--spacing-card-md);
  background: var(--color-button-bg);
  padding: var(--spacing-card-md);
  cursor: pointer;
  position: relative;
  overflow: hidden;

  &:not([aria-disabled='true']):hover {
    border-color: var(--color-button-bg-hover);
  }

  &:focus-visible,
  &:has(:focus-visible) {
    outline: 2px solid var(--color-brand);
  }

  &.errored {
    border-color: var(--color-red);

    &:hover {
      border-color: var(--color-red);
    }
  }

  &.pending::after {
    content: '';
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;

    background-image: linear-gradient(
      102deg,
      rgba(0, 0, 0, 0) 0%,
      rgba(0, 0, 0, 0) 20%,
      rgba(0, 0, 0, 0.1) 45%,
      rgba(0, 0, 0, 0.1) 50%,
      rgba(0, 0, 0, 0) 80%,
      rgba(0, 0, 0, 0) 100%
    );

    background-repeat: no-repeat;
    animation: shimmerSliding 2.5s ease-out infinite;

    .dark-mode &,
    .oled-mode & {
      background-image: linear-gradient(
        102deg,
        rgba(255, 255, 255, 0) 0%,
        rgba(255, 255, 255, 0) 20%,
        rgba(255, 255, 255, 0.1) 45%,
        rgba(255, 255, 255, 0.1) 50%,
        rgba(255, 255, 255, 0) 80%,
        rgba(255, 255, 255, 0) 100%
      );
    }

    @keyframes shimmerSliding {
      from {
        left: -100%;
      }
      to {
        left: 100%;
      }
    }
  }

  &[aria-disabled='true']:not(.pending) {
    opacity: 0.8;
    pointer-events: none;
    cursor: default;
  }
}

.language-load-error {
  color: var(--color-red);
  font-size: var(--font-size-sm);
  margin-left: 0.3rem;
  display: flex;
  align-items: center;
  gap: 0.3rem;
}

.radio {
  width: 24px;
  height: 24px;
}

.language-names {
  display: flex;
  justify-content: space-between;
  flex: 1;
  flex-wrap: wrap;
}

.language-name {
  font-weight: bold;
}

.language-search {
  width: 100%;
}

.search-container {
  margin-bottom: var(--spacing-card-md);
}

.card-description {
  margin-bottom: calc(var(--spacing-card-sm) + var(--spacing-card-md));

  a {
    color: var(--color-link);

    &:hover {
      color: var(--color-link-hover);
    }

    &:active {
      color: var(--color-link-active);
    }
  }
}

.category-name {
  margin-top: var(--spacing-card-md);
}
</style>
