import {RichUtils, EditorState, Modifier, ContentState, SelectionState, ContentBlock} from 'draft-js';
import * as entityUtils from '../components/links/entityUtils';
import {onChange} from './editor3';
import * as Links from '../helpers/links';
import * as Blocks from '../helpers/blocks';
import * as Highlights from '../helpers/highlights';
import {removeFormatFromState, removeAllFormatAndStyles} from '../helpers/removeFormat';
import {insertEntity} from '../helpers/draftInsertEntity';
import {IEditorStore} from '../store';
import {assertNever} from 'core/helpers/typescript-helpers';
import {ITextCase} from '../actions';
import {PopupTypes} from '../actions/popups';

/**
 * @description Contains the list of toolbar related reducers.
 */
const toolbar = (state: IEditorStore, action) => {
    switch (action.type) {
    case 'TOOLBAR_TOGGLE_BLOCK_STYLE':
        return toggleBlockStyle(state, action.payload);
    case 'TOOLBAR_TOGGLE_INLINE_STYLE':
        return toggleInlineStyle(state, action.payload);
    case 'TOOLBAR_APPLY_LINK':
        return applyLink(state, action.payload);
    case 'TOOLBAR_REMOVE_LINK':
        return removeLink(state);
    case 'TOOLBAR_REMOVE_FORMAT':
        return removeFormat(state);
    case 'TOOLBAR_REMOVE_ALL_FORMAT':
        return removeAllFormat(state);
    case 'TOOLBAR_INSERT_MEDIA':
        return insertMedia(state, action.payload);
    case 'TOOLBAR_UPDATE_IMAGE':
        return updateImage(state, action.payload);
    case 'TOOLBAR_REMOVE_BLOCK':
        return removeBlock(state, action.payload);
    case 'TOOLBAR_SET_POPUP':
        return setPopup(state, action.payload);
    case 'TOOLBAR_TOGGLE_INVISIBLES':
        return toggleInvisibles(state);
    case 'CHANGE_CASE':
        return changeCase(state, action.payload);
    case 'UNDO':
        return onChange(state, EditorState.undo(state.editorState));
    case 'REDO':
        return onChange(state, EditorState.redo(state.editorState));
    default:
        return state;
    }
};

/**
 * @ngdoc method
 * @name toggleBlockStyle
 * @param {string} blockStyle
 * @description Applies the given block style.
 */
const toggleBlockStyle = (state, blockType) => {
    const {editorState} = state;
    const stateAfterChange = RichUtils.toggleBlockType(
        editorState,
        blockType,
    );

    return onChange(state, stateAfterChange);
};

/**
 * @ngdoc method
 * @name toggleInlineStyle
 * @param {string} inlineStyle
 * @description Applies the given inline style.
 */
const toggleInlineStyle = (state, inlineStyle) => {
    const {editorState} = state;

    let stateAfterChange = RichUtils.toggleInlineStyle(
        editorState,
        inlineStyle,
    );

    // Check if there was a suggestions to toggle that style
    stateAfterChange = handleExistingInlineStyleSuggestionOnToggle(stateAfterChange, inlineStyle);

    return onChange(state, stateAfterChange);
};

const handleExistingInlineStyleSuggestionOnToggle = (editorState, inlineStyle) => {
    const type = Highlights.getTypeByInlineStyle(inlineStyle);
    const highlightName = Highlights.getHighlightStyleAtCurrentPosition(editorState, type);

    if (highlightName) {
        return Highlights.removeHighlight(editorState, highlightName);
    }

    return editorState;
};

/**
 * @ngdoc method
 * @name applyLink
 * @param {Object} link Link data to apply
 * @param {Entity|null} entity The entity to apply the URL too.
 * @description Applies the given URL to the current content selection. If an
 * entity is specified, it applies the link to that entity instead.
 */
const applyLink = (state, {link, entity}) => {
    let {editorState} = state;

    if (entity) {
        return onChange(state, entityUtils.replaceSelectedEntityData(editorState, {link}), true);
    }

    editorState = Links.createLink(editorState, link);
    return onChange(state, editorState);
};

/**
 * @ngdoc method
 * @name removeLink
 * @description Removes the link on the entire entity under the cursor.
 */
const removeLink = (state) => {
    let {editorState} = state;

    editorState = Links.removeLink(editorState);
    return onChange(state, editorState);
};

/*
 * Converts the state in plain text
 */
const removeAllFormat = (state) => {
    return onChange(state, removeAllFormatAndStyles(state.editorState));
};

/*
 * @ngdoc method
 * @name removeFormat
 * @description Set all blocks in selection to unstyled except atomic blocks
 *              and remove inline styles
 */
const removeFormat = (state) => {
    const {editorState} = state;
    const selection = editorState.getSelection();
    const stateWithoutFormat = removeFormatFromState(editorState);
    const newSelection = selection.merge({
        anchorOffset: selection.getEndOffset(),
        anchorKey: selection.getEndKey(),
        focusOffset: selection.getEndOffset(),
        focusKey: selection.getEndKey(),
        isBackward: false,
        hasFocus: true,
    });

    return onChange(state, EditorState.acceptSelection(stateWithoutFormat, newSelection));
};

/**
 * @ngdoc method
 * @name insertMedia
 * @param {Array} files List of media files to be inserted into document.
 * @param {String} targetBlockKey Block key where we want to insert the media
 * @description Inserts a list of media files into the document.
 */
const insertMedia = (state, {files = [], targetBlockKey = null}) => {
    let {editorState} = state;

    files.forEach((file) => {
        editorState = addMedia(editorState, file, targetBlockKey);
    });

    return onChange(state, editorState);
};

/**
 * @ngdoc method
 * @name addMedia
 * @param {Object} editorState Editor state to add the media to.
 * @param {Object} media Media data.
 * @param {String} targetBlockKey Block key where the media is going to
 * @returns {Object} New editor state with media inserted as atomic block.
 * @description Inserts the given media into the given editor state's content and returns
 * the updated editor state.
 */
export const addMedia = (editorState: EditorState, media, targetBlockKey = null): EditorState =>
    insertEntity(editorState, 'MEDIA', 'MUTABLE', {media}, targetBlockKey);

/**
 * @ngdoc method
 * @name updateImage
 * @param {Object} data Contains the entityKey and the new image data.
 * @description Updates the given entityKey with the new image data.
 */
const updateImage = (state, {entityKey, media}) => {
    const {editorState} = state;
    const selection = editorState.getSelection();
    const contentState = editorState.getCurrentContent();
    const newContentState = contentState.replaceEntityData(entityKey, {media});
    const newEditorState = EditorState.push(editorState, newContentState, 'change-block-data');
    // focus the editor and softly force a refresh
    const newState = EditorState.forceSelection(newEditorState, selection);
    const entityDataHasChanged = true;

    return onChange(state, newState, entityDataHasChanged);
};

/**
 * @ngdoc method
 * @name removeBlock
 * @param {Object} data Contains the key from of block to remove
 * @description Removes block from editor
 */
const removeBlock = (state, {blockKey}) => {
    const {editorState} = state;
    const newEditorState = Blocks.removeBlock(editorState, blockKey);

    return onChange(state, newEditorState);
};

/**
 * @ngdoc method
 * @name toggleInvisibles
 * @param {Object} state
 * @return {Object} returns new state
 * @description Enable/Disable the paragraph marks
 */
const toggleInvisibles = (state) => {
    const {invisibles} = state;

    return {
        ...state,
        invisibles: !invisibles,
    };
};

/**
 * @ngdoc method
 * @name setPopup
 * @param {Object} data Type of popup and popup data.
 * @description Sets the toolbar popup to the given type.
 */
const setPopup = (state: IEditorStore, {type, data}) => {
    const {editorState} = state;
    let newEditorState = editorState;

    // SDESK-3885
    // Whenever we hide a popup, the ContentState is not changed so it will
    // trigger these two DraftJS events:
    //     * editOnFocus
    //     * editOnSelect
    // The first one renders the right selection but the second one uses
    // global `window.getSelection()` (which doesn't exist, as the editor lost focus)
    // to check if the editorState selection matches that one. As it doesn't, it renders
    // the selection in the first character of the first block.
    // Using `forceSelection` won't trigger those events and the selection will be correct.
    if (type === PopupTypes.Hidden) {
        newEditorState = EditorState.forceSelection(editorState, editorState.getSelection());
    }

    return {...state, editorState: newEditorState, popup: {type, data}};
};

function changeCase(state: IEditorStore, payload: {changeTo: ITextCase, selection: SelectionState}) {
    const getChangedText = (text: string) => {
        if (changeTo === 'uppercase') {
            return text.toUpperCase();
        } else if (changeTo === 'lowercase') {
            return text.toLowerCase();
        } else {
            assertNever(changeTo);
        }
    };

    const {selection, changeTo} = payload;
    const contentState = state.editorState.getCurrentContent();

    let ended = false;
    let started = false;
    const startKey = selection.getStartKey();
    const endKey = selection.getEndKey();

    const nextBlockMap = state.editorState.getCurrentContent().getBlockMap().map((block) => {
        const key = block.getKey();
        const from = key === startKey ? selection.getStartOffset() : 0;
        const to = key === endKey ? selection.getEndOffset() : block.getLength();

        if (key === startKey) {
            started = true;
        }

        if (ended === true || started === false) {
            return block;
        }

        const text = block.getText();
        const before = text.slice(0, from);
        const toReplace = text.slice(from, to);
        const after = text.slice(to, block.getLength());

        if (key === endKey) {
            ended = true;
        }

        return block.merge({
            text: before + getChangedText(toReplace) + after,
        });
    }) as ContentState;

    const nextContentState = contentState.merge({
        blockMap: nextBlockMap,
    }) as ContentState;

    const nextEditorState = EditorState.push(state.editorState, nextContentState, 'spellcheck-change');

    return onChange(state, EditorState.forceSelection(nextEditorState, selection));
}

export default toolbar;
