import DiffMatchPatch from 'diff-match-patch';
import cledit from './cledit';
import utils from './utils';
import diffUtils from './diffUtils';
import store from '../store';
import EditorClassApplier from '../components/common/EditorClassApplier';
import PreviewClassApplier from '../components/common/PreviewClassApplier';

let clEditor;
// let discussionIds = {};
let discussionMarkers = {};
let markerKeys;
let markerIdxMap;
let previousPatchableText;
let currentPatchableText;
let isChangePatch;
let contentId;
let editorClassAppliers = {};
let previewClassAppliers = {};

function getDiscussionMarkers(discussion, discussionId, onMarker) {
  const getMarker = (offsetName) => {
    const markerKey = `${discussionId}:${offsetName}`;
    let marker = discussionMarkers[markerKey];
    if (!marker) {
      marker = new cledit.Marker(discussion[offsetName], offsetName === 'end');
      marker.discussionId = discussionId;
      marker.offsetName = offsetName;
      clEditor.addMarker(marker);
      discussionMarkers[markerKey] = marker;
    }
    onMarker(marker);
  };
  getMarker('start');
  getMarker('end');
}

function syncDiscussionMarkers(content, writeOffsets) {
  const discussions = {
    ...content.discussions,
  };
  const newDiscussion = store.getters['discussion/newDiscussion'];
  if (newDiscussion) {
    discussions[store.state.discussion.newDiscussionId] = {
      ...newDiscussion,
    };
  }
  Object.entries(discussionMarkers).forEach(([markerKey, marker]) => {
    // Remove marker if discussion was removed
    const discussion = discussions[marker.discussionId];
    if (!discussion) {
      clEditor.removeMarker(marker);
      delete discussionMarkers[markerKey];
    }
  });

  Object.entries(discussions).forEach(([discussionId, discussion]) => {
    getDiscussionMarkers(discussion, discussionId, writeOffsets
      ? (marker) => {
        discussion[marker.offsetName] = marker.offset;
      }
      : (marker) => {
        marker.offset = discussion[marker.offsetName];
      });
  });

  if (writeOffsets && newDiscussion) {
    store.commit('discussion/patchNewDiscussion',
      discussions[store.state.discussion.newDiscussionId]);
  }
}

function removeDiscussionMarkers() {
  Object.entries(discussionMarkers).forEach(([, marker]) => {
    clEditor.removeMarker(marker);
  });
  discussionMarkers = {};
  markerKeys = [];
  markerIdxMap = Object.create(null);
}

const diffMatchPatch = new DiffMatchPatch();

function makePatches() {
  const diffs = diffMatchPatch.diff_main(previousPatchableText, currentPatchableText);
  return diffMatchPatch.patch_make(previousPatchableText, diffs);
}

function applyPatches(patches) {
  const newPatchableText = diffMatchPatch.patch_apply(patches, currentPatchableText)[0];
  let result = newPatchableText;
  if (markerKeys.length) {
    // Strip text markers
    result = result.replace(new RegExp(`[\ue000-${String.fromCharCode((0xe000 + markerKeys.length) - 1)}]`, 'g'), '');
  }
  // Expect a `contentChanged` event
  if (result !== clEditor.getContent()) {
    previousPatchableText = currentPatchableText;
    currentPatchableText = newPatchableText;
    isChangePatch = true;
  }
  return result;
}

function reversePatches(patches) {
  const result = diffMatchPatch.patch_deepCopy(patches).reverse();
  result.forEach((patch) => {
    patch.diffs.forEach((diff) => {
      diff[0] = -diff[0];
    });
  });
  return result;
}

export default {
  createClEditor(editorElt) {
    this.clEditor = cledit(editorElt, editorElt.parentNode, true);
    clEditor = this.clEditor;
    clEditor.on('contentChanged', (text) => {
      const oldContent = store.getters['content/current'];
      const newContent = {
        ...utils.deepCopy(oldContent),
        text: utils.sanitizeText(text),
      };
      syncDiscussionMarkers(newContent, true);
      if (!isChangePatch) {
        previousPatchableText = currentPatchableText;
        currentPatchableText = diffUtils.makePatchableText(newContent, markerKeys, markerIdxMap);
      } else {
        // Take a chance to restore discussion offsets on undo/redo
        newContent.text = currentPatchableText;
        diffUtils.restoreDiscussionOffsets(newContent, markerKeys);
        syncDiscussionMarkers(newContent, false);
      }
      store.dispatch('content/patchCurrent', newContent);
      isChangePatch = false;
    });
    clEditor.on('focus', () => store.commit('discussion/setNewCommentFocus', false));
  },
  initClEditorInternal(opts) {
    const content = store.getters['content/current'];
    if (content) {
      removeDiscussionMarkers(); // Markers will be recreated on contentChanged
      const contentState = store.getters['contentState/current'];
      const options = Object.assign({
        selectionStart: contentState.selectionStart,
        selectionEnd: contentState.selectionEnd,
        patchHandler: {
          makePatches,
          applyPatches,
          reversePatches,
        },
      }, opts);

      if (contentId !== content.id) {
        contentId = content.id;
        currentPatchableText = diffUtils.makePatchableText(content, markerKeys, markerIdxMap);
        previousPatchableText = currentPatchableText;
        syncDiscussionMarkers(content, false);
        options.content = content.text;
      }

      clEditor.init(options);
    }
  },
  applyContent() {
    if (clEditor) {
      const content = store.getters['content/current'];
      if (clEditor.setContent(content.text, true).range) {
        // Marker will be recreated on contentChange
        removeDiscussionMarkers();
      } else {
        syncDiscussionMarkers(content, false);
      }
    }
  },
  getTrimmedSelection() {
    const selectionMgr = clEditor.selectionMgr;
    let start = Math.min(selectionMgr.selectionStart, selectionMgr.selectionEnd);
    let end = Math.max(selectionMgr.selectionStart, selectionMgr.selectionEnd);
    const text = clEditor.getContent();
    while ((text[start] || '').match(/\s/)) {
      start += 1;
    }
    while ((text[end - 1] || '').match(/\s/)) {
      end -= 1;
    }
    return start < end && { start, end };
  },
  initHighlighters() {
    store.watch(
      () => store.getters['discussion/newDiscussion'],
      () => syncDiscussionMarkers(store.getters['content/current'], false),
    );

    store.watch(
      () => store.getters['discussion/currentFileDiscussions'],
      (discussions) => {
        const classGetter = (type, discussionId) => () => {
          const classes = [`discussion-${type}-highlighting--${discussionId}`, `discussion-${type}-highlighting`];
          if (store.state.discussion.currentDiscussionId === discussionId) {
            classes.push(`discussion-${type}-highlighting--selected`);
          }
          return classes;
        };
        const offsetGetter = discussionId => () => {
          const startMarker = discussionMarkers[`${discussionId}:start`];
          const endMarker = discussionMarkers[`${discussionId}:end`];
          return startMarker && endMarker && {
            start: startMarker.offset,
            end: endMarker.offset,
          };
        };

        // Editor class appliers
        const oldEditorClassAppliers = editorClassAppliers;
        editorClassAppliers = {};
        Object.keys(discussions).forEach((discussionId) => {
          const classApplier = oldEditorClassAppliers[discussionId] || new EditorClassApplier(
            classGetter('editor', discussionId), offsetGetter(discussionId), { discussionId });
          editorClassAppliers[discussionId] = classApplier;
        });
        // Clean unused class appliers
        Object.entries(oldEditorClassAppliers).forEach(([discussionId, classApplier]) => {
          if (!editorClassAppliers[discussionId]) {
            classApplier.stop();
          }
        });

        // Preview class appliers
        const oldPreviewClassAppliers = previewClassAppliers;
        previewClassAppliers = {};
        Object.keys(discussions).forEach((discussionId) => {
          const classApplier = oldPreviewClassAppliers[discussionId] || new PreviewClassApplier(
            classGetter('preview', discussionId), offsetGetter(discussionId), { discussionId });
          previewClassAppliers[discussionId] = classApplier;
        });
        // Clean unused class appliers
        Object.entries(oldPreviewClassAppliers).forEach(([discussionId, classApplier]) => {
          if (!previewClassAppliers[discussionId]) {
            classApplier.stop();
          }
        });
      },
    );
  },
};