import * as linkify from "linkifyjs";
import linkifyHtml from "linkify-html";
import utils from "@/plugins/utils";
import Hammer from "hammerjs";

linkify.options.defaults.className = "link";
linkify.options.defaults.target = { url: "_blank" };

import { computed, onBeforeUnmount, Ref, ref, watch } from "vue";
import { EventTimelineSet, Relations, RelationsEvent } from "matrix-js-sdk";
import { EventAttachment, KeanuEvent, KeanuRoom } from "../../../models/eventAttachment";

export interface MessageProps {
  room: KeanuRoom;
  originalEvent: KeanuEvent;
  nextEvent: KeanuEvent | null | undefined;
  timelineSet: EventTimelineSet;
  componentFn: (event: KeanuEvent, forExport: boolean) => any;
}

export type MessageEmits = {
  (event: "own-avatar-clicked", value: { event: KeanuEvent }): void;
  (event: "other-avatar-clicked", value: { event: KeanuEvent; anchor: any }): void;
  (event: "contextMenu", value: { event: KeanuEvent; anchor: any }): void;
  (event: "addQuickHeartReaction", value: { position: { top: string; left: string } }): void;
  (event: "send-quick-reaction", value: {reaction: string, event: KeanuEvent }): void;  
  (event: "download", value: { event: KeanuEvent }): void;
  (event: "poll-closed", value: any): void;
  (event: "more", value: { event: KeanuEvent }): void;
  (event: "layout-change", value: { element: Element | undefined; action: () => void }): void;
};

export const useMessage = (
  $matrix: any,
  $t: any,
  props: MessageProps,
  emits?: MessageEmits,
  processThread?: () => void
) => {
  const event: Ref<KeanuEvent | undefined> = ref(undefined);
  const attachment: Ref<EventAttachment | undefined> = ref(undefined);
  const thread: Ref<Relations | undefined> = ref(undefined);

  const isIncoming = ref(props.originalEvent.getSender() != $matrix.currentUserId);

  onBeforeUnmount(() => {
    thread.value = undefined;
  });

  watch(
    props.originalEvent,
    (originalEvent) => {
      event.value = originalEvent;
      attachment.value = $matrix.attachmentManager.getEventAttachment(event.value);

      // Check not null and not {}
      if (originalEvent && originalEvent.isBeingDecrypted && originalEvent.isBeingDecrypted()) {
        originalEvent.getDecryptionPromise()?.then(() => {
          event.value = originalEvent;
        });
      }
    },
    { immediate: true }
  );

  watch(
    thread,
    (newValue, oldValue) => {
      if (oldValue) {
        oldValue.off(RelationsEvent.Add, onAddRelation);
      }
      if (newValue) {
        newValue.on(RelationsEvent.Add, onAddRelation);
        if (processThread) {
          processThread();
        }
      }
    },
    { immediate: true }
  );

  /**
   *
   * @returns true if event is non-null and contains data
   */
  const validEvent = computed(() => {
    return event.value !== undefined;
  });

  /**
   * If this is a thread event, we return the root here, so all reactions will land on the root event.
   */
  const eventForReactions = computed(() => {
    if (event.value && event.value.parentThread) {
      return event.value.parentThread;
    }
    return event.value;
  });

  const incoming = computed(() => {
    return event.value && event.value.getSender() != $matrix.currentUserId;
  });

  /**
   * Don't show sender and time if the next event is within 2 minutes and also from us (= back to back messages)
   */
  const showSenderAndTime = computed(() => {
    if (!isPinned.value && props.nextEvent && props.nextEvent.getSender() == event.value?.getSender()) {
      const ts1 = props.nextEvent.event.origin_server_ts ?? 0;
      const ts2 = event.value!.event.origin_server_ts ?? 0;
      return ts1 - ts2 < 2 * 60 * 1000; // less than 2 minutes
    }
    return true;
  });

  const inReplyToSender = computed(() => {
    const originalEvent = validEvent.value && event.value?.replyEvent;
    if (originalEvent) {
      const sender = eventSenderDisplayName(originalEvent);
      if (originalEvent.isThreadRoot || originalEvent.isMxThread) {
        return sender || $t("message.someone");
      } else {
        return $t("message.user_said", { user: sender || $t("message.someone") });
      }
    }
    return null;
  });

  const inReplyToEvent = computed(() => {
    return event.value?.replyEvent;
  });

  const inReplyToText = computed(() => {
    const relatesTo = event.value?.getWireContent()["m.relates_to"];
    if (relatesTo && relatesTo["m.in_reply_to"]) {
      if (inReplyToEvent.value && (inReplyToEvent.value.isThreadRoot || inReplyToEvent.value.isMxThread)) {
        const children = props.timelineSet.relations
          .getAllChildEventsForEvent(inReplyToEvent.value.getId()!)
          .filter((e) => utils.downloadableTypes().includes(e.getContent().msgtype));
        return $t("message.sent_media", { count: children.length });
      }
      const content = event.value?.getContent();
      if (content && content.body) {
        const lines = content.body.split("\n").reverse() || [];
        while (lines.length && !lines[0].startsWith("> ")) lines.shift();
        // Reply fallback has a blank line after it, so remove it to prevent leading newline
        if (lines[0] === "") lines.shift();
        const text = lines[0] && lines[0].replace(/^> (<.*> )?/g, "");
        if (text) {
          return text;
        }
      }
      if (inReplyToEvent.value) {
        var c = inReplyToEvent.value.getContent();
        return c.body;
      }
      // We don't have the original text (at the moment at least)
      return $t("fallbacks.original_text");
    }
    return null;
  });

  const messageText = computed(() => {
    const relatesTo = event.value?.getWireContent()["m.relates_to"];
    if (event.value && relatesTo && relatesTo["m.in_reply_to"]) {
      const content = event.value.getContent();
      if ("body" in content) {
        // Remove the new text and strip "> " from the old original text
        const lines = content.body.split("\n");
        while (lines.length && lines[0].startsWith("> ")) lines.shift();
        // Reply fallback has a blank line after it, so remove it to prevent leading newline
        if (lines[0] === "") lines.shift();
        return lines.join("\n");
      }
    }
    return event.value?.getContent().body;
  });

  const isPinned = computed(() => {
    return event.value && event.value.parentThread ? event.value.parentThread.isPinned : event.value?.isPinned || false;
  });

  /**
   * Classes to set for the message. Currently only for "messageIn"
   */

  const messageClasses = computed(() => {
    if (incoming.value) {
      return { messageIn: true, "from-admin": senderIsAdminOrModerator(event.value), pinned: isPinned.value };
    } else {
      return { messageOut: true, pinned: isPinned.value };
    }
  });

  const userAvatar = computed(() => {
    if (!$matrix.userAvatar) {
      return null;
    }
    return $matrix.matrixClient.mxcUrlToHttp(
      $matrix.userAvatar,
      80,
      80,
      "scale",
      true,
      undefined,
      $matrix.useAuthedMedia
    );
  });

  const userAvatarLetter = computed(() => {
    if (!$matrix.currentUser) {
      return null;
    }
    return ($matrix.currentUserDisplayName || $matrix.currentUserId.substring(1)).substring(0, 1).toUpperCase();
  });

  const onAddRelation = () => {
    if (processThread) {
      processThread();
    }
  };

  const ownAvatarClicked = () => {
    if (event.value && emits) {
      emits("own-avatar-clicked", { event: event.value });
    }
  };

  const otherAvatarClicked = (avatarRef: any) => {
    if (event.value && emits) {
      emits("other-avatar-clicked", { event: event.value, anchor: avatarRef });
    }
  };

  const showContextMenu = (buttonRef: any) => {
    if (event.value && emits) {
      emits("contextMenu", { event: event.value, anchor: buttonRef });
    }
  };

  /**
   * Get a display name given an event.
   */
  const eventSenderDisplayName = (e: KeanuEvent | undefined) => {
    if (e?.getSender() === $matrix.currentUserId) {
      return $t("message.you");
    }
    if (e && props.room) {
      const sender = e.getSender();
      const member = sender ? props.room.getMember(sender) : undefined;
      if (member) {
        return member.name;
      }
    }
    return e?.getContent().displayname || e?.getSender();
  };

  /**
   * In the case where the state_key points out a userId for an operation (e.g. membership events)
   * return the display name of the affected user.
   * @param event
   * @returns
   */
  const eventStateKeyDisplayName = (e: KeanuEvent | undefined) => {
    if (e?.getStateKey() === $matrix.currentUserId) {
      return $t("message.you");
    }
    if (e && props.room) {
      const key = e.getStateKey();
      const member = key ? props.room.getMember(key) : undefined;
      if (member) {
        return member.name;
      }
    }
    return e?.getStateKey();
  };

  const messageEventAvatar = (e: KeanuEvent | undefined) => {
    if (e && props.room) {
      const sender = e.getSender();
      const member = sender ? props.room.getMember(sender) : undefined;
      if (member) {
        return member.getAvatarUrl(
          $matrix.matrixClient.getHomeserverUrl(),
          40,
          40,
          "scale",
          true,
          false,
          $matrix.useAuthedMedia
        );
      }
    }
    return null;
  };

  /**
   * Return true if the event sender has a powel level > 0, e.g. is moderator or admin of some sort.
   */
  const senderIsAdminOrModerator = (e: KeanuEvent | undefined) => {
    if (e && props.room) {
      const sender = e.getSender();
      const member = sender ? props.room.getMember(sender) : undefined;
      if (member) {
        return member.powerLevel > 0;
      }
    }
    return false;
  };

  const redactedBySomeoneElse = (e: KeanuEvent | undefined) => {
    if (!e || !e.isRedacted()) return false;
    const redactionEvent = e.getUnsigned().redacted_because;
    if (redactionEvent) {
      return redactionEvent.sender !== $matrix.currentUserId;
    }
    return false;
  };

  const formatTimeAgo = (time: number | undefined) => {
    if (!time) return "";
    const date = new Date();
    date.setTime(time);
    var ti = Math.abs(new Date().getTime() - date.getTime());
    ti = ti / 1000; // Convert to seconds
    let s = "";
    if (ti < 60) {
      s = $t("global.time.recently");
    } else if (ti < 3600 && Math.round(ti / 60) < 60) {
      s = $t("global.time.minutes", Math.round(ti / 60));
    } else if (ti < 86400 && Math.round(ti / 60 / 60) < 24) {
      s = $t("global.time.hours", Math.round(ti / 60 / 60));
    } else {
      s = $t("global.time.days", Math.round(ti / 60 / 60 / 24));
    }
    return utils.toLocalNumbers(s);
  };

  const formatTime = (time: number | undefined) => {
    if (!time) return "";
    return utils.formatTime(time);
  };

  const linkify = (text: string) => {
    return linkifyHtml(text);
  };

  const mc: Ref<Hammer | undefined> = ref(undefined);
  const mcCustom: Ref<Hammer.Manager | undefined> = ref(undefined);

  const initMsgHammerJs = (element: Element, opbutton: Element) => {
    mc.value = new Hammer(element);
    mcCustom.value = new Hammer.Manager(element);
    mcCustom.value.add(new Hammer.Tap({ event: "doubletap", taps: 2 }));
    mcCustom.value.on("doubletap", (evt: Hammer.HammerInput) => {
      var { top, left } = evt.target.getBoundingClientRect();
      var position = { top: `${top}px`, left: `${left}px` };
      if (emits) emits("addQuickHeartReaction", { position });
    });
    mc.value.on("press", () => {
      showContextMenu(opbutton);
    });
  };

  return {
    event,
    isIncoming,
    attachment,
    thread,

    validEvent,
    eventForReactions,
    showSenderAndTime,
    inReplyToSender,
    inReplyToEvent,
    inReplyToText,
    messageText,
    isPinned,
    messageClasses,
    userAvatar,
    userAvatarLetter,
    ownAvatarClicked,
    otherAvatarClicked,
    showContextMenu,
    eventSenderDisplayName,
    eventStateKeyDisplayName,
    messageEventAvatar,
    senderIsAdminOrModerator,
    redactedBySomeoneElse,
    formatTimeAgo,
    formatTime,
    linkify,
    initMsgHammerJs,
  };
};
