import { LitElement, html, TemplateResult, PropertyValues } from 'lit'
import { property, query } from 'lit/decorators.js'
import { PrismHighlighter } from './PrismHighlighter.js'

/**
 * Syntax highlighting via Prism
 *
 * ### Example
 *
 * ```html
 * <prism-highlight id="c1" lang="markdown"></prism-highlight>
 * <script>
 *  document.querySelector('#c1').code = '# Test highlight';
 * &lt;/script>
 * ```
 *
 * The `lang` attribute is required and the component will not start parsing data without it.
 *
 * Changing the `lang` and `code` properties together, do it in less than 10 ms.
 * The element is set to commit changes after this time period. Otherwise it may display
 * old and new code due to the asynchronous nature of the code highlighter.
 *
 * @fires link - An event dispatched when the user click on the link. The detail object has the `url`
 *               property with the link value and the `asNew` property set to `true` when ctrl/cmd
 *               key was pressed when the link was clicked.
 */
export default class PrismHighlight extends LitElement {
  /**
   * A data to be highlighted and rendered.
   * @attribute
   */
  @property({ type: String }) accessor code: string | undefined

  /**
   * Prism supported language.
   * @attribute
   */
  @property({ type: String, reflect: true }) override accessor lang = ''

  /**
   * When set it ignores syntax highlighting and only renders the code.
   * @attribute
   */
  @property({ type: Boolean, reflect: true }) accessor raw: boolean | undefined

  @query('code')
  accessor outputElement!: HTMLElement | null

  protected highlighter = new PrismHighlighter(this.handleHighlight.bind(this))

  protected tokenizeResults?: string

  protected override updated(cp: PropertyValues | Map<PropertyKey, unknown>): void {
    super.updated(cp)
    if (cp.has('code') || cp.has('lang')) {
      this.highlight()
    }
    if (cp.has('raw')) {
      this.rawChanged()
    }
  }

  override firstUpdated(): void {
    /* istanbul ignore if */
    if (this.tokenizeResults && !this.raw) {
      const out = this.outputElement
      if (out) {
        out.innerHTML += this.tokenizeResults
      }
      this.tokenizeResults = undefined
    }
  }

  // Resets the state of the render to initial state.
  reset(): void {
    const node = this.outputElement
    if (node) {
      node.innerHTML = ''
    }
  }

  protected handleHighlight(code: string): void {
    const node = this.outputElement
    /* istanbul ignore else */
    if (node) {
      node.innerHTML += code
    } else {
      this.tokenizeResults = code
    }
  }

  protected rawChanged(): void {
    if (!this.raw) {
      this.highlight()
    }
  }

  /**
   * Highlights the code.
   */
  protected highlight(): void {
    const { code, lang, raw } = this
    if ((!code && lang) || raw) {
      return
    }
    this.reset()
    this.highlighter.debounce(code || '', lang)
  }

  /**
   * Handler for click events.
   */
  protected handleLinks(e: MouseEvent): void {
    const el = e.target as HTMLElement
    if (el.localName !== 'a') {
      return
    }
    const link = el as HTMLAnchorElement
    const newEntity = e.ctrlKey || e.metaKey
    e.preventDefault()
    e.stopPropagation()
    e.stopImmediatePropagation()
    const url = link.href
    this.dispatchEvent(
      new CustomEvent('link', {
        detail: {
          url,
          asNew: newEntity,
        },
      })
    )
  }

  override render(): TemplateResult {
    const { raw } = this
    return raw ? this.renderRaw() : this.renderHighlighted()
  }

  protected renderHighlighted(): TemplateResult {
    return html`
      <pre class="parsed-content">
      <code id="output" class="language-" @click="${this.handleLinks}"></code>
    </pre>
    `
  }

  protected renderRaw(): TemplateResult {
    return html`
      <pre class="raw-content">
      <code id="output" class="raw">${this.code}</code>
    </pre>
    `
  }
}
