import $ from 'jquery';
import { sanitize } from '~/lib/dompurify';
import { getSelectedFragment, insertText } from '~/lib/utils/common_utils';

export class CopyAsGFM {
  constructor() {
    // iOS currently does not support clipboardData.setData(). This bug should
    // be fixed in iOS 12, but for now we'll disable this for all iOS browsers
    // ref: https://trac.webkit.org/changeset/222228/webkit
    const userAgent = (typeof navigator !== 'undefined' && navigator.userAgent) || '';
    const isIOS = /\b(iPad|iPhone|iPod)(?=;)/.test(userAgent);
    if (isIOS) return;

    $(document).on('copy', '.md', (e) => {
      CopyAsGFM.copyAsGFM(e, CopyAsGFM.transformGFMSelection);
    });
    $(document).on('copy', 'pre.code.highlight, table.code td.line_content', (e) => {
      CopyAsGFM.copyAsGFM(e, CopyAsGFM.transformCodeSelection);
    });
    $(document).on('paste', '.js-gfm-input', CopyAsGFM.pasteGFM);
  }

  static copyAsGFM(e, transformer) {
    const { clipboardData } = e.originalEvent;
    if (!clipboardData) return;

    const documentFragment = getSelectedFragment();
    if (!documentFragment) return;

    const el = transformer(documentFragment.cloneNode(true), e.currentTarget);
    if (!el) return;

    e.preventDefault();
    e.stopPropagation();

    const div = document.createElement('div');
    div.appendChild(el.cloneNode(true));
    const html = div.innerHTML;

    clipboardData.setData('text/plain', el.textContent);
    clipboardData.setData('text/html', html);
    // We are also setting this as fallback to transform the selection to gfm on paste
    clipboardData.setData('text/x-gfm-html', html);

    CopyAsGFM.nodeToGFM(el)
      .then((res) => {
        clipboardData.setData('text/x-gfm', res);
      })
      .catch(() => {
        // Not showing the error as Firefox might doesn't allow
        // it or other browsers who have a time limit on the execution
        // of the copy event
      });
  }

  static pasteGFM(e) {
    const { clipboardData } = e.originalEvent;
    if (!clipboardData) return;

    const text = clipboardData.getData('text/plain');
    const gfm = clipboardData.getData('text/x-gfm');
    const gfmHtml = clipboardData.getData('text/x-gfm-html');
    if (!gfm && !gfmHtml) return;

    e.preventDefault();

    // We have the original selection already converted to gfm
    if (gfm) {
      CopyAsGFM.insertPastedText(e.target, text, gfm);
    } else {
      // Due to the async copy call we are not able to produce gfm so we transform the cached HTML
      const div = document.createElement('div');
      div.innerHTML = sanitize(gfmHtml);
      CopyAsGFM.nodeToGFM(div)
        .then((transformedGfm) => {
          CopyAsGFM.insertPastedText(e.target, text, transformedGfm);
        })
        .catch(() => {});
    }
  }

  static insertPastedText(target, text, gfm) {
    insertText(target, (textBefore) => {
      // If the text before the cursor contains an odd number of backticks,
      // we are either inside an inline code span that starts with 1 backtick
      // or a code block that starts with 3 backticks.
      // This logic still holds when there are one or more _closed_ code spans
      // or blocks that will have 2 or 6 backticks.
      // This will break down when the actual code block contains an uneven
      // number of backticks, but this is a rare edge case.
      const backtickMatch = textBefore.match(/`/g);
      const insideCodeBlock = backtickMatch && backtickMatch.length % 2 === 1;

      if (insideCodeBlock) {
        return text;
      }

      return gfm;
    });
  }

  static transformGFMSelection(documentFragment) {
    const gfmElements = documentFragment.querySelectorAll('.md');
    switch (gfmElements.length) {
      case 0: {
        return documentFragment;
      }
      case 1: {
        return gfmElements[0];
      }
      default: {
        const allGfmElement = document.createElement('div');

        for (let i = 0; i < gfmElements.length; i += 1) {
          const gfmElement = gfmElements[i];
          allGfmElement.appendChild(gfmElement);
          allGfmElement.appendChild(document.createTextNode('\n\n'));
        }

        return allGfmElement;
      }
    }
  }

  static transformCodeSelection(documentFragment, target) {
    let lineSelector = '.line';

    if (target) {
      const lineClass = ['left-side', 'right-side'].filter((name) =>
        target.classList.contains(name),
      )[0];
      if (lineClass) {
        lineSelector = `.line_content.${lineClass} ${lineSelector}`;
      }
    }

    const lineElements = documentFragment.querySelectorAll(lineSelector);

    let codeElement;
    if (lineElements.length > 1) {
      codeElement = document.createElement('pre');
      codeElement.className = 'code highlight';

      const lang = lineElements[0].getAttribute('lang');
      if (lang) {
        codeElement.setAttribute('lang', lang);
      }
    } else {
      codeElement = document.createElement('code');
    }

    if (lineElements.length > 0) {
      for (let i = 0; i < lineElements.length; i += 1) {
        const lineElement = lineElements[i];
        codeElement.appendChild(lineElement);
        codeElement.appendChild(document.createTextNode('\n'));
      }
    } else {
      codeElement.appendChild(documentFragment);
    }

    return codeElement;
  }

  static nodeToGFM(node) {
    return Promise.all([
      import(/* webpackChunkName: 'gfm_copy_extra' */ 'prosemirror-model'),
      import(/* webpackChunkName: 'gfm_copy_extra' */ './schema'),
      import(/* webpackChunkName: 'gfm_copy_extra' */ './serializer'),
    ])
      .then(([prosemirrorModel, schema, markdownSerializer]) => {
        const { DOMParser } = prosemirrorModel;
        const wrapEl = document.createElement('div');
        wrapEl.appendChild(node.cloneNode(true));
        const doc = DOMParser.fromSchema(schema.default).parse(wrapEl);

        const res = markdownSerializer.default.serialize(doc, {
          tightLists: true,
        });
        return res;
      })
      .catch(() => {});
  }

  static quoted(markdown) {
    return `> ${markdown.split('\n').join('\n> ')}`;
  }
}

// Export CopyAsGFM as a global for rspec to access
// see /spec/features/markdown/copy_as_gfm_spec.rb
if (process.env.NODE_ENV !== 'production') {
  window.CopyAsGFM = CopyAsGFM;
}

export default function initCopyAsGFM() {
  return new CopyAsGFM();
}
