 # TaskList updates trigger `tasklist:change` events. If the change is
 # successful, `tasklist:changed` is fired. The change can be canceled.
 #
-# jQuery is required.
-#
 # ### Methods
 #
 # `.taskList('enable')` or `.taskList()`

# TaskList Behavior
#
#= provides tasklist:enabled
#= provides tasklist:disabled
#= provides tasklist:change
#= provides tasklist:changed
#
#= require jquery
#
# Enables Task List update behavior.
#

# Task list checkboxes are rendered as disabled by default because rendered
# user content is cached without regard for the viewer.

NodeArray = (nodeList) -> Array.prototype.slice.apply(nodeList)

closest = (el, className) ->
  while el && !el.classList.contains className
    el = el.parentNode
  el

class TaskList
  constructor: (@el, method) ->
    @container = closest @el, 'js-task-list-container'
    @field = @container.querySelector '.js-task-list-field'
    # When the task list item checkbox is updated, submit the change
    @container.addEventListener 'change', (event) =>
      if event.target.classList.contains 'task-list-item-checkbox'
        @updateTaskList(event.target)

  enable: ->
    if @container.querySelectorAll('.js-task-list-field').length > 0
      NodeArray(@container.querySelectorAll('.task-list-item')).
      forEach (item) ->
        item.classList.add('enabled')
      NodeArray(@container.querySelectorAll('.task-list-item-checkbox')).
      forEach (checkbox) ->
        checkbox.disabled = false
      @container.classList.add 'is-task-list-enabled'
      event = new Event 'tasklist:enabled',
        cancelable: true
        bubbles: true
      @container.dispatchEvent event

  disable: ->
    NodeArray(@container.querySelectorAll('.task-list-item')).
    forEach (item) ->
      item.classList.remove('enabled')
    NodeArray(@container.querySelectorAll('.task-list-item-checkbox')).
    forEach (checkbox) ->
      checkbox.disabled = true
    @container.classList.remove('is-task-list-enabled')
    event = new Event 'tasklist:disabled',
      cancelable: true
      bubbles: true
    @container.dispatchEvent event

  # Updates the field value to reflect the state of item.
  # Triggers the `tasklist:change` event before the value has changed, and fires
  # a `tasklist:changed` event once the value has changed.
  updateTaskList: (item) ->
    checkboxes = @container.querySelectorAll('.task-list-item-checkbox')
    index = 1 + NodeArray(checkboxes).indexOf item

    changeEvent = new Event 'tasklist:change',
      cancelable: true
      bubbles: true
    changeEvent.detail =
      index: index
      checked: item.checked
    @field.dispatchEvent changeEvent

    unless changeEvent.defaultPrevented
      @field.value = TaskList.updateSource(@field.value, index, item.checked)
      changeEvent = new Event 'change',
        cancelable: true
        bubbles: true
      @field.dispatchEvent changeEvent
      changedEvent = new Event 'tasklist:changed',
        cancelable: true
        bubbles: true
      changedEvent.detail =
        index: index
        checked: item.checked
      @field.dispatchEvent changedEvent

  # Static interface

  @incomplete: "[ ]"
  @complete: "[x]"

  # Escapes the String for regular expression matching.
  @escapePattern: (str) ->
    str.
      replace(/([\[\]])/g, "\\$1"). # escape square brackets
      replace(/\s/, "\\s").         # match all white space
      replace("x", "[xX]")          # match all cases

  @incompletePattern: ///
    #{@escapePattern(@incomplete)}
  ///
  @completePattern: ///
    #{@escapePattern(@complete)}
  ///

  # Pattern used to identify all task list items.
  # Useful when you need iterate over all items.
  @itemPattern: ///
    ^
    (?:                     # prefix, consisting of
      \s*                   # optional leading whitespace
      (?:>\s*)*             # zero or more blockquotes
      (?:[-+*]|(?:\d+\.))   # list item indicator
    )
    \s*                     # optional whitespace prefix
    (                       # checkbox
      #{@escapePattern(@complete)}|
      #{@escapePattern(@incomplete)}
    )
    \s+                     # is followed by whitespace
    (?!
      \(.*?\)               # is not part of a [foo](url) link
    )
    (?=                     # and is followed by zero or more links
      (?:\[.*?\]\s*(?:\[.*?\]|\(.*?\))\s*)*
      (?:[^\[]|$)           # and either a non-link or the end of the string
    )
  ///

  # Used to skip checkbox markup inside of code fences.
  # http://rubular.com/r/TfCDNsy8x4
  @startFencesPattern: /^`{3}.*$/
  @endFencesPattern: /^`{3}$/

  # Used to filter out potential mismatches (items not in lists).
  # http://rubular.com/r/OInl6CiePy
  @itemsInParasPattern: ///
    ^
    (
      #{@escapePattern(@complete)}|
      #{@escapePattern(@incomplete)}
    )
    .+
    $
  ///g

  # Given the source text, updates the appropriate task list item to match the
  # given checked value.
  #
  # Returns the updated String text.
  @updateSource: (source, itemIndex, checked) ->
    clean = source.replace(/\r/g, '').
      replace(@itemsInParasPattern, '').
      split("\n")
    index = 0
    inCodeBlock = false
    result = for line in source.split("\n")
      if inCodeBlock
        # Lines inside of a code block are ignored.
        if line.match(@endFencesPattern)
          # Stop ignoring lines once the code block is closed.
          inCodeBlock = false
      else if line.match(@startFencesPattern)
        # Start ignoring lines inside a code block.
        inCodeBlock = true
      else if line in clean && line.match(@itemPattern)
        index += 1
        if index == itemIndex
          line =
            if checked
              line.replace(@incompletePattern, @complete)
            else
              line.replace(@completePattern, @incomplete)
      line
    result.join("\n")

if window.jQuery
  jQuery.fn.taskList = (method) ->
    this.each (index, el) ->
      taskList = jQuery(el).data('task-list')
      if !taskList
        taskList = new TaskList el
        jQuery(el).data 'task-list', taskList

      taskList[method || 'enable']()
