/**
 * Copyright Aquera Inc 2023
 *
 * This source code is licensed under the BSD-3-Clause license found in the
 * LICENSE file in the root directory of this source tree.
 */

import { LitElement, CSSResultArray, TemplateResult } from 'lit';

import '../nile-icon-button/nile-icon-button';
import { animateTo, stopAnimations } from '../internal/animate';
import { classMap } from 'lit/directives/class-map.js';
import { customElement, property, query } from 'lit/decorators.js';
import {
  getAnimation,
  setDefaultAnimation,
} from '../utilities/animation-registry';
import { HasSlotController } from '../internal/slot';
import { html } from 'lit';
import { waitForEvent } from '../internal/event';
import { watch } from '../internal/watch';
import NileElement from '../internal/nile-element';
import { styles } from './nile-toast.css';
import type { CSSResultGroup } from 'lit';

const toastStack = Object.assign(document.createElement('div'), {
  className: 'nile-toast-stack',
});

/**
 * Nile icon component.
 *
 * @tag nile-toast
 *
 * @dependency nile-icon-button
 *
 * @slot - The alert's main content.
 * @slot icon - An icon to show in the alert. Works best with `<nile-icon>`.
 *
 * @event nile-show - Emitted when the alert opens.
 * @event nile-after-show - Emitted after the alert opens and all animations are complete.
 * @event nile-hide - Emitted when the alert closes.
 * @event nile-after-hide - Emitted after the alert closes and all animations are complete.
 *
 * @csspart base - The component's base wrapper.
 * @csspart icon - The container that wraps the optional icon.
 * @csspart message - The container that wraps the alert's main content.
 * @csspart close-button - The close button, an `<nile-icon-button>`.
 * @csspart close-button__base - The close button's exported `base` part.
 *
 * @animation alert.show - The animation to use when showing the alert.
 * @animation alert.hide - The animation to use when hiding the alert.
 */

@customElement('nile-toast')
export class NileToast extends NileElement {
  static styles: CSSResultGroup = styles;

  private autoHideTimeout: number;
  private readonly hasSlotController = new HasSlotController(
    this,
    'icon',
    'suffix'
  );

  @query('[part~="base"]') base: HTMLElement;

  /**
   * Indicates whether or not the alert is open. You can toggle this attribute to show and hide the alert, or you can
   * use the `show()` and `hide()` methods and this attribute will reflect the alert's open state.
   */
  @property({ type: Boolean, reflect: true }) open = false;

  @property({ type: Boolean, reflect: true }) noIcon = false;

  /** Enables a close button that allows the user to dismiss the alert. */

  @property({ type: Boolean, reflect: true }) closable = false;

  @property({ type: Boolean, reflect: true }) hasSlottedContent = false;

  @property({ type: Boolean, reflect: true }) hasSlottedIcon = false;

  @property({ type: String, reflect: true }) prefixImageUrl = '';
  
  @property({ type: String, reflect: true }) closeIconName = 'var(--nile-icon-close, var(--ng-icon-x-close))';

  /** The alert's theme variant. */
  @property({ reflect: true }) variant:
    | 'success'
    | 'info'
    | 'warning'
    | 'error'
    | 'gray'
    | 'black' = 'success';

  /**
   * The length of time, in milliseconds, the alert will show before closing itself. If the user interacts with
   * the alert before it closes (e.g. moves the mouse over it), the timer will restart. Defaults to `Infinity`, meaning
   * the alert will not close on its own.
   */
  @property({ type: Number }) duration = Infinity;

  @property({ type: String }) title = '';

  @property({ type: String }) content = '';

  @property({ type: Array, reflect: true }) tags: any[] = [];

  firstUpdated() {
    this.base.hidden = !this.open;
  }

  private restartAutoHide() {
    clearTimeout(this.autoHideTimeout);
    if (this.open && this.duration < Infinity) {
      this.autoHideTimeout = window.setTimeout(
        () => this.hide(),
        this.duration
      );
    }
  }

  private handleCloseClick() {
    this.hide();
  }

  private handleMouseMove() {
    this.restartAutoHide();
  }

  @watch('open', { waitUntilFirstUpdate: true })
  async handleOpenChange() {
    if (this.open) {
      // Show
      this.emit('nile-show');

      if (this.duration < Infinity) {
        this.restartAutoHide();
      }

      await stopAnimations(this.base);
      this.base.hidden = false;
      const { keyframes, options } = getAnimation(this, 'alert.show', {
        dir: 'ltr',
      });
      await animateTo(this.base, keyframes, options);

      this.emit('nile-after-show');
    } else {
      // Hide
      this.emit('nile-hide');

      clearTimeout(this.autoHideTimeout);

      await stopAnimations(this.base);
      const { keyframes, options } = getAnimation(this, 'alert.hide', {
        dir: 'ltr',
      });
      await animateTo(this.base, keyframes, options);
      this.base.hidden = true;

      this.emit('nile-after-hide');
    }
  }

  @watch('duration')
  handleDurationChange() {
    this.restartAutoHide();
  }

  /** Shows the alert. */
  async show() {
    if (this.open) {
      return undefined;
    }

    this.open = true;
    return waitForEvent(this, 'nile-after-show');
  }

  /** Hides the alert */
  async hide() {
    if (!this.open) {
      return undefined;
    }

    this.open = false;
    return waitForEvent(this, 'nile-after-hide');
  }

  getIconNameByVariant() {
    switch (this.variant) {
      case 'success':
        return 'var(--nile-icon-radiodone, var(--ng-icon-check-circle))';
      case 'info':
        return 'var(--nile-icon-info, var(--ng-icon-info-circle))';
      case 'warning':
        return 'var(--nile-icon-warning, var(--ng-icon-alert-circle))';
      case 'error':
        return 'var(--nile-icon-warning, var(--ng-icon-alert-circle))';
      case 'gray':
        return 'var(--nile-icon-info, var(--ng-icon-info-circle))';
      case 'black':
        return 'var(--nile-icon-info, var(--ng-icon-info-circle))';
    }
  }

  getIconColorByVariant() {
    switch (this.variant) {
      case 'success':
        return 'var(--nile-toast-color-icon-color-success, var(--ng-colors-fg-success-primary))';
      case 'info':
        return 'var(--nile-toast-color-icon-color-info, var(--ng-colors-fg-brand-primary-600))';
      case 'warning':
        return 'var(--nile-toast-color-icon-color-warning, var(--ng-colors-fg-warning-primary))';
      case 'error':
        return 'var(--nile-toast-color-icon-color-error, var(--ng-colors-fg-error-primary))';
      case 'gray':
        return 'var(--nile-toast-color-icon-color-gray, var(--ng-colors-fg-tertiary-600))';
      case 'black':
        return 'var(--nile-toast-color-icon-color-black)';
    }
  }

  handleSlotChange(e: any) {
    const slot = e.target;
    const nodes = slot.assignedNodes({ flatten: true });
    if (slot.name === 'message') {
      const nodes = slot.assignedNodes({ flatten: true });
      this.hasSlottedContent = nodes.length > 0;
    }

    if (slot.name === 'icon') {
      const nodes = slot.assignedNodes({ flatten: true });
      this.hasSlottedIcon = nodes.length > 0;
    }
  }

  /**
   * Displays the alert as a toast notification. This will move the alert out of its position in the DOM and, when
   * dismissed, it will be removed from the DOM completely. By storing a reference to the alert, you can reuse it by
   * calling this method again. The returned promise will resolve after the alert is hidden.
   */
  async toast() {
    return new Promise<void>(resolve => {
      if (toastStack.parentElement === null) {
        document.body.append(toastStack);
      }

      toastStack.appendChild(this);

      // Wait for the toast stack to render
      requestAnimationFrame(() => {
        // eslint-disable-next-line @typescript-eslint/no-unused-expressions -- force a reflow for the initial transition
        this.clientWidth;
        this.show();
      });

      this.addEventListener(
        'nile-after-hide',
        () => {
          toastStack.removeChild(this);
          resolve();

          // Remove the toast stack from the DOM when there are no more alerts
          if (toastStack.querySelector('nile-alert') === null) {
            toastStack.remove();
          }
        },
        { once: true }
      );
    });
  }

  private toastTags(content: string, imageUrl: string) {
    return html`
      <div class="alert__tag">
        <img class="alert__tag-image" src="${imageUrl}" />
        <span class="alert__tag-content"> ${content} </span>
      </div>
    `;
  }

  render() {
    return html`
      <div
        part="base"
        class=${classMap({
          alert: true,
          'alert--open': this.open,
          'alert--closable': this.closable,
          'alert--has-icon': this.hasSlotController.test('icon'),
          'alert--success': this.variant === 'success',
          'alert--info': this.variant === 'info',
          'alert--warning': this.variant === 'warning',
          'alert--error': this.variant === 'error',
          'alert--gray': this.variant === 'gray',
          'alert--black': this.variant === 'black',
          'alert--no-content': this.content === '',
        })}
        role="alert"
        aria-hidden=${this.open ? 'false' : 'true'}
        @mousemove=${this.handleMouseMove}
      >
        <slot name="icon" part="icon" ?hidden="${this.noIcon === true}">
          ${!this.prefixImageUrl
            ? html`
                <div
                  class="alert__prefix-icon-container"
                  ?hidden="${this.hasSlottedIcon ||
                  this.prefixImageUrl.length > 0}"
                >
                  <nile-icon
                    name="${this.getIconNameByVariant()}"
                    size="20"
                    color="${this.getIconColorByVariant()}"
                    method="var(--nile-svg-method-fill, var(--ng-svg-method-stroke))"
                  ></nile-icon>
                </div>
              `
            : html`
                <img src="${this.prefixImageUrl}" class="alert__prefix-img" />
              `}
        </slot>

        <slot
          name="message"
          part="message"
          class="alert__message"
          aria-live="polite"
          @slotchange="${this.handleSlotChange}"
        >
          <span
            ?hidden="${this.hasSlottedContent && this.title === ''}"
            class="alert__message--title"
          >
            ${this.title}
          </span>
          ${this.title
            ? html` <span
                ?hidden="${this.content === ''}"
                class=${classMap({
                  'alert__message--content': true,
                  'alert__message--content-only': !!this.title,
                })}
              >
                ${this.content}
              </span>`
            : null}
          ${this.tags?.length > 0
            ? html`
                <div class="alert__tags">
                  ${this.tags.map(
                    tag =>
                      html`<span
                        >${this.toastTags(tag.content, tag.imageUrl)}</span
                      >`
                  )}
                </div>
              `
            : ``}
        </slot>

        ${this.closable
          ? html`
              <nile-icon-button
                part="close-button"
                exportparts="base:close-button__base"
                class="alert__close-button"
                name="${this.closeIconName}"
                label="close"
                size="20"
                color="var(--nile-colors-neutral-700,var(--ng-colors-fg-quaternary-400))"
                @click=${this.handleCloseClick}
              ></nile-icon-button>
            `
          : null}
      </div>
    `;
  }
}

setDefaultAnimation('alert.show', {
  keyframes: [
    { opacity: 0, scale: 0.8 },
    { opacity: 1, scale: 1 },
  ],
  options: { duration: 250, easing: 'ease' },
});

setDefaultAnimation('alert.hide', {
  keyframes: [
    { opacity: 1, scale: 1 },
    { opacity: 0, scale: 0.8 },
  ],
  options: { duration: 250, easing: 'ease' },
});

export default NileToast;

declare global {
  interface HTMLElementTagNameMap {
    'nile-toast': NileToast;
  }
}
