/**
 * @license
 * Copyright 2017 Google Inc.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

import { MDCComponent } from '@smui/common/base/component';
import type { SpecificEventListener } from '@smui/common/base/types';
import { FocusTrap } from '@smui/common/dom/focus-trap';
import { closest, matches } from '@smui/common/dom/ponyfill';
import { MDCRipple } from '@smui/ripple/component';

import type { MDCDialogAdapter } from './adapter';
import { MDCDialogFoundation } from './foundation';
import type { MDCDialogCloseEventDetail } from './types';
import * as util from './util';
import type { MDCDialogFocusTrapFactory } from './util';

const { strings } = MDCDialogFoundation;

/** MDC Dialog */
export class MDCDialog extends MDCComponent<MDCDialogFoundation> {
  get isOpen() {
    return this.foundation.isOpen();
  }

  get escapeKeyAction() {
    return this.foundation.getEscapeKeyAction();
  }

  set escapeKeyAction(action) {
    this.foundation.setEscapeKeyAction(action);
  }

  get scrimClickAction() {
    return this.foundation.getScrimClickAction();
  }

  set scrimClickAction(action) {
    this.foundation.setScrimClickAction(action);
  }

  get autoStackButtons() {
    return this.foundation.getAutoStackButtons();
  }

  set autoStackButtons(autoStack) {
    this.foundation.setAutoStackButtons(autoStack);
  }

  static override attachTo(root: HTMLElement) {
    return new MDCDialog(root);
  }

  private buttonRipples!: MDCRipple[]; // assigned in initialize()
  private buttons!: HTMLButtonElement[]; // assigned in initialize()
  private container!: HTMLElement; // assigned in initialize()
  private content!: HTMLElement | null; // assigned in initialize()
  private defaultButton!: HTMLButtonElement | null; // assigned in initialize()

  private focusTrap!: FocusTrap; // assigned in initialSyncWithDOM()
  private focusTrapFactory!: MDCDialogFocusTrapFactory; // assigned in initialize()

  private handleClick!: SpecificEventListener<'click'>; // assigned in initialSyncWithDOM()
  private handleKeydown!: SpecificEventListener<'keydown'>; // assigned in initialSyncWithDOM()
  private handleDocumentKeydown!: SpecificEventListener<'keydown'>; // assigned in initialSyncWithDOM()
  private handleOpening!: EventListener; // assigned in initialSyncWithDOM()
  private handleClosing!: () => void; // assigned in initialSyncWithDOM()

  override initialize(
    focusTrapFactory: MDCDialogFocusTrapFactory = (el, focusOptions) =>
      new FocusTrap(el, focusOptions),
  ) {
    const container = this.root.querySelector<HTMLElement>(
      strings.CONTAINER_SELECTOR,
    );
    if (!container) {
      throw new Error(
        `Dialog component requires a ${
          strings.CONTAINER_SELECTOR
        } container element`,
      );
    }
    this.container = container;
    this.content = this.root.querySelector<HTMLElement>(
      strings.CONTENT_SELECTOR,
    );
    this.buttons = Array.from(
      this.root.querySelectorAll<HTMLButtonElement>(strings.BUTTON_SELECTOR),
    );
    this.defaultButton = this.root.querySelector<HTMLButtonElement>(
      `[${strings.BUTTON_DEFAULT_ATTRIBUTE}]`,
    );
    this.focusTrapFactory = focusTrapFactory;
    this.buttonRipples = [];

    for (const buttonEl of this.buttons) {
      this.buttonRipples.push(new MDCRipple(buttonEl));
    }
  }

  override initialSyncWithDOM() {
    this.focusTrap = util.createFocusTrapInstance(
      this.container,
      this.focusTrapFactory,
      this.getInitialFocusEl() || undefined,
    );

    this.handleClick = this.foundation.handleClick.bind(this.foundation);
    this.handleKeydown = this.foundation.handleKeydown.bind(this.foundation);
    this.handleDocumentKeydown = this.foundation.handleDocumentKeydown.bind(
      this.foundation,
    );
    // this.handleLayout = this.layout.bind(this);

    this.handleOpening = () => {
      document.addEventListener('keydown', this.handleDocumentKeydown);
    };
    this.handleClosing = () => {
      document.removeEventListener('keydown', this.handleDocumentKeydown);
    };

    this.listen('click', this.handleClick);
    this.listen('keydown', this.handleKeydown);
    this.listen(strings.OPENING_EVENT, this.handleOpening);
    this.listen(strings.CLOSING_EVENT, this.handleClosing);
  }

  override destroy() {
    this.unlisten('click', this.handleClick);
    this.unlisten('keydown', this.handleKeydown);
    this.unlisten(strings.OPENING_EVENT, this.handleOpening);
    this.unlisten(strings.CLOSING_EVENT, this.handleClosing);
    this.handleClosing();

    this.buttonRipples.forEach((ripple) => {
      ripple.destroy();
    });
    super.destroy();
  }

  layout() {
    this.foundation.layout();
  }

  open() {
    this.foundation.open();
  }

  close(action = '') {
    this.foundation.close(action);
  }

  override getDefaultFoundation() {
    // DO NOT INLINE this variable. For backward compatibility, foundations take
    // a Partial<MDCFooAdapter>. To ensure we don't accidentally omit any
    // methods, we need a separate, strongly typed adapter variable.
    const adapter: MDCDialogAdapter = {
      addBodyClass: (className) => {
        document.body.classList.add(className);
      },
      addClass: (className) => {
        this.root.classList.add(className);
      },
      areButtonsStacked: () => util.areTopsMisaligned(this.buttons),
      clickDefaultButton: () => {
        if (this.defaultButton && !this.defaultButton.disabled) {
          this.defaultButton.click();
        }
      },
      eventTargetMatches: (target, selector) =>
        target ? matches(target as Element, selector) : false,
      getActionFromEvent: (event: Event) => {
        if (!event.target) {
          return '';
        }
        const element = closest(
          event.target as Element,
          `[${strings.ACTION_ATTRIBUTE}]`,
        );
        return element && element.getAttribute(strings.ACTION_ATTRIBUTE);
      },
      getInitialFocusEl: () => this.getInitialFocusEl(),
      hasClass: (className) => this.root.classList.contains(className),
      isContentScrollable: () => util.isScrollable(this.content),
      notifyClosed: (action) => {
        this.emit<MDCDialogCloseEventDetail>(
          strings.CLOSED_EVENT,
          action ? { action } : {},
        );
      },
      notifyClosing: (action) => {
        this.emit<MDCDialogCloseEventDetail>(
          strings.CLOSING_EVENT,
          action ? { action } : {},
        );
      },
      notifyOpened: () => {
        this.emit(strings.OPENED_EVENT, {});
      },
      notifyOpening: () => {
        this.emit(strings.OPENING_EVENT, {});
      },
      releaseFocus: () => {
        this.focusTrap.releaseFocus();
      },
      removeBodyClass: (className) => {
        document.body.classList.remove(className);
      },
      removeClass: (className) => {
        this.root.classList.remove(className);
      },
      reverseButtons: () => {
        this.buttons.reverse();
        this.buttons.forEach((button) => {
          button.parentElement!.appendChild(button);
        });
      },
      trapFocus: () => {
        this.focusTrap.trapFocus();
      },
      registerContentEventHandler: (event, handler) => {
        if (this.content instanceof HTMLElement) {
          this.content.addEventListener(event, handler);
        }
      },
      deregisterContentEventHandler: (event, handler) => {
        if (this.content instanceof HTMLElement) {
          this.content.removeEventListener(event, handler);
        }
      },
      isScrollableContentAtTop: () => {
        return util.isScrollAtTop(this.content);
      },
      isScrollableContentAtBottom: () => {
        return util.isScrollAtBottom(this.content);
      },
      registerWindowEventHandler: (event, handler) => {
        window.addEventListener(event, handler);
      },
      deregisterWindowEventHandler: (event, handler) => {
        window.removeEventListener(event, handler);
      },
    };
    return new MDCDialogFoundation(adapter);
  }

  private getInitialFocusEl(): HTMLElement | null {
    return this.root.querySelector<HTMLElement>(
      `[${strings.INITIAL_FOCUS_ATTRIBUTE}]`,
    );
  }
}
