import React, { useState, useRef } from 'react'
import { Plus, X, MoreHorizontal } from 'react-feather'
import { Popover } from 'react-tiny-popover'
import classnames from 'classnames'
import { DragDropContext, Droppable, Draggable } from 'react-beautiful-dnd'

import { useQuery } from '@wasp/queries'
import getListsAndCards from '@wasp/queries/getListsAndCards'
import createList from '@wasp/actions/createList'
import updateList from '@wasp/actions/updateList'
import deleteList from '@wasp/actions/deleteList'

import createCard from '@wasp/actions/createCard'
import updateCard from '@wasp/actions/updateCard'

import UserPageLayout from './UserPageLayout'

import waspLogo from './waspLogo.png'
import './Main.css'


const DND_ITEM_POS_SPACING = 2 ** 16

// It is expected that each item has .pos property.
const calcNewDndItemPos = (items) => {
  if (!Array.isArray(items) || items.length === 0) return DND_ITEM_POS_SPACING - 1

  return Math.max(...items.map(l => l.pos)) + DND_ITEM_POS_SPACING
}

// It is assummed that items are sorted by pos, ascending.
const calcNewPosOfDndItemMovedWithinList = (items, srcIdx, destIdx) => {
  if (srcIdx === destIdx) return items[srcIdx].pos
  if (destIdx === 0) return (items[0].pos / 2)
  if (destIdx === items.length - 1) return items[items.length - 1].pos + DND_ITEM_POS_SPACING

  if (destIdx > srcIdx) return (items[destIdx].pos + items[destIdx + 1].pos) / 2
  if (destIdx < srcIdx) return (items[destIdx - 1].pos + items[destIdx].pos) / 2
}

// It is assummed that items is sorted by pos, ascending.
const calcNewPosOfDndItemInsertedInAnotherList = (items, destIdx) => {
  if (items.length === 0) return DND_ITEM_POS_SPACING - 1
  if (destIdx === 0) return (items[0].pos / 2)
  if (destIdx === items.length) return items[items.length - 1].pos + DND_ITEM_POS_SPACING

  return (items[destIdx - 1].pos + items[destIdx].pos) / 2
}

const createListIdToSortedCardsMap = (listsAndCards) => {
  const listIdToSortedCardsMap = {}

  listsAndCards.forEach(list => {
    listIdToSortedCardsMap[list.id] = [...list.cards].sort((a, b) => a.pos - b.pos)
  })
  
  return listIdToSortedCardsMap
}

const MainPage = ({ user }) => {
  const { data: listsAndCards, isFetchingListsAndCards, errorListsAndCards }
    = useQuery(getListsAndCards)

  // NOTE(matija): this is only a shallow copy.
  const listsSortedByPos = listsAndCards && [...listsAndCards].sort((a, b) => a.pos - b.pos)

  // Create a map with listId -> cards sorted by pos.
  const listIdToSortedCardsMap = listsAndCards && createListIdToSortedCardsMap(listsAndCards)

  const onDragEnd = async (result) => {
    // Item was dropped outside of the droppable area.
    if (!result.destination) {
      return
    }

    // TODO(matija): make an enum for type strings (BOARD, CARD).
    if (result.type === 'BOARD') {
      const newPos =
        calcNewPosOfDndItemMovedWithinList(
          listsSortedByPos, result.source.index, result.destination.index
        )

      try {
        const movedListId = listsSortedByPos[result.source.index].id
        await updateList({ listId: movedListId, data: { pos: newPos } })
      } catch (err) {
        window.alert('Error while updating list position: ' + err.message)
      }
    } else if (result.type === 'CARD') {
      const sourceListId = result.source.droppableId
      const destListId = result.destination.droppableId
      // TODO(matija): this is not the nicest solution, we should have a consistent naming system
      // for draggable ids (for lists we put prefix in the id, while for cards we use 
      // their db id directly, because that saves us a bit of work in the further code.
      //
      // NOTE(matija): All draggable ids must be unique, even if they belong to different
      // droppable areas. This is why for lists we didn't use a db id directly, because it would
      // overlap with card ids. And for cards it was handy to have db id as draggable id, because
      // then we can easily access the data for the specific card.
      const movedCardId = Number(result.draggableId)

      const destListCardsSortedByPos = listIdToSortedCardsMap[destListId]

      let newPos = undefined
      if (sourceListId === destListId) { // Card got moved within the same list.
        newPos = calcNewPosOfDndItemMovedWithinList(
          destListCardsSortedByPos, result.source.index, result.destination.index
        )
      } else { // Card got inserted from another list.
        newPos = calcNewPosOfDndItemInsertedInAnotherList(
          destListCardsSortedByPos, result.destination.index
        )
      }

      try {
        await updateCard({ cardId: movedCardId, data: { pos: newPos, listId: destListId } })
      } catch (err) {
        window.alert('Error while updating card position: ' + err.message)
      }
    } else {
      // TODO(matija): throw error.
    }
  }

  return (
    <UserPageLayout user={user}>
      <div className='board-header'>
        <div className='board-name'>
          <h1 className='board-header-text'>Your board</h1>
        </div>
      </div>

      <DragDropContext onDragEnd={onDragEnd}>
        <Droppable droppableId="board" direction="horizontal" type="BOARD" >
          {(provided, snapshot) => (
            <div id='board' className='u-fancy-scrollbar'
              ref={provided.innerRef}
              {...provided.droppableProps}
            >
              { listsSortedByPos && listIdToSortedCardsMap &&
                <Lists
                  lists={listsSortedByPos}
                  listIdToCardsMap={listIdToSortedCardsMap}
                /> 
              }
              {provided.placeholder}
              <AddList newPos={calcNewDndItemPos(listsAndCards)} />
            </div>
          )}
        </Droppable>
      </DragDropContext>

    </UserPageLayout>
  )
}

const Lists = ({ lists, listIdToCardsMap }) => {
    // TODO(matija): what if some of the props is empty? Although we make sure not to add it
    // to DOM in that case.

    return lists.map((list, index) => {
      return (
        <List list={list} key={list.id} index={index}
          cards={listIdToCardsMap[list.id]}
        />
      )
    }) 
}

const List = ({ list, index, cards }) => {
  const [isPopoverOpen, setIsPopoverOpen] = useState(false)

  const handleListNameUpdated = async (listId, newName) => {
    try {
      await updateList({ listId, data: { name: newName } })
    } catch (err) {
      window.alert('Error while updating list name: ' + err.message)
    }
  }

  const handleDeleteList = async (listId) => {
    try {
      await deleteList({ listId })
    } catch (err) {
      window.alert('Error while deleting list: ' + err.message)
    }
    setIsPopoverOpen(false)
  }

  const ListMenu = () => {
    return (
      <div className='popover-menu'>
        <div className='popover-header'>
          <div className='popover-header-item'>
            <button className='popover-header-close-btn dark-hover fake-invisible-item'>
              <X size={16}/>
            </button>
          </div>
          <span className='popover-header-title popover-header-item'>List&nbsp;actions</span>
          <div className='popover-header-item'>
            <button
              className='popover-header-close-btn dark-hover'
              onClick={() => setIsPopoverOpen(false)}
            >
              <X size={16}/>
            </button>
          </div>
        </div>
        <div className='popover-content'>
          <ul className='popover-content-list'>
            <li><button>Add card...</button></li>
            <li><button>Copy list...</button></li>
            <li>
              <button onClick={() => handleDeleteList(list.id)}>
                Delete this list
              </button>
            </li>
          </ul>
        </div>
      </div>
    )
  }

  return (
    <Draggable
      key={list.id}
      draggableId={`listDraggable-${list.id}`}
      index={index}
    >
      {(provided, snapshot) => (
        <div className='list-wrapper'
          ref={provided.innerRef}
          {...provided.draggableProps}
          {...provided.dragHandleProps}
        >
          <div className='list'>
            <div className='list-header'>
              <textarea
                className='list-header-name mod-list-name'
                onBlur={(e) => handleListNameUpdated(list.id, e.target.value)}
                defaultValue={ list.name }
              />
              <div className='list-header-extras'>
                <Popover
                  isOpen={isPopoverOpen}
                  onClickOutside={() => setIsPopoverOpen(false)}
                  positions={['bottom', 'right', 'left']}
                  align='start'
                  padding={6}
                  content={<ListMenu/>}
                >
                  <div
                    className='list-header-extras-menu dark-hover'
                    onClick={() => setIsPopoverOpen(!isPopoverOpen)}
                  >
                    <MoreHorizontal size={16}/>
                  </div>
                </Popover>
              </div>
            </div> {/* eof list-header */}

            <Droppable
              droppableId={`${list.id}`}
              direction="vertical"
              type="CARD"
            >
              {(provided, snapshot) => (
                <div className='cards'
                  ref={provided.innerRef}
                  {...provided.droppableProps}
                >
                  { cards && <Cards cards={cards} /> }
                  {provided.placeholder}
                </div>
              )}
            </Droppable>

            <div className='card-composer-container'>
              <AddCard listId={list.id} newPos={calcNewDndItemPos(cards)} />
            </div>
          </div>
        </div>
      )}
    </Draggable>
  )
}

const Cards = ({ cards }) => {
  return (
    <div className='list-cards'>
      { cards.map((card, index) => <Card card={card} key={card.id} index={index} />) }
    </div>
  )
}

const Card = ({ card, index }) => {
  return (
    <Draggable
      key={card.id}
      draggableId={`${card.id}`}
      index={index}
    >
      {(provided, snapshot) => (
        <div className='list-card'
          ref={provided.innerRef}
          {...provided.draggableProps}
          {...provided.dragHandleProps}
        >
          <span className='list-card-title'>{ card.title }</span>
        </div>
      )}
    </Draggable>
  )
}

const AddList = ({ newPos }) => {
  const [isInEditMode, setIsInEditMode] = useState(false)

  const AddListButton = () => {
    return (
      <button
        className='open-add-list'
        onClick={() => setIsInEditMode(true)}
      >
          <div className='add-icon'>
            <Plus size={16} strokeWidth={2} />
          </div>
          Add a list
      </button>
    )
  }

  const AddListInput = () => {
    const handleAddList = async (event) => {
      event.preventDefault()
      try {
        const listName = event.target.listName.value
        event.target.reset()
        await createList({ name: listName, pos: newPos })
      } catch (err) {
        window.alert('Error: ' + err.message)
      }
    }

    return (
      <form onSubmit={handleAddList}>
        <input
          className='list-name-input'
          autoFocus
          name='listName'
          type='text'
          defaultValue=''
          placeholder='Enter list title...'
        />
        <div className='list-add-controls'>
          <input className='list-add-button' type='submit' value='Add list' />
          <div
            className='list-cancel-edit'
            onClick={() => setIsInEditMode(false)}
          >
            <X/>
          </div>
        </div>
      </form>
    )
  }

  return (
    <div
      className={classnames(
        'add-list', 'list-wrapper', 'mod-add', { 'is-idle': !isInEditMode }
      )}
    >
      { isInEditMode ? <AddListInput /> : <AddListButton /> }
    </div>
  )
}

const AddCard = ({ listId, newPos }) => {
  const [isInEditMode, setIsInEditMode] = useState(false)

  const AddCardButton = () => {
    return (
      <button
        className='open-card-composer dark-hover'
        onClick={() => setIsInEditMode(true)}
      >
        <div className='add-icon'>
          <Plus size={16} strokeWidth={2} />
        </div>
        Add a card
      </button>
    )
  }

  const AddCardInput = ({ listId }) => {
    const formRef = useRef(null)

    const submitOnEnter = (e) => {
      if (e.keyCode === 13 /* && e.shiftKey == false */) {
        e.preventDefault()

        formRef.current.dispatchEvent(
          new Event('submit', { cancelable: true, bubbles: true })
        )
      }
    }

    const handleAddCard = async (event, listId) => {
      event.preventDefault()
      try {
        const cardTitle = event.target.cardTitle.value
        event.target.reset()
        await createCard({ title: cardTitle, pos: newPos, listId })
      } catch (err) {
        window.alert('Error: ' + err.message)
      }
    }

    return (
      <form className='card-composer' ref={formRef} onSubmit={(e) => handleAddCard(e, listId)}>
        <div className='list-card'>
          <textarea
            className='card-composer-textarea'
            onKeyDown={submitOnEnter}
            autoFocus
            name='cardTitle'
            placeholder='Enter a title for this card...'
          />
        </div>
        <div className='card-add-controls'>
          <input className='card-add-button' type='submit' value='Add card' />
          <div
            className='card-cancel-edit'
            onClick={() => setIsInEditMode(false)}
          >
            <X/>
          </div>
        </div>
      </form>
    )
  }
  return (
    <div>
      { isInEditMode ? <AddCardInput listId={listId} /> : <AddCardButton /> }
    </div>
  )
}

export default MainPage