import {
  Component,
  ViewChild,
  ElementRef,
  ViewChildren,
  QueryList,
  OnInit,
} from '@angular/core'
import { Router } from '@angular/router'
import { NgbDropdown, NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'
import { Subject, debounceTime, distinctUntilChanged, filter, map } from 'rxjs'
import {
  FILTER_FULLTEXT_QUERY,
  FILTER_HAS_CORRESPONDENT_ANY,
  FILTER_HAS_DOCUMENT_TYPE_ANY,
  FILTER_HAS_STORAGE_PATH_ANY,
  FILTER_HAS_TAGS_ALL,
} from 'src/app/data/filter-rule-type'
import { DataType } from 'src/app/data/datatype'
import { ObjectWithId } from 'src/app/data/object-with-id'
import { DocumentListViewService } from 'src/app/services/document-list-view.service'
import {
  PermissionsService,
  PermissionAction,
} from 'src/app/services/permissions.service'
import { DocumentService } from 'src/app/services/rest/document.service'
import {
  GlobalSearchResult,
  SearchService,
} from 'src/app/services/rest/search.service'
import { ToastService } from 'src/app/services/toast.service'
import { CorrespondentEditDialogComponent } from '../../common/edit-dialog/correspondent-edit-dialog/correspondent-edit-dialog.component'
import { CustomFieldEditDialogComponent } from '../../common/edit-dialog/custom-field-edit-dialog/custom-field-edit-dialog.component'
import { DocumentTypeEditDialogComponent } from '../../common/edit-dialog/document-type-edit-dialog/document-type-edit-dialog.component'
import { EditDialogMode } from '../../common/edit-dialog/edit-dialog.component'
import { GroupEditDialogComponent } from '../../common/edit-dialog/group-edit-dialog/group-edit-dialog.component'
import { MailAccountEditDialogComponent } from '../../common/edit-dialog/mail-account-edit-dialog/mail-account-edit-dialog.component'
import { MailRuleEditDialogComponent } from '../../common/edit-dialog/mail-rule-edit-dialog/mail-rule-edit-dialog.component'
import { StoragePathEditDialogComponent } from '../../common/edit-dialog/storage-path-edit-dialog/storage-path-edit-dialog.component'
import { TagEditDialogComponent } from '../../common/edit-dialog/tag-edit-dialog/tag-edit-dialog.component'
import { UserEditDialogComponent } from '../../common/edit-dialog/user-edit-dialog/user-edit-dialog.component'
import { WorkflowEditDialogComponent } from '../../common/edit-dialog/workflow-edit-dialog/workflow-edit-dialog.component'
import { HotKeyService } from 'src/app/services/hot-key.service'
import { paramsFromViewState } from 'src/app/utils/query-params'

@Component({
  selector: 'pngx-global-search',
  templateUrl: './global-search.component.html',
  styleUrl: './global-search.component.scss',
})
export class GlobalSearchComponent implements OnInit {
  public DataType = DataType
  public query: string
  public queryDebounce: Subject<string>
  public searchResults: GlobalSearchResult
  private currentItemIndex: number = -1
  private domIndex: number = -1
  public loading: boolean = false

  @ViewChild('searchInput') searchInput: ElementRef
  @ViewChild('resultsDropdown') resultsDropdown: NgbDropdown
  @ViewChildren('resultItem') resultItems: QueryList<ElementRef>
  @ViewChildren('primaryButton') primaryButtons: QueryList<ElementRef>
  @ViewChildren('secondaryButton') secondaryButtons: QueryList<ElementRef>

  constructor(
    public searchService: SearchService,
    private router: Router,
    private modalService: NgbModal,
    private documentService: DocumentService,
    private documentListViewService: DocumentListViewService,
    private permissionsService: PermissionsService,
    private toastService: ToastService,
    private hotkeyService: HotKeyService
  ) {
    this.queryDebounce = new Subject<string>()

    this.queryDebounce
      .pipe(
        debounceTime(400),
        map((query) => query?.trim()),
        filter((query) => !query?.length || query?.length > 2),
        distinctUntilChanged()
      )
      .subscribe((text) => {
        this.query = text
        if (text) this.search(text)
      })
  }

  public ngOnInit() {
    this.hotkeyService
      .addShortcut({ keys: '/', description: $localize`Global search` })
      .subscribe(() => {
        this.searchInput.nativeElement.focus()
      })
  }

  private search(query: string) {
    this.loading = true
    this.searchService.globalSearch(query).subscribe((results) => {
      this.searchResults = results
      this.loading = false
      this.resultsDropdown.open()
    })
  }

  public primaryAction(
    type: string,
    object: ObjectWithId,
    event: PointerEvent = null
  ) {
    const newWindow = event?.metaKey || event?.ctrlKey
    this.reset(true)
    let filterRuleType: number
    let editDialogComponent: any
    let size: string = 'md'
    switch (type) {
      case DataType.Document:
        this.navigateOrOpenInNewWindow(['/documents', object.id], newWindow)
        return
      case DataType.SavedView:
        this.navigateOrOpenInNewWindow(['/view', object.id], newWindow)
        return
      case DataType.Correspondent:
        filterRuleType = FILTER_HAS_CORRESPONDENT_ANY
        break
      case DataType.DocumentType:
        filterRuleType = FILTER_HAS_DOCUMENT_TYPE_ANY
        break
      case DataType.StoragePath:
        filterRuleType = FILTER_HAS_STORAGE_PATH_ANY
        break
      case DataType.Tag:
        filterRuleType = FILTER_HAS_TAGS_ALL
        break
      case DataType.User:
        editDialogComponent = UserEditDialogComponent
        size = 'lg'
        break
      case DataType.Group:
        editDialogComponent = GroupEditDialogComponent
        size = 'lg'
        break
      case DataType.MailAccount:
        editDialogComponent = MailAccountEditDialogComponent
        size = 'xl'
        break
      case DataType.MailRule:
        editDialogComponent = MailRuleEditDialogComponent
        size = 'xl'
        break
      case DataType.CustomField:
        editDialogComponent = CustomFieldEditDialogComponent
        break
      case DataType.Workflow:
        editDialogComponent = WorkflowEditDialogComponent
        size = 'xl'
        break
    }

    if (filterRuleType) {
      let params = paramsFromViewState({
        filterRules: [
          { rule_type: filterRuleType, value: object.id.toString() },
        ],
        currentPage: 1,
        sortField: this.documentListViewService.sortField ?? 'created',
        sortReverse: this.documentListViewService.sortReverse,
      })
      this.navigateOrOpenInNewWindow(['/documents'], newWindow, {
        queryParams: params,
      })
    } else if (editDialogComponent) {
      const modalRef: NgbModalRef = this.modalService.open(
        editDialogComponent,
        { size }
      )
      modalRef.componentInstance.dialogMode = EditDialogMode.EDIT
      modalRef.componentInstance.object = object
      modalRef.componentInstance.succeeded.subscribe(() => {
        this.toastService.showInfo($localize`Successfully updated object.`)
      })
      modalRef.componentInstance.failed.subscribe((e) => {
        this.toastService.showError($localize`Error occurred saving object.`, e)
      })
    }
  }

  public secondaryAction(type: string, object: ObjectWithId) {
    this.reset(true)
    let editDialogComponent: any
    let size: string = 'md'
    switch (type) {
      case DataType.Document:
        window.open(this.documentService.getDownloadUrl(object.id))
        break
      case DataType.Correspondent:
        editDialogComponent = CorrespondentEditDialogComponent
        break
      case DataType.DocumentType:
        editDialogComponent = DocumentTypeEditDialogComponent
        break
      case DataType.StoragePath:
        editDialogComponent = StoragePathEditDialogComponent
        break
      case DataType.Tag:
        editDialogComponent = TagEditDialogComponent
        break
    }

    if (editDialogComponent) {
      const modalRef: NgbModalRef = this.modalService.open(
        editDialogComponent,
        { size }
      )
      modalRef.componentInstance.dialogMode = EditDialogMode.EDIT
      modalRef.componentInstance.object = object
      modalRef.componentInstance.succeeded.subscribe(() => {
        this.toastService.showInfo($localize`Successfully updated object.`)
      })
      modalRef.componentInstance.failed.subscribe((e) => {
        this.toastService.showError($localize`Error occurred saving object.`, e)
      })
    }
  }

  private reset(close: boolean = false) {
    this.queryDebounce.next(null)
    this.query = null
    this.searchResults = null
    this.currentItemIndex = -1
    if (close) {
      this.resultsDropdown.close()
    }
  }

  private setCurrentItem() {
    // QueryLists do not always reflect the current DOM order, so we need to find the actual element
    // Yes, using some vanilla JS
    const result: HTMLElement = this.resultItems.first.nativeElement.parentNode
      .querySelectorAll('.dropdown-item')
      .item(this.currentItemIndex)
    this.domIndex = this.resultItems
      .toArray()
      .indexOf(this.resultItems.find((item) => item.nativeElement === result))
    const item: ElementRef = this.primaryButtons.get(this.domIndex)
    item.nativeElement.focus()
  }

  public onItemHover(event: MouseEvent) {
    const item: ElementRef = this.resultItems
      .toArray()
      .find((item) => item.nativeElement === event.currentTarget)
    this.currentItemIndex = this.resultItems.toArray().indexOf(item)
    this.setCurrentItem()
  }

  public onButtonHover(event: MouseEvent) {
    ;(event.currentTarget as HTMLElement).focus()
  }

  public searchInputKeyDown(event: KeyboardEvent) {
    if (
      event.key === 'ArrowDown' &&
      this.searchResults?.total &&
      this.resultsDropdown.isOpen()
    ) {
      event.preventDefault()
      this.currentItemIndex = 0
      this.setCurrentItem()
    } else if (
      event.key === 'ArrowUp' &&
      this.searchResults?.total &&
      this.resultsDropdown.isOpen()
    ) {
      event.preventDefault()
      this.currentItemIndex = this.searchResults.total - 1
      this.setCurrentItem()
    } else if (event.key === 'Enter') {
      if (this.searchResults?.total === 1 && this.resultsDropdown.isOpen()) {
        this.primaryButtons.first.nativeElement.click()
        this.searchInput.nativeElement.blur()
      } else if (this.query?.length) {
        this.runAdvanedSearch()
        this.reset(true)
      }
    } else if (event.key === 'Escape' && !this.resultsDropdown.isOpen()) {
      if (this.query?.length) {
        this.reset(true)
      } else {
        this.searchInput.nativeElement.blur()
      }
    }
  }

  public dropdownKeyDown(event: KeyboardEvent) {
    if (
      this.searchResults?.total &&
      this.resultsDropdown.isOpen() &&
      document.activeElement !== this.searchInput.nativeElement
    ) {
      if (event.key === 'ArrowDown') {
        event.preventDefault()
        event.stopImmediatePropagation()
        if (this.currentItemIndex < this.searchResults.total - 1) {
          this.currentItemIndex++
          this.setCurrentItem()
        } else {
          this.searchInput.nativeElement.focus()
          this.currentItemIndex = -1
        }
      } else if (event.key === 'ArrowUp') {
        event.preventDefault()
        event.stopImmediatePropagation()
        if (this.currentItemIndex > 0) {
          this.currentItemIndex--
          this.setCurrentItem()
        } else {
          this.searchInput.nativeElement.focus()
          this.currentItemIndex = -1
        }
      } else if (event.key === 'ArrowRight') {
        event.preventDefault()
        event.stopImmediatePropagation()
        this.secondaryButtons.get(this.domIndex)?.nativeElement.focus()
      } else if (event.key === 'ArrowLeft') {
        event.preventDefault()
        event.stopImmediatePropagation()
        this.primaryButtons.get(this.domIndex).nativeElement.focus()
      } else if (event.key === 'Escape') {
        event.preventDefault()
        event.stopImmediatePropagation()
        this.reset(true)
        this.searchInput.nativeElement.focus()
      }
    }
  }

  public onButtonKeyDown(event: KeyboardEvent) {
    if (event.key === 'Enter' && (event.metaKey || event.ctrlKey)) {
      event.target.dispatchEvent(new MouseEvent('click', { ctrlKey: true }))
    }
  }

  public onDropdownOpenChange(open: boolean) {
    if (!open) {
      this.reset()
    }
  }

  public disablePrimaryButton(type: DataType, object: ObjectWithId): boolean {
    if (
      [
        DataType.Workflow,
        DataType.CustomField,
        DataType.Group,
        DataType.User,
      ].includes(type)
    ) {
      return !this.permissionsService.currentUserHasObjectPermissions(
        PermissionAction.Change,
        object
      )
    }

    return false
  }

  public disableSecondaryButton(type: DataType, object: ObjectWithId): boolean {
    if (DataType.Document === type) {
      return false
    }

    return !this.permissionsService.currentUserHasObjectPermissions(
      PermissionAction.Change,
      object
    )
  }

  public runAdvanedSearch() {
    this.documentListViewService.quickFilter([
      { rule_type: FILTER_FULLTEXT_QUERY, value: this.query },
    ])
    this.reset(true)
  }

  private navigateOrOpenInNewWindow(
    commands: any,
    newWindow: boolean = false,
    extras: Object = {}
  ) {
    if (newWindow) {
      const url = this.router.serializeUrl(
        this.router.createUrlTree(commands, extras)
      )
      window.open(url, '_blank')
    } else {
      this.router.navigate(commands, extras)
    }
  }
}