/**
 * Copyright since 2007 PrestaShop SA and Contributors
 * PrestaShop is an International Registered Trademark & Property of PrestaShop SA
 *
 * NOTICE OF LICENSE
 *
 * This source file is subject to the Open Software License (OSL 3.0)
 * that is bundled with this package in the file LICENSE.md.
 * It is also available through the world-wide-web at this URL:
 * https://opensource.org/licenses/OSL-3.0
 * If you did not receive a copy of the license and are unable to
 * obtain it through the world-wide-web, please send an email
 * to license@prestashop.com so we can send you a copy immediately.
 *
 * DISCLAIMER
 *
 * Do not edit or add to this file if you wish to upgrade PrestaShop to newer
 * versions in the future. If you wish to customize PrestaShop for your
 * needs please refer to https://devdocs.prestashop.com/ for more information.
 *
 * @author    PrestaShop SA and Contributors <contact@prestashop.com>
 * @copyright Since 2007 PrestaShop SA and Contributors
 * @license   https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0)
 */

/* eslint max-classes-per-file: ["error", 2] */

import ResizeObserver from 'resize-observer-polyfill';
import {
  ModalContainerType, ModalContainer, ModalType, ModalParams, Modal,
} from '@components/modal/modal';
import IframeEvent from '@components/modal/iframe-event';
import {isUndefined} from '@components/typeguard';

export interface IframeModalContainerType extends ModalContainerType {
  iframe: HTMLIFrameElement;
  loader: HTMLElement;
  spinner: HTMLElement;
  closeButton?: HTMLElement;
  confirmButton?: HTMLButtonElement;
}
export interface IframeModalType extends ModalType {
  modal: IframeModalContainerType;
  render: (content: string, hideIframe?: boolean) => void;
}
export type IframeCallbackFunction = (iframe:HTMLIFrameElement, event: Event) => void;
export type IframeEventCallbackFunction = (event: IframeEvent) => void;
export type IframeModalParams = ModalParams & {
  // Callback method executed each time the iframe loads an url
  onLoaded?: IframeCallbackFunction,
  // Callback method executed each time the iframe is about to unload its content
  onUnload?: IframeCallbackFunction,
  // The iframe can launch IframeEvent to communicate with its parent via this callback
  onIframeEvent?: IframeEventCallbackFunction,
  // Initial url of the iframe
  iframeUrl: string;
  // When true the iframe height is computed based on its content
  autoSize: boolean;
  // By default the body of the iframe is used as a reference of its content's size but this option can customize it
  autoSizeContainer: string;
  // Optional, when set a close button is added in the modal's footer
  closeButtonLabel?: string;
  // Optional, when set a confirm button is added in the modal's footer
  confirmButtonLabel?: string;
  // Callback when the confirm button is clicked
  confirmCallback?: (iframe: HTMLIFrameElement, event: Event) => void;
  // By default the iframe closes when confirm button is clicked, this options overrides this behaviour
  closeOnConfirm: boolean;
  // When the iframe is refreshed auto scroll up the body container (true by default)
  autoScrollUp: boolean;
}
export type InputIframeModalParams = Partial<IframeModalParams> & {
  iframeUrl: string; // iframeUrl is mandatory in input
};

/**
 * This class is used to build the modal DOM elements, it is not usable as is because it doesn't even have a show
 * method and the elements are created but not added to the DOM. It just creates a basic DOM structure of a
 * Bootstrap modal, thus keeping the logic class of the modal separated.
 *
 * This container is built on the basic ModalContainer and adds an iframe to load external content along with a
 * loader div on top of it.
 *
 * @param {InputIframeModalParams} inputParams
 */
export class IframeModalContainer extends ModalContainer implements IframeModalContainerType {
  iframe!: HTMLIFrameElement;

  loader!: HTMLElement;

  spinner!: HTMLElement;

  footer?: HTMLElement;

  closeButton?: HTMLElement;

  confirmButton?: HTMLButtonElement;

  /* This constructor is important to force the input type but ESLint is not happy about it*/
  /* eslint-disable no-useless-constructor */
  constructor(params: IframeModalParams) {
    super(params);
  }

  protected buildModalContainer(params: IframeModalParams): void {
    super.buildModalContainer(params);
    this.container.classList.add('modal-iframe');

    // Message is hidden by default
    this.message.classList.add('d-none');

    this.iframe = document.createElement('iframe');
    this.iframe.frameBorder = '0';
    this.iframe.scrolling = 'no';
    this.iframe.width = '100%';
    this.iframe.setAttribute('name', `${params.id}-iframe`);
    if (!params.autoSize) {
      this.iframe.height = '100%';
    }

    this.loader = document.createElement('div');
    this.loader.classList.add('modal-iframe-loader');

    this.spinner = document.createElement('div');
    this.spinner.classList.add('spinner');

    this.loader.appendChild(this.spinner);
    this.body.append(this.loader, this.iframe);

    // Modal footer element
    if (!isUndefined(params.closeButtonLabel) || !isUndefined(params.confirmButtonLabel)) {
      this.footer = document.createElement('div');
      this.footer.classList.add('modal-footer');

      // Modal close button element
      if (!isUndefined(params.closeButtonLabel)) {
        this.closeButton = document.createElement('button');
        this.closeButton.setAttribute('type', 'button');
        this.closeButton.classList.add('btn', 'btn-outline-secondary', 'btn-lg');
        this.closeButton.dataset.dismiss = 'modal';
        this.closeButton.innerHTML = params.closeButtonLabel;
        this.footer.append(this.closeButton);
      }

      // Modal confirm button element
      if (!isUndefined(params.confirmButtonLabel)) {
        this.confirmButton = document.createElement('button');
        this.confirmButton.setAttribute('type', 'button');
        this.confirmButton.classList.add('btn', 'btn-primary', 'btn-lg', 'btn-confirm-submit');
        if (params.closeOnConfirm) {
          this.confirmButton.dataset.dismiss = 'modal';
        }
        this.confirmButton.innerHTML = params.confirmButtonLabel;
        this.footer.append(this.confirmButton);
      }

      // Appending element to the modal
      this.content.append(this.footer);
    }
  }
}

/**
 * This modal opens an url inside a modal, it then can handle two specific callbacks
 * - onLoaded: called when the iframe has juste been refreshed
 * - onUnload: called when the iframe is about to refresh (so it is unloaded)
 */
export class IframeModal extends Modal implements IframeModalType {
  modal!: IframeModalContainerType;

  protected autoSize!: boolean;

  protected autoSizeContainer!: string;

  protected resizeObserver?: ResizeObserver | null;

  constructor(
    inputParams: InputIframeModalParams,
  ) {
    const params: IframeModalParams = {
      id: 'iframe-modal',
      closable: false,
      autoSize: true,
      autoSizeContainer: 'body',
      closeOnConfirm: true,
      autoScrollUp: true,
      ...inputParams,
    };
    super(params);
  }

  protected initContainer(params: IframeModalParams): void {
    // Construct the container
    this.modal = new IframeModalContainer(params);
    super.initContainer(params);

    this.autoSize = params.autoSize;
    this.autoSizeContainer = params.autoSizeContainer;
    this.modal.iframe.addEventListener('load', (loadedEvent: Event) => {
      // Scroll the body container back to the top after iframe loaded
      this.modal.body.scroll(0, 0);
      this.hideLoading();
      if (params.onLoaded) {
        params.onLoaded(this.modal.iframe, loadedEvent);
      }

      if (this.modal.iframe.contentWindow) {
        this.modal.iframe.contentWindow.addEventListener('beforeunload', (unloadEvent: BeforeUnloadEvent) => {
          if (params.onUnload) {
            params.onUnload(this.modal.iframe, unloadEvent);
          }
          this.showLoading();
        });

        // Auto resize the iframe container
        this.initAutoResize();
      }
    });

    this.$modal.on('shown.bs.modal', () => {
      this.modal.iframe.src = params.iframeUrl;
    });

    window.addEventListener(IframeEvent.parentWindowEvent, ((event: IframeEvent) => {
      if (params.onIframeEvent) {
        params.onIframeEvent(event);
      }
    }) as EventListener);

    if (this.modal.confirmButton && params.confirmCallback) {
      this.modal.confirmButton.addEventListener('click', (event) => {
        if (params.confirmCallback) {
          params.confirmCallback(this.modal.iframe, event);
        }
      });
    }
  }

  render(content: string, hideIframe: boolean = true, useInnerText: boolean = false): this {
    if (useInnerText) {
      this.modal.message.innerText = content;
    } else {
      this.modal.message.innerHTML = content;
    }
    this.modal.message.classList.remove('d-none');

    if (hideIframe) {
      this.hideIframe();
    }

    this.autoResize();
    this.hideLoading();

    return this;
  }

  showLoading(): this {
    const bodyHeight = this.getOuterHeight(this.modal.body);
    const bodyWidth = this.getOuterWidth(this.modal.body);
    this.modal.loader.style.height = `${bodyHeight}px`;
    this.modal.loader.style.width = `${bodyWidth}px`;
    this.modal.loader.classList.remove('d-none');
    this.modal.iframe.classList.remove('invisible');
    this.modal.iframe.classList.add('invisible');

    return this;
  }

  hideLoading(): this {
    this.modal.iframe.classList.remove('invisible');
    this.modal.iframe.classList.add('visible');
    this.modal.loader.classList.add('d-none');

    return this;
  }

  hide(): this {
    super.hide();
    this.cleanResizeObserver();

    return this;
  }

  hideIframe(): void {
    this.modal.iframe.classList.add('d-none');
  }

  private getResizableContainer(): HTMLElement | null {
    if (this.autoSize && this.modal.iframe.contentWindow) {
      return this.modal.iframe.contentWindow.document.querySelector(this.autoSizeContainer);
    }

    return null;
  }

  private initAutoResize(): void {
    const iframeContainer: HTMLElement | null = this.getResizableContainer();

    if (iframeContainer) {
      this.cleanResizeObserver();
      this.resizeObserver = new ResizeObserver(() => {
        this.autoResize();
      });

      this.resizeObserver.observe(iframeContainer);
    }
    this.autoResize();
  }

  private cleanResizeObserver(): void {
    if (this.resizeObserver) {
      this.resizeObserver.disconnect();
      this.resizeObserver = null;
    }
  }

  private autoResize(): void {
    const iframeContainer: HTMLElement | null = this.getResizableContainer();

    if (iframeContainer) {
      const iframeScrollHeight = iframeContainer.scrollHeight;
      const contentHeight = this.getOuterHeight(this.modal.message)
        + iframeScrollHeight;

      // Avoid applying height of 0 (on first load for example)
      if (contentHeight) {
        // We force the iframe to its real height and it's the container that handles the overflow with scrollbars
        this.modal.iframe.style.height = `${contentHeight}px`;
      }
    }
  }

  private getOuterHeight(element: HTMLElement): number {
    // If the element height is 0 it is likely empty or hidden, then no need to compute the margin
    if (!element.offsetHeight) {
      return 0;
    }

    let height = element.offsetHeight;
    const style: CSSStyleDeclaration = getComputedStyle(element);

    height += parseInt(style.marginTop, 10) + parseInt(style.marginBottom, 10);

    return height;
  }

  private getOuterWidth(element: HTMLElement): number {
    // If the element height is 0 it is likely empty or hidden, then no need to compute the margin
    if (!element.offsetWidth) {
      return 0;
    }

    let width = element.offsetWidth;
    const style: CSSStyleDeclaration = getComputedStyle(element);

    width += parseInt(style.marginLeft, 10) + parseInt(style.marginRight, 10);

    return width;
  }
}

export default IframeModal;
