<template>
  <!-- eslint-disable vue/no-v-html -->
  <div
    :data-scrollto="value.id"
    class="assignment"
    ref="assignmentDiv"
  >
    <div
      class="assignment__main-text assignment-text"
      data-cy="assignment-main-text"
      v-if="assignment.assignment > ''"
      v-html="assignmentHtml"
    />

    <assignment-solution
      :value="solution"
      v-if="assignment.solution"
    />

    <template v-if="isStudent">
      <submission-form
        :user-input="submission"
        :spellcheck-loading="spellcheckLoading"
        :saved="!unsaved"
        :spellcheck="true"
        :read-only="me.readOnly || me.selectedClass?.readOnly"
        placeholder="Ergebnis erfassen"
        action="Ergebnis mit Lehrperson teilen"
        shared-msg="Das Ergebnis wurde mit der Lehrperson geteilt."
        v-if="isStudent"
        @turnIn="turnIn"
        @saveInput="saveInput"
        @reopen="reopen"
        @changeDocumentUrl="changeDocumentUrl"
        @spellcheck="spellcheck"
      />

      <spell-check
        :corrections="corrections"
        :text="submission.text"
      />

      <p
        class="assignment__feedback"
        v-if="assignment.submission.submissionFeedback"
        v-html="feedbackText"
      />
    </template>
    <template v-if="!isStudent && assignment.id">
      <router-link
        :to="{ name: 'submissions', params: { id: assignment.id } }"
        class="button button--primary"
      >
        Zu den Ergebnissen
      </router-link>
    </template>
  </div>
</template>

<script setup lang="ts">
import { defineAsyncComponent, nextTick, onMounted, ref, watch } from 'vue';
import { useRoute } from 'vue-router';
import { useMutation, useQuery } from '@vue/apollo-composable';
import { graphql } from '@/__generated__';
import { HighlightNode } from '@/__generated__/graphql';
import { computed } from '@vue/reactivity';
import { markHighlight } from '@/helpers/highlight';
import { sanitize } from '@/helpers/text';
import { matomoTrackEvent } from '@/helpers/matomo-client';
import { getMe } from '@/graphql/queries';
import { PAGE_LOAD_TIMEOUT } from '@/consts/navigation.consts';
import debounce from 'lodash/debounce';
import Mark from 'mark.js';

export interface Props {
  value: any;
  highlights: HighlightNode[];
}

const SubmissionForm = defineAsyncComponent(() => import('@/components/content-blocks/assignment/SubmissionForm.vue'));
const AssignmentSolution = defineAsyncComponent(() => import('@/components/content-blocks/Solution.vue'));
const SpellCheck = defineAsyncComponent(() => import('@/components/content-blocks/assignment/SpellCheck.vue'));

const route = useRoute();
const assignmentDiv = ref<HTMLElement | null>(null);

const props = defineProps<Props>();

const initialSubmission = {
  text: '',
  document: '',
  final: false,
};
const assignment = ref({ submission: initialSubmission });
const corrections = ref('');
const unsaved = ref(false);
const saving = ref(0);
const spellcheckLoading = ref(false);

graphql(`
  fragment SubmissionParts on StudentSubmissionNode {
    id
    text
    final
    document
    submissionFeedback {
      id
      text
      teacher {
        firstName
        lastName
      }
    }
  }
`);

const assignmentFragment = graphql(`
  fragment AssignmentParts on AssignmentNode {
    id
    title
    assignment
    solution
    submission {
      ...SubmissionParts
    }
  }
`);

const { me } = getMe();

const { result, onResult } = useQuery(
  graphql(`
    query AssignmentQuery($id: ID!) {
      assignment(id: $id) {
        ...AssignmentParts
      }
    }
  `),
  { id: props.value.id }
);

const { mutate: doUpdateAssignment } = useMutation(
  graphql(`
    mutation UpdateAssignment($input: UpdateAssignmentInput!) {
      updateAssignment(input: $input) {
        updatedAssignment {
          ...AssignmentParts
        }
      }
    }
  `)
);
const { mutate: doUpdateAssignmentWithSuccess } = useMutation(
  graphql(`
    mutation UpdateAssignmentWithSuccess($input: UpdateAssignmentInput!) {
      updateAssignment(input: $input) {
        successful
        updatedAssignment {
          ...AssignmentParts
        }
      }
    }
  `)
);
const { mutate: doSpellCheck } = useMutation(
  graphql(`
    mutation SpellCheck($input: SpellCheckInput!) {
      spellCheck(input: $input) {
        correct
        results {
          sentence
          offset
          sentenceOffset
          length
          affected
          corrected
        }
      }
    }
  `)
);

onMounted(() => {
  if (assignmentDiv.value !== null) {
    if (route.hash === `#${props.value.id}`) {
      setTimeout(() => {
        const rect = assignmentDiv.value.getBoundingClientRect();

        window.scrollTo({
          top: rect.y,
          behavior: 'smooth',
        });
      }, PAGE_LOAD_TIMEOUT);
    }
  }
});

const submission = computed(() => {
  return assignment.value?.submission || {};
});
const isStudent = computed(() => {
  return !me.value.isTeacher;
});
const solution = computed(() => {
  return {
    text: assignment.value.solution,
  };
});
const feedbackText = computed(() => {
  let feedback = assignment.value.submission.submissionFeedback;
  let sanitizedFeedbackText = sanitize(feedback.text);
  return `<span class="inline-title">Feedback von ${feedback.teacher.firstName} ${feedback.teacher.lastName}:</span> ${sanitizedFeedbackText}`;
});
const containsParagraphs = (str: string) => {
  const parser = new DOMParser();
  const doc = parser.parseFromString(str, 'text/html');
  return doc.querySelectorAll('p').length > 0;
};
const assignmentHtml = computed(() => {
  const assignmentText = assignment.value.assignment;
  if (containsParagraphs(assignmentText)) {
    return assignmentText;
  }
  // text needs to be inside a paragraph, otherwise the highlighting will fail
  return `<p>${assignmentText}</p>`;
});

const childElements = computed(() => {
  // todo: refactor and merge with the one in ContentComponent.vue
  if (assignmentDiv.value) {
    const highlightParentSelector = '.assignment-text';
    const parent = assignmentDiv.value.querySelector(highlightParentSelector);
    if (!parent) {
      console.warn('Parent does not exist, this should not be possible');
      // or can it be, if the children did not load yet, e.g. with a dynamic component?
      return [];
    }

    const elements = Array.from(parent.children).reduce((acc: Element[], current: Element) => {
      if (current.tagName.toLowerCase() === 'ul') {
        return [...acc, ...current.children];
      }
      return [...acc, current];
    }, []);
    return elements as HTMLElement[];
  }
  return [];
});

const markHighlights = () => {
  for (const highlight of props.highlights) {
    const element = childElements.value[highlight.paragraphIndex];
    markHighlight(highlight, element, assignmentDiv.value as HTMLElement);
  }
};

const unmark = () => {
  // todo: refactor, this is used in multiple files
  for (const paragraph of childElements.value) {
    const instance = new Mark(paragraph);
    instance.unmark();
  }
};

onResult(async () => {
  const { assignment: loadedAssignment } = result.value;
  assignment.value = {
    ...loadedAssignment,
  };
  assignment.value.submission = Object.assign(initialSubmission, assignment.value.submission);

  if (assignmentDiv.value) {
    await nextTick();
    unmark();
    markHighlights();
  }
});
watch(
  () => props.highlights.map((h) => h.color),
  () => {
    unmark();
    markHighlights();
  }
);

const turnIn = () => {
  corrections.value = '';
  const variables = {
    input: {
      assignment: {
        id: assignment.value.id,
        answer: assignment.value.submission.text,
        document: assignment.value.submission.document,
        final: true,
      },
    },
  };
  doUpdateAssignment(variables, {
    update: (
      _cache,
      {
        data: {
          updateAssignment: {
            updatedAssignment: { submission },
          },
        },
      }
    ) => {
      assignment.value.submission = submission;
    },
  });
  matomoTrackEvent('Auftrag', 'Ergebnis mit Lehrperson geteilt', assignment.value.title);
};
const reopen = () => {
  const variables = {
    input: {
      assignment: {
        id: assignment.value.id,
        answer: assignment.value.submission.text,
        document: assignment.value.submission.document,
        final: false,
      },
    },
  };
  doUpdateAssignment(variables, {
    update: (
      _cache,
      {
        data: {
          updateAssignment: {
            updatedAssignment: { submission },
          },
        },
      }
    ) => {
      assignment.value.submission = submission;
    },
  });
};
const _save = debounce(function (submission) {
  saving.value++;
  const variables = {
    input: {
      assignment: {
        id: assignment.value.id,
        answer: assignment.value.submission.text,
        document: assignment.value.submission.document,
      },
    },
  };
  doUpdateAssignmentWithSuccess(variables, {
    update(
      cache,
      {
        data: {
          updateAssignment: { successful, updatedAssignment },
        },
      }
    ) {
      try {
        if (successful) {
          const id = cache.identify({
            id: updatedAssignment.id,
            __typename: updatedAssignment.__typename,
          });
          const fragment = assignmentFragment;
          const fragmentName = 'AssignmentParts';
          cache.writeFragment({
            fragment,
            fragmentName,
            id,
            data: {
              ...updatedAssignment,
              submission,
            },
          });
        }
      } catch (e) {
        console.error(e);
        // Query did not exist in the cache, and apollo throws a generic Error. Do nothing
      }
    },
  }).then(() => {
    saving.value--;
    if (saving.value === 0) {
      unsaved.value = false;
    }
  });
}, 500);
const saveInput = (answer: string) => {
  // reset corrections on input
  corrections.value = '';
  unsaved.value = true;
  /*
    We update the assignment on this component, so the changes are reflected on it. The server does not return
    the updated entity, to prevent the UI to update when the user is entering his input
   */
  assignment.value.submission.text = answer;
  _save(assignment.value.submission);

  if (assignment.value.submission.text.length > 0) {
    matomoTrackEvent('Auftrag', 'Text mit Eingabe gespeichert', assignment.value.title);
  }
};
const changeDocumentUrl = (documentUrl: string) => {
  assignment.value.submission.document = documentUrl;
  _save(assignment.value.submission);
};
const spellcheck = () => {
  spellcheckLoading.value = true;
  const variables = {
    input: {
      assignment: assignment.value.id,
      text: assignment.value.submission.text,
    },
  };
  doSpellCheck(variables, {
    update(
      _cache,
      {
        data: {
          spellCheck: { results },
        },
      }
    ) {
      corrections.value = results;
    },
  }).then(() => {
    spellcheckLoading.value = false;
  });
  matomoTrackEvent('Auftrag', 'Rechtschreibung geprüft', assignment.value.title);
};
</script>

<style scoped lang="scss">
@import '@/styles/_variables.scss';
@import '@/styles/_functions.scss';
@import '@/styles/_mixins.scss';

.assignment {
  margin-bottom: 3rem;
  position: relative;

  &__title {
    font-size: toRem(17px);
    margin-bottom: 1rem;
  }

  &__main-text {
    :deep(ul) {
      @include list-parent;
    }

    :deep(li) {
      @include list-child;
    }
  }

  &__toggle-input-container {
    display: flex;
    margin-bottom: 15px;
  }

  &__toggle-input {
    border: 0;
    font-family: $sans-serif-font-family;
    background: transparent;
    font-size: toRem(14px);
    padding: 5px 0;
    margin-right: 15px;
    outline: 0;
    color: $color-silver-dark;
    cursor: pointer;
    border-bottom: 2px solid transparent;

    &--active {
      border-bottom-color: $color-charcoal-dark;
      color: $color-charcoal-dark;
    }
  }

  &__feedback {
    @include regular-text;
  }
}
</style>
