import React from 'react';
import {
    Editor,
    EditorState,
    Modifier,
    ContentState,
    ContentBlock,
    convertFromHTML,
    DefaultDraftBlockRenderMap,
    DefaultDraftInlineStyle,
    CompositeDecorator,
    SelectionState,
    Entity,
} from 'draft-js';
import * as sdk from './index';
import * as emojione from 'emojione';
import {stateToHTML} from 'draft-js-export-html';
import {SelectionRange} from "./autocomplete/Autocompleter";
import {stateToMarkdown as __stateToMarkdown} from 'draft-js-export-markdown';

const MARKDOWN_REGEX = {
    LINK: /(?:\[([^\]]+)\]\(([^\)]+)\))|\<(\w+:\/\/[^\>]+)\>/g,
    ITALIC: /([\*_])([\w\s]+?)\1/g,
    BOLD: /([\*_])\1([\w\s]+?)\1\1/g,
    HR: /(\n|^)((-|\*|_) *){3,}(\n|$)/g,
    CODE: /`[^`]*`/g,
    STRIKETHROUGH: /~{2}[^~]*~{2}/g,
};

const USERNAME_REGEX = /@\S+:\S+/g;
const ROOM_REGEX = /#\S+:\S+/g;
const EMOJI_REGEX = new RegExp(emojione.unicodeRegexp, 'g');

const ZWS_CODE = 8203;
const ZWS = String.fromCharCode(ZWS_CODE); // zero width space
export function stateToMarkdown(state) {
    return __stateToMarkdown(state)
        .replace(
            ZWS, // draft-js-export-markdown adds these
            ''); // this is *not* a zero width space, trust me :)
}

export const contentStateToHTML = (contentState: ContentState) => {
    return stateToHTML(contentState, {
        inlineStyles: {
            UNDERLINE: {
                element: 'u',
            },
        },
    });
};

export function htmlToContentState(html: string): ContentState {
    const blockArray = convertFromHTML(html).contentBlocks;
    return ContentState.createFromBlockArray(blockArray);
}

function unicodeToEmojiUri(str) {
    let replaceWith, unicode, alt;
    if ((!emojione.unicodeAlt) || (emojione.sprites)) {
        // if we are using the shortname as the alt tag then we need a reversed array to map unicode code point to shortnames
        const mappedUnicode = emojione.mapUnicodeToShort();
    }

    str = str.replace(emojione.regUnicode, function(unicodeChar) {
        if ( (typeof unicodeChar === 'undefined') || (unicodeChar === '') || (!(unicodeChar in emojione.jsEscapeMap)) ) {
            // if the unicodeChar doesnt exist just return the entire match
            return unicodeChar;
        } else {
            // Remove variant selector VS16 (explicitly emoji) as it is unnecessary and leads to an incorrect URL below
            if(unicodeChar.length == 2 && unicodeChar[1] == '\ufe0f') {
                unicodeChar = unicodeChar[0];
            }

            // get the unicode codepoint from the actual char
            unicode = emojione.jsEscapeMap[unicodeChar];

            return emojione.imagePathSVG+unicode+'.svg'+emojione.cacheBustParam;
        }
    });

    return str;
}

/**
 * Utility function that looks for regex matches within a ContentBlock and invokes {callback} with (start, end)
 * From https://facebook.github.io/draft-js/docs/advanced-topics-decorators.html
 */
function findWithRegex(regex, contentBlock: ContentBlock, callback: (start: number, end: number) => any) {
    const text = contentBlock.getText();
    let matchArr, start;
    while ((matchArr = regex.exec(text)) !== null) {
        start = matchArr.index;
        callback(start, start + matchArr[0].length);
    }
}

// Workaround for https://github.com/facebook/draft-js/issues/414
const emojiDecorator = {
    strategy: (contentState, contentBlock, callback) => {
        findWithRegex(EMOJI_REGEX, contentBlock, callback);
    },
    component: (props) => {
        const uri = unicodeToEmojiUri(props.children[0].props.text);
        const shortname = emojione.toShort(props.children[0].props.text);
        const style = {
            display: 'inline-block',
            width: '1em',
            maxHeight: '1em',
            background: `url(${uri})`,
            backgroundSize: 'contain',
            backgroundPosition: 'center center',
            overflow: 'hidden',
        };
        return (<span title={shortname} style={style}><span style={{opacity: 0}}>{ props.children }</span></span>);
    },
};

/**
 * Returns a composite decorator which has access to provided scope.
 */
export function getScopedRTDecorators(scope: any): CompositeDecorator {
    return [emojiDecorator];
}

export function getScopedMDDecorators(scope: any): CompositeDecorator {
    const markdownDecorators = ['HR', 'BOLD', 'ITALIC', 'CODE', 'STRIKETHROUGH'].map(
        (style) => ({
            strategy: (contentState, contentBlock, callback) => {
                return findWithRegex(MARKDOWN_REGEX[style], contentBlock, callback);
            },
            component: (props) => (
                <span className={"mx_MarkdownElement mx_Markdown_" + style}>
                    { props.children }
                </span>
            ),
        }));

    markdownDecorators.push({
        strategy: (contentState, contentBlock, callback) => {
            return findWithRegex(MARKDOWN_REGEX.LINK, contentBlock, callback);
        },
        component: (props) => (
            <a href="#" className="mx_MarkdownElement mx_Markdown_LINK">
                { props.children }
            </a>
        ),
    });
    // markdownDecorators.push(emojiDecorator);
    // TODO Consider renabling "syntax highlighting" when we can do it properly
    return [emojiDecorator];
}

/**
 * Passes rangeToReplace to modifyFn and replaces it in contentState with the result.
 */
export function modifyText(contentState: ContentState, rangeToReplace: SelectionState,
                           modifyFn: (text: string) => string, inlineStyle, entityKey): ContentState {
    let getText = (key) => contentState.getBlockForKey(key).getText(),
        startKey = rangeToReplace.getStartKey(),
        startOffset = rangeToReplace.getStartOffset(),
        endKey = rangeToReplace.getEndKey(),
        endOffset = rangeToReplace.getEndOffset(),
        text = "";


    for (let currentKey = startKey;
            currentKey && currentKey !== endKey;
            currentKey = contentState.getKeyAfter(currentKey)) {
        const blockText = getText(currentKey);
        text += blockText.substring(startOffset, blockText.length);

        // from now on, we'll take whole blocks
        startOffset = 0;
    }

    // add remaining part of last block
    text += getText(endKey).substring(startOffset, endOffset);

    return Modifier.replaceText(contentState, rangeToReplace, modifyFn(text), inlineStyle, entityKey);
}

/**
 * Computes the plaintext offsets of the given SelectionState.
 * Note that this inherently means we make assumptions about what that means (no separator between ContentBlocks, etc)
 * Used by autocomplete to show completions when the current selection lies within, or at the edges of a command.
 */
export function selectionStateToTextOffsets(selectionState: SelectionState,
                                            contentBlocks: Array<ContentBlock>): {start: number, end: number} {
    let offset = 0, start = 0, end = 0;
    for (const block of contentBlocks) {
        if (selectionState.getStartKey() === block.getKey()) {
            start = offset + selectionState.getStartOffset();
        }
        if (selectionState.getEndKey() === block.getKey()) {
            end = offset + selectionState.getEndOffset();
            break;
        }
        offset += block.getLength();
    }

    return {
        start,
        end,
    };
}

export function textOffsetsToSelectionState({start, end}: SelectionRange,
                                            contentBlocks: Array<ContentBlock>): SelectionState {
    let selectionState = SelectionState.createEmpty();
    // Subtract block lengths from `start` and `end` until they are less than the current
    // block length (accounting for the NL at the end of each block). Set them to -1 to
    // indicate that the corresponding selection state has been determined.
    for (const block of contentBlocks) {
        const blockLength = block.getLength();
        // -1 indicating that the position start position has been found
        if (start !== -1) {
            if (start < blockLength + 1) {
                selectionState = selectionState.merge({
                    anchorKey: block.getKey(),
                    anchorOffset: start,
                });
                start = -1; // selection state for the start calculated
            } else {
                start -= blockLength + 1; // +1 to account for newline between blocks
            }
        }
        // -1 indicating that the position end position has been found
        if (end !== -1) {
            if (end < blockLength + 1) {
                selectionState = selectionState.merge({
                    focusKey: block.getKey(),
                    focusOffset: end,
                });
                end = -1; // selection state for the end calculated
            } else {
                end -= blockLength + 1; // +1 to account for newline between blocks
            }
        }
    }
    return selectionState;
}

// modified version of https://github.com/draft-js-plugins/draft-js-plugins/blob/master/draft-js-emoji-plugin/src/modifiers/attachImmutableEntitiesToEmojis.js
export function attachImmutableEntitiesToEmoji(editorState: EditorState): EditorState {
    const contentState = editorState.getCurrentContent();
    const blocks = contentState.getBlockMap();
    let newContentState = contentState;

    blocks.forEach((block) => {
        const plainText = block.getText();

        const addEntityToEmoji = (start, end) => {
            const existingEntityKey = block.getEntityAt(start);
            if (existingEntityKey) {
                // avoid manipulation in case the emoji already has an entity
                const entity = newContentState.getEntity(existingEntityKey);
                if (entity && entity.get('type') === 'emoji') {
                    return;
                }
            }

            const selection = SelectionState.createEmpty(block.getKey())
                .set('anchorOffset', start)
                .set('focusOffset', end);
            const emojiText = plainText.substring(start, end);
            newContentState = newContentState.createEntity(
                'emoji', 'IMMUTABLE', { emojiUnicode: emojiText },
            );
            const entityKey = newContentState.getLastCreatedEntityKey();
            newContentState = Modifier.replaceText(
                newContentState,
                selection,
                emojiText,
                null,
                entityKey,
            );
        };

        findWithRegex(EMOJI_REGEX, block, addEntityToEmoji);
    });

    if (!newContentState.equals(contentState)) {
        const oldSelection = editorState.getSelection();
        editorState = EditorState.push(
            editorState,
            newContentState,
            'convert-to-immutable-emojis',
        );
        // this is somewhat of a hack, we're undoing selection changes caused above
        // it would be better not to make those changes in the first place
        editorState = EditorState.forceSelection(editorState, oldSelection);
    }

    return editorState;
}

export function hasMultiLineSelection(editorState: EditorState): boolean {
    const selectionState = editorState.getSelection();
    const anchorKey = selectionState.getAnchorKey();
    const currentContent = editorState.getCurrentContent();
    const currentContentBlock = currentContent.getBlockForKey(anchorKey);
    const start = selectionState.getStartOffset();
    const end = selectionState.getEndOffset();
    const selectedText = currentContentBlock.getText().slice(start, end);
    return selectedText.includes('\n');
}
