import merge from 'lodash/merge';

import ProgressBar from './additions/ProgressBar';
import './toastr.scss';
import { version } from '../package.json';
import addClasses from './helpers/addClasses';

type Required<T> = {
  [P in keyof T]-?: T[P];
}

export type ToastType = {
  info?: string;
  error?: string;
  warning?: string;
  success?: string;
};

export type RequiredToastType = Required<ToastType>;

export type ToastrOptions<T = ToastType> = {
  tapToDismiss?: boolean;
  toastClass?: string | string[];
  containerId?: string;
  debug?: boolean;

  showMethod?: 'fadeIn' | 'slideDown' | 'show';
  showDuration?: number;
  showEasing?: 'swing' | 'linear';
  onShown?: () => void;
  hideMethod?: 'fadeOut';
  hideDuration?: number;
  hideEasing?: 'swing';
  onHidden?: () => void;
  closeMethod?: boolean;
  closeDuration?: number | false;
  closeEasing?: boolean;
  closeOnHover?: boolean;

  extendedTimeOut?: number;
  iconClasses?: T;
  iconClass?: string | string[];
  positionClass?: string | string[];
  timeOut?: number; // Set timeOut and extendedTimeOut to 0 to make it sticky
  titleClass?: string | string[];
  messageClass?: string | string[];
  escapeHtml?: boolean;
  target?: string;
  closeHtml?: string;
  closeClass?: string | string[];
  newestOnTop?: boolean;
  preventDuplicates?: boolean;
  progressBar?: boolean;
  progressClass?: string | string[];
  onclick?: (event: MouseEvent) => void;

  onCloseClick?: (event: Event) => void;
  closeButton?: boolean;
  rtl?: boolean;
}

export type NotifyMap = {
  type: string;
  optionsOverride?: ToastrOptions;
  iconClass: string;
  title?: string;
  message?: string;
}

class Toastr {
  private listener: any;

  private toastId = 0;

  private previousToast: string | null = null;

  private toastType: RequiredToastType = {
    info: 'info',
    error: 'error',
    warning: 'warning',
    success: 'success',
  };

  private version = version;

  public options: Required<ToastrOptions<RequiredToastType>> = {
    tapToDismiss: true,
    toastClass: 'toast',
    containerId: 'toast-container',
    debug: false,

    showMethod: 'fadeIn', // fadeIn, slideDown, and show are built into jQuery
    showDuration: 300,
    showEasing: 'swing', // swing and linear are built into jQuery
    onShown: () => { },
    hideMethod: 'fadeOut',
    hideDuration: 1000,
    hideEasing: 'swing',
    onHidden: () => { },
    closeMethod: false,
    closeDuration: false,
    closeEasing: false,
    closeOnHover: true,

    extendedTimeOut: 1000,
    iconClasses: {
      error: 'toast-error',
      info: 'toast-info',
      success: 'toast-success',
      warning: 'toast-warning',
    },
    iconClass: 'toast-info',
    positionClass: 'toast-top-right',
    timeOut: 5000, // Set timeOut and extendedTimeOut to 0 to make it sticky
    titleClass: 'toast-title',
    messageClass: 'toast-message',
    escapeHtml: false,
    target: 'body',
    closeHtml: '<button type="button">&times;</button>',
    closeClass: 'toast-close-button',
    newestOnTop: true,
    preventDuplicates: false,
    progressBar: false,
    progressClass: 'toast-progress',
    rtl: false,

    onCloseClick: () => { },
    closeButton: false,

    onclick: () => { },
  };

  public $container: HTMLElement = document.createElement('div');

  public constructor(options?: ToastrOptions) {
    this.options = merge({}, this.options, options);

    this.createContainer();
  }

  public createContainer(): HTMLElement {
    this.$container = document.createElement('div');

    this.$container.setAttribute('id', this.options.containerId);
    addClasses(this.$container, this.options.positionClass);

    const target = document.getElementsByTagName(this.options.target);

    if (target && target[0]) {
      target[0].appendChild(this.$container);
    }

    return this.$container;
  }

  public getContainer(options: Partial<ToastrOptions> = this.options, create = false): HTMLElement {
    const $container = document.getElementById(options.containerId || '');

    if ($container) {
      this.$container = $container;

      return this.$container;
    }

    if (create) {
      this.$container = this.createContainer();
    }

    return this.$container;
  }

  public error(
    message?: string,
    title?: string,
    optionsOverride?: ToastrOptions,
  ): HTMLElement | null {
    return this.notify({
      type: this.toastType.error,
      iconClass: this.options.iconClasses.error,
      message,
      optionsOverride,
      title,
    });
  }

  public warning(
    message?: string,
    title?: string,
    optionsOverride?: ToastrOptions,
  ): HTMLElement | null {
    return this.notify({
      type: this.toastType.warning,
      iconClass: this.options.iconClasses.warning,
      message,
      optionsOverride,
      title,
    });
  }

  public success(
    message?: string,
    title?: string,
    optionsOverride?: ToastrOptions,
  ): HTMLElement | null {
    return this.notify({
      type: this.toastType.success,
      iconClass: this.options.iconClasses.success,
      message,
      optionsOverride,
      title,
    });
  }

  public info(
    message?: string,
    title?: string,
    optionsOverride?: ToastrOptions,
  ): HTMLElement | null {
    return this.notify({
      type: this.toastType.info,
      iconClass: this.options.iconClasses.info,
      message,
      optionsOverride,
      title,
    });
  }

  public subscribe(callback: (response: Toastr) => void): void {
    this.listener = callback;
  }

  public publish(args: Toastr): void {
    if (!this.listener) {
      return;
    }

    this.listener(args);
  }

  public clear(toastElement?: HTMLElement | null, clearOptions: { force?: boolean } = {}) {
    if (!this.$container) {
      this.getContainer(this.options);
    }

    if (!this.clearToast(toastElement, this.options, clearOptions)) {
      this.clearContainer(this.options);
    }
  }

  public remove(toastElement?: HTMLElement | null) {
    if (!this.$container) {
      this.getContainer(this.options);
    }

    if (!this.$container) {
      return;
    }

    if (toastElement && toastElement !== document.activeElement) {
      this.removeToast(toastElement);

      return;
    }

    if (!this.$container.hasChildNodes()) {
      const parentNode = this.$container.parentElement;

      if (parentNode) {
        parentNode.removeChild(this.$container);
      }
    }
  }

  public removeToast(toastElement: HTMLElement) {
    if (!this.$container) {
      this.getContainer();
    }

    if (!this.$container || !toastElement.parentNode) {
      return;
    }

    // todo set after visible state
    // as this will be a transition of css
    toastElement.parentNode.removeChild(toastElement);
    // check if visible
    if (toastElement.offsetWidth > 0 && toastElement.offsetHeight > 0) {
      return;
    }

    // todo check if null makes sense
    // toastElement = null;

    if (!this.$container.hasChildNodes()) {
      if (this.$container.parentNode) {
        this.$container.parentNode.removeChild(this.$container);
      }

      this.previousToast = null;
    }
  }

  private clearContainer(options: Partial<ToastrOptions> = this.options) {
    if (!this.$container) {
      return;
    }

    const toastsToClear = Array.from(this.$container.childNodes) as HTMLElement[];

    for (let i = toastsToClear.length - 1; i >= 0; i -= 1) {
      this.clearToast(toastsToClear[i], options);
    }
  }

  private clearToast(
    toastElement?: HTMLElement | null,
    // eslint-disable-next-line no-unused-vars
    options: Partial<ToastrOptions> = this.options,
    clearOptions: { force?: boolean } = {},
  ): boolean {
    if (!toastElement) {
      return false;
    }

    const force = clearOptions.force || false;

    if (toastElement && (force || toastElement !== document.activeElement)) {
      // todo hide effect
      this.removeToast(toastElement);
      // toastElement[options.hideMethod]({
      //     duration: options.hideDuration,
      //     easing: options.hideEasing,
      //     complete: function () { removeToast(toastElement); }
      // });
      return true;
    }

    return false;
  }

  private notify(map: NotifyMap): HTMLElement | null {
    let { options } = this;
    let iconClass = map.iconClass || this.options.iconClass;

    const shouldExit = (opts: ToastrOptions, exitMap: NotifyMap): boolean => {
      if (opts.preventDuplicates) {
        if (exitMap.message === this.previousToast) {
          return true;
        }

        this.previousToast = exitMap.message || '';
      }
      return false;
    };

    if (typeof map.optionsOverride !== 'undefined') {
      options = merge({}, options, map.optionsOverride);
      iconClass = map.optionsOverride.iconClass || iconClass;
    }

    if (shouldExit(options, map)) {
      return null;
    }

    this.toastId += 1;

    this.$container = this.getContainer(options, true);

    let intervalId: NodeJS.Timeout | null = null;
    let progressBar: null | ProgressBar = null;
    const toastElement = document.createElement('div');
    const $titleElement = document.createElement('div');
    const $messageElement = document.createElement('div');
    const createdElement = document.createElement('div');
    createdElement.innerHTML = options.closeHtml.trim();
    const closeElement = createdElement.firstChild as HTMLElement | null;

    const response: any = {
      toastId: this.toastId,
      state: 'visible',
      startTime: new Date(),
      endTime: undefined,
      options,
      map,
    };

    const hideToast = (override: any = null): void => {
      // const method = override && this.options.closeMethod !== false
      //   ? this.options.closeMethod
      //   : this.options.hideMethod;

      // const duration = override && this.options.closeDuration !== false
      //   ? this.options.closeDuration
      //   : this.options.hideDuration;

      // const easing = override && this.options.closeEasing !== false
      //   ? this.options.closeEasing
      //   : this.options.hideEasing;

      if (toastElement === document.activeElement && !override) {
        return;
      }

      if (progressBar) {
        progressBar.stop();
      }

      // todo fade out toast
      this.removeToast(toastElement);

      if (intervalId) {
        clearTimeout(intervalId);
      }

      if (options.onHidden && response.state !== 'hidden') {
        options.onHidden();
      }

      response.state = 'hidden';
      response.endTime = new Date();
      this.publish(response);

      // return toastElement[method]({
      //     duration: duration,
      //     easing: easing,
      // });
    };

    const escapeHtml = (source: string | null): string => {
      const newSource = source !== null ? source : '';

      return newSource
        .replace(/&/g, '&amp;')
        .replace(/"/g, '&quot;')
        .replace(/'/g, '&#39;')
        .replace(/</g, '&lt;')
        .replace(/>/g, '&gt;');
    };

    const setAria = (): void => {
      let ariaValue = '';

      switch (iconClass) {
        case 'toast-success':
        case 'toast-info':
          ariaValue = 'polite';

          break;

        default:
          ariaValue = 'assertive';
      }

      toastElement.setAttribute('aria-live', ariaValue);
    };

    const delayedHideToast = (): void => {
      if (options.timeOut > 0 || options.extendedTimeOut > 0) {
        intervalId = setTimeout(hideToast, options.extendedTimeOut);

        if (progressBar) {
          progressBar.reset(options.extendedTimeOut);
          progressBar.start();
        }
      }
    };

    const stickAround = (): void => {
      if (intervalId) {
        clearTimeout(intervalId);
      }

      if (progressBar) {
        progressBar.stop();
      }
      // todo
      // toastElement.stop(true, true)[options.showMethod](
      //     {duration: options.showDuration, easing: options.showEasing}
      // );
    };

    const handleEvents = (): void => {
      if (options.closeOnHover) {
        toastElement.addEventListener('mouseover', () => stickAround());
        toastElement.addEventListener('mouseout', () => delayedHideToast());
      }

      if (!options.onclick && options.tapToDismiss) {
        toastElement.addEventListener('click', hideToast);
      }

      if (options.closeButton && closeElement) {
        closeElement.addEventListener('click', (event) => {
          if (event.stopPropagation) {
            event.stopPropagation();
          } else if (event.cancelBubble !== undefined && event.cancelBubble !== true) {
            // eslint-disable-next-line no-param-reassign
            event.cancelBubble = true;
          }

          if (options.onCloseClick) {
            options.onCloseClick(event);
          }

          hideToast(true);
        });
      }

      if (options.onclick) {
        toastElement.addEventListener('click', (event) => {
          // ts needs another check here
          if (options.onclick) {
            options.onclick(event);
          }

          hideToast();
        });
      }
    };

    const setTitle = (): void => {
      if (map.title) {
        let suffix = map.title;
        if (options.escapeHtml) {
          suffix = escapeHtml(map.title);
        }
        $titleElement.innerHTML = suffix;
        addClasses($titleElement, options.titleClass);
        toastElement.appendChild($titleElement);
      }
    };

    const setMessage = (): void => {
      if (map.message) {
        let suffix = map.message;

        if (options.escapeHtml) {
          suffix = escapeHtml(map.message);
        }

        $messageElement.innerHTML = suffix;
        addClasses($messageElement, options.messageClass);
        toastElement.appendChild($messageElement);
      }
    };

    const setCloseButton = (): void => {
      if (options.closeButton && closeElement) {
        addClasses(closeElement, options.closeClass);
        closeElement.setAttribute('role', 'button');
        toastElement.insertBefore(closeElement, toastElement.firstChild);
      }
    };

    const setProgressBar = (): void => {
      if (options.progressBar) {
        progressBar = new ProgressBar(toastElement, options.progressClass);
      }
    };

    const setRTL = (): void => {
      if (options.rtl) {
        addClasses(toastElement, 'rtl');
      }
    };

    const setIcon = (): void => {
      if (iconClass) {
        addClasses(toastElement, options.toastClass, iconClass);
      }
    };

    const setSequence = (): void => {
      if (options.newestOnTop) {
        this.$container.insertBefore(toastElement, this.$container.firstChild);
      } else {
        this.$container.appendChild(toastElement);
      }
    };

    const displayToast = (): void => {
      // todo hide toast
      // toastElement.hide();

      // todo fade out toast
      if (options.onShown) {
        options.onShown();
      }
      // toastElement[options.showMethod](
      // eslint-disable-next-line
      //     {duration: options.showDuration, easing: options.showEasing, complete: options.onShown}
      // );

      if (options.timeOut > 0) {
        intervalId = setTimeout(hideToast, options.timeOut);

        if (progressBar) {
          progressBar.reset(options.timeOut);
          progressBar.start();
        }
      }
    };

    const personalizeToast = (): void => {
      setIcon();
      setTitle();
      setMessage();
      setCloseButton();
      setProgressBar();
      setRTL();
      setSequence();
      setAria();
    };

    personalizeToast();

    displayToast();

    handleEvents();

    this.publish(response);

    if (options.debug && console) {
      // eslint-disable-next-line no-console
      console.log(response);
    }

    return toastElement;
  }
}

export default Toastr;
