import './compose.css';

import '@github/text-expander-element';
import equal from 'fast-deep-equal';
import { forwardRef } from 'preact/compat';
import { useEffect, useRef, useState } from 'preact/hooks';
import { useHotkeys } from 'react-hotkeys-hook';
import stringLength from 'string-length';
import { uid } from 'uid/single';
import { useDebouncedCallback } from 'use-debounce';
import { useSnapshot } from 'valtio';

import supportedLanguages from '../data/status-supported-languages';
import urlRegex from '../data/url-regex';
import { api } from '../utils/api';
import db from '../utils/db';
import emojifyText from '../utils/emojify-text';
import openCompose from '../utils/open-compose';
import states, { saveStatus } from '../utils/states';
import store from '../utils/store';
import {
  getCurrentAccount,
  getCurrentAccountNS,
  getCurrentInstance,
} from '../utils/store-utils';
import supports from '../utils/supports';
import useInterval from '../utils/useInterval';
import visibilityIconsMap from '../utils/visibility-icons-map';

import Avatar from './avatar';
import Icon from './icon';
import Loader from './loader';
import Modal from './modal';
import Status from './status';

const supportedLanguagesMap = supportedLanguages.reduce((acc, l) => {
  const [code, common, native] = l;
  acc[code] = {
    common,
    native,
  };
  return acc;
}, {});

/* NOTES:
  - Max character limit includes BOTH status text and Content Warning text
*/

const expiryOptions = {
  '5 minutes': 5 * 60,
  '30 minutes': 30 * 60,
  '1 hour': 60 * 60,
  '6 hours': 6 * 60 * 60,
  '12 hours': 12 * 60 * 60,
  '1 day': 24 * 60 * 60,
  '3 days': 3 * 24 * 60 * 60,
  '7 days': 7 * 24 * 60 * 60,
};
const expirySeconds = Object.values(expiryOptions);
const oneDay = 24 * 60 * 60;

const expiresInFromExpiresAt = (expiresAt) => {
  if (!expiresAt) return oneDay;
  const delta = (new Date(expiresAt).getTime() - Date.now()) / 1000;
  return expirySeconds.find((s) => s >= delta) || oneDay;
};

const menu = document.createElement('ul');
menu.role = 'listbox';
menu.className = 'text-expander-menu';

// Set IntersectionObserver on menu, reposition it because text-expander doesn't handle it
const windowMargin = 16;
const observer = new IntersectionObserver((entries) => {
  entries.forEach((entry) => {
    if (entry.isIntersecting) {
      const { left, width } = entry.boundingClientRect;
      const { innerWidth } = window;
      if (left + width > innerWidth) {
        menu.style.left = innerWidth - width - windowMargin + 'px';
      }
    }
  });
});
observer.observe(menu);

const DEFAULT_LANG = 'en';

// https://github.com/mastodon/mastodon/blob/c4a429ed47e85a6bbf0d470a41cc2f64cf120c19/app/javascript/mastodon/features/compose/util/counter.js
const urlRegexObj = new RegExp(urlRegex.source, urlRegex.flags);
const usernameRegex = /(^|[^\/\w])@(([a-z0-9_]+)@[a-z0-9\.\-]+[a-z0-9]+)/gi;
const urlPlaceholder = '$2xxxxxxxxxxxxxxxxxxxxxxx';
function countableText(inputText) {
  return inputText
    .replace(urlRegexObj, urlPlaceholder)
    .replace(usernameRegex, '$1@$3');
}

function Compose({
  onClose,
  replyToStatus,
  editStatus,
  draftStatus,
  standalone,
  hasOpener,
}) {
  console.warn('RENDER COMPOSER');
  const { masto } = api();
  const [uiState, setUIState] = useState('default');
  const UID = useRef(draftStatus?.uid || uid());
  console.log('Compose UID', UID.current);

  const currentAccount = getCurrentAccount();
  const currentAccountInfo = currentAccount.info;

  const { configuration } = getCurrentInstance();
  console.log('⚙️ Configuration', configuration);

  const {
    statuses: { maxCharacters, maxMediaAttachments, charactersReservedPerUrl },
    mediaAttachments: {
      supportedMimeTypes,
      imageSizeLimit,
      imageMatrixLimit,
      videoSizeLimit,
      videoMatrixLimit,
      videoFrameRateLimit,
    },
    polls: { maxOptions, maxCharactersPerOption, maxExpiration, minExpiration },
  } = configuration;

  const textareaRef = useRef();
  const spoilerTextRef = useRef();
  const [visibility, setVisibility] = useState('public');
  const [sensitive, setSensitive] = useState(false);
  const [language, setLanguage] = useState(
    store.session.get('currentLanguage') || DEFAULT_LANG,
  );
  const [mediaAttachments, setMediaAttachments] = useState([]);
  const [poll, setPoll] = useState(null);

  const prefs = store.account.get('preferences') || {};

  const customEmojis = useRef();
  useEffect(() => {
    (async () => {
      try {
        const emojis = await masto.v1.customEmojis.list();
        console.log({ emojis });
        customEmojis.current = emojis;
      } catch (e) {
        // silent fail
        console.error(e);
      }
    })();
  }, []);

  const oninputTextarea = () => {
    if (!textareaRef.current) return;
    textareaRef.current.dispatchEvent(new Event('input'));
  };
  const focusTextarea = () => {
    setTimeout(() => {
      console.debug('FOCUS textarea');
      textareaRef.current?.focus();
    }, 300);
  };

  useEffect(() => {
    if (replyToStatus) {
      const { spoilerText, visibility, language, sensitive } = replyToStatus;
      if (spoilerText && spoilerTextRef.current) {
        spoilerTextRef.current.value = spoilerText;
      }
      const mentions = new Set([
        replyToStatus.account.acct,
        ...replyToStatus.mentions.map((m) => m.acct),
      ]);
      const allMentions = [...mentions].filter(
        (m) => m !== currentAccountInfo.acct,
      );
      if (allMentions.length > 0) {
        textareaRef.current.value = `${allMentions
          .map((m) => `@${m}`)
          .join(' ')} `;
        oninputTextarea();
      }
      focusTextarea();
      setVisibility(visibility);
      setLanguage(language || prefs.postingDefaultLanguage || DEFAULT_LANG);
      setSensitive(sensitive);
    }
    if (draftStatus) {
      const {
        status,
        spoilerText,
        visibility,
        language,
        sensitive,
        poll,
        mediaAttachments,
      } = draftStatus;
      const composablePoll = !!poll?.options && {
        ...poll,
        options: poll.options.map((o) => o?.title || o),
        expiresIn: poll?.expiresIn || expiresInFromExpiresAt(poll.expiresAt),
      };
      textareaRef.current.value = status;
      oninputTextarea();
      focusTextarea();
      spoilerTextRef.current.value = spoilerText;
      setVisibility(visibility);
      setLanguage(language || prefs.postingDefaultLanguage || DEFAULT_LANG);
      setSensitive(sensitive);
      setPoll(composablePoll);
      setMediaAttachments(mediaAttachments);
    } else if (editStatus) {
      const { visibility, language, sensitive, poll, mediaAttachments } =
        editStatus;
      const composablePoll = !!poll?.options && {
        ...poll,
        options: poll.options.map((o) => o?.title || o),
        expiresIn: poll?.expiresIn || expiresInFromExpiresAt(poll.expiresAt),
      };
      setUIState('loading');
      (async () => {
        try {
          const statusSource = await masto.v1.statuses.fetchSource(
            editStatus.id,
          );
          console.log({ statusSource });
          const { text, spoilerText } = statusSource;
          textareaRef.current.value = text;
          textareaRef.current.dataset.source = text;
          oninputTextarea();
          focusTextarea();
          spoilerTextRef.current.value = spoilerText;
          setVisibility(visibility);
          setLanguage(language || presf.postingDefaultLanguage || DEFAULT_LANG);
          setSensitive(sensitive);
          setPoll(composablePoll);
          setMediaAttachments(mediaAttachments);
          setUIState('default');
        } catch (e) {
          console.error(e);
          alert(e?.reason || e);
          setUIState('error');
        }
      })();
    } else {
      focusTextarea();
      console.log('Apply prefs', prefs);
      if (prefs.postingDefaultVisibility) {
        setVisibility(prefs.postingDefaultVisibility);
      }
      if (prefs.postingDefaultLanguage) {
        setLanguage(prefs.postingDefaultLanguage);
      }
      if (prefs.postingDefaultSensitive) {
        setSensitive(prefs.postingDefaultSensitive);
      }
    }
  }, [draftStatus, editStatus, replyToStatus]);

  const formRef = useRef();

  const beforeUnloadCopy = 'You have unsaved changes. Discard this post?';
  const canClose = () => {
    const { value, dataset } = textareaRef.current;

    // check if loading
    if (uiState === 'loading') {
      console.log('canClose', { uiState });
      return false;
    }

    // check for status and media attachments
    const hasMediaAttachments = mediaAttachments.length > 0;
    if (!value && !hasMediaAttachments) {
      console.log('canClose', { value, mediaAttachments });
      return true;
    }

    // check if all media attachments have IDs
    const hasIDMediaAttachments =
      mediaAttachments.length > 0 &&
      mediaAttachments.every((media) => media.id);
    if (hasIDMediaAttachments) {
      console.log('canClose', { hasIDMediaAttachments });
      return true;
    }

    // check if status contains only "@acct", if replying
    const isSelf = replyToStatus?.account.id === currentAccountInfo.id;
    const hasOnlyAcct =
      replyToStatus && value.trim() === `@${replyToStatus.account.acct}`;
    // TODO: check for mentions, or maybe just generic "@username<space>", including multiple mentions like "@username1<space>@username2<space>"
    if (!isSelf && hasOnlyAcct) {
      console.log('canClose', { isSelf, hasOnlyAcct });
      return true;
    }

    // check if status is same with source
    const sameWithSource = value === dataset?.source;
    if (sameWithSource) {
      console.log('canClose', { sameWithSource });
      return true;
    }

    console.log('canClose', {
      value,
      hasMediaAttachments,
      hasIDMediaAttachments,
      poll,
      isSelf,
      hasOnlyAcct,
      sameWithSource,
      uiState,
    });

    return false;
  };

  const confirmClose = () => {
    if (!canClose()) {
      const yes = confirm(beforeUnloadCopy);
      return yes;
    }
    return true;
  };

  useEffect(() => {
    // Show warning if user tries to close window with unsaved changes
    const handleBeforeUnload = (e) => {
      if (!canClose()) {
        e.preventDefault();
        e.returnValue = beforeUnloadCopy;
      }
    };
    window.addEventListener('beforeunload', handleBeforeUnload, {
      capture: true,
    });
    return () =>
      window.removeEventListener('beforeunload', handleBeforeUnload, {
        capture: true,
      });
  }, []);

  const getCharCount = () => {
    const { value } = textareaRef.current;
    const { value: spoilerText } = spoilerTextRef.current;
    return stringLength(countableText(value)) + stringLength(spoilerText);
  };
  const updateCharCount = () => {
    const count = getCharCount();
    states.composerCharacterCount = count;
  };
  useEffect(updateCharCount, []);

  useHotkeys(
    'esc',
    () => {
      if (!standalone && confirmClose()) {
        onClose();
      }
    },
    {
      enableOnFormTags: true,
      // Use keyup because Esc keydown will close the confirm dialog on Safari
      keyup: true,
    },
  );

  const prevBackgroundDraft = useRef({});
  const draftKey = () => {
    const ns = getCurrentAccountNS();
    return `${ns}#${UID.current}`;
  };
  const saveUnsavedDraft = () => {
    // Not enabling this for editing status
    // I don't think this warrant a draft mode for a status that's already posted
    // Maybe it could be a big edit change but it should be rare
    if (editStatus) return;
    const key = draftKey();
    const backgroundDraft = {
      key,
      replyTo: replyToStatus
        ? {
            /* Smaller payload of replyToStatus. Reasons:
              - No point storing whole thing
              - Could have media attachments
              - Could be deleted/edited later
            */
            id: replyToStatus.id,
            account: {
              id: replyToStatus.account.id,
              username: replyToStatus.account.username,
              acct: replyToStatus.account.acct,
            },
          }
        : null,
      draftStatus: {
        uid: UID.current,
        status: textareaRef.current.value,
        spoilerText: spoilerTextRef.current.value,
        visibility,
        language,
        sensitive,
        poll,
        mediaAttachments,
      },
    };
    if (!equal(backgroundDraft, prevBackgroundDraft.current) && !canClose()) {
      console.debug('not equal', backgroundDraft, prevBackgroundDraft.current);
      db.drafts
        .set(key, {
          ...backgroundDraft,
          state: 'unsaved',
          updatedAt: Date.now(),
        })
        .then(() => {
          console.debug('DRAFT saved', key, backgroundDraft);
        })
        .catch((e) => {
          console.error('DRAFT failed', key, e);
        });
      prevBackgroundDraft.current = structuredClone(backgroundDraft);
    }
  };
  useInterval(saveUnsavedDraft, 5000); // background save every 5s
  useEffect(() => {
    saveUnsavedDraft();
    // If unmounted, means user discarded the draft
    // Also means pop-out 🙈, but it's okay because the pop-out will persist the ID and re-create the draft
    return () => {
      db.drafts.del(draftKey());
    };
  }, []);

  useEffect(() => {
    const handleItems = (e) => {
      if (mediaAttachments.length >= maxMediaAttachments) {
        alert(`You can only attach up to ${maxMediaAttachments} files.`);
        return;
      }
      const { items } = e.clipboardData || e.dataTransfer;
      const files = [];
      for (let i = 0; i < items.length; i++) {
        const item = items[i];
        if (item.kind === 'file') {
          const file = item.getAsFile();
          if (file && supportedMimeTypes.includes(file.type)) {
            files.push(file);
          }
        }
      }
      console.log({ files });
      if (files.length > 0) {
        e.preventDefault();
        e.stopPropagation();
        // Auto-cut-off files to avoid exceeding maxMediaAttachments
        const max = maxMediaAttachments - mediaAttachments.length;
        const allowedFiles = files.slice(0, max);
        if (allowedFiles.length <= 0) {
          alert(`You can only attach up to ${maxMediaAttachments} files.`);
          return;
        }
        const mediaFiles = allowedFiles.map((file) => ({
          file,
          type: file.type,
          size: file.size,
          url: URL.createObjectURL(file),
          id: null,
          description: null,
        }));
        setMediaAttachments([...mediaAttachments, ...mediaFiles]);
      }
    };
    window.addEventListener('paste', handleItems);
    const handleDragover = (e) => {
      // Prevent default if there's files
      if (e.dataTransfer.items.length > 0) {
        e.preventDefault();
        e.stopPropagation();
      }
    };
    window.addEventListener('dragover', handleDragover);
    window.addEventListener('drop', handleItems);
    return () => {
      window.removeEventListener('paste', handleItems);
      window.removeEventListener('dragover', handleDragover);
      window.removeEventListener('drop', handleItems);
    };
  }, [mediaAttachments]);

  return (
    <div id="compose-container" class={standalone ? 'standalone' : ''}>
      <div class="compose-top">
        {currentAccountInfo?.avatarStatic && (
          <Avatar
            url={currentAccountInfo.avatarStatic}
            size="l"
            alt={currentAccountInfo.username}
          />
        )}
        {!standalone ? (
          <span>
            <button
              type="button"
              class="light pop-button"
              disabled={uiState === 'loading'}
              onClick={() => {
                // If there are non-ID media attachments (not yet uploaded), show confirmation dialog because they are not going to be passed to the new window
                // const containNonIDMediaAttachments =
                //   mediaAttachments.length > 0 &&
                //   mediaAttachments.some((media) => !media.id);
                // if (containNonIDMediaAttachments) {
                //   const yes = confirm(
                //     'You have media attachments that are not yet uploaded. Opening a new window will discard them and you will need to re-attach them. Are you sure you want to continue?',
                //   );
                //   if (!yes) {
                //     return;
                //   }
                // }

                // const mediaAttachmentsWithIDs = mediaAttachments.filter(
                //   (media) => media.id,
                // );

                const newWin = openCompose({
                  editStatus,
                  replyToStatus,
                  draftStatus: {
                    uid: UID.current,
                    status: textareaRef.current.value,
                    spoilerText: spoilerTextRef.current.value,
                    visibility,
                    language,
                    sensitive,
                    poll,
                    mediaAttachments,
                  },
                });

                if (!newWin) {
                  alert('Looks like your browser is blocking popups.');
                  return;
                }

                onClose();
              }}
            >
              <Icon icon="popout" alt="Pop out" />
            </button>{' '}
            <button
              type="button"
              class="light close-button"
              disabled={uiState === 'loading'}
              onClick={() => {
                if (confirmClose()) {
                  onClose();
                }
              }}
            >
              <Icon icon="x" />
            </button>
          </span>
        ) : (
          hasOpener && (
            <button
              type="button"
              class="light pop-button"
              disabled={uiState === 'loading'}
              onClick={() => {
                // If there are non-ID media attachments (not yet uploaded), show confirmation dialog because they are not going to be passed to the new window
                // const containNonIDMediaAttachments =
                //   mediaAttachments.length > 0 &&
                //   mediaAttachments.some((media) => !media.id);
                // if (containNonIDMediaAttachments) {
                //   const yes = confirm(
                //     'You have media attachments that are not yet uploaded. Opening a new window will discard them and you will need to re-attach them. Are you sure you want to continue?',
                //   );
                //   if (!yes) {
                //     return;
                //   }
                // }

                if (!window.opener) {
                  alert('Looks like you closed the parent window.');
                  return;
                }

                if (window.opener.__STATES__.showCompose) {
                  const yes = confirm(
                    'Looks like you already have a compose field open in the parent window. Popping in this window will discard the changes you made in the parent window. Continue?',
                  );
                  if (!yes) return;
                }

                // const mediaAttachmentsWithIDs = mediaAttachments.filter(
                //   (media) => media.id,
                // );

                onClose({
                  fn: () => {
                    const passData = {
                      editStatus,
                      replyToStatus,
                      draftStatus: {
                        uid: UID.current,
                        status: textareaRef.current.value,
                        spoilerText: spoilerTextRef.current.value,
                        visibility,
                        language,
                        sensitive,
                        poll,
                        mediaAttachments,
                      },
                    };
                    window.opener.__COMPOSE__ = passData; // Pass it here instead of `showCompose` due to some weird proxy issue again
                    window.opener.__STATES__.showCompose = true;
                  },
                });
              }}
            >
              <Icon icon="popin" alt="Pop in" />
            </button>
          )
        )}
      </div>
      {!!replyToStatus && (
        <div class="status-preview">
          <Status status={replyToStatus} size="s" />
          <div class="status-preview-legend reply-to">
            Replying to @
            {replyToStatus.account.acct || replyToStatus.account.username}
            &rsquo;s status
          </div>
        </div>
      )}
      {!!editStatus && (
        <div class="status-preview">
          <Status status={editStatus} size="s" />
          <div class="status-preview-legend">Editing source status</div>
        </div>
      )}
      <form
        ref={formRef}
        style={{
          pointerEvents: uiState === 'loading' ? 'none' : 'auto',
          opacity: uiState === 'loading' ? 0.5 : 1,
        }}
        onKeyDown={(e) => {
          if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) {
            formRef.current.dispatchEvent(
              new Event('submit', { cancelable: true }),
            );
          }
        }}
        onSubmit={(e) => {
          e.preventDefault();

          const formData = new FormData(e.target);
          const entries = Object.fromEntries(formData.entries());
          console.log('ENTRIES', entries);
          let { status, visibility, sensitive, spoilerText } = entries;

          // Pre-cleanup
          sensitive = sensitive === 'on'; // checkboxes return "on" if checked

          // Validation
          /* Let the backend validate this
          if (stringLength(status) > maxCharacters) {
            alert(`Status is too long! Max characters: ${maxCharacters}`);
            return;
          }
          if (
            sensitive &&
            stringLength(status) + stringLength(spoilerText) > maxCharacters
          ) {
            alert(
              `Status and content warning is too long! Max characters: ${maxCharacters}`,
            );
            return;
          }
          */
          if (poll) {
            if (poll.options.length < 2) {
              alert('Poll must have at least 2 options');
              return;
            }
            if (poll.options.some((option) => option === '')) {
              alert('Some poll choices are empty');
              return;
            }
          }
          // TODO: check for URLs and use `charactersReservedPerUrl` to calculate max characters

          // Post-cleanup
          spoilerText = (sensitive && spoilerText) || undefined;
          status = status === '' ? undefined : status;

          setUIState('loading');
          (async () => {
            try {
              console.log('MEDIA ATTACHMENTS', mediaAttachments);
              if (mediaAttachments.length > 0) {
                // Upload media attachments first
                const mediaPromises = mediaAttachments.map((attachment) => {
                  const { file, description, id } = attachment;
                  console.log('UPLOADING', attachment);
                  if (id) {
                    // If already uploaded
                    return attachment;
                  } else {
                    const params = removeNullUndefined({
                      file,
                      description,
                    });
                    return masto.v2.mediaAttachments
                      .create(params)
                      .then((res) => {
                        if (res.id) {
                          attachment.id = res.id;
                        }
                        return res;
                      });
                  }
                });
                const results = await Promise.allSettled(mediaPromises);

                // If any failed, return
                if (
                  results.some((result) => {
                    return result.status === 'rejected' || !result.value?.id;
                  })
                ) {
                  setUIState('error');
                  // Alert all the reasons
                  results.forEach((result) => {
                    if (result.status === 'rejected') {
                      console.error(result);
                      alert(result.reason || `Attachment #${i} failed`);
                    }
                  });
                  return;
                }

                console.log({ results, mediaAttachments });
              }

              /* NOTE:
                Using snakecase here because masto.js's `isObject` returns false for `params`, ONLY happens when opening in pop-out window. This is maybe due to `window.masto` variable being passed from the parent window. The check that failed is `x.constructor === Object`, so maybe the `Object` in new window is different than parent window's?
                Code: https://github.com/neet/masto.js/blob/dd0d649067b6a2b6e60fbb0a96597c373a255b00/src/serializers/is-object.ts#L2
              */
              let params = {
                status,
                // spoilerText,
                spoiler_text: spoilerText,
                language,
                sensitive,
                poll,
                // mediaIds: mediaAttachments.map((attachment) => attachment.id),
                media_ids: mediaAttachments.map((attachment) => attachment.id),
              };
              if (editStatus && supports('@mastodon/edit-media-attributes')) {
                params.media_attributes = mediaAttachments.map((attachment) => {
                  return {
                    id: attachment.id,
                    description: attachment.description,
                    // focus
                    // thumbnail
                  };
                });
              } else if (!editStatus) {
                params.visibility = visibility;
                // params.inReplyToId = replyToStatus?.id || undefined;
                params.in_reply_to_id = replyToStatus?.id || undefined;
              }
              params = removeNullUndefined(params);
              console.log('POST', params);

              let newStatus;
              if (editStatus) {
                newStatus = await masto.v1.statuses.update(
                  editStatus.id,
                  params,
                );
                saveStatus(newStatus, {
                  skipThreading: true,
                });
              } else {
                newStatus = await masto.v1.statuses.create(params, {
                  idempotencyKey: UID.current,
                });
              }
              setUIState('default');

              // Close
              onClose({
                newStatus,
              });
            } catch (e) {
              console.error(e);
              alert(e?.reason || e);
              setUIState('error');
            }
          })();
        }}
      >
        <div class="toolbar stretch">
          <input
            ref={spoilerTextRef}
            type="text"
            name="spoilerText"
            placeholder="Content warning"
            disabled={uiState === 'loading'}
            class="spoiler-text-field"
            lang={language}
            spellCheck="true"
            style={{
              opacity: sensitive ? 1 : 0,
              pointerEvents: sensitive ? 'auto' : 'none',
            }}
            onInput={() => {
              updateCharCount();
            }}
          />
          <label
            class="toolbar-button"
            title="Content warning or sensitive media"
          >
            <input
              name="sensitive"
              type="checkbox"
              checked={sensitive}
              disabled={uiState === 'loading'}
              onChange={(e) => {
                const sensitive = e.target.checked;
                setSensitive(sensitive);
                if (sensitive) {
                  spoilerTextRef.current?.focus();
                } else {
                  textareaRef.current?.focus();
                }
              }}
            />
            <Icon icon={`eye-${sensitive ? 'close' : 'open'}`} />
          </label>{' '}
          <label
            class={`toolbar-button ${
              visibility !== 'public' && !sensitive ? 'show-field' : ''
            }`}
            title={`Visibility: ${visibility}`}
          >
            <Icon icon={visibilityIconsMap[visibility]} alt={visibility} />
            <select
              name="visibility"
              value={visibility}
              onChange={(e) => {
                setVisibility(e.target.value);
              }}
              disabled={uiState === 'loading' || !!editStatus}
            >
              <option value="public">
                Public <Icon icon="earth" />
              </option>
              <option value="unlisted">Unlisted</option>
              <option value="private">Followers only</option>
              <option value="direct">Direct</option>
            </select>
          </label>{' '}
        </div>
        <Textarea
          ref={textareaRef}
          placeholder={
            replyToStatus
              ? 'Post your reply'
              : editStatus
              ? 'Edit your status'
              : 'What are you doing?'
          }
          required={mediaAttachments.length === 0}
          disabled={uiState === 'loading'}
          lang={language}
          onInput={() => {
            updateCharCount();
          }}
          maxCharacters={maxCharacters}
          performSearch={(params) => {
            return masto.v2.search(params);
          }}
        />
        {mediaAttachments.length > 0 && (
          <div class="media-attachments">
            {mediaAttachments.map((attachment, i) => {
              const { id, file } = attachment;
              const fileID = file?.size + file?.type + file?.name;
              return (
                <MediaAttachment
                  key={id || fileID || i}
                  attachment={attachment}
                  disabled={uiState === 'loading'}
                  lang={language}
                  onDescriptionChange={(value) => {
                    setMediaAttachments((attachments) => {
                      const newAttachments = [...attachments];
                      newAttachments[i].description = value;
                      return newAttachments;
                    });
                  }}
                  onRemove={() => {
                    setMediaAttachments((attachments) => {
                      return attachments.filter((_, j) => j !== i);
                    });
                  }}
                />
              );
            })}
            <label class="media-sensitive">
              <input
                name="sensitive"
                type="checkbox"
                checked={sensitive}
                disabled={uiState === 'loading'}
                onChange={(e) => {
                  const sensitive = e.target.checked;
                  setSensitive(sensitive);
                }}
              />{' '}
              <span>Mark media as sensitive</span>{' '}
              <Icon icon={`eye-${sensitive ? 'close' : 'open'}`} />
            </label>
          </div>
        )}
        {!!poll && (
          <Poll
            lang={language}
            maxOptions={maxOptions}
            maxExpiration={maxExpiration}
            minExpiration={minExpiration}
            maxCharactersPerOption={maxCharactersPerOption}
            poll={poll}
            disabled={uiState === 'loading'}
            onInput={(poll) => {
              if (poll) {
                const newPoll = { ...poll };
                setPoll(newPoll);
              } else {
                setPoll(null);
              }
            }}
          />
        )}
        <div class="toolbar">
          <label class="toolbar-button">
            <input
              type="file"
              accept={supportedMimeTypes.join(',')}
              multiple={mediaAttachments.length < maxMediaAttachments - 1}
              disabled={
                uiState === 'loading' ||
                mediaAttachments.length >= maxMediaAttachments ||
                !!poll
              }
              onChange={(e) => {
                const files = e.target.files;
                if (!files) return;

                const mediaFiles = Array.from(files).map((file) => ({
                  file,
                  type: file.type,
                  size: file.size,
                  url: URL.createObjectURL(file),
                  id: null, // indicate uploaded state
                  description: null,
                }));
                console.log('MEDIA ATTACHMENTS', files, mediaFiles);

                // Validate max media attachments
                if (
                  mediaAttachments.length + mediaFiles.length >
                  maxMediaAttachments
                ) {
                  alert(
                    `You can only attach up to ${maxMediaAttachments} files.`,
                  );
                } else {
                  setMediaAttachments((attachments) => {
                    return attachments.concat(mediaFiles);
                  });
                }
                // Reset
                e.target.value = '';
              }}
            />
            <Icon icon="attachment" />
          </label>{' '}
          <button
            type="button"
            class="toolbar-button"
            disabled={
              uiState === 'loading' || !!poll || !!mediaAttachments.length
            }
            onClick={() => {
              setPoll({
                options: ['', ''],
                expiresIn: 24 * 60 * 60, // 1 day
                multiple: false,
              });
            }}
          >
            <Icon icon="poll" alt="Add poll" />
          </button>{' '}
          <div class="spacer" />
          {uiState === 'loading' && <Loader abrupt />}{' '}
          {uiState !== 'loading' && (
            <CharCountMeter maxCharacters={maxCharacters} />
          )}
          <label class="toolbar-button">
            <span class="icon-text">
              {supportedLanguagesMap[language]?.native}
            </span>
            <select
              name="language"
              value={language}
              onChange={(e) => {
                const { value } = e.target;
                setLanguage(value || DEFAULT_LANG);
                store.session.set('language', value);
              }}
              disabled={uiState === 'loading'}
            >
              {supportedLanguages
                .sort(([, commonA], [, commonB]) => {
                  return commonA.localeCompare(commonB);
                })
                .map(([code, common, native]) => (
                  <option value={code}>
                    {common} ({native})
                  </option>
                ))}
            </select>
          </label>{' '}
          <button type="submit" class="large" disabled={uiState === 'loading'}>
            {replyToStatus ? 'Reply' : editStatus ? 'Update' : 'Post'}
          </button>
        </div>
      </form>
    </div>
  );
}

const Textarea = forwardRef((props, ref) => {
  const [text, setText] = useState(ref.current?.value || '');
  const { maxCharacters, performSearch = () => {}, ...textareaProps } = props;
  const snapStates = useSnapshot(states);
  const charCount = snapStates.composerCharacterCount;

  const textExpanderRef = useRef();
  const textExpanderTextRef = useRef('');
  useEffect(() => {
    let handleChange, handleValue, handleCommited;
    if (textExpanderRef.current) {
      handleChange = (e) => {
        // console.log('text-expander-change', e);
        const { key, provide, text } = e.detail;
        textExpanderTextRef.current = text;

        if (text === '') {
          provide(
            Promise.resolve({
              matched: false,
            }),
          );
          return;
        }

        if (key === ':') {
          // const emojis = customEmojis.current.filter((emoji) =>
          //   emoji.shortcode.startsWith(text),
          // );
          const emojis = filterShortcodes(customEmojis.current, text);
          let html = '';
          emojis.forEach((emoji) => {
            const { shortcode, url } = emoji;
            html += `
                <li role="option" data-value="${encodeHTML(shortcode)}">
                <img src="${encodeHTML(
                  url,
                )}" width="16" height="16" alt="" loading="lazy" /> 
                :${encodeHTML(shortcode)}:
              </li>`;
          });
          // console.log({ emojis, html });
          menu.innerHTML = html;
          provide(
            Promise.resolve({
              matched: emojis.length > 0,
              fragment: menu,
            }),
          );
          return;
        }

        const type = {
          '@': 'accounts',
          '#': 'hashtags',
        }[key];
        provide(
          new Promise((resolve) => {
            const searchResults = performSearch({
              type,
              q: text,
              limit: 5,
            });
            searchResults.then((value) => {
              if (text !== textExpanderTextRef.current) {
                return;
              }
              console.log({ value, type, v: value[type] });
              const results = value[type];
              console.log('RESULTS', value, results);
              let html = '';
              results.forEach((result) => {
                const {
                  name,
                  avatarStatic,
                  displayName,
                  username,
                  acct,
                  emojis,
                } = result;
                const displayNameWithEmoji = emojifyText(displayName, emojis);
                // const item = menuItem.cloneNode();
                if (acct) {
                  html += `
                    <li role="option" data-value="${encodeHTML(acct)}">
                      <span class="avatar">
                        <img src="${encodeHTML(
                          avatarStatic,
                        )}" width="16" height="16" alt="" loading="lazy" />
                      </span>
                      <span>
                        <b>${displayNameWithEmoji || username}</b>
                        <br>@${encodeHTML(acct)}
                      </span>
                    </li>
                  `;
                } else {
                  html += `
                    <li role="option" data-value="${encodeHTML(name)}">
                      <span>#<b>${encodeHTML(name)}</b></span>
                    </li>
                  `;
                }
                menu.innerHTML = html;
              });
              console.log('MENU', results, menu);
              resolve({
                matched: results.length > 0,
                fragment: menu,
              });
            });
          }),
        );
      };

      textExpanderRef.current.addEventListener(
        'text-expander-change',
        handleChange,
      );

      handleValue = (e) => {
        const { key, item } = e.detail;
        if (key === ':') {
          e.detail.value = `:${item.dataset.value}:`;
        } else {
          e.detail.value = `${key}${item.dataset.value}`;
        }
      };

      textExpanderRef.current.addEventListener(
        'text-expander-value',
        handleValue,
      );

      handleCommited = (e) => {
        const { input } = e.detail;
        setText(input.value);
      };

      textExpanderRef.current.addEventListener(
        'text-expander-committed',
        handleCommited,
      );
    }

    return () => {
      if (textExpanderRef.current) {
        textExpanderRef.current.removeEventListener(
          'text-expander-change',
          handleChange,
        );
        textExpanderRef.current.removeEventListener(
          'text-expander-value',
          handleValue,
        );
        textExpanderRef.current.removeEventListener(
          'text-expander-committed',
          handleCommited,
        );
      }
    };
  }, []);

  return (
    <text-expander ref={textExpanderRef} keys="@ # :">
      <textarea
        autoCapitalize="sentences"
        autoComplete="on"
        autoCorrect="on"
        spellCheck="true"
        dir="auto"
        rows="6"
        cols="50"
        {...textareaProps}
        ref={ref}
        name="status"
        value={text}
        onInput={(e) => {
          const { scrollHeight, offsetHeight, clientHeight, value } = e.target;
          setText(value);
          const offset = offsetHeight - clientHeight;
          e.target.style.height = value ? scrollHeight + offset + 'px' : null;
          props.onInput?.(e);
        }}
        style={{
          width: '100%',
          height: '4em',
          '--text-weight': (1 + charCount / 140).toFixed(1) || 1,
        }}
      />
    </text-expander>
  );
});

function CharCountMeter({ maxCharacters = 500 }) {
  const snapStates = useSnapshot(states);
  const charCount = snapStates.composerCharacterCount;
  const leftChars = maxCharacters - charCount;
  if (charCount <= maxCharacters / 2) {
    return null;
  }
  return (
    <meter
      class={`donut ${
        leftChars <= -10
          ? 'explode'
          : leftChars <= 0
          ? 'danger'
          : leftChars <= 20
          ? 'warning'
          : ''
      }`}
      value={charCount}
      max={maxCharacters}
      data-left={leftChars}
      style={{
        '--percentage': (charCount / maxCharacters) * 100,
      }}
    />
  );
}

function MediaAttachment({
  attachment,
  disabled,
  lang,
  onDescriptionChange = () => {},
  onRemove = () => {},
}) {
  const supportsEdit = supports('@mastodon/edit-media-attributes');
  const { url, type, id } = attachment;
  console.log({ attachment });
  const [description, setDescription] = useState(attachment.description);
  const suffixType = type.split('/')[0];
  const debouncedOnDescriptionChange = useDebouncedCallback(
    onDescriptionChange,
    500,
  );

  const [showModal, setShowModal] = useState(false);
  const textareaRef = useRef(null);
  useEffect(() => {
    let timer;
    if (showModal && textareaRef.current) {
      timer = setTimeout(() => {
        textareaRef.current.focus();
      }, 100);
    }
    return () => {
      clearTimeout(timer);
    };
  }, [showModal]);

  const descTextarea = (
    <>
      {!!id && !supportsEdit ? (
        <div class="media-desc">
          <span class="tag">Uploaded</span>
          <p title={description}>
            {attachment.description || <i>No description</i>}
          </p>
        </div>
      ) : (
        <textarea
          ref={textareaRef}
          value={description || ''}
          lang={lang}
          placeholder={
            {
              image: 'Image description',
              video: 'Video description',
              audio: 'Audio description',
            }[suffixType]
          }
          autoCapitalize="sentences"
          autoComplete="on"
          autoCorrect="on"
          spellCheck="true"
          dir="auto"
          disabled={disabled}
          maxlength="1500" // Not unicode-aware :(
          // TODO: Un-hard-code this maxlength, ref: https://github.com/mastodon/mastodon/blob/b59fb28e90bc21d6fd1a6bafd13cfbd81ab5be54/app/models/media_attachment.rb#L39
          onInput={(e) => {
            const { value } = e.target;
            setDescription(value);
            debouncedOnDescriptionChange(value);
          }}
        ></textarea>
      )}
    </>
  );

  return (
    <>
      <div class="media-attachment">
        <div
          class="media-preview"
          onClick={() => {
            setShowModal(true);
          }}
        >
          {suffixType === 'image' ? (
            <img src={url} alt="" />
          ) : suffixType === 'video' || suffixType === 'gifv' ? (
            <video src={url} playsinline muted />
          ) : suffixType === 'audio' ? (
            <audio src={url} controls />
          ) : null}
        </div>
        {descTextarea}
        <div class="media-aside">
          <button
            type="button"
            class="plain close-button"
            disabled={disabled}
            onClick={onRemove}
          >
            <Icon icon="x" />
          </button>
        </div>
      </div>
      {showModal && (
        <Modal
          onClick={(e) => {
            if (e.target === e.currentTarget) {
              setShowModal(false);
            }
          }}
        >
          <div id="media-sheet" class="sheet sheet-max">
            <header>
              <h2>
                {
                  {
                    image: 'Edit image description',
                    video: 'Edit video description',
                    audio: 'Edit audio description',
                  }[suffixType]
                }
              </h2>
            </header>
            <main tabIndex="-1">
              <div class="media-preview">
                {suffixType === 'image' ? (
                  <img src={url} alt="" />
                ) : suffixType === 'video' || suffixType === 'gifv' ? (
                  <video src={url} playsinline controls />
                ) : suffixType === 'audio' ? (
                  <audio src={url} controls />
                ) : null}
              </div>
              {descTextarea}
            </main>
          </div>
        </Modal>
      )}
    </>
  );
}

function Poll({
  lang,
  poll,
  disabled,
  onInput = () => {},
  maxOptions,
  maxExpiration,
  minExpiration,
  maxCharactersPerOption,
}) {
  const { options, expiresIn, multiple } = poll;

  return (
    <div class={`poll ${multiple ? 'multiple' : ''}`}>
      <div class="poll-choices">
        {options.map((option, i) => (
          <div class="poll-choice" key={i}>
            <input
              required
              type="text"
              value={option}
              disabled={disabled}
              maxlength={maxCharactersPerOption}
              placeholder={`Choice ${i + 1}`}
              lang={lang}
              spellCheck="true"
              onInput={(e) => {
                const { value } = e.target;
                options[i] = value;
                onInput(poll);
              }}
            />
            <button
              type="button"
              class="plain2 poll-button"
              disabled={disabled || options.length <= 1}
              onClick={() => {
                options.splice(i, 1);
                onInput(poll);
              }}
            >
              <Icon icon="x" size="s" />
            </button>
          </div>
        ))}
      </div>
      <div class="poll-toolbar">
        <button
          type="button"
          class="plain2 poll-button"
          disabled={disabled || options.length >= maxOptions}
          onClick={() => {
            options.push('');
            onInput(poll);
          }}
        >
          +
        </button>{' '}
        <label class="multiple-choices">
          <input
            type="checkbox"
            checked={multiple}
            disabled={disabled}
            onChange={(e) => {
              const { checked } = e.target;
              poll.multiple = checked;
              onInput(poll);
            }}
          />{' '}
          Multiple choices
        </label>
        <label class="expires-in">
          Duration{' '}
          <select
            value={expiresIn}
            disabled={disabled}
            onChange={(e) => {
              const { value } = e.target;
              poll.expiresIn = value;
              onInput(poll);
            }}
          >
            {Object.entries(expiryOptions)
              .filter(([label, value]) => {
                return value >= minExpiration && value <= maxExpiration;
              })
              .map(([label, value]) => (
                <option value={value} key={value}>
                  {label}
                </option>
              ))}
          </select>
        </label>
      </div>
      <div class="poll-toolbar">
        <button
          type="button"
          class="plain remove-poll-button"
          disabled={disabled}
          onClick={() => {
            onInput(null);
          }}
        >
          Remove poll
        </button>
      </div>
    </div>
  );
}

function filterShortcodes(emojis, searchTerm) {
  searchTerm = searchTerm.toLowerCase();

  // Return an array of shortcodes that start with or contain the search term, sorted by relevance and limited to the first 5
  return emojis
    .sort((a, b) => {
      let aLower = a.shortcode.toLowerCase();
      let bLower = b.shortcode.toLowerCase();

      let aStartsWith = aLower.startsWith(searchTerm);
      let bStartsWith = bLower.startsWith(searchTerm);
      let aContains = aLower.includes(searchTerm);
      let bContains = bLower.includes(searchTerm);
      let bothStartWith = aStartsWith && bStartsWith;
      let bothContain = aContains && bContains;

      return bothStartWith
        ? a.length - b.length
        : aStartsWith
        ? -1
        : bStartsWith
        ? 1
        : bothContain
        ? a.length - b.length
        : aContains
        ? -1
        : bContains
        ? 1
        : 0;
    })
    .slice(0, 5);
}

function encodeHTML(str) {
  return str.replace(/[&<>"']/g, function (char) {
    return '&#' + char.charCodeAt(0) + ';';
  });
}

function removeNullUndefined(obj) {
  for (let key in obj) {
    if (obj[key] === null || obj[key] === undefined) {
      delete obj[key];
    }
  }
  return obj;
}

export default Compose;