<!--
// Copyright © 2022 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. You may
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
//
// See the License for the specific language governing permissions and
// limitations under the License.
-->
<script lang="ts">
  import { AttachmentStyledBox } from '@hcengineering/attachment-resources'
  import chunter from '@hcengineering/chunter'
  import { Employee } from '@hcengineering/contact'
  import core, {
    Account,
    AttachedData,
    Data,
    Doc,
    fillDefaults,
    generateId,
    Ref,
    SortingOrder
  } from '@hcengineering/core'
  import { getResource, translate } from '@hcengineering/platform'
  import {
    Card,
    createQuery,
    getClient,
    getUserDraft,
    KeyedAttribute,
    MessageBox,
    SpaceSelector,
    updateUserDraft
  } from '@hcengineering/presentation'
  import tags, { TagElement, TagReference } from '@hcengineering/tags'
  import {
    calcRank,
    Component as ComponentType,
    DraftIssueChild,
    Issue,
    IssueDraft,
    IssuePriority,
    IssueStatus,
    IssueTemplate,
    Project,
    Sprint
  } from '@hcengineering/tracker'
  import {
    ActionIcon,
    addNotification,
    Button,
    Component,
    DatePresenter,
    EditBox,
    IconAttachment,
    IconMoreH,
    Label,
    Menu,
    showPopup
  } from '@hcengineering/ui'
  import view from '@hcengineering/view'
  import { ObjectBox } from '@hcengineering/view-resources'
  import { deepEqual } from 'fast-equals'
  import { createEventDispatcher } from 'svelte'
  import { activeComponent, activeSprint, generateIssueShortLink, getIssueId, updateIssueRelation } from '../issues'
  import tracker from '../plugin'
  import ComponentSelector from './ComponentSelector.svelte'
  import AssigneeEditor from './issues/AssigneeEditor.svelte'
  import IssueNotification from './issues/IssueNotification.svelte'
  import ParentIssue from './issues/ParentIssue.svelte'
  import PriorityEditor from './issues/PriorityEditor.svelte'
  import StatusEditor from './issues/StatusEditor.svelte'
  import EstimationEditor from './issues/timereport/EstimationEditor.svelte'
  import SetDueDateActionPopup from './SetDueDateActionPopup.svelte'
  import SetParentIssueActionPopup from './SetParentIssueActionPopup.svelte'
  import SprintSelector from './sprints/SprintSelector.svelte'
  import SubIssues from './SubIssues.svelte'

  export let space: Ref<Project>
  export let status: Ref<IssueStatus> | undefined = undefined
  export let priority: IssuePriority = IssuePriority.NoPriority
  export let assignee: Ref<Employee> | null = null
  export let component: Ref<ComponentType> | null = $activeComponent ?? null
  export let sprint: Ref<Sprint> | null = $activeSprint ?? null
  export let relatedTo: Doc | undefined
  export let shouldSaveDraft: boolean = false
  export let parentIssue: Issue | undefined
  export let originalIssue: Issue | undefined
  export let onDraftChanged: () => void

  const draft: IssueDraft | undefined = shouldSaveDraft ? getUserDraft(tracker.class.IssueDraft) : undefined
  const client = getClient()
  const hierarchy = client.getHierarchy()

  let subIssuesComponent: SubIssues

  let labels: TagReference[] = draft?.labels || []
  let objectId: Ref<Issue> = draft?.issueId || generateId()
  let saveTimer: number | undefined
  let currentProject: Project | undefined

  function toIssue (initials: AttachedData<Issue>, draft: IssueDraft | undefined): AttachedData<Issue> {
    if (draft === undefined) {
      return { ...initials }
    }
    const { labels, subIssues, ...issue } = draft
    return { ...initials, ...issue }
  }

  const defaultIssue = {
    title: '',
    description: '',
    assignee,
    component,
    sprint,
    number: 0,
    rank: '',
    status: '' as Ref<IssueStatus>,
    priority,
    dueDate: null,
    comments: 0,
    attachments: 0,
    subIssues: 0,
    parents: [],
    reportedTime: 0,
    estimation: 0,
    reports: 0,
    childInfo: [],
    createOn: Date.now()
  }

  $: _space = draft?.project || space
  $: !originalIssue && !draft && updateIssueStatusId(currentProject, status)
  $: !originalIssue && !draft && updateAssigneeId(currentProject)
  $: canSave = getTitle(object.title ?? '').length > 0

  $: if (object.space !== _space) {
    object.space = _space
  }

  let object = originalIssue
    ? {
        ...originalIssue,
        title: `${originalIssue.title} (copy)`,
        subIssues: 0,
        attachments: 0,
        reportedTime: 0,
        reports: 0,
        childInfo: [],
        space: _space
      }
    : { ...toIssue(defaultIssue, draft), space: _space }
  fillDefaults(hierarchy, object, tracker.class.Issue)

  function resetObject (): void {
    templateId = undefined
    template = undefined
    object = { ...defaultIssue, space: _space }
    subIssues = []
    labels = []
    if (!originalIssue && !draft) {
      updateIssueStatusId(currentProject, status)
      updateAssigneeId(currentProject)
    }
    fillDefaults(hierarchy, object, tracker.class.Issue)
  }

  let templateId: Ref<IssueTemplate> | undefined = draft?.template?.template
  let appliedTemplateId: Ref<IssueTemplate> | undefined = draft?.template?.template

  let template: IssueTemplate | undefined = undefined
  const templateQuery = createQuery()

  let subIssues: DraftIssueChild[] = draft?.subIssues || []

  $: if (templateId !== undefined) {
    templateQuery.query(tracker.class.IssueTemplate, { _id: templateId }, (res) => {
      template = res[0]
    })
  } else {
    template = undefined
    templateQuery.unsubscribe()
  }

  function tagAsRef (tag: TagElement): TagReference {
    return {
      _class: tags.class.TagReference,
      _id: generateId() as Ref<TagReference>,
      attachedTo: '' as Ref<Doc>,
      attachedToClass: tracker.class.Issue,
      collection: 'labels',
      space: tags.space.Tags,
      modifiedOn: 0,
      modifiedBy: '' as Ref<Account>,
      title: tag.title,
      tag: tag._id,
      color: tag.color
    }
  }

  async function updateObject (template: IssueTemplate): Promise<void> {
    if (object.template?.template === template._id) {
      return
    }
    const { _class, _id, space, children, comments, attachments, labels: labels_, description, ...templBase } = template

    subIssues = template.children.map((p) => {
      return { ...p, status: currentProject?.defaultIssueStatus ?? ('' as Ref<IssueStatus>) }
    })

    object = {
      ...object,
      description: description ?? '',
      ...templBase,
      template: {
        template: template._id
      }
    }
    appliedTemplateId = templateId
    const tagElements = await client.findAll(tags.class.TagElement, { _id: { $in: labels_ } })
    labels = tagElements.map(tagAsRef)
  }

  function updateTemplate (template?: IssueTemplate): void {
    if (template !== undefined) {
      updateObject(template)
    }
  }

  $: updateTemplate(template)

  const dispatch = createEventDispatcher()
  const spaceQuery = createQuery()

  let descriptionBox: AttachmentStyledBox

  const key: KeyedAttribute = {
    key: 'labels',
    attr: client.getHierarchy().getAttribute(tracker.class.Issue, 'labels')
  }

  $: spaceQuery.query(tracker.class.Project, { _id: _space }, (res) => {
    currentProject = res.shift()
  })

  async function setPropsFromOriginalIssue () {
    if (!originalIssue) {
      return
    }
    const { _id, relations, parents } = originalIssue

    if (relations?.[0]) {
      relatedTo = await client.findOne(tracker.class.Issue, { _id: relations[0]._id as Ref<Issue> })
    }
    if (parents?.[0]) {
      parentIssue = await client.findOne(tracker.class.Issue, { _id: parents[0].parentId })
    }
    if (originalIssue.labels) {
      labels = await client.findAll(tags.class.TagReference, { attachedTo: _id })
    }
  }

  async function setPropsFromDraft () {
    if (!draft?.parentIssue) {
      return
    }

    parentIssue = await client.findOne(tracker.class.Issue, { _id: draft.parentIssue as Ref<Issue> })
  }

  $: originalIssue && setPropsFromOriginalIssue()
  $: draft && setPropsFromDraft()
  $: object && updateDraft()

  async function updateDraft () {
    if (saveTimer) {
      clearTimeout(saveTimer)
    }
    saveTimer = setTimeout(() => {
      saveDraft()
    }, 200)
  }

  async function saveDraft () {
    if (!shouldSaveDraft) {
      return
    }

    let newDraft: Data<IssueDraft> | undefined = createDraftFromObject()
    const isEmpty = await isDraftEmpty(newDraft)

    if (isEmpty) {
      newDraft = undefined
    }
    updateUserDraft(tracker.class.IssueDraft, newDraft)

    if (onDraftChanged) {
      return onDraftChanged()
    }
  }

  async function updateIssueStatusId (currentProject: Project | undefined, issueStatusId?: Ref<IssueStatus>) {
    if (issueStatusId !== undefined) {
      object.status = issueStatusId
      return
    }

    if (currentProject?.defaultIssueStatus) {
      object.status = currentProject.defaultIssueStatus
    }
  }

  function updateAssigneeId (currentProject: Project | undefined) {
    if (currentProject?.defaultAssignee !== undefined) {
      object.assignee = currentProject.defaultAssignee
    } else {
      object.assignee = null
    }
  }
  function clearParentIssue () {
    parentIssue = undefined
  }

  function getTitle (value: string) {
    return value.trim()
  }

  async function isDraftEmpty (draft: Data<IssueDraft>): Promise<boolean> {
    const emptyDraft: Partial<IssueDraft> = {
      description: '',
      dueDate: null,
      estimation: 0,
      attachments: 0,
      labels: [],
      parentIssue: undefined,
      priority: 0,
      subIssues: [],
      template: undefined,
      title: ''
    }

    for (const key of Object.keys(emptyDraft)) {
      if (!deepEqual((emptyDraft as any)[key], (draft as any)[key])) {
        return false
      }
    }

    if (object.attachments && object.attachments > 0) {
      return false
    }

    if (draft.component && draft.component !== defaultIssue.component) {
      return false
    }

    if (draft.sprint && draft.sprint !== defaultIssue.sprint) {
      return false
    }

    if (draft.status === '') {
      return true
    }

    if (currentProject?.defaultIssueStatus) {
      return draft.status === currentProject.defaultIssueStatus
    }

    if (draft.assignee === null) {
      return true
    }

    if (currentProject?.defaultAssignee) {
      return draft.assignee === currentProject.defaultAssignee
    }

    return false
  }

  export function canClose (): boolean {
    return true
  }

  function createDraftFromObject () {
    const newDraft: Data<IssueDraft> = {
      issueId: objectId,
      title: getTitle(object.title),
      description: (object.description as string).replaceAll('<p></p>', ''),
      assignee: object.assignee,
      component: object.component,
      sprint: object.sprint,
      status: object.status,
      priority: object.priority,
      dueDate: object.dueDate,
      estimation: object.estimation,
      template: object.template,
      attachments: object.attachments,
      labels,
      parentIssue: parentIssue?._id,
      project: _space,
      subIssues
    }

    return newDraft
  }

  export async function onOutsideClick () {
    saveDraft()

    if (onDraftChanged) {
      return onDraftChanged()
    }
  }

  async function createIssue () {
    if (!canSave) {
      return
    }

    const lastOne = await client.findOne<Issue>(tracker.class.Issue, {}, { sort: { rank: SortingOrder.Descending } })
    const incResult = await client.updateDoc(
      tracker.class.Project,
      core.space.Space,
      _space,
      {
        $inc: { sequence: 1 }
      },
      true
    )

    const value: AttachedData<Issue> = {
      title: getTitle(object.title),
      description: object.description,
      assignee: object.assignee,
      component: object.component,
      sprint: object.sprint,
      number: (incResult as any).object.sequence,
      status: object.status,
      priority: object.priority,
      rank: calcRank(lastOne, undefined),
      comments: 0,
      subIssues: 0,
      dueDate: object.dueDate,
      parents: parentIssue
        ? [{ parentId: parentIssue._id, parentTitle: parentIssue.title }, ...parentIssue.parents]
        : [],
      reportedTime: 0,
      estimation: object.estimation,
      reports: 0,
      relations: relatedTo !== undefined ? [{ _id: relatedTo._id, _class: relatedTo._class }] : [],
      childInfo: [],
      createOn: Date.now()
    }

    await client.addCollection(
      tracker.class.Issue,
      _space,
      parentIssue?._id ?? tracker.ids.NoParent,
      parentIssue?._class ?? tracker.class.Issue,
      'subIssues',
      value,
      objectId
    )
    for (const label of labels) {
      await client.addCollection(label._class, label.space, objectId, tracker.class.Issue, 'labels', {
        title: label.title,
        color: label.color,
        tag: label.tag
      })
    }
    await descriptionBox.createAttachments()

    if (relatedTo !== undefined) {
      const doc = await client.findOne(tracker.class.Issue, { _id: objectId })
      if (doc !== undefined) {
        if (client.getHierarchy().isDerived(relatedTo._class, tracker.class.Issue)) {
          await updateIssueRelation(client, relatedTo as Issue, doc, 'relations', '$push')
        } else {
          const update = await getResource(chunter.backreference.Update)
          await update(doc, 'relations', [relatedTo], tracker.string.AddedReference)
        }
      }
    }
    const parents = parentIssue
      ? [
          { parentId: objectId, parentTitle: value.title },
          { parentId: parentIssue._id, parentTitle: parentIssue.title },
          ...parentIssue.parents
        ]
      : [{ parentId: objectId, parentTitle: value.title }]
    await subIssuesComponent.save(parents)
    addNotification(await translate(tracker.string.IssueCreated, {}), getTitle(object.title), IssueNotification, {
      issueId: objectId,
      subTitlePostfix: (await translate(tracker.string.Created, { value: 1 })).toLowerCase(),
      issueUrl: currentProject && generateIssueShortLink(getIssueId(currentProject, value as Issue))
    })

    objectId = generateId()
    resetObject()
    saveDraft()
    descriptionBox?.removeDraft(false)
  }

  async function showMoreActions (ev: Event) {
    ev.preventDefault()

    const selectDueDate = {
      label: object.dueDate === null ? tracker.string.SetDueDate : tracker.string.ChangeDueDate,
      icon: tracker.icon.DueDate,
      action: async () =>
        showPopup(
          SetDueDateActionPopup,
          { value: object },
          'top',
          undefined,
          (newDueDate) => newDueDate !== undefined && (object.dueDate = newDueDate)
        )
    }

    const setParentIssue = {
      label: parentIssue ? tracker.string.ChangeParent : tracker.string.SetParent,
      icon: tracker.icon.Parent,
      action: async () =>
        showPopup(
          SetParentIssueActionPopup,
          { value: { ...object, space: _space, attachedTo: parentIssue?._id } },
          'top',
          (selectedIssue) => selectedIssue !== undefined && (parentIssue = selectedIssue)
        )
    }

    const removeParentIssue = parentIssue && {
      label: tracker.string.RemoveParent,
      icon: tracker.icon.Parent,
      action: clearParentIssue
    }

    showPopup(
      Menu,
      {
        actions: [selectDueDate, setParentIssue, ...(removeParentIssue ? [removeParentIssue] : [])]
      },
      ev.target as HTMLElement
    )
  }

  const handleComponentIdChanged = (componentId: Ref<ComponentType> | null | undefined) => {
    if (componentId === undefined) {
      return
    }

    object = { ...object, component: componentId }
  }

  const handleSprintIdChanged = async (sprintId: Ref<Sprint> | null | undefined) => {
    if (sprintId === undefined) {
      return
    }
    let componentSprintId: Ref<ComponentType> | null
    if (sprintId != null) {
      const sprint = await client.findOne(tracker.class.Sprint, { _id: sprintId })
      componentSprintId = sprint && sprint.component ? sprint.component : null
    } else componentSprintId = null

    object = { ...object, sprint: sprintId, component: componentSprintId }
  }

  function addTagRef (tag: TagElement): void {
    labels = [...labels, tagAsRef(tag)]
  }
  function handleTemplateChange (evt: CustomEvent<Ref<IssueTemplate>>): void {
    if (templateId == null) {
      templateId = evt.detail
      return
    }
    // Template is already specified, ask to replace.
    showPopup(
      MessageBox,
      {
        label: tracker.string.TemplateReplace,
        message: tracker.string.TemplateReplaceConfirm
      },
      'top',
      (result?: boolean) => {
        if (result === true) {
          templateId = evt.detail ?? undefined

          if (templateId === undefined) {
            subIssues = []
            resetObject()
          }
        }
      }
    )
  }

  async function showConfirmationDialog () {
    const newDraft = createDraftFromObject()
    const isFormEmpty = await isDraftEmpty(newDraft)

    if (isFormEmpty) {
      console.log('isFormEmpty')
      dispatch('close')
    } else {
      showPopup(
        MessageBox,
        {
          label: tracker.string.NewIssueDialogClose,
          message: tracker.string.NewIssueDialogCloseNote
        },
        'top',
        (result?: boolean) => {
          if (result === true) {
            dispatch('close')
            resetObject()
            saveDraft()
            descriptionBox?.removeDraft(true)
          }
        }
      )
    }
  }
</script>

<Card
  label={tracker.string.NewIssue}
  okAction={createIssue}
  {canSave}
  okLabel={tracker.string.SaveIssue}
  on:close={() => dispatch('close')}
  createMore={false}
  onCancel={showConfirmationDialog}
  on:changeContent
>
  <svelte:fragment slot="header">
    <div class="flex-row-center">
      <SpaceSelector _class={tracker.class.Project} label={tracker.string.Project} bind:space={_space} />
    </div>
    <ObjectBox
      _class={tracker.class.IssueTemplate}
      value={templateId}
      docQuery={{
        space: _space
      }}
      on:change={handleTemplateChange}
      size={'small'}
      label={tracker.string.NoIssueTemplate}
      icon={tracker.icon.Issues}
      searchField={'title'}
      allowDeselect={true}
      showNavigate={false}
      docProps={{ disableClick: true }}
    />
  </svelte:fragment>
  <svelte:fragment slot="title" let:label>
    <div class="flex-row-center gap-1">
      <div class="mr-2">
        <Label {label} />
      </div>
      {#if relatedTo}
        <div class="mr-2">
          <Label label={tracker.string.RelatedTo} />
        </div>
        <Component
          is={view.component.ObjectPresenter}
          props={{ value: relatedTo, _class: relatedTo._class, objectId: relatedTo._id, inline: true }}
        />
      {/if}
    </div>
  </svelte:fragment>
  {#if parentIssue}
    <ParentIssue issue={parentIssue} on:close={clearParentIssue} />
  {/if}
  <EditBox bind:value={object.title} placeholder={tracker.string.IssueTitlePlaceholder} kind={'large-style'} focus />
  {#key [objectId, appliedTemplateId]}
    <AttachmentStyledBox
      bind:this={descriptionBox}
      {objectId}
      {shouldSaveDraft}
      _class={tracker.class.Issue}
      space={_space}
      alwaysEdit
      showButtons={false}
      emphasized
      bind:content={object.description}
      placeholder={tracker.string.IssueDescriptionPlaceholder}
      on:changeSize={() => dispatch('changeContent')}
      on:attach={(ev) => {
        if (ev.detail.action === 'saved') {
          object.attachments = ev.detail.value
        }
      }}
    />
  {/key}
  <SubIssues
    bind:this={subIssuesComponent}
    projectId={_space}
    parent={objectId}
    project={currentProject}
    sprint={object.sprint}
    component={object.component}
    bind:subIssues
  />
  <svelte:fragment slot="pool">
    <div id="status-editor">
      <StatusEditor
        value={object}
        kind="no-border"
        size="small"
        shouldShowLabel={true}
        on:change={({ detail }) => (object.status = detail)}
      />
    </div>
    <PriorityEditor
      value={object}
      shouldShowLabel
      isEditable
      kind="no-border"
      size="small"
      justify="center"
      on:change={({ detail }) => (object.priority = detail)}
    />
    <AssigneeEditor
      value={object}
      size="small"
      kind="no-border"
      width={'min-content'}
      on:change={({ detail }) => (object.assignee = detail)}
    />
    <Component
      is={tags.component.TagsDropdownEditor}
      props={{
        items: labels,
        key,
        targetClass: tracker.class.Issue,
        countLabel: tracker.string.NumberLabels
      }}
      on:open={(evt) => {
        addTagRef(evt.detail)
      }}
      on:delete={(evt) => {
        labels = labels.filter((it) => it._id !== evt.detail)
      }}
    />
    <EstimationEditor kind={'no-border'} size={'small'} value={object} {currentProject} />
    <ComponentSelector value={object.component} onChange={handleComponentIdChanged} isEditable={true} />
    <SprintSelector
      value={object.sprint}
      onChange={handleSprintIdChanged}
      useComponent={(!originalIssue && object.component) || undefined}
    />
    {#if object.dueDate !== null}
      <DatePresenter bind:value={object.dueDate} editable />
    {/if}
    <ActionIcon icon={IconMoreH} size={'medium'} action={showMoreActions} />
  </svelte:fragment>
  <svelte:fragment slot="footer">
    <Button
      icon={IconAttachment}
      kind={'transparent'}
      on:click={() => {
        descriptionBox.attach()
      }}
    />
  </svelte:fragment>
</Card>
