/**
 * Clipboard component manages the copy/cut/paste events by capturing these events from the browser,
 * managing their state, and exposing an API to other components to get/set the data.
 *
 * Because of a lack of standardization of ClipboardEvents between browsers, the way Clipboard
 * captures the events is by creating a hidden textarea element that's always focused with some text
 * selected. Here is a good write-up of this:
 * https://www.lucidchart.com/techblog/2014/12/02/definitive-guide-copying-pasting-javascript/
 *
 * When ClipboardEvent is detected, Clipboard captures the event and calls the corresponding
 * copy/cut/paste/input command actions, which will get called on the appropriate component.
 *
 * Usage:
 *    Components need to register copy/cut/paste actions with command.js:
 *      .copy() should return @pasteObj (defined below).
 *      .paste(plainText, [cutSelection]) should take a plainText value and an optional cutSelection
 *      parameter which will specify the selection that should be cleared as part of paste.
 *      .input(char) should take a single input character and will be called when the user types a
 *      visible character (useful if component wants to interpret typing into a cell, for example).
 */

/**
 * Paste object that should be returned by implementation of `copy`.
 *
 * @typedef pasteObj {{
 *    docName: string,
 *    tableId: string,
 *    data:    object,
 *    selection: object
 * }}
 */



/* global window, document */

var {getHumanKey, isMac} = require('app/client/components/commands');
var {copyToClipboard, readDataFromClipboard} = require('app/client/lib/clipboardUtils');
var {FocusLayer} = require('app/client/lib/FocusLayer');
var {makeT} = require('app/client/lib/localization');

var {tsvDecode} = require('app/common/tsvFormat');
var {ShortcutKey, ShortcutKeyContent} = require('app/client/ui/ShortcutKey');
var {confirmModal} = require('app/client/ui2018/modals');
var {styled} = require('grainjs');

var commands = require('./commands');
var dom = require('../lib/dom');
var Base = require('./Base');
var tableUtil = require('../lib/tableUtil');

const t = makeT('Clipboard');

function Clipboard(app) {
  Base.call(this, null);
  this._app = app;
  this.copypasteField = this.autoDispose(dom('textarea.copypaste.mousetrap', ''));
  this.timeoutId = null;

  this.onEvent(this.copypasteField, 'input', function(elem, event) {
    var value = elem.value;
    elem.value = '';
    commands.allCommands.input.run(value);
    return false;
  });
  this.onEvent(this.copypasteField, 'copy',  this._onCopy);
  this.onEvent(this.copypasteField, 'cut',   this._onCut);
  this.onEvent(this.copypasteField, 'paste', this._onPaste);

  document.body.appendChild(this.copypasteField);

  FocusLayer.create(this, {
    defaultFocusElem: this.copypasteField,
    allowFocus: allowFocus,
    onDefaultFocus: () => {
      this.copypasteField.value = ' ';
      this.copypasteField.select();
      this._app.trigger('clipboard_focus');
    },
    onDefaultBlur: () => {
      this._app.trigger('clipboard_blur');
    },
  });

  // Expose the grabber as a global to allow upload from tests to explicitly restore focus
  window.gristClipboardGrabFocus = () => FocusLayer.grabFocus();

  // Some bugs may prevent Clipboard from re-grabbing focus. To limit the impact of such bugs on
  // the user, recover from a bad state in mousedown events. (At the moment of this comment, all
  // such known bugs are fixed.)
  this.onEvent(window, 'mousedown', (ev) => {
    if (!document.activeElement || document.activeElement === document.body) {
      FocusLayer.grabFocus();
    }
  });

  // In the event of a cut a callback is provided by the viewsection that is the target of the cut.
  // When called it returns the additional removal action needed for a cut.
  this._cutCallback = null;
  // The plaintext content of the cut callback. Used to verify that we are pasting the results
  // of the cut, rather than new data from outside.
  this._cutData = null;

  this.autoDispose(commands.createGroup(Clipboard.commands, this, true));
}
Base.setBaseFor(Clipboard);

Clipboard.commands = {
  contextMenuCopy: function() { this._doContextMenuCopy(); },
  contextMenuCut: function() { this._doContextMenuCut(); },
  contextMenuPaste: function() { this._doContextMenuPaste(); },
};

/**
 * Internal helper fired on `copy` events. If a callback was registered from a component, calls the
 * callback to get selection data and puts it on the clipboard.
 */
Clipboard.prototype._onCopy = function(elem, event) {
  event.preventDefault();

  let pasteObj = commands.allCommands.copy.run();

  this._setCBdata(pasteObj, event.originalEvent.clipboardData);
};

Clipboard.prototype._doContextMenuCopy = function() {
  let pasteObj = commands.allCommands.copy.run();

  this._copyToClipboard(pasteObj, 'copy');
};

Clipboard.prototype._onCut = function(elem, event) {
  event.preventDefault();

  let pasteObj = commands.allCommands.cut.run();

  this._setCBdata(pasteObj, event.originalEvent.clipboardData);
};

Clipboard.prototype._doContextMenuCut = function() {
  let pasteObj = commands.allCommands.cut.run();

  this._copyToClipboard(pasteObj, 'cut');
};

Clipboard.prototype._setCBdata = function(pasteObj, clipboardData) {
  if (!pasteObj) { return; }

  const plainText = tableUtil.makePasteText(pasteObj.data, pasteObj.selection);
  clipboardData.setData('text/plain', plainText);
  const htmlText = tableUtil.makePasteHtml(pasteObj.data, pasteObj.selection);
  clipboardData.setData('text/html', htmlText);

  this._setCutCallback(pasteObj, plainText);
};

Clipboard.prototype._copyToClipboard = async function(pasteObj, action) {
  if (!pasteObj) { return; }

  const plainText = tableUtil.makePasteText(pasteObj.data, pasteObj.selection);
  let data;
  if (typeof ClipboardItem === 'function') {
    const htmlText = tableUtil.makePasteHtml(pasteObj.data, pasteObj.selection);
    // eslint-disable-next-line no-undef
    data = new ClipboardItem({
      // eslint-disable-next-line no-undef
      'text/plain': new Blob([plainText], {type: 'text/plain'}),
      // eslint-disable-next-line no-undef
      'text/html': new Blob([htmlText], {type: 'text/html'}),
    });
  } else {
    data = plainText;
  }

  try {
    await copyToClipboard(data);
  } catch {
    showUnavailableMenuCommandModal(action);
    return;
  }

  this._setCutCallback(pasteObj, plainText);
};

/**
 * Sets the cut callback from the `pasteObj` if one exists. Otherwise clears the
 * cut callback.
 *
 * The callback is called on paste, and only if the pasted data matches the `cutData`
 * that was cut from within Grist. The callback handles removal of the data that was
 * cut.
 */
Clipboard.prototype._setCutCallback = function(pasteObj, cutData) {
  if (pasteObj.cutCallback) {
    this._cutCallback = pasteObj.cutCallback;
    this._cutData = cutData;
  } else {
    this._cutCallback = null;
    this._cutData = null;
  }
};

/**
 * Internal helper fired on `paste` events. If a callback was registered from a component, calls the
 * callback with data from the clipboard.
 */
Clipboard.prototype._onPaste = function(elem, event) {
  event.preventDefault();
  const cb = event.originalEvent.clipboardData;
  const plainText = cb.getData('text/plain');
  const htmlText = cb.getData('text/html');
  const pasteData = getPasteData(plainText, htmlText);
  this._doPaste(pasteData, plainText);
};

var FOCUS_TARGET_TAGS = {
  'INPUT': true,
  'TEXTAREA': true,
  'SELECT': true,
  'IFRAME': true,
};

Clipboard.prototype._doContextMenuPaste = async function() {
  let clipboardItem;
  try {
    clipboardItem = (await readDataFromClipboard())?.[0];
  } catch {
    showUnavailableMenuCommandModal('paste');
    return;
  }
  const plainText = await getTextFromClipboardItem(clipboardItem, 'text/plain');
  const htmlText = await getTextFromClipboardItem(clipboardItem, 'text/html');
  const pasteData = getPasteData(plainText, htmlText);
  this._doPaste(pasteData, plainText);
};

Clipboard.prototype._doPaste = function(pasteData, plainText) {
  console.log(this._cutData, plainText, this._cutCallback);
  if (this._cutData === plainText) {
    if (this._cutCallback) {
      // Cuts should only be possible on the first paste after a cut and only if the data being
      // pasted matches the data that was cut.
      commands.allCommands.paste.run(pasteData, this._cutCallback);
    }
  } else {
    this._cutData = null;
    commands.allCommands.paste.run(pasteData, null);
  }
  // The cut callback should only be usable once so it needs to be cleared after every paste.
  this._cutCallback = null;
}

/**
 * Returns data formatted as a 2D array of strings, suitable for pasting within Grist.
 *
 * Grist stores both text/html and text/plain when copying data. When pasting back, we first
 * check if text/html exists (should exist for Grist and other spreadsheet software), and fall
 * back to text/plain otherwise.
 */
function getPasteData(plainText, htmlText) {
  try {
    return tableUtil.parsePasteHtml(htmlText);
  } catch (e) {
    if (plainText === '' || plainText.charCodeAt(0) === 0xFEFF) {
      return [['']];
    } else {
      return tsvDecode(plainText.replace(/\r\n?/g, "\n").trimEnd());
    }
  }
}

/**
 * Returns clipboard data of the given `type` from `clipboardItem` as text.
 *
 * Returns an empty string if `clipboardItem` is nullish or no data exists
 * for the given `type`.
 */
async function getTextFromClipboardItem(clipboardItem, type) {
  if (!clipboardItem) { return ''; }

  try {
    return (await clipboardItem.getType(type)).text();
  } catch {
    // No clipboard data exists for the MIME type.
    return '';
  }
}

/**
 * Helper to determine if the currently active element deserves to keep its own focus, and capture
 * copy-paste events. Besides inputs and textareas, any element can be marked to be a valid
 * copy-paste target by adding 'clipboard_focus' class to it.
 */
function allowFocus(elem) {
  return elem && (FOCUS_TARGET_TAGS.hasOwnProperty(elem.tagName) ||
    elem.hasAttribute("tabindex") ||
    elem.classList.contains('clipboard_focus'));
}

Clipboard.allowFocus = allowFocus;

function showUnavailableMenuCommandModal(action) {
  let keys;
  switch (action) {
    case 'cut': {
      keys = 'Mod+X'
      break;
    }
    case 'copy': {
      keys = 'Mod+C'
      break;
    }
    case 'paste': {
      keys = 'Mod+V'
      break;
    }
    default: {
      throw new Error(`Clipboard: unrecognized action ${action}`);
    }
  }

  confirmModal(
    t("Unavailable Command"),
    t("Got it"),
    () => {},
    {
      explanation: cssModalContent(
        t(
          'The {{action}} menu command is not available in this browser. You can still {{action}}' +
          ' by using the keyboard shortcut {{shortcut}}.',
          {
            action,
            shortcut: ShortcutKey(ShortcutKeyContent(getHumanKey(keys, isMac))),
          }
        ),
      ),
      hideCancel: true,
    },
  );
}

module.exports = Clipboard;

const cssModalContent = styled('div', `
  line-height: 18px;
`);