<template>
  <div class="richtext-editor" v-if="isRichtextReady">
    <tiny-mce v-model="computedValue" :disabled="disabled" :init="richtextConf" />

    <b-modal scroll="keep" :width="1200" :aria-modal="true" :active.sync="isRichtextSourceVisible">
      <div>
        <section expanded class="modal-card-body preview">
          <code-editor lang="html" v-model="richTextSourceBody" key="richtext-source" />
        </section>
        <footer class="modal-card-foot has-text-right">
          <b-button @click="onFormatRichtextHTML">
            {{ $t('campaigns.formatHTML') }}
          </b-button>
          <b-button @click="() => { this.isRichtextSourceVisible = false; }">
            {{ $t('globals.buttons.close') }}
          </b-button>
          <b-button @click="onSaveRichTextSource" class="is-primary">
            {{ $t('globals.buttons.save') }}
          </b-button>
        </footer>
      </div>
    </b-modal>

    <b-modal scroll="keep" :width="750" :aria-modal="true" :active.sync="isInsertHTMLVisible">
      <div>
        <section expanded class="modal-card-body preview">
          <code-editor lang="html" v-model="insertHTMLSnippet" key="richtext-snippet" />
        </section>
        <footer class="modal-card-foot has-text-right">
          <b-button @click="onFormatRichtextHTMLSnippet">
            {{ $t('campaigns.formatHTML') }}
          </b-button>
          <b-button @click="() => { this.isInsertHTMLVisible = false; }">
            {{ $t('globals.buttons.close') }}
          </b-button>
          <b-button @click="onInsertHTML" class="is-primary">
            {{ $t('globals.buttons.insert') }}
          </b-button>
        </footer>
      </div>
    </b-modal>

    <!-- image picker -->
    <b-modal scroll="keep" :aria-modal="true" :active.sync="isMediaVisible" :width="900">
      <div class="modal-card content" style="width: auto">
        <section expanded class="modal-card-body">
          <media is-modal @selected="onMediaSelect" />
        </section>
      </div>
    </b-modal>
  </div>
</template>

<script>
import { html } from 'js-beautify';
import { mapState } from 'vuex';

import TinyMce from '@tinymce/tinymce-vue';
import 'tinymce';
import 'tinymce/icons/default';
import 'tinymce/plugins/anchor';
import 'tinymce/plugins/autolink';
import 'tinymce/plugins/autoresize';
import 'tinymce/plugins/charmap';
import 'tinymce/plugins/colorpicker';
import 'tinymce/plugins/contextmenu';
import 'tinymce/plugins/emoticons';
import 'tinymce/plugins/emoticons/js/emojis';
import 'tinymce/plugins/fullscreen';
import 'tinymce/plugins/help';
import 'tinymce/plugins/hr';
import 'tinymce/plugins/image';
import 'tinymce/plugins/imagetools';
import 'tinymce/plugins/link';
import 'tinymce/plugins/lists';
import 'tinymce/plugins/paste';
import 'tinymce/plugins/searchreplace';
import 'tinymce/plugins/table';
import 'tinymce/plugins/textcolor';
import 'tinymce/plugins/visualblocks';
import 'tinymce/plugins/visualchars';
import 'tinymce/plugins/wordcount';
import 'tinymce/skins/ui/oxide/skin.css';
import 'tinymce/themes/silver';

import { colors, uris } from '../constants';
import Media from '../views/Media.vue';
import CodeEditor from './CodeEditor.vue';

// Map of eaglecast language codes to corresponding TinyMCE language files.
const LANGS = {
  cs: 'cs',
  de: 'de',
  es: 'es_419',
  fr: 'fr_FR',
  it: 'it_IT',
  pl: 'pl',
  pt: 'pt_PT',
  'pt-BR': 'pt_BR',
  ro: 'ro',
  tr: 'tr',
};

const TRACK_LINK = 'trackLink';
const TRACK_SUFFIX = '@TrackLink';
const EMBED_IMAGE = 'embedImage';

export default {
  components: {
    Media,
    'tiny-mce': TinyMce,
    'code-editor': CodeEditor,
  },

  props: {
    disabled: { type: Boolean, default: false },
    value: {
      type: String,
      default: '',
    },
  },

  data() {
    return {
      isPreviewing: false,
      isMediaVisible: false,
      isReady: false,
      isRichtextReady: false,
      isRichtextSourceVisible: false,
      isInsertHTMLVisible: false,
      insertHTMLSnippet: '',
      richtextConf: {},
      richTextSourceBody: '',
      contentType: '',
    };
  },

  methods: {
    initRichtextEditor() {
      const { lang } = this.serverConfig;

      this.richtextConf = {
        init_instance_callback: () => { this.isReady = true; },
        urlconverter_callback: this.onEditorURLConvert,

        setup: (editor) => {
          editor.addShortcut('ctrl+s', 'Save content', () => {
            this.$events.$emit('campaign.update', {});
          });
          editor.addShortcut('f9', 'Preview', () => {
            this.$events.$emit('campaign.preview', {});
          });

          editor.on('init', () => {
            editor.focus();
            this.onEditorDialogOpen(editor);
          });

          // Custom HTML editor.
          editor.ui.registry.addButton('html', {
            icon: 'sourcecode',
            tooltip: 'Source code',
            onAction: this.onRichtextViewSource,
          });

          editor.ui.registry.addButton('insert-html', {
            icon: 'code-sample',
            tooltip: 'Insert HTML',
            onAction: this.onOpenInsertHTML,
          });

          editor.on('CloseWindow', () => {
            editor.selection.getNode().scrollIntoView(false);
          });
        },

        browser_spellcheck: true,
        min_height: 500,
        toolbar_sticky: true,
        entity_encoding: 'raw',
        convert_urls: true,
        relative_urls: false,
        remove_script_host: false,
        extended_valid_elements: 'img[*]',
        plugins: [
          'anchor', 'autoresize', 'autolink', 'charmap', 'emoticons', 'fullscreen',
          'help', 'hr', 'image', 'imagetools', 'link', 'lists', 'paste', 'searchreplace',
          'table', 'visualblocks', 'visualchars', 'wordcount',
        ],
        toolbar: `undo redo | formatselect styleselect fontsizeselect |
                  bold italic underline strikethrough forecolor backcolor subscript superscript |
                  alignleft aligncenter alignright alignjustify |
                  bullist numlist table image insert-html | outdent indent | link hr removeformat |
                  html fullscreen help`,
        fontsize_formats: '10px 11px 12px 14px 15px 16px 18px 24px 36px',
        skin: false,
        content_css: false,
        content_style: `
          body { font-family: 'Inter', sans-serif; font-size: 15px; }
          img { max-width: 100%; }
          img.img-float-left { float: left; margin: 0 1em 1em 0; }
          img.img-float-right { float: right; margin: 0 0 1em 1em; }
          a { color: ${colors.primary}; }
          table, td { border-color: #ccc;}
        `,

        language: LANGS[lang] || null,
        language_url: LANGS[lang] ? `${uris.static}/tinymce/lang/${LANGS[lang]}.js` : null,

        image_advtab: true,
        image_class_list: [
          { title: 'None', value: '' },
          { title: 'Float left', value: 'img-float-left' },
          { title: 'Float right', value: 'img-float-right' },
        ],

        file_picker_types: 'image',
        file_picker_callback: (callback) => {
          this.isMediaVisible = true;
          this.imageCallack = callback;
        },
      };

      this.isRichtextReady = true;
    },

    onEditorURLConvert(url) {
      return url;
    },

    onRichtextViewSource() {
      this.richTextSourceBody = this.computedValue;
      this.isRichtextSourceVisible = true;
    },

    onOpenInsertHTML() {
      this.isInsertHTMLVisible = true;
    },

    onInsertHTML() {
      this.isInsertHTMLVisible = false;
      window.tinymce.editors[0].execCommand('mceInsertContent', false, this.insertHTMLSnippet);
      this.insertHTMLSnippet = '';
    },

    onFormatRichtextHTML() {
      this.richTextSourceBody = this.beautifyHTML(this.richTextSourceBody);
    },

    onFormatRichtextHTMLSnippet() {
      this.insertHTMLSnippet = this.beautifyHTML(this.insertHTMLSnippet);
    },

    onSaveRichTextSource() {
      this.computedValue = this.richTextSourceBody;
      window.tinymce.editors[0].setContent(this.computedValue);
      this.richTextSourceBody = '';
      this.isRichtextSourceVisible = false;
    },

    onEditorDialogOpen(editor) {
      const ed = editor;
      const oldEd = ed.windowManager.open;

      ed.windowManager.open = (t, r) => {
        const data = t.initialData || {};
        const isLink = data.url && 'anchor' in data;
        const isImage = data.src && !isLink;

        if (!isLink && !isImage) {
          return oldEd.call(ed.windowManager, t, r);
        }

        const { onSubmit } = t;
        const checkbox = isLink ? { type: 'checkbox', name: TRACK_LINK, label: 'Track link?' } : { type: 'checkbox', name: EMBED_IMAGE, label: this.$t('media.embed') };
        const spec = { ...t, body: this.withDialogCheckbox(t.body, checkbox) };

        if (isLink) {
          const cleanURL = (data.url.value || '').replace(/@TrackLink$/, '');
          const checked = data.url.value !== cleanURL
            || (!cleanURL && JSON.parse(localStorage.getItem(TRACK_LINK) || 'false'));
          spec.initialData = { ...data, [TRACK_LINK]: checked, url: { ...data.url, value: cleanURL } };
          spec.onSubmit = (api) => {
            const d = api.getData();
            const shouldTrack = Boolean(d[TRACK_LINK]);
            const url = (d.url.value || '').replace(/@TrackLink$/, '');
            localStorage.setItem(TRACK_LINK, JSON.stringify(shouldTrack));
            if (shouldTrack && /^https?:\/\//i.test(url)) {
              api.setData({ url: { ...d.url, value: `${url}${TRACK_SUFFIX}` } });
            }
            onSubmit(api);
          };
        } else {
          const img = this.getSelectedImage(ed);
          spec.initialData = { ...data, [EMBED_IMAGE]: Boolean(img && img.hasAttribute('data-embed')) };
          spec.onSubmit = (api) => {
            const d = api.getData();
            const shouldEmbed = d[EMBED_IMAGE] === true || d[EMBED_IMAGE] === 'true';

            onSubmit(api);

            // Apply 'embed' attr.
            const node = (img && ed.getBody().contains(img)) ? img : this.getSelectedImage(ed);
            if (!node) {
              return;
            }
            if (shouldEmbed) {
              ed.dom.setAttrib(node, 'data-embed', 'true');
            } else {
              node.removeAttribute('data-embed');
            }
            ed.fire('change');
            ed.save();
            this.computedValue = ed.getContent();
          };
        }

        return oldEd.call(ed.windowManager, spec, r);
      };
    },

    withDialogCheckbox(body, checkbox) {
      if (body.type === 'tabpanel') {
        return {
          ...body,
          tabs: body.tabs.map((tab) => (
            tab.name === 'general' || tab.title === 'General'
              ? { ...tab, items: [...tab.items, checkbox] }
              : tab
          )),
        };
      }
      return { ...body, items: [...body.items, checkbox] };
    },

    getSelectedImage(editor) {
      const node = editor.selection.getNode();
      if (!node) {
        return null;
      }
      if (node.nodeName === 'IMG') {
        return node;
      }
      const figure = editor.dom.getParent(node, 'figure.image');
      return figure ? figure.querySelector('img') : null;
    },

    onMediaSelect(media) {
      this.imageCallack(media.url);
    },

    beautifyHTML(str) {
      // Pad all tags with linebreaks.
      let s = this.trimLines(str.replace(/(<(?!(\/)?a|span)([^>]+)>)/gi, '\n$1\n'), true);
      // Remove extra linebreaks.
      s = s.replace(/\n+/g, '\n');

      try {
        s = html(s).trim();
      } catch (error) {
        // eslint-disable-next-line no-console
        console.log('error formatting HTML', error);
      }

      return s;
    },

    trimLines(str, removeEmptyLines) {
      const out = str.split('\n');
      for (let i = 0; i < out.length; i += 1) {
        const line = out[i].trim();
        if (removeEmptyLines) {
          out[i] = line;
        } else if (line === '') {
          out[i] = '';
        }
      }

      return out.join('\n').replace(/\n\s*\n\s*\n/g, '\n\n');
    },
  },

  mounted() {
    this.initRichtextEditor();
  },

  computed: {
    ...mapState(['serverConfig']),

    computedValue: {
      get() {
        return this.value;
      },
      set(newValue) {
        this.$emit('input', newValue);
      },
    },
  },
};
</script>
