// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
/* eslint-disable max-lines */
import React from 'react'
import {FormattedMessage, injectIntl, IntlShape} from 'react-intl'

import {BlockIcons} from '../blockIcons'
import {IPropertyOption, IPropertyTemplate} from '../blocks/board'
import {Card, MutableCard} from '../blocks/card'
import {CardFilter} from '../cardFilter'
import {Constants} from '../constants'
import mutator from '../mutator'
import {Utils} from '../utils'
import {BoardTree, BoardTreeGroup} from '../viewModel/boardTree'
import Button from '../widgets/buttons/button'
import IconButton from '../widgets/buttons/iconButton'
import AddIcon from '../widgets/icons/add'
import DeleteIcon from '../widgets/icons/delete'
import HideIcon from '../widgets/icons/hide'
import OptionsIcon from '../widgets/icons/options'
import ShowIcon from '../widgets/icons/show'
import Menu from '../widgets/menu'
import MenuWrapper from '../widgets/menuWrapper'

import BoardCard from './boardCard'
import {BoardColumn} from './boardColumn'
import './boardComponent.scss'
import CardDialog from './cardDialog'
import {Editable} from './editable'
import RootPortal from './rootPortal'
import ViewHeader from './viewHeader'
import ViewTitle from './viewTitle'

type Props = {
    boardTree: BoardTree
    showView: (id: string) => void
    setSearchText: (text?: string) => void
    intl: IntlShape
}

type State = {
    isSearching: boolean
    shownCardId?: string
    viewMenu: boolean
    selectedCardIds: string[]
    showFilter: boolean
}

class BoardComponent extends React.Component<Props, State> {
    private draggedCards: Card[] = []
    private draggedHeaderOption?: IPropertyOption
    private backgroundRef = React.createRef<HTMLDivElement>()
    private searchFieldRef = React.createRef<Editable>()

    private keydownHandler = (e: KeyboardEvent) => {
        if (e.target !== document.body) {
            return
        }

        if (e.keyCode === 27) {
            if (this.state.selectedCardIds.length > 0) {
                this.setState({selectedCardIds: []})
                e.stopPropagation()
            }
        }

        if (e.keyCode === 8 || e.keyCode === 46) {
            // Backspace or Del: Delete selected cards
            this.deleteSelectedCards()
            e.stopPropagation()
        }
    }

    componentDidMount(): void {
        document.addEventListener('keydown', this.keydownHandler)
    }

    componentWillUnmount(): void {
        document.removeEventListener('keydown', this.keydownHandler)
    }

    constructor(props: Props) {
        super(props)
        this.state = {
            isSearching: Boolean(this.props.boardTree.getSearchText()),
            viewMenu: false,
            selectedCardIds: [],
            showFilter: false,
        }
    }

    shouldComponentUpdate(): boolean {
        return true
    }

    componentDidUpdate(prevPros: Props, prevState: State): void {
        if (this.state.isSearching && !prevState.isSearching) {
            this.searchFieldRef.current?.focus()
        }
    }

    render(): JSX.Element {
        const {boardTree, showView} = this.props
        const {groupByProperty} = boardTree

        if (!groupByProperty) {
            Utils.assertFailure('Board views must have groupByProperty set')
            return <div/>
        }

        const propertyValues = groupByProperty.options || []
        Utils.log(`${propertyValues.length} propertyValues`)

        const {board, activeView, visibleGroups, hiddenGroups} = boardTree
        const visiblePropertyTemplates = board.cardProperties.filter((template) => activeView.visiblePropertyIds.includes(template.id))
        const isManualSort = activeView.sortOptions.length < 1

        return (
            <div
                className='BoardComponent octo-app'
                ref={this.backgroundRef}
                onClick={(e) => {
                    this.backgroundClicked(e)
                }}
            >
                {this.state.shownCardId &&
                <RootPortal>
                    <CardDialog
                        key={this.state.shownCardId}
                        boardTree={boardTree}
                        cardId={this.state.shownCardId}
                        onClose={() => this.setState({shownCardId: undefined})}
                        showCard={(cardId) => this.setState({shownCardId: cardId})}
                    />
                </RootPortal>}

                <div className='octo-frame'>
                    <ViewTitle
                        key={board.id + board.title}
                        board={board}
                    />

                    <div className='octo-board'>
                        <ViewHeader
                            boardTree={boardTree}
                            showView={showView}
                            setSearchText={this.props.setSearchText}
                            addCard={() => this.addCard()}
                            addCardFromTemplate={this.addCardFromTemplate}
                            addCardTemplate={this.addCardTemplate}
                            editCardTemplate={this.editCardTemplate}
                            withGroupBy={true}
                        />
                        <div
                            className='octo-board-header'
                            id='mainBoardHeader'
                        >
                            {/* Column headers */}

                            {visibleGroups.map((group) => this.renderColumnHeader(group))}

                            {/* Hidden column header */}

                            {hiddenGroups.length > 0 &&
                                <div className='octo-board-header-cell narrow'>
                                    <FormattedMessage
                                        id='BoardComponent.hidden-columns'
                                        defaultMessage='Hidden Columns'
                                    />
                                </div>}

                            <div className='octo-board-header-cell narrow'>
                                <Button
                                    onClick={this.addGroupClicked}
                                >
                                    <FormattedMessage
                                        id='BoardComponent.add-a-group'
                                        defaultMessage='+ Add a group'
                                    />
                                </Button>
                            </div>
                        </div>

                        {/* Main content */}

                        <div
                            className='octo-board-body'
                            id='mainBoardBody'
                        >
                            {/* Columns */}

                            {visibleGroups.map((group) => (
                                <BoardColumn
                                    key={group.option.id || 'empty'}
                                    isDropZone={!isManualSort || group.cards.length < 1}
                                    onDrop={() => this.onDropToColumn(group.option)}
                                >
                                    {group.cards.map((card) => this.renderCard(card, visiblePropertyTemplates))}
                                    <Button
                                        onClick={() => {
                                            this.addCard(group.option.id)
                                        }}
                                    >
                                        <FormattedMessage
                                            id='BoardComponent.neww'
                                            defaultMessage='+ New'
                                        />
                                    </Button>
                                </BoardColumn>
                            ))}

                            {/* Hidden columns */}

                            {hiddenGroups.length > 0 &&
                                <div className='octo-board-column narrow'>
                                    {hiddenGroups.map((group) => this.renderHiddenColumnItem(group))}
                                </div>}
                        </div>
                    </div>
                </div>
            </div>
        )
    }

    private renderCard(card: Card, visiblePropertyTemplates: IPropertyTemplate[]) {
        const {boardTree} = this.props
        const {activeView} = boardTree
        const isManualSort = activeView.sortOptions.length < 1
        return (
            <BoardCard
                card={card}
                visiblePropertyTemplates={visiblePropertyTemplates}
                key={card.id}
                isSelected={this.state.selectedCardIds.includes(card.id)}
                onClick={(e) => {
                    this.cardClicked(e, card)
                }}
                onDragStart={() => {
                    if (this.state.selectedCardIds.includes(card.id)) {
                        this.draggedCards = this.state.selectedCardIds.map((id) => boardTree.allCards.find((o) => o.id === id)!)
                    } else {
                        this.draggedCards = [card]
                    }
                }}
                onDragEnd={() => {
                    this.draggedCards = []
                }}

                isDropZone={isManualSort}
                onDrop={() => {
                    this.onDropToCard(card)
                }}
            />
        )
    }

    private renderColumnHeader(group: BoardTreeGroup) {
        const {boardTree, intl} = this.props
        const {activeView} = boardTree

        // TODO: Refactor this as a component
        if (!group.option.id) {
            // Empty group
            const ref = React.createRef<HTMLDivElement>()
            return (
                <div
                    key='empty'
                    ref={ref}
                    className='octo-board-header-cell'

                    draggable={true}
                    onDragStart={() => {
                        this.draggedHeaderOption = group.option
                    }}
                    onDragEnd={() => {
                        this.draggedHeaderOption = undefined
                    }}

                    onDragOver={(e) => {
                        ref.current?.classList.add('dragover')
                        e.preventDefault()
                    }}
                    onDragEnter={(e) => {
                        ref.current?.classList.add('dragover')
                        e.preventDefault()
                    }}
                    onDragLeave={(e) => {
                        ref.current?.classList.remove('dragover')
                        e.preventDefault()
                    }}
                    onDrop={(e) => {
                        ref.current?.classList.remove('dragover')
                        e.preventDefault()
                        this.onDropToColumn(group.option)
                    }}
                >
                    <div
                        className='octo-label'
                        title={intl.formatMessage({
                            id: 'BoardComponent.no-property-title',
                            defaultMessage: 'Items with an empty {property} property will go here. This column cannot be removed.',
                        }, {property: boardTree.groupByProperty!.name})}
                    >
                        <FormattedMessage
                            id='BoardComponent.no-property'
                            defaultMessage='No {property}'
                            values={{
                                property: boardTree.groupByProperty!.name,
                            }}
                        />
                    </div>
                    <Button>{`${group.cards.length}`}</Button>
                    <div className='octo-spacer'/>
                    <MenuWrapper>
                        <IconButton icon={<OptionsIcon/>}/>
                        <Menu>
                            <Menu.Text
                                id='hide'
                                icon={<HideIcon/>}
                                name={intl.formatMessage({id: 'BoardComponent.hide', defaultMessage: 'Hide'})}
                                onClick={() => mutator.hideViewColumn(activeView, '')}
                            />
                        </Menu>
                    </MenuWrapper>
                    <IconButton
                        icon={<AddIcon/>}
                        onClick={() => this.addCard(undefined)}
                    />
                </div>
            )
        }

        const ref = React.createRef<HTMLDivElement>()
        return (
            <div
                key={group.option.id}
                ref={ref}
                className='octo-board-header-cell'

                draggable={true}
                onDragStart={() => {
                    this.draggedHeaderOption = group.option
                }}
                onDragEnd={() => {
                    this.draggedHeaderOption = undefined
                }}

                onDragOver={(e) => {
                    ref.current?.classList.add('dragover')
                    e.preventDefault()
                }}
                onDragEnter={(e) => {
                    ref.current?.classList.add('dragover')
                    e.preventDefault()
                }}
                onDragLeave={(e) => {
                    ref.current?.classList.remove('dragover')
                    e.preventDefault()
                }}
                onDrop={(e) => {
                    ref.current?.classList.remove('dragover')
                    e.preventDefault()
                    this.onDropToColumn(group.option)
                }}
            >
                <Editable
                    className={`octo-label ${group.option.color}`}
                    text={group.option.value}
                    placeholderText='New Select'
                    allowEmpty={false}
                    onChanged={(text) => {
                        this.propertyNameChanged(group.option, text)
                    }}
                />
                <Button>{`${group.cards.length}`}</Button>
                <div className='octo-spacer'/>
                <MenuWrapper>
                    <IconButton icon={<OptionsIcon/>}/>
                    <Menu>
                        <Menu.Text
                            id='hide'
                            icon={<HideIcon/>}
                            name={intl.formatMessage({id: 'BoardComponent.hide', defaultMessage: 'Hide'})}
                            onClick={() => mutator.hideViewColumn(activeView, group.option.id)}
                        />
                        <Menu.Text
                            id='delete'
                            icon={<DeleteIcon/>}
                            name={intl.formatMessage({id: 'BoardComponent.delete', defaultMessage: 'Delete'})}
                            onClick={() => mutator.deletePropertyOption(boardTree, boardTree.groupByProperty!, group.option)}
                        />
                        <Menu.Separator/>
                        {Constants.menuColors.map((color) => (
                            <Menu.Color
                                key={color.id}
                                id={color.id}
                                name={color.name}
                                onClick={() => mutator.changePropertyOptionColor(boardTree.board, boardTree.groupByProperty!, group.option, color.id)}
                            />
                        ))}
                    </Menu>
                </MenuWrapper>
                <IconButton
                    icon={<AddIcon/>}
                    onClick={() => this.addCard(group.option.id)}
                />
            </div>
        )
    }

    private renderHiddenColumnItem(group: BoardTreeGroup) {
        const {boardTree, intl} = this.props
        const {activeView} = boardTree

        const ref = React.createRef<HTMLDivElement>()
        return (
            <div
                ref={ref}
                key={group.option.id || 'empty'}
                className='octo-board-hidden-item'
                onDragOver={(e) => {
                    if (this.draggedCards?.length < 1) {
                        return
                    }
                    ref.current?.classList.add('dragover')
                    e.preventDefault()
                }}
                onDragEnter={(e) => {
                    if (this.draggedCards?.length < 1) {
                        return
                    }
                    ref.current?.classList.add('dragover')
                    e.preventDefault()
                }}
                onDragLeave={(e) => {
                    if (this.draggedCards?.length < 1) {
                        return
                    }
                    ref.current?.classList.remove('dragover')
                    e.preventDefault()
                }}
                onDrop={(e) => {
                    ref.current?.classList.remove('dragover')
                    e.preventDefault()
                    if (this.draggedCards?.length < 1) {
                        return
                    }
                    this.onDropToColumn(group.option)
                }}
            >
                <MenuWrapper>
                    <div
                        key={group.option.id || 'empty'}
                        className={`octo-label ${group.option.color}`}
                    >
                        {group.option.value}
                    </div>
                    <Menu>
                        <Menu.Text
                            id='show'
                            icon={<ShowIcon/>}
                            name={intl.formatMessage({id: 'BoardComponent.show', defaultMessage: 'Show'})}
                            onClick={() => mutator.unhideViewColumn(activeView, group.option.id)}
                        />
                    </Menu>
                </MenuWrapper>
                <Button>{`${group.cards.length}`}</Button>
            </div>
        )
    }

    private backgroundClicked(e: React.MouseEvent) {
        if (this.state.selectedCardIds.length > 0) {
            this.setState({selectedCardIds: []})
            e.stopPropagation()
        }
    }

    private addCardFromTemplate = async (cardTemplateId: string) => {
        await mutator.duplicateCard(
            cardTemplateId,
            this.props.intl.formatMessage({id: 'Mutator.new-card-from-template', defaultMessage: 'new card from template'}),
            false,
            async (newCardId) => {
                this.setState({shownCardId: newCardId})
            },
            async () => {
                this.setState({shownCardId: undefined})
            },
        )
    }

    private async addCard(groupByOptionId?: string): Promise<void> {
        const {boardTree} = this.props
        const {activeView, board} = boardTree

        const card = new MutableCard()

        card.parentId = boardTree.board.id
        card.rootId = boardTree.board.rootId
        const propertiesThatMeetFilters = CardFilter.propertiesThatMeetFilterGroup(activeView.filter, board.cardProperties)
        if (boardTree.groupByProperty) {
            if (groupByOptionId) {
                propertiesThatMeetFilters[boardTree.groupByProperty.id] = groupByOptionId
            } else {
                delete propertiesThatMeetFilters[boardTree.groupByProperty.id]
            }
        }
        card.properties = {...card.properties, ...propertiesThatMeetFilters}
        if (!card.icon) {
            card.icon = BlockIcons.shared.randomIcon()
        }
        await mutator.insertBlock(
            card,
            'add card',
            async () => {
                this.setState({shownCardId: card.id})
            },
            async () => {
                this.setState({shownCardId: undefined})
            },
        )
    }

    private addCardTemplate = async () => {
        const {boardTree} = this.props

        const cardTemplate = new MutableCard()
        cardTemplate.isTemplate = true
        cardTemplate.parentId = boardTree.board.id
        cardTemplate.rootId = boardTree.board.rootId
        await mutator.insertBlock(
            cardTemplate,
            'add card template',
            async () => {
                this.setState({shownCardId: cardTemplate.id})
            }, async () => {
                this.setState({shownCardId: undefined})
            },
        )
    }

    private editCardTemplate = (cardTemplateId: string) => {
        this.setState({shownCardId: cardTemplateId})
    }

    private async propertyNameChanged(option: IPropertyOption, text: string): Promise<void> {
        const {boardTree} = this.props

        await mutator.changePropertyOptionValue(boardTree, boardTree.groupByProperty!, option, text)
    }

    private cardClicked(e: React.MouseEvent, card: Card): void {
        if (e.shiftKey) {
            let selectedCardIds = this.state.selectedCardIds.slice()
            if (selectedCardIds.length > 0 && (e.metaKey || e.ctrlKey)) {
                // Cmd+Shift+Click: Extend the selection
                const {boardTree} = this.props
                const orderedCardIds = boardTree.orderedCards().map((o) => o.id)
                const lastCardId = selectedCardIds[selectedCardIds.length - 1]
                const srcIndex = orderedCardIds.indexOf(lastCardId)
                const destIndex = orderedCardIds.indexOf(card.id)
                const newCardIds = (srcIndex < destIndex) ? orderedCardIds.slice(srcIndex, destIndex + 1) : orderedCardIds.slice(destIndex, srcIndex + 1)
                for (const newCardId of newCardIds) {
                    if (!selectedCardIds.includes(newCardId)) {
                        selectedCardIds.push(newCardId)
                    }
                }
                this.setState({selectedCardIds})
            } else {
                // Shift+Click: add to selection
                if (selectedCardIds.includes(card.id)) {
                    selectedCardIds = selectedCardIds.filter((o) => o !== card.id)
                } else {
                    selectedCardIds.push(card.id)
                }
                this.setState({selectedCardIds})
            }
        } else {
            this.setState({selectedCardIds: [], shownCardId: card.id})
        }

        e.stopPropagation()
    }

    private addGroupClicked = async () => {
        Utils.log('onAddGroupClicked')

        const {boardTree} = this.props

        const option: IPropertyOption = {
            id: Utils.createGuid(),
            value: 'New group',
            color: 'propColorDefault',
        }

        await mutator.insertPropertyOption(boardTree, boardTree.groupByProperty!, option, 'add group')
    }

    private async onDropToColumn(option: IPropertyOption) {
        const {boardTree} = this.props
        const {draggedCards, draggedHeaderOption} = this
        const optionId = option ? option.id : undefined

        Utils.assertValue(boardTree)

        if (draggedCards.length > 0) {
            await mutator.performAsUndoGroup(async () => {
                const description = draggedCards.length > 1 ? `drag ${draggedCards.length} cards` : 'drag card'
                const awaits = []
                for (const draggedCard of draggedCards) {
                    Utils.log(`ondrop. Card: ${draggedCard.title}, column: ${optionId}`)
                    const oldValue = draggedCard.properties[boardTree.groupByProperty!.id]
                    if (optionId !== oldValue) {
                        awaits.push(mutator.changePropertyValue(draggedCard, boardTree.groupByProperty!.id, optionId, description))
                    }
                }
                await Promise.all(awaits)
            })
        } else if (draggedHeaderOption) {
            Utils.log(`ondrop. Header option: ${draggedHeaderOption.value}, column: ${option?.value}`)

            // Move option to new index
            const visibleOptionIds = boardTree.visibleGroups.map((o) => o.option.id)

            const {activeView} = boardTree
            const srcIndex = visibleOptionIds.indexOf(draggedHeaderOption.id)
            const destIndex = visibleOptionIds.indexOf(option.id)

            visibleOptionIds.splice(destIndex, 0, visibleOptionIds.splice(srcIndex, 1)[0])

            await mutator.changeViewVisibleOptionIds(activeView, visibleOptionIds)
        }
    }

    private async onDropToCard(card: Card) {
        Utils.log(`onDropToCard: ${card.title}`)
        const {boardTree} = this.props
        const {activeView} = boardTree
        const {draggedCards} = this
        const optionId = card.properties[activeView.groupById!]

        if (draggedCards.length < 1 || draggedCards.includes(card)) {
            return
        }

        const description = draggedCards.length > 1 ? `drag ${draggedCards.length} cards` : 'drag card'

        // Update card order
        let cardOrder = boardTree.orderedCards().map((o) => o.id)
        const draggedCardIds = draggedCards.map((o) => o.id)
        const firstDraggedCard = draggedCards[0]
        const isDraggingDown = cardOrder.indexOf(firstDraggedCard.id) <= cardOrder.indexOf(card.id)
        cardOrder = cardOrder.filter((id) => !draggedCardIds.includes(id))
        let destIndex = cardOrder.indexOf(card.id)
        if (firstDraggedCard.properties[boardTree.groupByProperty!.id] === optionId && isDraggingDown) {
            // If the cards are in the same column and dragging down, drop after the target card
            destIndex += 1
        }
        cardOrder.splice(destIndex, 0, ...draggedCardIds)

        await mutator.performAsUndoGroup(async () => {
            // Update properties of dragged cards
            const awaits = []
            for (const draggedCard of draggedCards) {
                Utils.log(`draggedCard: ${draggedCard.title}, column: ${optionId}`)
                const oldOptionId = draggedCard.properties[boardTree.groupByProperty!.id]
                if (optionId !== oldOptionId) {
                    awaits.push(mutator.changePropertyValue(draggedCard, boardTree.groupByProperty!.id, optionId, description))
                }
            }
            await Promise.all(awaits)
            await mutator.changeViewCardOrder(activeView, cardOrder, description)
        })
    }

    private async deleteSelectedCards() {
        const {selectedCardIds} = this.state
        if (selectedCardIds.length < 1) {
            return
        }

        mutator.performAsUndoGroup(async () => {
            for (const cardId of selectedCardIds) {
                const card = this.props.boardTree.allCards.find((o) => o.id === cardId)
                if (card) {
                    mutator.deleteBlock(card, selectedCardIds.length > 1 ? `delete ${selectedCardIds.length} cards` : 'delete card')
                } else {
                    Utils.assertFailure(`Selected card not found: ${cardId}`)
                }
            }
        })

        this.setState({selectedCardIds: []})
    }
}

export default injectIntl(BoardComponent)
