/**
 @license
Copyright 2018 The Advanced REST client authors <arc@mulesoft.com>
Licensed under the Apache 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
http://www.apache.org/licenses/LICENSE-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.
*/
import { html, TemplateResult } from 'lit'
import { classMap } from 'lit/directives/class-map.js'
import { StyleInfo, styleMap } from 'lit/directives/style-map.js'
import { eventOptions, property, query, state } from 'lit/decorators.js'
import type { IUrl } from '@api-client/core/models/Url.js'
import { sortUrls } from '../../lib/http/Url.js'
import { Events } from '../../events/Events.js'
import { EventTypes } from '../../events/EventTypes.js'
import ApiElement from '../ApiElement.js'
import { positionOverlay } from '../../lib/ElementPositioning.js'
import type UiList from '../../md/list/internals/List.js'
import type UiListItem from '../../md/list/internals/ListItem.js'

import '../../md/listbox/ui-listbox.js'
import '../../md/list/ui-list-item.js'
import '../../md/divider/ui-divider.js'

const NAVIGATION_KEYS = {
  ArrowDown: 'ArrowDown',
  ArrowUp: 'ArrowUp',
  Home: 'Home',
  End: 'End',
  Escape: 'Escape',
  Enter: 'Enter',
  Space: ' ',
}

/**
 * The request URL editor
 *
 * The element renders an editor for a HTTP request editor.
 *
 * @fires change - When the value change through user interaction. This event does not bubble.
 * @fires requeststateurlchange - When the value change through user interaction. This even bubbles and is cancelable.
 * @fires requestsend - A bubbling event when the user pressed Enter to send the request.
 */
export default class UrlInput extends ApiElement {
  /**
   * The current URL value.
   * @attribute
   */
  @property({ type: String }) accessor value = 'http://'

  /**
   * When set the editor is in read only mode.
   * @attribute
   */
  @property({ type: Boolean, reflect: true }) accessor readOnly = false

  /**
   * Whether the URL suggestions are rendered.
   */
  @state() protected accessor suggestionsOpened = false

  @state() protected accessor suggestions: IUrl[] | undefined

  @state() protected accessor renderedSuggestions: IUrl[] | undefined

  protected previousValue?: string

  protected isFocused = false

  @query('.main-input')
  protected accessor input!: HTMLInputElement

  @query('.suggestions')
  protected accessor suggestionsContainer!: HTMLDivElement

  @query('ui-listbox.url-autocomplete')
  protected accessor suggestionsList!: UiList

  /**
   * Computed positioning styles for the suggestion box.
   */
  protected suggestionStyles?: StyleInfo

  constructor() {
    super()
    this.handleUrlChange = this.handleUrlChange.bind(this)
    this.handleHistoryDeleted = this.handleHistoryDeleted.bind(this)
    this.handleHistoryClear = this.handleHistoryClear.bind(this)
  }

  override connectedCallback(): void {
    super.connectedCallback()
    const { eventsTarget } = this
    eventsTarget.addEventListener(EventTypes.Http.Request.State.urlChange, this.handleUrlChange)
    eventsTarget.addEventListener(EventTypes.HttpClient.Model.UrlHistory.State.delete, this.handleHistoryDeleted)
    eventsTarget.addEventListener(EventTypes.HttpClient.Model.UrlHistory.State.clear, this.handleHistoryClear)
  }

  override disconnectedCallback(): void {
    super.disconnectedCallback()
    const { eventsTarget } = this
    eventsTarget.removeEventListener(EventTypes.Http.Request.State.urlChange, this.handleUrlChange)
    eventsTarget.removeEventListener(EventTypes.HttpClient.Model.UrlHistory.State.delete, this.handleHistoryDeleted)
    eventsTarget.removeEventListener(EventTypes.HttpClient.Model.UrlHistory.State.clear, this.handleHistoryClear)
  }

  /**
   * A handler that is called on input
   */
  override notifyChange(): void {
    Events.Http.Request.State.urlChange(this.value, this)
    super.notifyChange()
  }

  /**
   * A handler for the `url-value-changed` event.
   * If this element is not the source of the event then it will update the `value` property.
   * It's to be used besides the Polymer's data binding system.
   */
  protected handleUrlChange(e: Event): void {
    if (e.composedPath()[0] === this || this.readOnly) {
      return
    }
    const { value } = (e as CustomEvent).detail
    if (value !== this.value) {
      this.value = value
      super.notifyChange()
    }
  }

  /**
   * Queries the data model for history data and sets the suggestions
   * @param q User query from the input field
   */
  protected async readAutocomplete(q: string): Promise<void> {
    try {
      const value = await Events.HttpClient.Model.UrlHistory.query(q, this)
      this.suggestions = value && value.items.filter((i) => !!i).map((i) => i.doc)
    } catch {
      this.suggestions = undefined
    }
  }

  /**
   * Validates the element.
   */
  checkValidity(): boolean {
    const element = this.input
    if (!element) {
      return true
    }
    return element.validity.valid
  }

  /**
   * @param e A handler for either main input or the details editor value change
   */
  protected handleInput(e: Event): void {
    if (this.readOnly) {
      return
    }
    const node = e.target as HTMLInputElement
    this.previousValue = this.value
    this.value = node.value
    this.renderSuggestions()
  }

  protected handleInputChange(): void {
    if (this.readOnly) {
      return
    }
    this.notifyChange()
  }

  protected handlerFocusBlur(e: Event): void {
    this.isFocused = e.type === 'focus'
    // this.requestUpdate();
    if (this.isFocused && !this.suggestionsOpened) {
      this.renderSuggestions()
    } else if (!this.isFocused && this.suggestionsOpened) {
      // requestAnimationFrame(() => {
      //   this.suggestionsOpened = false;
      // });
    }
  }

  /**
   * Triggers URL suggestions rendering.
   * If there are suggestions to render this will enable the dropdown.
   */
  async renderSuggestions(): Promise<void> {
    const { value = '' } = this
    if (this.previousValue && value.startsWith(this.previousValue || '')) {
      this.filterSuggestions()
      return
    }
    await this.readAutocomplete(value)
    this.filterSuggestions()
  }

  /**
   * Performs the query on the current suggestions and, if any, renders them.
   */
  protected async filterSuggestions(): Promise<void> {
    const items = this.suggestions
    if (!Array.isArray(items) || !items.length) {
      this.toggleSuggestions(false)
      return
    }
    const { value = '' } = this
    const q = String(value).toLowerCase()
    const rendered = items.filter((i) => i.key.toLowerCase().includes(q))
    if (!rendered.length) {
      this.toggleSuggestions(false)
      return
    }
    if (rendered.length === 1 && rendered[0].key.toLowerCase() === q) {
      this.toggleSuggestions(false)
      return
    }
    sortUrls(rendered, q)
    this.renderedSuggestions = rendered
    this.toggleSuggestions(true)
    this.requestUpdate()
  }

  protected toggleSuggestions(opened: boolean): void {
    if (!opened) {
      this.suggestionsOpened = false
      this.input.focus()
    } else {
      this.suggestionStyles = positionOverlay(this.suggestionsContainer, this.input, {
        vertical: 'top',
        noOverlap: true,
        constrain: true,
        matchAnchorWidth: true,
      })
      this.suggestionsOpened = true
    }
  }

  @eventOptions({ capture: true })
  protected handleSuggestionClick(e: Event): void {
    const path = e.composedPath() as HTMLElement[]
    const target = path.find((i) => !!i.classList && i.classList.contains('remove-suggestion'))
    if (!target) {
      return
    }
    e.preventDefault()
    e.stopPropagation()
    e.stopImmediatePropagation()
    const { key } = target.dataset
    if (!key) {
      return
    }

    Events.HttpClient.Model.UrlHistory.delete(key, this)
  }

  protected handleSuggestionSelect(e: CustomEvent): void {
    const item = e.detail.item as UiListItem
    const { key } = item.dataset
    if (!key) {
      return
    }
    this.selectSuggestion(key)
  }

  protected selectSuggestion(key: string): void {
    this.value = key
    this.toggleSuggestions(false)
    this.notifyChange()
  }

  /**
   * Removes all stored history URLs.
   */
  protected clearSuggestionsHandler(e: Event): void {
    e.preventDefault()
    e.stopPropagation()
    Events.HttpClient.Model.UrlHistory.clear(this)
  }

  protected handleHistoryDeleted(e: Event): void {
    const items = this.suggestions
    if (!Array.isArray(items)) {
      return
    }
    const url = (e as CustomEvent).detail
    const index = items.findIndex((i) => i.key === url)
    if (index >= 0) {
      items.splice(index, 1)
    }
    if (this.suggestionsOpened && this.renderedSuggestions) {
      const renderedIndex = this.renderedSuggestions.findIndex((i) => i.key === url)
      if (renderedIndex >= 0) {
        this.renderedSuggestions.splice(renderedIndex, 1)
        this.requestUpdate()
      }
    }
  }

  protected handleHistoryClear(): void {
    this.suggestions = undefined
    this.renderedSuggestions = undefined
    this.toggleSuggestions(false)
  }

  /**
   * We handle key events here as we never focus on the list.
   */
  protected handleKeydown(event: KeyboardEvent): void {
    if (Object.values(NAVIGATION_KEYS).indexOf(event.key) === -1) return

    if (event.key === NAVIGATION_KEYS.Escape) {
      this.toggleSuggestions(false)
    } else if (event.key === NAVIGATION_KEYS.ArrowDown) {
      event.preventDefault()
      if (this.suggestionsOpened) {
        this.suggestionsList.highlightNext()
      } else {
        this.filterSuggestions()
      }
    } else if (event.key === NAVIGATION_KEYS.ArrowUp) {
      event.preventDefault()
      if (this.suggestionsOpened) {
        this.suggestionsList.highlightPrevious()
      } else {
        this.filterSuggestions()
      }
    } else if (event.key === NAVIGATION_KEYS.Home) {
      if (this.suggestionsOpened) {
        event.preventDefault()
        this.suggestionsList.highlightFirst()
      }
    } else if (event.key === NAVIGATION_KEYS.End) {
      if (this.suggestionsOpened) {
        event.preventDefault()
        this.suggestionsList.highlightLast()
      }
    } else if (event.key === NAVIGATION_KEYS.Enter) {
      event.preventDefault()
      if (this.suggestionsOpened) {
        const list = this.suggestionsList
        const item = list.highlightListItem
        if (item) {
          list.notifySelect(item)
        }
      } else {
        Events.Http.Request.send(this)
      }
    }
  }

  override render(): TemplateResult {
    const classes = {
      container: true,
      autocomplete: this.suggestionsOpened,
    }
    return html` <div class="${classMap(classes)}">${this.renderInput()} ${this.renderAutocomplete()}</div>`
  }

  /**
   * @returns A template for the main input element
   */
  protected renderInput(): TemplateResult {
    const { value } = this
    return html`
      <div class="input-wrapper">
        <input
          .value="${value}"
          class="main-input"
          required
          placeholder="Request URL"
          id="mainInput"
          autocomplete="off"
          spellcheck="false"
          @focus="${this.handlerFocusBlur}"
          @blur="${this.handlerFocusBlur}"
          @input="${this.handleInput}"
          @change="${this.handleInputChange}"
          @keydown="${this.handleKeydown}"
          aria-label="The URL value"
          ?readonly="${this.readOnly}"
        />
      </div>
    `
  }

  /**
   * @returns A template for the autocomplete element
   */
  protected renderAutocomplete(): TemplateResult {
    let opened = this.suggestionsOpened
    const items = this.renderedSuggestions
    if (opened && (!items || !items.length)) {
      opened = false
    }
    const classes = {
      suggestions: true,
      opened,
    }
    return html`
      <div class="${classMap(classes)}" style="${styleMap(this.suggestionStyles || {})}">
        <ui-listbox
          class="url-autocomplete"
          role="listbox"
          aria-label="Suggested URLs from your history."
          tabindex="-1"
          @select="${this.handleSuggestionSelect}"
          @click="${this.handleSuggestionClick}"
        >
          ${this.renderSuggestionList()}
        </ui-listbox>
        <ui-divider></ui-divider>
        <p class="clear-all-history">
          <span class="clear-all-history-label" @click="${this.clearSuggestionsHandler}">Clear all history</span>
        </p>
      </div>
    `
  }

  /**
   * @returns The template for the suggestions list.
   */
  protected renderSuggestionList(): TemplateResult[] | string {
    const items = this.renderedSuggestions
    if (!Array.isArray(items) || !items.length) {
      return ''
    }
    return items.map((i) => this.renderSuggestionItem(i))
  }

  /**
   * @returns The template for an URL suggestion item.
   */
  protected renderSuggestionItem(item: IUrl): TemplateResult {
    const { key } = item
    return html` <ui-list-item class="suggestion-item" data-key="${item.key}" role="option" aria-selected="false">
      <div>${key}</div>
      <span slot="end-text" class="remove-suggestion" data-key="${key}">Remove</span>
    </ui-list-item>`
  }
}
