Unread =
  init: ->
    return unless g.VIEW is 'thread' and (
      Conf['Unread Count'] or
      Conf['Unread Favicon'] or
      Conf['Unread Line'] or
      Conf['Remember Last Read Post'] or
      Conf['Desktop Notifications'] or
      Conf['Quote Threading']
    )

    if Conf['Remember Last Read Post']
      $.sync 'Remember Last Read Post', (enabled) -> Conf['Remember Last Read Post'] = enabled
      @db = new DataBoard 'lastReadPosts', @sync

    @hr = $.el 'hr',
      id: 'unread-line'
      className: 'unread-line'
    @posts = new Set()
    @postsQuotingYou = new Set()
    @order = new RandomAccessList()
    @position = null

    Callbacks.Thread.push
      name: 'Unread'
      cb:   @node

    Callbacks.Post.push
      name: 'Unread'
      cb:   @addPost

  node: ->
    Unread.thread = @
    Unread.title  = d.title
    Unread.lastReadPost = Unread.db?.get(
      boardID: @board.ID
      threadID: @ID
    ) or 0
    Unread.readCount = 0
    Unread.readCount++ for ID in @posts.keys when +ID <= Unread.lastReadPost
    $.one d, '4chanXInitFinished', Unread.ready
    $.on  d, 'PostsInserted',      Unread.onUpdate
    $.on  d, 'ThreadUpdate',       (e) -> Unread.update() if e.detail[404]
    resetLink = $.el 'a',
      href: 'javascript:;'
      className: 'unread-reset'
      textContent: 'Mark all unread'
    $.on resetLink, 'click', Unread.reset
    Header.menu.addEntry
      el: resetLink
      order: 70

  ready: ->
    Unread.scroll() if Conf['Remember Last Read Post'] and Conf['Scroll to Last Read Post']
    Unread.setLine true
    Unread.read()
    Unread.update()
    $.on  d, 'scroll visibilitychange', Unread.read
    $.on  d, 'visibilitychange',        Unread.setLine if Conf['Unread Line']

  positionPrev: ->
    if Unread.position then Unread.position.prev else Unread.order.last

  scroll: ->
    # Let the header's onload callback handle it.
    return if (hash = location.hash.match /\d+/) and hash[0] of Unread.thread.posts

    position = Unread.positionPrev()
    while position
      {bottom} = position.data.nodes
      if !bottom.getBoundingClientRect().height
        # Don't try to scroll to posts with display: none
        position = position.prev
      else
        Header.scrollToIfNeeded bottom, true
        break
    return

  reset: ->
    return unless Unread.lastReadPost?

    Unread.posts = new Set()
    Unread.postsQuotingYou = new Set()
    Unread.order = new RandomAccessList()
    Unread.position = null
    Unread.lastReadPost = 0
    Unread.readCount = 0
    Unread.thread.posts.forEach (post) -> Unread.addPost.call post

    $.forceSync 'Remember Last Read Post'
    if Conf['Remember Last Read Post'] and (!Unread.thread.isDead or Unread.thread.isArchived)
      Unread.db.set
        boardID:  Unread.thread.board.ID
        threadID: Unread.thread.ID
        val:      0

    Unread.updatePosition()
    Unread.setLine()
    Unread.update()

  sync: ->
    return unless Unread.lastReadPost?
    lastReadPost = Unread.db.get
      boardID: Unread.thread.board.ID
      threadID: Unread.thread.ID
      defaultValue: 0
    return unless Unread.lastReadPost < lastReadPost
    Unread.lastReadPost = lastReadPost

    postIDs = Unread.thread.posts.keys
    for i in [Unread.readCount...postIDs.length] by 1
      ID = +postIDs[i]
      unless Unread.thread.posts.get(ID).isFetchedQuote
        break if ID > Unread.lastReadPost
        Unread.posts.delete ID
        Unread.postsQuotingYou.delete ID
      Unread.readCount++

    Unread.updatePosition()
    Unread.setLine()
    Unread.update()

  addPost: ->
    return if @isFetchedQuote or @isClone
    Unread.order.push @
    return if @ID <= Unread.lastReadPost or @isHidden or QuoteYou.isYou(@)
    Unread.posts.add (Unread.posts.last = @ID)
    Unread.addPostQuotingYou @
    Unread.position ?= Unread.order[@ID]

  addPostQuotingYou: (post) ->
    for quotelink in post.nodes.quotelinks when QuoteYou.db?.get Get.postDataFromLink quotelink
      Unread.postsQuotingYou.add (Unread.postsQuotingYou.last = post.ID)
      Unread.openNotification post
      return

  openNotification: (post, predicate=' replied to you') ->
    return unless Header.areNotificationsEnabled
    notif = new Notification "#{post.info.nameBlock}#{predicate}",
      body: post.commentDisplay()
      icon: Favicon.logo
    notif.onclick = ->
      Header.scrollToIfNeeded post.nodes.bottom, true
      window.focus()
    notif.onshow = ->
      setTimeout ->
        notif.close()
      , 7 * $.SECOND

  onUpdate: ->
    $.queueTask -> # ThreadUpdater may scroll immediately after inserting posts
      Unread.setLine()
      Unread.read()
      Unread.update()

  readSinglePost: (post) ->
    {ID} = post
    return unless Unread.posts.has ID
    Unread.posts.delete ID
    Unread.postsQuotingYou.delete ID
    Unread.updatePosition()
    Unread.saveLastReadPost()
    Unread.update()

  read: $.debounce 100, (e) ->
    # Update the lastReadPost when hidden posts are added to the thread.
    if !Unread.posts.size and Unread.readCount isnt Unread.thread.posts.keys.length
      Unread.saveLastReadPost()

    return if d.hidden or !Unread.posts.size

    count = 0
    while Unread.position
      {ID, data} = Unread.position
      {bottom} = data.nodes
      break unless !bottom.getBoundingClientRect().height or # post has been hidden
        Header.getBottomOf(bottom) > -1                      # post is completely read
      count++
      Unread.posts.delete ID
      Unread.postsQuotingYou.delete ID
      Unread.position = Unread.position.next

    return unless count
    Unread.updatePosition()
    Unread.saveLastReadPost()
    (Unread.update() if e)

  updatePosition: ->
    while Unread.position and !Unread.posts.has Unread.position.ID
      Unread.position = Unread.position.next
    return

  saveLastReadPost: $.debounce 2 * $.SECOND, ->
    $.forceSync 'Remember Last Read Post'
    return unless Conf['Remember Last Read Post'] and Unread.db
    postIDs = Unread.thread.posts.keys
    for i in [Unread.readCount...postIDs.length] by 1
      ID = +postIDs[i]
      unless Unread.thread.posts.get(ID).isFetchedQuote
        break if Unread.posts.has ID
        Unread.lastReadPost = ID
      Unread.readCount++
    return if Unread.thread.isDead and !Unread.thread.isArchived
    Unread.db.set
      boardID:  Unread.thread.board.ID
      threadID: Unread.thread.ID
      val:      Unread.lastReadPost

  setLine: (force) ->
    return unless Conf['Unread Line']
    if Unread.hr.hidden or d.hidden or (force is true)
      oldPosition = Unread.linePosition
      if (Unread.linePosition = Unread.positionPrev())
        if Unread.linePosition isnt oldPosition
          node = Unread.linePosition.data.nodes.bottom
          node = node.nextSibling if node.nextSibling?.tagName is 'BR'
          $.after node, Unread.hr
      else
        $.rm Unread.hr
    Unread.hr.hidden = Unread.linePosition is Unread.order.last

  update: ->
    count = Unread.posts.size
    countQuotingYou = Unread.postsQuotingYou.size

    if Conf['Unread Count']
      titleQuotingYou = if Conf['Quoted Title'] and countQuotingYou then '(!) ' else ''
      titleCount = if count or !Conf['Hide Unread Count at (0)'] then "(#{count}) " else ''
      titleDead = if Unread.thread.isDead
        Unread.title.replace '-', (if Unread.thread.isArchived then '- Archived -' else '- 404 -')
      else
        Unread.title
      d.title = "#{titleQuotingYou}#{titleCount}#{titleDead}"

    Unread.saveThreadWatcherCount()

    if Conf['Unread Favicon'] and g.SITE.software is 'yotsuba'
      {isDead} = Unread.thread
      Favicon.el.href =
        if countQuotingYou
          Favicon[if isDead then 'unreadDeadY' else 'unreadY']
        else if count
          Favicon[if isDead then 'unreadDead' else 'unread']
        else
          Favicon[if isDead then 'dead' else 'default']
      # `favicon.href = href` doesn't work on Firefox.
      $.add d.head, Favicon.el

  saveThreadWatcherCount: $.debounce 2 * $.SECOND, ->
    $.forceSync 'Remember Last Read Post'
    if Conf['Remember Last Read Post'] and (!Unread.thread.isDead or Unread.thread.isArchived)
      quotingYou = if !Conf['Require OP Quote Link'] and QuoteYou.isYou(Unread.thread.OP) then Unread.posts else Unread.postsQuotingYou
      if !quotingYou.size
        quotingYou.last = 0
      else if !quotingYou.has(quotingYou.last)
        quotingYou.last = 0
        posts = Unread.thread.posts.keys
        for i in [posts.length - 1 .. 0] by -1
          if quotingYou.has(+posts[i])
            quotingYou.last = posts[i]
            break
      ThreadWatcher.update g.SITE.ID, Unread.thread.board.ID, Unread.thread.ID,
        last: Unread.thread.lastPost
        isDead: Unread.thread.isDead
        isArchived: Unread.thread.isArchived
        unread: Unread.posts.size
        quotingYou: (quotingYou.last or 0)
