// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {
    EditorState,
    CharacterMetadata,
    ContentState,
    ContentBlock,
    EditorChangeType,
    DraftStyleMap,
} from 'draft-js'
import {EditorPlugin} from '@draft-js-plugins/editor'
import {Repeat, List} from 'immutable'

// Inline style handlers
import createBoldStyleStrategy from './inline-styles/boldStyleStrategy'
import createItalicStyleStrategy from './inline-styles/italicStyleStrategy'
import createStrikethroughStyleStrategy from './inline-styles/strikethroughStyleStrategy'
import createHeadingDelimiterStyleStrategy from './inline-styles/headingDelimiterStyleStrategy'
import createULDelimiterStyleStrategy from './inline-styles/ulDelimiterStyleStrategy'
import createOLDelimiterStyleStrategy from './inline-styles/olDelimiterStyleStrategy'
import createQuoteStyleStrategy from './inline-styles/quoteStyleStrategy'
import createInlineCodeStyleStrategy from './inline-styles/inlineCodeStyleStrategy'

// Block type handlers
import createCodeBlockStrategy from './block-types/codeBlockStrategy'
import createHeadingBlockStrategy from './block-types/headingBlockStrategy'
import {BlockStrategy, InlineStrategy} from './pluginStrategy'

export interface LiveMarkdownPluginConfig {
    inlineStyleStrategies?: InlineStrategy[],
    blockTypeStrategies?: BlockStrategy[],
}

function createLiveMarkdownPlugin(config: LiveMarkdownPluginConfig = {}): EditorPlugin {
    const {
        inlineStyleStrategies = [
            createBoldStyleStrategy(),
            createItalicStyleStrategy(),
            createStrikethroughStyleStrategy(),
            createHeadingDelimiterStyleStrategy(),
            createULDelimiterStyleStrategy(),
            createOLDelimiterStyleStrategy(),
            createQuoteStyleStrategy(),
            createInlineCodeStyleStrategy(),
        ],
        blockTypeStrategies = [
            createCodeBlockStrategy(),
            createHeadingBlockStrategy(),
        ],
    } = config

    // Construct the editor style map from our inline style strategies
    const customStyleMap: DraftStyleMap = {}
    inlineStyleStrategies.forEach((styleStrategy) => {
        if (styleStrategy.style && styleStrategy.styles) {
            customStyleMap[styleStrategy.style] = styleStrategy.styles
        }
        if (styleStrategy.delimiterStyle && styleStrategy.delimiterStyles) {
            customStyleMap[styleStrategy.delimiterStyle] =
                styleStrategy.delimiterStyles
        }
    })

    // Construct the block style fn
    const blockStyleMap = blockTypeStrategies.reduce((map: Record<string, string>, blockStrategy) => {
        map[blockStrategy.type] = blockStrategy.className
        return map
    }, {})
    const blockStyleFn = (block: ContentBlock) => {
        const blockType = block.getType()
        return blockStyleMap[blockType]
    }

    return {

        // We must handle the maintenance of block types and inline styles on changes.
        // To make sure the code is efficient we only perform maintenance on content
        // blocks that have been changed. We only perform maintenance for change types
        // that result in actual text changes (ignore cursing through text, etc).
        onChange: (editorState) => {
            // if (editorState.getLastChangeType() === 'insert-fragment')
            //   return maintainWholeEditorState();
            return maintainEditorState(
                editorState,
                blockTypeStrategies,
                inlineStyleStrategies,
            )
        },
        customStyleMap,
        blockStyleFn,
    }
}

// Takes an EditorState and returns a ContentState updated with block types and
// inline styles according to the provided strategies
// Takes a targeted approach that only updates the modified block/blocks
const maintainEditorState = (
    editorState: EditorState,
    blockTypeStrategies: BlockStrategy[],
    inlineStyleStrategies: InlineStrategy[],
) => {
    // Bypass maintenance if text was not changed
    const lastChangeType = editorState.getLastChangeType()
    const bypassOnChangeTypes = [
        'adjust-depth',
        'apply-entity',
        'change-block-data',
        'change-block-type',
        'change-inline-style',
        'maintain-markdown',
    ]
    if (bypassOnChangeTypes.includes(lastChangeType)) {
        return editorState
    }

    // Maintain block types then inline styles
    // Order is important bc we want the inline style strategies to be able to
    // look at block type to avoid unnecessary regex searching when possible
    const contentState = editorState.getCurrentContent()
    let newContentState = maintainBlockTypes(contentState, blockTypeStrategies)
    newContentState = maintainInlineStyles(
        newContentState,
        editorState,
        inlineStyleStrategies,
    )

    // Apply the updated content state
    let newEditorState = editorState
    if (contentState !== newContentState) {
        newEditorState = EditorState.push(
            editorState,
            newContentState,
            'maintain-markdown' as EditorChangeType,
        )
    }
    newEditorState = EditorState.forceSelection(
        newEditorState,
        editorState.getSelection(),
    )

    return newEditorState
}

// Takes a ContentState and returns a ContentState with block types and inline styles
// applied or removed as necessary
const maintainBlockTypes = (contentState: ContentState, blockTypeStrategies: BlockStrategy[]) => {
    return blockTypeStrategies.reduce((cs, blockTypeStrategy) => {
        return blockTypeStrategy.mapBlockType(cs)
    }, contentState)
}

// Takes a ContentState (and EditorState for getting the selection and change type)
// and returns a ContentState with inline styles applied or removed as necessary
const maintainInlineStyles = (
    contentState: ContentState,
    editorState: EditorState,
    inlineStyleStrategies: InlineStrategy[],
): ContentState => {
    const lastChangeType = editorState.getLastChangeType()
    const selection = editorState.getSelection()
    const blockKey = selection.getStartKey()
    const block = contentState.getBlockForKey(blockKey)
    const blockMap = contentState.getBlockMap()
    let newBlockMap = blockMap

    // If text has been pasted (potentially modifying/creating multiple blocks) or
    // the editor is new we must maintain the styles for all content blocks
    if (lastChangeType === 'insert-fragment' || !lastChangeType) {
        blockMap.forEach((b, k) => {
            if (!b || !k) {
                return
            }
            const newBlock = mapInlineStyles(b, inlineStyleStrategies) as ContentBlock
            newBlockMap = newBlockMap.set(k, newBlock)
        })
    } else {
        const newBlock = mapInlineStyles(block, inlineStyleStrategies) as ContentBlock
        newBlockMap = newBlockMap.set(blockKey, newBlock)
    }

    // If enter was pressed (or the block was otherwise split) we must maintain
    // styles in the previous block as well
    if (lastChangeType === 'split-block') {
        const newPrevBlock = mapInlineStyles(
            contentState.getBlockBefore(blockKey)!,
            inlineStyleStrategies,
        ) as ContentBlock
        newBlockMap = newBlockMap.set(
            contentState.getKeyBefore(blockKey),
            newPrevBlock,
        )
    }

    const newContentState = contentState.merge({
        blockMap: newBlockMap,
    }) as ContentState

    return newContentState
}

// Maps inline styles to the provided ContentBlock's CharacterMetadata list based
// on the plugin's inline style strategies
const mapInlineStyles = (block: ContentBlock, strategies: InlineStrategy[]) => {
    // This will be called upon any change that has the potential to effect the styles
    // of a content block.
    // Find all of the ranges that should have styles applied to them (i.e. all bold,
    // italic, or strikethrough delimited ranges of the block).
    const blockText = block.getText()

    // Create a list of empty CharacterMetadata to map styles to
    // eslint-disable-next-line new-cap
    let characterMetadataList = List(
        // eslint-disable-next-line new-cap
        Repeat(CharacterMetadata.create(), blockText.length),
    )

    // Evaluate block text with each style strategy and apply styles to matching
    // ranges of text and delimiters
    strategies.forEach((strategy) => {
        const styleRanges = strategy.findStyleRanges(block)
        const delimiterRanges = strategy.findDelimiterRanges ? strategy.findDelimiterRanges(block, styleRanges) : []

        characterMetadataList = applyStyleRangesToCharacterMetadata(
            strategy.style,
            styleRanges,
            characterMetadataList,
        )

        characterMetadataList = applyStyleRangesToCharacterMetadata(
            strategy.delimiterStyle,
            delimiterRanges,
            characterMetadataList,
        )
    })

    // Apply the list of CharacterMetadata to the content block
    return block.set('characterList', characterMetadataList)
}

// Applies the provided style to the corresponding ranges of the character metadata
const applyStyleRangesToCharacterMetadata = (
    style: string | undefined,
    ranges: number[][],
    characterMetadataList: List<CharacterMetadata>,
) => {
    let styledCharacterMetadataList = characterMetadataList
    if (!style) {
        return styledCharacterMetadataList
    }

    ranges.forEach((range) => {
        for (let i = range[0]; i <= range[1]; i++) {
            const styled = CharacterMetadata.applyStyle(
                characterMetadataList.get(i),
                style,
            )
            styledCharacterMetadataList = styledCharacterMetadataList.set(i, styled)
        }
    })
    return styledCharacterMetadataList
}

export default createLiveMarkdownPlugin
