/**
 * Copyright (c) Tiny Technologies, Inc. All rights reserved.
 * Licensed under the LGPL or a commercial license.
 * For LGPL see License.txt in the project root for license information.
 * For commercial licenses see https://www.tiny.cloud/
 */

import { Cell } from '@ephox/katamari';

import Editor from 'tinymce/core/api/Editor';
import Env from 'tinymce/core/api/Env';
import Tools from 'tinymce/core/api/util/Tools';

interface PasteBin {
  readonly create: () => void;
  readonly remove: () => void;
  readonly getEl: () => HTMLElement | null;
  readonly getHtml: () => string;
  readonly getLastRng: () => Range | null;
  readonly isDefault: () => boolean;
  readonly isDefaultContent: (content: any) => boolean;
}

// We can't attach the pastebin to a H1 inline element on IE since it won't allow H1 or other
// non valid parents to be pasted into the pastebin so we need to attach it to the body
const getPasteBinParent = (editor: Editor): Element =>
  Env.ie && editor.inline ? document.body : editor.getBody();

const isExternalPasteBin = (editor: Editor): boolean =>
  getPasteBinParent(editor) !== editor.getBody();

const delegatePasteEvents = (editor: Editor, pasteBinElm: Element, pasteBinDefaultContent: string): void => {
  if (isExternalPasteBin(editor)) {
    editor.dom.bind(pasteBinElm, 'paste keyup', (_e) => {
      if (!isDefault(editor, pasteBinDefaultContent)) {
        editor.fire('paste');
      }
    });
  }
};

/**
 * Creates a paste bin element as close as possible to the current caret location and places the focus inside that element
 * so that when the real paste event occurs the contents gets inserted into this element
 * instead of the current editor selection element.
 */
const create = (editor: Editor, lastRngCell: Cell<Range | null>, pasteBinDefaultContent: string): void => {
  const dom = editor.dom, body = editor.getBody();

  lastRngCell.set(editor.selection.getRng());

  // Create a pastebin
  const pasteBinElm = editor.dom.add(getPasteBinParent(editor), 'div', {
    'id': 'mcepastebin',
    'class': 'mce-pastebin',
    'contentEditable': true,
    'data-mce-bogus': 'all',
    'style': 'position: fixed; top: 50%; width: 10px; height: 10px; overflow: hidden; opacity: 0'
  }, pasteBinDefaultContent);

  // Move paste bin out of sight since the controlSelection rect gets displayed otherwise on IE and Gecko
  if (Env.ie || Env.gecko) {
    dom.setStyle(pasteBinElm, 'left', dom.getStyle(body, 'direction', true) === 'rtl' ? 0xFFFF : -0xFFFF);
  }

  // Prevent focus events from bubbeling fixed FocusManager issues
  dom.bind(pasteBinElm, 'beforedeactivate focusin focusout', (e) => {
    e.stopPropagation();
  });

  delegatePasteEvents(editor, pasteBinElm, pasteBinDefaultContent);

  pasteBinElm.focus();
  editor.selection.select(pasteBinElm, true);
};

/**
 * Removes the paste bin if it exists.
 */
const remove = (editor: Editor, lastRngCell: Cell<Range | null>) => {
  if (getEl(editor)) {
    let pasteBinClone;
    const lastRng = lastRngCell.get();

    // WebKit/Blink might clone the div so
    // lets make sure we remove all clones
    // TODO: Man o man is this ugly. WebKit is the new IE! Remove this if they ever fix it!
    while ((pasteBinClone = editor.dom.get('mcepastebin'))) {
      editor.dom.remove(pasteBinClone);
      editor.dom.unbind(pasteBinClone);
    }

    if (lastRng) {
      editor.selection.setRng(lastRng);
    }
  }

  lastRngCell.set(null);
};

const getEl = (editor: Editor): HTMLElement | null =>
  editor.dom.get('mcepastebin');

/**
 * Returns the contents of the paste bin as a HTML string.
 *
 * @return {String} Get the contents of the paste bin.
 */
const getHtml = (editor: Editor): string => {
  // Since WebKit/Chrome might clone the paste bin when pasting
  // for example: <img style="float: right"> we need to check if any of them contains some useful html.
  // TODO: Man o man is this ugly. WebKit is the new IE! Remove this if they ever fix it!

  const copyAndRemove = (toElm: HTMLElement, fromElm: HTMLElement) => {
    toElm.appendChild(fromElm);
    editor.dom.remove(fromElm, true); // remove, but keep children
  };

  // find only top level elements (there might be more nested inside them as well, see TINY-1162)
  const pasteBinClones = Tools.grep(getPasteBinParent(editor).childNodes, (elm) => {
    return (elm as HTMLElement).id === 'mcepastebin';
  }) as HTMLElement[];
  const pasteBinElm = pasteBinClones.shift();

  // if clones were found, move their content into the first bin
  Tools.each(pasteBinClones, (pasteBinClone) => {
    copyAndRemove(pasteBinElm, pasteBinClone);
  });

  // TINY-1162: when copying plain text (from notepad for example) WebKit clones
  // paste bin (with styles and attributes) and uses it as a default  wrapper for
  // the chunks of the content, here we cycle over the whole paste bin and replace
  // those wrappers with a basic div
  const dirtyWrappers = editor.dom.select('div[id=mcepastebin]', pasteBinElm);
  for (let i = dirtyWrappers.length - 1; i >= 0; i--) {
    const cleanWrapper = editor.dom.create('div');
    pasteBinElm.insertBefore(cleanWrapper, dirtyWrappers[i]);
    copyAndRemove(cleanWrapper, dirtyWrappers[i]);
  }

  return pasteBinElm ? pasteBinElm.innerHTML : '';
};

const isDefaultContent = (pasteBinDefaultContent: string, content: string): boolean =>
  content === pasteBinDefaultContent;

const isPasteBin = (elm: Element | null): boolean =>
  elm && elm.id === 'mcepastebin';

const isDefault = (editor: Editor, pasteBinDefaultContent: string): boolean => {
  const pasteBinElm = getEl(editor);
  return isPasteBin(pasteBinElm) && isDefaultContent(pasteBinDefaultContent, pasteBinElm.innerHTML);
};

/**
 * @class tinymce.pasteplugin.PasteBin
 * @private
 */

const PasteBin = (editor: Editor): PasteBin => {
  const lastRng = Cell(null);
  const pasteBinDefaultContent = '%MCEPASTEBIN%';

  return {
    create: () => create(editor, lastRng, pasteBinDefaultContent),
    remove: () => remove(editor, lastRng),
    getEl: () => getEl(editor),
    getHtml: () => getHtml(editor),
    getLastRng: lastRng.get,
    isDefault: () => isDefault(editor, pasteBinDefaultContent),
    isDefaultContent: (content) => isDefaultContent(pasteBinDefaultContent, content)
  };
};

export {
  PasteBin,
  getPasteBinParent
};
