/**
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 *
 * Copyright (c) KALEIDOS INC
 */
import pkg from "draft-js";

export const {
  BlockMapBuilder,
  CharacterMetadata,
  CompositeDecorator,
  EditorState,
  Modifier,
  RichTextEditorUtil,
  SelectionState,
  convertFromRaw,
  convertToRaw
} = pkg;

import DraftPasteProcessor from 'draft-js/lib/DraftPasteProcessor.js';
import {Map, OrderedSet} from "immutable";

function isDefined(v) {
  return v !== undefined && v !== null;
}

function mergeBlockData(block, newData) {
  let data = block.getData();

  for (let key of Object.keys(newData)) {
    const oldVal = data.get(key);
    if (oldVal === newData[key]) {
      data = data.delete(key);
    } else {
      data = data.set(key, newData[key]);
    }
  }

  return block.mergeDeep({
    data: data
  });
}

export function createEditorState(content, decorator) {
  if (content === null) {
    return EditorState.createEmpty(decorator);
  } else {
    return EditorState.createWithContent(content, decorator);
  }
}

export function createDecorator(type, component) {
  const strategy = (block, callback, content) => {
    return block.findEntityRanges((cmeta) => {
      const entityKey = cmeta.getEntity();
      return isDefined(entityKey) && (type === content.getEntity(entityKey).getType());
    }, callback);
  };

  return new CompositeDecorator([
    {"strategy": strategy, "component": component}
  ]);
}

function getSelectAllSelection(state) {
  const content = state.getCurrentContent();
  const firstBlock = content.getBlockMap().first();
  const lastBlock = content.getBlockMap().last();

  return new SelectionState({
    "anchorKey": firstBlock.getKey(),
    "anchorOffset": 0,
    "focusKey": lastBlock.getKey(),
    "focusOffset": lastBlock.getLength()
  });
}

function getCursorInEndPosition(state) {
  const content = state.getCurrentContent();
  const lastBlock = content.getBlockMap().last();

  return new SelectionState({
    "anchorKey": lastBlock.getKey(),
    "anchorOffset": lastBlock.getLength(),
    "focusKey": lastBlock.getKey(),
    "focusOffset": lastBlock.getLength()
  });
}

export function selectAll(state) {
  return EditorState.forceSelection(state, getSelectAllSelection(state));
}

function modifySelectedBlocks(contentState, selectionState, operation) {
  var startKey = selectionState.getStartKey();
  var endKey = selectionState.getEndKey();
  var blockMap = contentState.getBlockMap();

  var newBlocks = blockMap.toSeq().skipUntil(function (_, k) {
    return k === startKey;
  }).takeUntil(function (_, k) {
    return k === endKey;
  }).concat(Map([[endKey, blockMap.get(endKey)]])).map(operation);

  return contentState.merge({
    "blockMap": blockMap.merge(newBlocks),
    "selectionBefore": selectionState,
    "selectionAfter": selectionState
  });
}

export function updateCurrentBlockData(state, attrs) {
  const selection = state.getSelection();
  let content = state.getCurrentContent();

  content = modifySelectedBlocks(content, selection, (block) => {
    return mergeBlockData(block, attrs);
  });

  return EditorState.push(state, content, "change-block-data");
}

function addStylesToOverride(styles, other) {
  let result = styles;

  for (let style of other) {
    const [p, k, v] = style.split("$$$");
    const prefix = [p, k, ""].join("$$$");

    const curValue = result.find((it) => it.startsWith(prefix))
    if (curValue) {
      result = result.remove(curValue);
    }
    result = result.add(style);
  }
  return result
}

export function applyInlineStyle(state, styles) {
  const userSelection = state.getSelection();
  let selection = userSelection;
  let result = state;

  if (selection.isCollapsed()) {
    const currentOverride = state.getCurrentInlineStyle() || new OrderedSet();
    const styleOverride = addStylesToOverride(currentOverride, styles)
    return EditorState.setInlineStyleOverride(state, styleOverride);
  }

  let content = null;

  for (let style of styles) {
    const [p, k, v] = style.split("$$$");
    const prefix = [p, k, ""].join("$$$");

    content = result.getCurrentContent();
    content = removeInlineStylePrefix(content, selection, prefix);

    if (v !== "z:null") {
      content = Modifier.applyInlineStyle(content, selection, style);
    }

    result = EditorState.push(result, content, "change-inline-style");
  }

  return EditorState.acceptSelection(result, userSelection);
}

export function splitBlockPreservingData(state) {
  let content = state.getCurrentContent();
  const selection = state.getSelection();

  content = Modifier.splitBlock(content, selection);

  const blockData = content.blockMap.get(content.selectionBefore.getStartKey()).getData();
  const blockKey = content.selectionAfter.getStartKey();
  const blockMap = content.blockMap.update(blockKey, (block) => {
    return block.set("data", blockData);
  });

  content = content.set("blockMap", blockMap);

  return EditorState.push(state, content, "split-block");
}

export function addBlurSelectionEntity(state) {
  let content = state.getCurrentContent(state);
  const selection = state.getSelection();

  content = content.createEntity("PENPOT_SELECTION", "MUTABLE");
  const entityKey = content.getLastCreatedEntityKey();

  content = Modifier.applyEntity(content, selection, entityKey);
  return EditorState.push(state, content, "apply-entity");
}

export function removeBlurSelectionEntity(state) {
  const selectionAll = getSelectAllSelection(state);
  const selection = state.getSelection();

  let content = state.getCurrentContent();
  content = Modifier.applyEntity(content, selectionAll, null);

  state = EditorState.push(state, content, "apply-entity");
  state = EditorState.forceSelection(state, selection);

  return state;
}

export function getCurrentBlock(state) {
  const content = state.getCurrentContent();
  const selection = state.getSelection();
  const startKey = selection.getStartKey();
  return content.getBlockForKey(startKey);
}

export function getCurrentEntityKey(state) {
  const block = getCurrentBlock(state);
  const selection = state.getSelection();
  const startOffset = selection.getStartOffset();
  return block.getEntityAt(startOffset);
}

export function removeInlineStylePrefix(contentState, selectionState, stylePrefix) {
  const startKey = selectionState.getStartKey();
  const startOffset = selectionState.getStartOffset();
  const endKey = selectionState.getEndKey();
  const endOffset = selectionState.getEndOffset();

  return modifySelectedBlocks(contentState, selectionState, (block, blockKey) => {
    let sliceStart;
    let sliceEnd;

    if (startKey === endKey) {
      sliceStart = startOffset;
      sliceEnd = endOffset;
    } else {
      sliceStart = blockKey === startKey ? startOffset : 0;
      sliceEnd = blockKey === endKey ? endOffset : block.getLength();
    }

    let chars = block.getCharacterList();
    let current;

    while (sliceStart < sliceEnd) {
      current = chars.get(sliceStart);
      current = current.set("style", current.getStyle().filter((s) => !s.startsWith(stylePrefix)))
      chars = chars.set(sliceStart, CharacterMetadata.create(current));

      sliceStart++;
    }

    return block.set("characterList", chars);
  });
}

export function cursorToEnd(state) {
  const newSelection = getCursorInEndPosition(state);
  const selection = state.getSelection();

  let content = state.getCurrentContent();
  content = Modifier.applyEntity(content, newSelection, null);

  state = EditorState.forceSelection(state, newSelection);
  state = EditorState.push(state, content, "apply-entity");

  return state;
}

export function isCurrentEmpty(state) {
  const selection = state.getSelection();

  if (!selection.isCollapsed()) {
    return false;
  }

  const blockKey = selection.getStartKey();
  const content = state.getCurrentContent();

  const block = content.getBlockForKey(blockKey);

  return block.getText() === "";
}

/*
  Returns the block keys between a selection
*/
export function getSelectedBlocks(state) {
  const selection = state.getSelection();
  const startKey = selection.getStartKey();
  const endKey = selection.getEndKey();
  const content = state.getCurrentContent();
  const result = [ startKey ];

  let currentKey = startKey;

  while (currentKey !== endKey) {
    const currentBlock = content.getBlockAfter(currentKey);
    currentKey = currentBlock.getKey();
    result.push(currentKey);
  }

  return result;
}

export function getBlockContent(state, blockKey) {
  const content = state.getCurrentContent();
  const block = content.getBlockForKey(blockKey);
  return block.getText();
}

export function getBlockData(state, blockKey) {
  const content = state.getCurrentContent();
  const block = content.getBlockForKey(blockKey);
  return block && block.getData().toJS();
}

export function updateBlockData(state, blockKey, data) {
  const userSelection = state.getSelection();
  const inlineStyleOverride = state.getInlineStyleOverride();
  const content = state.getCurrentContent();
  const block = content.getBlockForKey(blockKey);
  const newBlock = mergeBlockData(block, data);

  const blockData = newBlock.getData();

  const newContent = Modifier.setBlockData(
    state.getCurrentContent(),
    SelectionState.createEmpty(blockKey),
    blockData
  );

  let result = EditorState.push(state, newContent, 'change-block-data');
  result = EditorState.acceptSelection(result, userSelection);
  result = EditorState.setInlineStyleOverride(result, inlineStyleOverride);
  return result;
}

export function getSelection(state) {
  return state.getSelection();
}

export function setSelection(state, selection) {
  return EditorState.acceptSelection(state, selection);
}

export function selectBlock(state, blockKey) {
  const block = state.getCurrentContent().getBlockForKey(blockKey);
  const length = block.getText().length;
  const selection = SelectionState.createEmpty(blockKey).merge({
    focusOffset: length
  });
  return EditorState.acceptSelection(state, selection);
}

export function getInlineStyle(state, blockKey, offset) {
  const content = state.getCurrentContent();
  const block = content.getBlockForKey(blockKey);
  return block.getInlineStyleAt(offset).toJS();
}

const NEWLINE_REGEX = /\r\n?|\n/g;

function splitTextIntoTextBlocks(text) {
  return text.split(NEWLINE_REGEX);
}

export function insertText(state, text, attrs, inlineStyles) {
  const blocks = splitTextIntoTextBlocks(text);

  const character = CharacterMetadata.create({style: OrderedSet(inlineStyles)});

  let blockArray = DraftPasteProcessor.processText(
    blocks,
    character,
    "unstyled",
  );

  blockArray = blockArray.map((b) => {
      return mergeBlockData(b, attrs);
  });

  const fragment = BlockMapBuilder.createFromArray(blockArray);
  const content = state.getCurrentContent();
  const selection = state.getSelection();

  const newContent = Modifier.replaceWithFragment(
    content,
    selection,
    fragment
  );

  const resultSelection = SelectionState.createEmpty(selection.getStartKey());
  return EditorState.push(state, newContent, 'insert-fragment');
}

export function setInlineStyleOverride(state, inlineStyles) {
  return EditorState.setInlineStyleOverride(state, inlineStyles);
}

export function selectionEquals(selection, other) {
  return selection.getAnchorKey() === other.getAnchorKey() &&
    selection.getAnchorOffset() === other.getAnchorOffset() &&
    selection.getFocusKey() === other.getFocusKey() &&
    selection.getFocusOffset() === other.getFocusOffset() &&
    selection.getIsBackward() === other.getIsBackward();
}