<template>
  <div>
    <form
      v-if="isAbleToComment"
      @submit.prevent="createCommentForEvent(newCommentValue)"
      class="mt-2"
    >
      <o-notification
        v-if="isEventOrganiser && !areCommentsClosed"
        :closable="false"
        class="my-2"
        >{{ t("Comments are closed for everybody else.") }}</o-notification
      >
      <article class="flex flex-wrap items-start gap-2">
        <figure class="" v-if="newCommentValue.actor">
          <identity-picker-wrapper
            :inline="false"
            v-model="newCommentValue.actor"
          />
        </figure>
        <div class="flex-1">
          <div class="flex flex-col gap-2">
            <div class="editor-wrapper">
              <Editor
                ref="commenteditor"
                v-if="currentActor"
                :currentActor="currentActor"
                mode="comment"
                v-model="newCommentValue.text"
                :aria-label="t('Comment body')"
                @submit="createCommentForEvent(newCommentValue)"
                :placeholder="t('Write a new comment')"
              />
              <p class="" v-if="emptyCommentError">
                {{ t("Comment text can't be empty") }}
              </p>
            </div>
            <div class="" v-if="isEventOrganiser">
              <o-switch
                aria-labelledby="notify-participants-toggle"
                v-model="newCommentValue.isAnnouncement"
                >{{ t("Notify participants") }}</o-switch
              >
            </div>
          </div>
        </div>
        <div class="">
          <o-button native-type="submit" variant="primary" icon-left="send">{{
            t("Send")
          }}</o-button>
        </div>
      </article>
    </form>
    <o-notification v-else-if="isConnected" :closable="false">{{
      t("The organiser has chosen to close comments.")
    }}</o-notification>
    <p v-if="commentsLoading" class="text-center">
      {{ t("Loading comments…") }}
    </p>
    <transition-group tag="div" name="comment-empty-list" v-else class="mt-2">
      <transition-group
        key="list"
        name="comment-list"
        v-if="
          filteredOrderedComments &&
          filteredOrderedComments.length &&
          currentActor
        "
        class="comment-list"
        tag="ul"
      >
        <event-comment
          class="root-comment my-2"
          :comment="comment"
          :event="event"
          :currentActor="currentActor"
          v-for="comment in filteredOrderedComments"
          :key="comment.id"
          @create-comment="createCommentForEvent"
          @delete-comment="
            (commentToDelete) =>
              deleteComment({
                commentId: commentToDelete.id as string,
                originCommentId: commentToDelete.originComment?.id,
              })
          "
        />
      </transition-group>
      <empty-content v-else icon="comment" key="no-comments" :inline="true">
        <span>{{ t("No comments yet") }}</span>
      </empty-content>
    </transition-group>
  </div>
</template>

<script lang="ts" setup>
import EventComment from "@/components/Comment/EventComment.vue";
import IdentityPickerWrapper from "@/views/Account/IdentityPickerWrapper.vue";
import { CommentModeration } from "@/types/enums";
import { CommentModel, IComment } from "../../types/comment.model";
import {
  CREATE_COMMENT_FROM_EVENT,
  DELETE_COMMENT,
  COMMENTS_THREADS_WITH_REPLIES,
} from "../../graphql/comment";
import { IEvent } from "../../types/event.model";
import { ApolloCache, FetchResult, InMemoryCache } from "@apollo/client/core";
import EmptyContent from "@/components/Utils/EmptyContent.vue";
import { useCurrentActorClient } from "@/composition/apollo/actor";
import { useMutation, useQuery } from "@vue/apollo-composable";
import { computed, defineAsyncComponent, inject, ref, watch } from "vue";
import { IPerson } from "@/types/actor";
import { AbsintheGraphQLError } from "@/types/errors.model";
import { useI18n } from "vue-i18n";
import { Notifier } from "@/plugins/notifier";

const props = defineProps<{
  event: IEvent;
  newComment?: IComment;
}>();

const event = computed(() => props.event);
const newCommentProps = computed(() => props.newComment);

const { currentActor } = useCurrentActorClient();

const { result: commentsResult, loading: commentsLoading } = useQuery<{
  event: Pick<IEvent, "id" | "uuid" | "comments">;
}>(
  COMMENTS_THREADS_WITH_REPLIES,
  () => ({ eventUUID: event.value?.uuid }),
  () => ({ enabled: event.value?.uuid !== undefined })
);

const comments = computed(() => commentsResult.value?.event.comments ?? []);

const Editor = defineAsyncComponent(
  () => import("@/components/TextEditor.vue")
);

const newCommentValue = ref<IComment>(new CommentModel(newCommentProps.value));

const emptyCommentError = ref(false);

const { t } = useI18n({ useScope: "global" });

watch(currentActor, () => {
  newCommentValue.value.actor = currentActor.value as IPerson;
});

watch(newCommentValue, (newCommentUpdated: IComment) => {
  if (emptyCommentError.value) {
    emptyCommentError.value = ["", "<p></p>"].includes(newCommentUpdated.text);
  }
});

const {
  mutate: createCommentForEventMutation,
  onDone: createCommentForEventMutationDone,
  onError: createCommentForEventMutationError,
} = useMutation<
  { createComment: IComment },
  {
    eventId: string;
    text: string;
    inReplyToCommentId?: string;
    isAnnouncement?: boolean;
    originCommentId?: string | undefined;
  }
>(CREATE_COMMENT_FROM_EVENT, () => ({
  update: (
    store: ApolloCache<InMemoryCache>,
    { data }: FetchResult,
    { variables }
  ) => {
    if (data == null) return;
    // comments are attached to the event, so we can pass it to replies later
    const newCommentLocal = { ...data.createComment, event: event.value };

    // we load all existing threads
    const commentThreadsData = store.readQuery<{ event: IEvent }>({
      query: COMMENTS_THREADS_WITH_REPLIES,
      variables: {
        eventUUID: event.value?.uuid,
      },
    });
    if (!commentThreadsData) return;
    const { event: cachedEvent } = commentThreadsData;
    const oldComments = [...cachedEvent.comments];

    // if it's no a root comment, we first need to find
    // existing replies and add the new reply to it
    if (variables?.originCommentId !== undefined) {
      const parentCommentIndex = oldComments.findIndex(
        (oldComment) => oldComment.id === variables.originCommentId
      );
      const parentComment = oldComments[parentCommentIndex];

      // replace the root comment with has the updated list of replies in the thread list
      oldComments.splice(parentCommentIndex, 1, {
        ...parentComment,
        replies: [...parentComment.replies, newCommentLocal],
      });
    } else {
      // otherwise it's simply a new thread and we add it to the list
      oldComments.push(newCommentLocal);
    }

    // finally we save the thread list
    store.writeQuery({
      query: COMMENTS_THREADS_WITH_REPLIES,
      data: {
        event: {
          ...cachedEvent,
          comments: oldComments,
        },
      },
      variables: {
        eventUUID: event.value?.uuid,
      },
    });
  },
}));

createCommentForEventMutationDone(() => {
  // and reset the new comment field
  newCommentValue.value = new CommentModel();
});

const notifier = inject<Notifier>("notifier");

createCommentForEventMutationError((errors) => {
  console.error(errors);
  if (errors.graphQLErrors && errors.graphQLErrors.length > 0) {
    const error = errors.graphQLErrors[0] as AbsintheGraphQLError;
    if (error.field !== "text" && error.message[0] !== "can't be blank") {
      notifier?.error(error.message);
    }
  }
});

const createCommentForEvent = (comment: IComment) => {
  emptyCommentError.value = ["", "<p></p>"].includes(comment.text);

  if (emptyCommentError.value) return;
  if (!comment.actor) return;
  if (!event.value?.id) return;

  createCommentForEventMutation({
    eventId: event.value?.id,
    text: comment.text,
    inReplyToCommentId: comment.inReplyToComment?.id,
    isAnnouncement: comment.isAnnouncement,
    originCommentId: comment.originComment?.id,
  });
};

const { mutate: deleteComment, onError: deleteCommentMutationError } =
  useMutation<
    { deleteComment: { id: string } },
    { commentId: string; originCommentId?: string }
  >(DELETE_COMMENT, () => ({
    update: (
      store: ApolloCache<InMemoryCache>,
      { data }: FetchResult,
      { variables }
    ) => {
      if (data == null) return;
      const deletedCommentId = data.deleteComment.id;

      const commentsData = store.readQuery<{ event: IEvent }>({
        query: COMMENTS_THREADS_WITH_REPLIES,
        variables: {
          eventUUID: event.value?.uuid,
        },
      });
      if (!commentsData) return;
      const { event: cachedEvent } = commentsData;
      let updatedComments: IComment[] = [...cachedEvent.comments];

      if (variables?.originCommentId) {
        // we have deleted a reply to a thread
        const parentCommentIndex = updatedComments.findIndex(
          (oldComment) => oldComment.id === variables.originCommentId
        );
        const parentComment = updatedComments[parentCommentIndex];
        const updatedReplies = parentComment.replies.map((reply) => {
          if (reply.id === deletedCommentId) {
            return {
              ...reply,
              deletedAt: new Date().toString(),
            };
          }
          return reply;
        });
        updatedComments.splice(parentCommentIndex, 1, {
          ...parentComment,
          replies: updatedReplies,
          totalReplies: parentComment.totalReplies - 1,
        });
        console.debug("updatedComments", updatedComments);
      } else {
        // we have deleted a thread itself
        updatedComments = updatedComments.map((reply) => {
          if (reply.id === deletedCommentId) {
            return {
              ...reply,
              deletedAt: new Date().toString(),
            };
          }
          return reply;
        });
      }
      store.writeQuery({
        query: COMMENTS_THREADS_WITH_REPLIES,
        variables: {
          eventUUID: event.value?.uuid,
        },
        data: {
          event: {
            ...cachedEvent,
            comments: updatedComments,
          },
        },
      });
    },
  }));

deleteCommentMutationError((error) => {
  console.error(error);
  if (error.graphQLErrors && error.graphQLErrors.length > 0) {
    notifier?.error(error.graphQLErrors[0].message);
  }
});

const orderedComments = computed((): IComment[] => {
  return comments.value
    .filter((comment: IComment) => comment.inReplyToComment == null)
    .sort((a: IComment, b: IComment) => {
      if (a.isAnnouncement !== b.isAnnouncement) {
        return (
          (b.isAnnouncement === true ? 1 : 0) -
          (a.isAnnouncement === true ? 1 : 0)
        );
      }
      if (a.publishedAt && b.publishedAt) {
        return (
          new Date(b.publishedAt).getTime() - new Date(a.publishedAt).getTime()
        );
      } else if (a.updatedAt && b.updatedAt) {
        return (
          new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()
        );
      }
      return 0;
    });
});

const filteredOrderedComments = computed((): IComment[] => {
  return orderedComments.value.filter(
    (comment) => !comment.deletedAt || comment.totalReplies > 0
  );
});

const isEventOrganiser = computed((): boolean => {
  const organizerId =
    event.value?.organizerActor?.id || event.value?.attributedTo?.id;
  return organizerId !== undefined && currentActor.value?.id === organizerId;
});

const areCommentsClosed = computed((): boolean => {
  return (
    currentActor.value?.id !== undefined &&
    event.value?.options.commentModeration !== CommentModeration.CLOSED
  );
});

const isAbleToComment = computed((): boolean => {
  if (isConnected.value) {
    return areCommentsClosed.value || isEventOrganiser.value;
  }
  return false;
});

const isConnected = computed((): boolean => {
  return currentActor.value?.id != undefined;
});
</script>

<style lang="scss" scoped>
// @use "@/styles/_mixins" as *;
// form.new-comment {
//   padding-bottom: 1rem;

//   .media {
//     flex-wrap: wrap;
//     justify-content: center;
//     // .media-left {
//     //   @include >mobile {
//     //     @include margin-right(0.5rem);
//     //     @include margin-left(0.5rem);
//     //   }
//     // }

//     .media-content {
//       display: flex;
//       align-items: center;
//       align-content: center;
//       width: min-content;

//       .field {
//         flex: 1;
//         // @include padding-right(10px);
//         margin-bottom: 0;

//         &.notify-participants {
//           margin-top: 0.5rem;
//         }
//       }
//     }
//   }
// }

// .no-comments {
//   display: flex;
//   flex-direction: column;

//   span {
//     text-align: center;
//     margin-bottom: 10px;
//   }

//   img {
//     max-width: 250px;
//     align-self: center;
//   }
// }

// ul.comment-list li {
//   margin-bottom: 16px;
// }

.comment-list-enter-active,
.comment-list-leave-active,
.comment-list-move {
  transition: 500ms cubic-bezier(0.59, 0.12, 0.34, 0.95);
  transition-property: opacity, transform;
}

.comment-list-enter {
  opacity: 0;
  transform: translateX(50px) scaleY(0.5);
}

.comment-list-enter-to {
  opacity: 1;
  transform: translateX(0) scaleY(1);
}

.comment-list-leave-active,
.comment-empty-list-active {
  position: absolute;
}

.comment-list-leave-to,
.comment-empty-list-leave-to {
  opacity: 0;
  transform: scaleY(0);
  transform-origin: center top;
}

// .comment-empty-list-enter-active {
//     transition: opacity .5s;
// }

// .comment-empty-list-enter {
//     opacity: 0;
// }
</style>
