_ = require 'underscore'
React = require 'react'
ReactDOM = require 'react-dom'
classNames = require 'classnames'
FindInThread = require('./find-in-thread').default
MessageItemContainer = require './message-item-container'

{Utils,
 Actions,
 Message,
 DraftStore,
 MessageStore,
 AccountStore,
 DatabaseStore,
 WorkspaceStore,
 ChangeLabelsTask,
 ComponentRegistry,
 SearchableComponentStore
 SearchableComponentMaker} = require("nylas-exports")

{Spinner,
 RetinaImg,
 MailLabelSet,
 ScrollRegion,
 MailImportantIcon,
 InjectedComponent,
 KeyCommandsRegion,
 InjectedComponentSet} = require('nylas-component-kit')

class MessageListScrollTooltip extends React.Component
  @displayName: 'MessageListScrollTooltip'
  @propTypes:
    viewportCenter: React.PropTypes.number.isRequired
    totalHeight: React.PropTypes.number.isRequired

  componentWillMount: =>
    @setupForProps(@props)

  componentWillReceiveProps: (newProps) =>
    @setupForProps(newProps)

  shouldComponentUpdate: (newProps, newState) =>
    not _.isEqual(@state,newState)

  setupForProps: (props) ->
    # Technically, we could have MessageList provide the currently visible
    # item index, but the DOM approach is simple and self-contained.
    #
    els = document.querySelectorAll('.message-item-wrap')
    idx = _.findIndex els, (el) -> el.offsetTop > props.viewportCenter
    if idx is -1
      idx = els.length

    @setState
      idx: idx
      count: els.length

  render: ->
    <div className="scroll-tooltip">
      {@state.idx} of {@state.count}
    </div>

class MessageList extends React.Component
  @displayName: 'MessageList'
  @containerRequired: false
  @containerStyles:
    minWidth: 500
    maxWidth: 999999

  constructor: (@props) ->
    @state = @_getStateFromStores()
    @state.minified = true
    @_draftScrollInProgress = false
    @MINIFY_THRESHOLD = 3

  componentDidMount: =>
    @_unsubscribers = []
    @_unsubscribers.push MessageStore.listen @_onChange
    @_unsubscribers.push Actions.focusDraft.listen ({draftClientId}) =>
      Utils.waitFor( => @_getMessageContainer(draftClientId)?).then =>
        @_focusDraft(@_getMessageContainer(draftClientId))
      .catch =>

  componentWillUnmount: =>
    unsubscribe() for unsubscribe in @_unsubscribers

  shouldComponentUpdate: (nextProps, nextState) =>
    not Utils.isEqualReact(nextProps, @props) or
    not Utils.isEqualReact(nextState, @state)

  componentDidUpdate: (prevProps, prevState) =>

  _globalMenuItems: ->
    toggleExpandedLabel = if @state.hasCollapsedItems then "Expand" else "Collapse"
    [
      {
        "label": "Thread",
        "submenu": [{
          "label": "#{toggleExpandedLabel} conversation",
          "command": "message-list:toggle-expanded",
          "position": "endof=view-actions",
        }]
      }
    ]

  _globalKeymapHandlers: ->
    handlers =
      'core:reply': =>
        Actions.composeReply({
          thread: @state.currentThread,
          message: @_lastMessage(),
          type: 'reply',
          behavior: 'prefer-existing',
        })
      'core:reply-all': =>
        Actions.composeReply({
          thread: @state.currentThread,
          message: @_lastMessage(),
          type: 'reply-all',
          behavior: 'prefer-existing',
        })
      'core:forward': => @_onForward()
      'core:print-thread': => @_onPrintThread()
      'core:messages-page-up': => @_onScrollByPage(-1)
      'core:messages-page-down': => @_onScrollByPage(1)

    if @state.canCollapse
      handlers['message-list:toggle-expanded'] = => @_onToggleAllMessagesExpanded()

    handlers

  _getMessageContainer: (clientId) =>
    @refs["message-container-#{clientId}"]

  _focusDraft: (draftElement) =>
    # Note: We don't want the contenteditable view competing for scroll offset,
    # so we block incoming childScrollRequests while we scroll to the new draft.
    @_draftScrollInProgress = true
    draftElement.focus()
    @refs.messageWrap.scrollTo(draftElement, {
      position: ScrollRegion.ScrollPosition.Top,
      settle: true,
      done: =>
        @_draftScrollInProgress = false
    })

  _onForward: =>
    return unless @state.currentThread
    Actions.composeForward(thread: @state.currentThread)

  render: =>
    if not @state.currentThread
      return <span />

    wrapClass = classNames
      "messages-wrap": true
      "ready": not @state.loading

    messageListClass = classNames
      "message-list": true
      "height-fix": SearchableComponentStore.searchTerm isnt null

    <KeyCommandsRegion
      globalHandlers={@_globalKeymapHandlers()}
      globalMenuItems={@_globalMenuItems()}>
      <FindInThread ref="findInThread" />
      <div className={messageListClass} id="message-list">
        <ScrollRegion tabIndex="-1"
             className={wrapClass}
             scrollbarTickProvider={SearchableComponentStore}
             scrollTooltipComponent={MessageListScrollTooltip}
             ref="messageWrap">
          {@_renderSubject()}
          <div className="headers" style={position:'relative'}>
            <InjectedComponentSet
              className="message-list-headers"
              matching={{role: "MessageListHeaders"}}
              exposedProps={{thread: @state.currentThread, messages: @state.messages}}
              direction="column"
            />
          </div>
          {@_messageElements()}
        </ScrollRegion>
        <Spinner visible={@state.loading} />
      </div>
    </KeyCommandsRegion>

  _renderSubject: ->
    subject = @state.currentThread.subject
    subject = "(No Subject)" if not subject or subject.length is 0

    <div className="message-subject-wrap">
      <MailImportantIcon thread={@state.currentThread}/>
      <div style={flex: 1}>
        <span className="message-subject">{subject}</span>
        <MailLabelSet
          removable={true}
          messages={@state.messages}
          thread={@state.currentThread}
          includeCurrentCategories={true}
        />
      </div>
      {@_renderIcons()}
    </div>

  _renderIcons: =>
    <div className="message-icons-wrap">
      {@_renderExpandToggle()}
      <div onClick={@_onPrintThread}>
        <RetinaImg name="print.png" title="Print Thread" mode={RetinaImg.Mode.ContentIsMask}/>
      </div>
      {@_renderPopoutToggle()}
    </div>

  _renderExpandToggle: =>
    return <span/> unless @state.canCollapse

    if @state.hasCollapsedItems
      <div onClick={@_onToggleAllMessagesExpanded}>
        <RetinaImg name={"expand.png"} title={"Expand All"} mode={RetinaImg.Mode.ContentIsMask}/>
      </div>
    else
      <div onClick={@_onToggleAllMessagesExpanded}>
        <RetinaImg name={"collapse.png"} title={"Collapse All"} mode={RetinaImg.Mode.ContentIsMask}/>
      </div>

  _renderPopoutToggle: =>
    if NylasEnv.isThreadWindow()
      <div onClick={@_onPopThreadIn}>
        <RetinaImg name="thread-popin.png" title="Pop thread in" mode={RetinaImg.Mode.ContentIsMask}/>
      </div>
    else
      <div onClick={@_onPopoutThread}>
        <RetinaImg name="thread-popout.png" title="Popout thread" mode={RetinaImg.Mode.ContentIsMask}/>
      </div>


  _renderReplyArea: =>
    <div className="footer-reply-area-wrap" onClick={@_onClickReplyArea} key='reply-area'>
      <div className="footer-reply-area">
        <RetinaImg name="#{@_replyType()}-footer.png" mode={RetinaImg.Mode.ContentIsMask}/>
        <span className="reply-text">Write a reply…</span>
      </div>
    </div>

  _lastMessage: =>
    _.last(_.filter((@state.messages ? []), (m) -> not m.draft))

  # Returns either "reply" or "reply-all"
  _replyType: =>
    defaultReplyType = NylasEnv.config.get('core.sending.defaultReplyType')
    lastMessage = @_lastMessage()
    return 'reply' unless lastMessage

    if lastMessage.canReplyAll()
      if defaultReplyType is 'reply-all'
        return 'reply-all'
      else
        return 'reply'
    else
      return 'reply'

  _onToggleAllMessagesExpanded: ->
    Actions.toggleAllMessagesExpanded()

  _onPrintThread: =>
    node = ReactDOM.findDOMNode(@)
    Actions.printThread(@state.currentThread, node.innerHTML)

  _onPopThreadIn: =>
    return unless @state.currentThread
    Actions.focusThreadMainWindow(@state.currentThread)
    NylasEnv.close()

  _onPopoutThread: =>
    return unless @state.currentThread
    Actions.popoutThread(@state.currentThread)
    # This returns the single-pane view to the inbox, and does nothing for
    # double-pane view because we're at the root sheet.
    Actions.popSheet()

  _onClickReplyArea: =>
    return unless @state.currentThread
    Actions.composeReply({
      thread: @state.currentThread,
      message: @_lastMessage(),
      type: @_replyType(),
      behavior: 'prefer-existing-if-pristine',
    })

  _messageElements: =>
    elements = []

    hasReplyArea = not _.last(@state.messages)?.draft
    messages = @_messagesWithMinification(@state.messages)
    messages.forEach (message, idx) =>

      if message.type is "minifiedBundle"
        elements.push(@_renderMinifiedBundle(message))
        return

      collapsed = !@state.messagesExpandedState[message.id]
      isLastMsg = (messages.length - 1 is idx)
      isBeforeReplyArea = isLastMsg and hasReplyArea

      elements.push(
        <MessageItemContainer
          key={message.clientId}
          ref={"message-container-#{message.clientId}"}
          thread={@state.currentThread}
          message={message}
          messages={@state.messages}
          collapsed={collapsed}
          isLastMsg={isLastMsg}
          isBeforeReplyArea={isBeforeReplyArea}
          scrollTo={@_scrollTo}
          onLoad={@_onMessageLoaded}
        />
      )

    if hasReplyArea
      elements.push(@_renderReplyArea())

    return elements

  _renderMinifiedBundle: (bundle) ->
    BUNDLE_HEIGHT = 36
    lines = bundle.messages[0...10]
    h = Math.round(BUNDLE_HEIGHT / lines.length)

    <div className="minified-bundle"
         onClick={ => @setState minified: false }
         key={Utils.generateTempId()}>
      <div className="num-messages">{bundle.messages.length} older messages</div>
      <div className="msg-lines" style={height: h*lines.length}>
        {lines.map (msg, i) ->
          <div key={msg.id} style={height: h*2, top: -h*i} className="msg-line"></div>}
      </div>
    </div>

  _messagesWithMinification: (messages=[]) =>
    return messages unless @state.minified

    messages = _.clone(messages)
    minifyRanges = []
    consecutiveCollapsed = 0

    messages.forEach (message, idx) =>
      return if idx is 0 # Never minify the 1st message

      expandState = @state.messagesExpandedState[message.id]

      if not expandState
        consecutiveCollapsed += 1
      else
        # We add a +1 because we don't minify the last collapsed message,
        # but the MINIFY_THRESHOLD refers to the smallest N that can be in
        # the "N older messages" minified block.
        if expandState is "default"
          minifyOffset = 1
        else # if expandState is "explicit"
          minifyOffset = 0

        if consecutiveCollapsed >= @MINIFY_THRESHOLD + minifyOffset
          minifyRanges.push
            start: idx - consecutiveCollapsed
            length: (consecutiveCollapsed - minifyOffset)
        consecutiveCollapsed = 0

    indexOffset = 0
    for range in minifyRanges
      start = range.start - indexOffset
      minified =
        type: "minifiedBundle"
        messages: messages[start...(start+range.length)]
      messages.splice(start, range.length, minified)

      # While we removed `range.length` items, we also added 1 back in.
      indexOffset += (range.length - 1)

    return messages

  # Some child components (like the composer) might request that we scroll
  # to a given location. If `selectionTop` is defined that means we should
  # scroll to that absolute position.
  #
  # If messageId and location are defined, that means we want to scroll
  # smoothly to the top of a particular message.
  _scrollTo: ({clientId, rect, position}={}) =>
    return if @_draftScrollInProgress
    if clientId
      messageElement = @_getMessageContainer(clientId)
      return unless messageElement
      pos = position ? ScrollRegion.ScrollPosition.Visible
      @refs.messageWrap.scrollTo(messageElement, {
        position: pos
      })
    else if rect
      @refs.messageWrap.scrollToRect(rect, {
        position: ScrollRegion.ScrollPosition.CenterIfInvisible
      })
    else
      throw new Error("onChildScrollRequest: expected clientId or rect")

  _onMessageLoaded: =>
    if @state.currentThread
      timerKey = "select-thread-#{@state.currentThread.id}"
      if NylasEnv.timer.isPending(timerKey)
        actionTimeMs = NylasEnv.timer.stop(timerKey)
        messageCount = (@state.messages || []).length
        Actions.recordPerfMetric({
          sample: 0.1,
          action: 'select-thread',
          actionTimeMs,
          messageCount,
        })

  _onScrollByPage: (direction) =>
    height = ReactDOM.findDOMNode(@refs.messageWrap).clientHeight
    @refs.messageWrap.scrollTop += height * direction

  _onChange: =>
    newState = @_getStateFromStores()
    if @state.currentThread?.id isnt newState.currentThread?.id
      newState.minified = true
    @setState(newState)

  _getStateFromStores: =>
    messages: (MessageStore.items() ? [])
    messagesExpandedState: MessageStore.itemsExpandedState()
    canCollapse: MessageStore.items().length > 1
    hasCollapsedItems: MessageStore.hasCollapsedItems()
    currentThread: MessageStore.thread()
    loading: MessageStore.itemsLoading()

module.exports = SearchableComponentMaker.extend(MessageList)
