<!--
SPDX-FileCopyrightText: syuilo and other misskey contributors
SPDX-License-Identifier: AGPL-3.0-only
-->

<template>
    <div class="omfetrab" :class="['s' + size, 'w' + width, 'h' + height, { asDrawer, asWindow }]" :style="{ maxHeight: maxHeight ? maxHeight + 'px' : undefined }">
        <input ref="searchEl" :value="q" class="search" data-prevent-emoji-insert :class="{ filled: q != null && q != '' }" :placeholder="i18n.ts.search" type="search" autocapitalize="off" @input="input()" @paste.stop="paste" @keydown.stop.prevent.enter="onEnter">
        <!-- FirefoxのTabフォーカスが想定外の挙動となるためtabindex="-1"を追加 https://github.com/misskey-dev/misskey/issues/10744 -->
        <div ref="emojisEl" class="emojis" tabindex="-1">
            <section class="result">
                <div v-if="searchResultCustom.length > 0" class="body">
                    <button
                            v-for="emoji in searchResultCustom"
                            :key="emoji.name"
                            class="_button item"
                            :title="emoji.name"
                            tabindex="0"
                            @click="chosen(emoji, $event)"
                    >
                        <MkCustomEmoji class="emoji" :name="emoji.name"/>
                    </button>
                </div>
                <div v-if="searchResultUnicode.length > 0" class="body">
                    <button
                            v-for="emoji in searchResultUnicode"
                            :key="emoji.name"
                            class="_button item"
                            :title="emoji.name"
                            tabindex="0"
                            @click="chosen(emoji, $event)"
                    >
                        <MkEmoji class="emoji" :emoji="emoji.char"/>
                    </button>
                </div>
            </section>

            <div v-if="tab === 'index'" class="group index">
                <section v-if="showPinned && pinned.length > 0">
                    <div class="body">
                        <button
                                v-for="emoji in pinned"
                                :key="emoji"
                                :data-emoji="emoji"
                                class="_button item"
                                tabindex="0"
                                @pointerenter="computeButtonTitle"
                                @click="chosen(emoji, $event)"
                        >
                            <MkCustomEmoji v-if="emoji[0] === ':'" class="emoji" :name="emoji" :normal="true"/>
                            <MkEmoji v-else class="emoji" :emoji="emoji" :normal="true"/>
                        </button>
                    </div>
                </section>

                <section>
                    <header class="_acrylic"><i class="ti ti-clock ti-fw"></i> {{ i18n.ts.recentUsed }}</header>
                    <div class="body">
                        <button
                                v-for="emoji in recentlyUsedEmojis"
                                :key="emoji"
                                class="_button item"
                                :data-emoji="emoji"
                                @pointerenter="computeButtonTitle"
                                @click="chosen(emoji, $event)"
                        >
                            <MkCustomEmoji v-if="emoji[0] === ':'" class="emoji" :name="emoji" :normal="true"/>
                            <MkEmoji v-else class="emoji" :emoji="emoji" :normal="true"/>
                        </button>
                    </div>
                </section>
            </div>
            <div v-once class="group">
                <header class="_acrylic">{{ i18n.ts.customEmojis }}</header>
                <XSection
                        v-for="child in customEmojiFolderRoot.children"
                        :key="`custom:${child.value}`"
                        :initialShown="false"
                        :emojis="computed(() => customEmojis.filter(e => child.value === '' ? (e.category === 'null' || !e.category) : e.category === child.value &&  !customEmojis.some(emoji => emoji.category !== null && emoji.category.includes(e.category+'/')) || e.category === child.category+'/'+child.category && !e.category).filter(filterAvailable).map(e => `:${e.name}:`))"
                        :hasChildSection="child.children.length !== 0"
                        :customEmojiTree="child.children"
                        @chosen="chosen"
                >
                    {{ child.value || i18n.ts.other }}
                </XSection>
            </div>
            <div v-once class="group">
                <header class="_acrylic">{{ i18n.ts.emoji }}</header>
                <XSection v-for="category in categories" :key="category" :emojis="emojiCharByCategory.get(category) ?? []" :hasChildSection="false" @chosen="chosen">{{ category }}</XSection>
            </div>
        </div>
        <div class="tabs">
            <button class="_button tab" :class="{ active: tab === 'index' }" @click="tab = 'index'"><i class="ti ti-asterisk ti-fw"></i></button>
            <button class="_button tab" :class="{ active: tab === 'custom' }" @click="tab = 'custom'"><i class="ti ti-mood-happy ti-fw"></i></button>
            <button class="_button tab" :class="{ active: tab === 'unicode' }" @click="tab = 'unicode'"><i class="ti ti-leaf ti-fw"></i></button>
            <button class="_button tab" :class="{ active: tab === 'tags' }" @click="tab = 'tags'"><i class="ti ti-hash ti-fw"></i></button>
        </div>
    </div>
</template>

<script lang="ts" setup>
import { ref, shallowRef, computed, watch, onMounted } from 'vue';
import * as Misskey from 'misskey-js';
import XSection from '@/components/MkEmojiPicker.section.vue';
import {
    emojilist,
    emojiCharByCategory,
    UnicodeEmojiDef,
    unicodeEmojiCategories as categories,
    getEmojiName,
    CustomEmojiFolderTree,
} from '@/scripts/emojilist.js';
import MkRippleEffect from '@/components/MkRippleEffect.vue';
import * as os from '@/os.js';
import { isTouchUsing } from '@/scripts/touch.js';
import { deviceKind } from '@/scripts/device-kind.js';
import { i18n } from '@/i18n.js';
import { defaultStore } from '@/store.js';
import { customEmojiCategories, customEmojis, customEmojisMap } from '@/custom-emojis.js';
import { $i } from '@/account.js';

const props = withDefaults(defineProps<{
    showPinned?: boolean;
    pinnedEmojis?: string[];
    maxHeight?: number;
    asDrawer?: boolean;
    asWindow?: boolean;
    asReactionPicker?: boolean; // 今は使われてないが将来的に使いそう
}>(), {
    showPinned: true,
});

const emit = defineEmits<{
    (ev: 'chosen', v: string): void;
}>();

const searchEl = shallowRef<HTMLInputElement>();
const emojisEl = shallowRef<HTMLDivElement>();

const {
    emojiPickerScale,
    emojiPickerWidth,
    emojiPickerHeight,
    recentlyUsedEmojis,
} = defaultStore.reactiveState;

const pinned = computed(() => props.pinnedEmojis);
const size = computed(() => emojiPickerScale.value);
const width = computed(() => emojiPickerWidth.value);
const height = computed(() => emojiPickerHeight.value);
const q = ref<string>('');
const searchResultCustom = ref<Misskey.entities.EmojiSimple[]>([]);
const searchResultUnicode = ref<UnicodeEmojiDef[]>([]);
const tab = ref<'index' | 'custom' | 'unicode' | 'tags'>('index');

const customEmojiFolderRoot: CustomEmojiFolderTree = { value: '', category: '', children: [] };

function parseAndMergeCategories(input: string, root: CustomEmojiFolderTree): CustomEmojiFolderTree {
    const parts = input.split('/').map(p => p.trim()); // スラッシュで区切って配列にしてる
    let currentNode: CustomEmojiFolderTree = root; // currentNode は root
    let includesPart = customEmojis.value.some(emoji => emoji.category.includes(parts[0]+'/')) ;
    console.log(includesPart)
   if (parts.length === 1 && parts[0] !== '' && includesPart) { // parts が 1 つで空じゃなかったら
       parts.push(parts[0]) // parts に parts[0] を追加 (test category だったら test/test category になる)
   }

    for (const part of parts) { // parts を順番に見ていく
        let existingNode = currentNode.children.find((node) => node.value === part); // currentNode の children から part と同じ value を持つ node を探す

        if (!existingNode) { // なかったら
            const newNode: CustomEmojiFolderTree = { value: part, category: input, children: [] }; // 新しい node を作る

            currentNode.children.push(newNode); // currentNode の children に newNode を追加
            existingNode = newNode; // existingNode に newNode を代入
        }

        currentNode = existingNode; // currentNode に existingNode を代入
    }
    return currentNode;
}

customEmojiCategories.value.forEach(ec => {
    if (ec !== null) {
        parseAndMergeCategories(ec, customEmojiFolderRoot);
    }
});

parseAndMergeCategories('', customEmojiFolderRoot);

watch(q, () => {
    if (emojisEl.value) emojisEl.value.scrollTop = 0;

    if (q.value === '') {
        searchResultCustom.value = [];
        searchResultUnicode.value = [];
        return;
    }

    const newQ = q.value.replace(/:/g, '').toLowerCase();

    const searchCustom = () => {
        const max = 100;
        const emojis = customEmojis.value;
        const matches = new Set<Misskey.entities.EmojiSimple>();

        const exactMatch = emojis.find(emoji => emoji.name === newQ);
        if (exactMatch) matches.add(exactMatch);

        if (newQ.includes(' ')) { // AND検索
            const keywords = newQ.split(' ');

            // 名前にキーワードが含まれている
            for (const emoji of emojis) {
                if (keywords.every(keyword => emoji.name.includes(keyword))) {
                    matches.add(emoji);
                    if (matches.size >= max) break;
                }
            }
            if (matches.size >= max) return matches;

            // 名前またはエイリアスにキーワードが含まれている
            for (const emoji of emojis) {
                if (keywords.every(keyword => emoji.name.includes(keyword) || emoji.aliases.some(alias => alias.includes(keyword)))) {
                    matches.add(emoji);
                    if (matches.size >= max) break;
                }
            }
        } else {
            for (const emoji of emojis) {
                if (emoji.name.startsWith(newQ)) {
                    matches.add(emoji);
                    if (matches.size >= max) break;
                }
            }
            if (matches.size >= max) return matches;

            for (const emoji of emojis) {
                if (emoji.aliases.some(alias => alias.startsWith(newQ))) {
                    matches.add(emoji);
                    if (matches.size >= max) break;
                }
            }
            if (matches.size >= max) return matches;

            for (const emoji of emojis) {
                if (emoji.name.includes(newQ)) {
                    matches.add(emoji);
                    if (matches.size >= max) break;
                }
            }
            if (matches.size >= max) return matches;

            for (const emoji of emojis) {
                if (emoji.aliases.some(alias => alias.includes(newQ))) {
                    matches.add(emoji);
                    if (matches.size >= max) break;
                }
            }
        }

        return matches;
    };

    const searchUnicode = () => {
        const max = 100;
        const emojis = emojilist;
        const matches = new Set<UnicodeEmojiDef>();

        const exactMatch = emojis.find(emoji => emoji.name === newQ);
        if (exactMatch) matches.add(exactMatch);

        if (newQ.includes(' ')) { // AND検索
            const keywords = newQ.split(' ');

            for (const emoji of emojis) {
                if (keywords.every(keyword => emoji.name.includes(keyword))) {
                    matches.add(emoji);
                    if (matches.size >= max) break;
                }
            }
            if (matches.size >= max) return matches;

            for (const index of Object.values(defaultStore.state.additionalUnicodeEmojiIndexes)) {
                for (const emoji of emojis) {
                    if (keywords.every(keyword => index[emoji.char].some(k => k.includes(keyword)))) {
                        matches.add(emoji);
                        if (matches.size >= max) break;
                    }
                }
            }
        } else {
            for (const emoji of emojis) {
                if (emoji.name.startsWith(newQ)) {
                    matches.add(emoji);
                    if (matches.size >= max) break;
                }
            }
            if (matches.size >= max) return matches;

            for (const index of Object.values(defaultStore.state.additionalUnicodeEmojiIndexes)) {
                for (const emoji of emojis) {
                    if (index[emoji.char].some(k => k.startsWith(newQ))) {
                        matches.add(emoji);
                        if (matches.size >= max) break;
                    }
                }
            }

            for (const emoji of emojis) {
                if (emoji.name.includes(newQ)) {
                    matches.add(emoji);
                    if (matches.size >= max) break;
                }
            }
            if (matches.size >= max) return matches;

            for (const index of Object.values(defaultStore.state.additionalUnicodeEmojiIndexes)) {
                for (const emoji of emojis) {
                    if (index[emoji.char].some(k => k.includes(newQ))) {
                        matches.add(emoji);
                        if (matches.size >= max) break;
                    }
                }
            }
        }

        return matches;
    };

    searchResultCustom.value = Array.from(searchCustom()).filter(filterAvailable);
    searchResultUnicode.value = Array.from(searchUnicode());
});

function filterAvailable(emoji: Misskey.entities.EmojiSimple): boolean {
    return (emoji.roleIdsThatCanBeUsedThisEmojiAsReaction == null || emoji.roleIdsThatCanBeUsedThisEmojiAsReaction.length === 0) || ($i && $i.roles.some(r => emoji.roleIdsThatCanBeUsedThisEmojiAsReaction.includes(r.id)));
}

function focus() {
    if (!['smartphone', 'tablet'].includes(deviceKind) && !isTouchUsing) {
        searchEl.value?.focus({
            preventScroll: true,
        });
    }
}

function reset() {
    if (emojisEl.value) emojisEl.value.scrollTop = 0;
    q.value = '';
}

function getKey(emoji: string | Misskey.entities.EmojiSimple | UnicodeEmojiDef): string {
    return typeof emoji === 'string' ? emoji : 'char' in emoji ? emoji.char : `:${emoji.name}:`;
}

/** @see MkEmojiPicker.section.vue */
function computeButtonTitle(ev: MouseEvent): void {
    const elm = ev.target as HTMLElement;
    const emoji = elm.dataset.emoji as string;
    elm.title = getEmojiName(emoji) ?? emoji;
}

function chosen(emoji: any, ev?: MouseEvent) {
    const el = ev && (ev.currentTarget ?? ev.target) as HTMLElement | null | undefined;
    if (el) {
        const rect = el.getBoundingClientRect();
        const x = rect.left + (el.offsetWidth / 2);
        const y = rect.top + (el.offsetHeight / 2);
        os.popup(MkRippleEffect, { x, y }, {}, 'end');
    }

    const key = getKey(emoji);
    emit('chosen', key);

    // 最近使った絵文字更新
    if (!pinned.value?.includes(key)) {
        let recents = defaultStore.state.recentlyUsedEmojis;
        recents = recents.filter((emoji: any) => emoji !== key);
        recents.unshift(key);
        defaultStore.set('recentlyUsedEmojis', recents.splice(0, 32));
    }
}

function input(): void {
    // Using custom input event instead of v-model to respond immediately on
    // Android, where composition happens on all languages
    // (v-model does not update during composition)
    q.value = searchEl.value?.value.trim() ?? '';
}

function paste(event: ClipboardEvent): void {
    const pasted = event.clipboardData?.getData('text') ?? '';
    if (done(pasted)) {
        event.preventDefault();
    }
}

function onEnter(ev: KeyboardEvent) {
    if (ev.isComposing || ev.key === 'Process' || ev.keyCode === 229) return;
    done();
}

function done(query?: string): boolean | void {
    if (query == null) query = q.value;
    if (query == null || typeof query !== 'string') return;

    const q2 = query.replace(/:/g, '');
    const exactMatchCustom = customEmojisMap.get(q2);
    if (exactMatchCustom) {
        chosen(exactMatchCustom);
        return true;
    }
    const exactMatchUnicode = emojilist.find(emoji => emoji.char === q2 || emoji.name === q2);
    if (exactMatchUnicode) {
        chosen(exactMatchUnicode);
        return true;
    }
    if (searchResultCustom.value.length > 0) {
        chosen(searchResultCustom.value[0]);
        return true;
    }
    if (searchResultUnicode.value.length > 0) {
        chosen(searchResultUnicode.value[0]);
        return true;
    }
}

onMounted(() => {
    focus();
});

defineExpose({
    focus,
    reset,
});
</script>

<style lang="scss" scoped>
.omfetrab {
  $pad: 8px;

  display: flex;
  flex-direction: column;

  &.s1 {
    --eachSize: 40px;
  }

  &.s2 {
    --eachSize: 45px;
  }

  &.s3 {
    --eachSize: 50px;
  }

  &.w1 {
    width: calc((var(--eachSize) * 5) + (#{$pad} * 2));
    --columns: 1fr 1fr 1fr 1fr 1fr;
  }

  &.w2 {
    width: calc((var(--eachSize) * 6) + (#{$pad} * 2));
    --columns: 1fr 1fr 1fr 1fr 1fr 1fr;
  }

  &.w3 {
    width: calc((var(--eachSize) * 7) + (#{$pad} * 2));
    --columns: 1fr 1fr 1fr 1fr 1fr 1fr 1fr;
  }

  &.w4 {
    width: calc((var(--eachSize) * 8) + (#{$pad} * 2));
    --columns: 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr;
  }

  &.w5 {
    width: calc((var(--eachSize) * 9) + (#{$pad} * 2));
    --columns: 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr;
  }

  &.h1 {
    height: calc((var(--eachSize) * 4) + (#{$pad} * 2));
  }

  &.h2 {
    height: calc((var(--eachSize) * 6) + (#{$pad} * 2));
  }

  &.h3 {
    height: calc((var(--eachSize) * 8) + (#{$pad} * 2));
  }

  &.h4 {
    height: calc((var(--eachSize) * 10) + (#{$pad} * 2));
  }

  &.asDrawer {
    width: 100% !important;

    > .emojis {
      ::v-deep(section) {
        > header {
          height: 32px;
          line-height: 32px;
          padding: 0 12px;
          font-size: 15px;
        }

        > .body {
          display: grid;
          grid-template-columns: var(--columns);
          font-size: 30px;

          > .item {
            aspect-ratio: 1 / 1;
            width: auto;
            height: auto;
            min-width: 0;
          }
        }
      }
    }
  }

  &.asWindow {
    width: 100% !important;
    height: 100% !important;

    > .emojis {
      ::v-deep(section) {
        > .body {
          display: grid;
          grid-template-columns: var(--columns);
          font-size: 30px;

          > .item {
            aspect-ratio: 1 / 1;
            width: auto;
            height: auto;
            min-width: 0;
          }
        }
      }
    }
  }

  > .search {
    width: 100%;
    padding: 12px;
    box-sizing: border-box;
    font-size: 1em;
    outline: none;
    border: none;
    background: transparent;
    color: var(--fg);

    &:not(:focus):not(.filled) {
      margin-bottom: env(safe-area-inset-bottom, 0px);
    }

    &:not(.filled) {
      order: 1;
      z-index: 2;
      box-shadow: 0px -1px 0 0px var(--divider);
    }
  }

  > .tabs {
    display: flex;
    display: none;

    > .tab {
      flex: 1;
      height: 38px;
      border-top: solid 0.5px var(--divider);

      &.active {
        border-top: solid 1px var(--accent);
        color: var(--accent);
      }
    }
  }

  > .emojis {
    height: 100%;
    overflow-y: auto;
    overflow-x: hidden;

    scrollbar-width: none;

    &::-webkit-scrollbar {
      display: none;
    }

    > .group {
      &:not(.index) {
        padding: 4px 0 8px 0;
        border-top: solid 0.5px var(--divider);
      }

      > header {
        /*position: sticky;
top: 0;
left: 0;*/
        height: 32px;
        line-height: 32px;
        z-index: 2;
        padding: 0 8px;
        font-size: 12px;
      }
    }

    ::v-deep(section) {
      > header {
        position: sticky;
        top: 0;
        left: 0;
        height: 32px;
        line-height: 32px;
        z-index: 1;
        padding: 0 8px;
        font-size: 12px;
        cursor: pointer;

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

      > .body {
        position: relative;
        padding: $pad;

        > .item {
          position: relative;
          padding: 0;
          width: var(--eachSize);
          height: var(--eachSize);
          contain: strict;
          border-radius: 4px;
          font-size: 24px;

          &:focus-visible {
            outline: solid 2px var(--focus);
            z-index: 1;
          }

          &:hover {
            background: rgba(0, 0, 0, 0.05);
          }

          &:active {
            background: var(--accent);
            box-shadow: inset 0 0.15em 0.3em rgba(27, 31, 35, 0.15);
          }

          > .emoji {
            height: 1.25em;
            vertical-align: -.25em;
            pointer-events: none;
            width: 100%;
            object-fit: contain;
          }
        }
      }

      &.result {
        border-bottom: solid 0.5px var(--divider);

        &:empty {
          display: none;
        }
      }
    }
  }
}
</style>