/* eslint-env browser, jquery */

import { preventXSS } from './render'
import { md } from './extra'

/**
 * The reveal.js markdown plugin. Handles parsing of
 * markdown inside of presentations as well as loading
 * of external markdown documents.
 */
(function (root, factory) {
  if (typeof exports === 'object') {
    module.exports = factory()
  } else {
    // Browser globals (root is window)
    root.RevealMarkdown = factory()
    root.RevealMarkdown.initialize()
  }
}(this, function () {
  var DEFAULT_SLIDE_SEPARATOR = '^\r?\n---\r?\n$'
  var DEFAULT_NOTES_SEPARATOR = '^note:'
  var DEFAULT_ELEMENT_ATTRIBUTES_SEPARATOR = '\\.element\\s*?(.+?)$'
  var DEFAULT_SLIDE_ATTRIBUTES_SEPARATOR = '\\.slide:\\s*?(\\S.+?)$'

  var SCRIPT_END_PLACEHOLDER = '__SCRIPT_END__'

  /**
   * Retrieves the markdown contents of a slide section
   * element. Normalizes leading tabs/whitespace.
   */
  function getMarkdownFromSlide (section) {
    var template = section.querySelector('script')

    // strip leading whitespace so it isn't evaluated as code
    var text = (template || section).textContent

    // restore script end tags
    text = text.replace(new RegExp(SCRIPT_END_PLACEHOLDER, 'g'), '</script>')

    var leadingWs = text.match(/^\n?(\s*)/)[1].length
    var leadingTabs = text.match(/^\n?(\t*)/)[1].length

    if (leadingTabs > 0) {
      text = text.replace(new RegExp('\\n?\\t{' + leadingTabs + '}', 'g'), '\n')
    } else if (leadingWs > 1) {
      text = text.replace(new RegExp('\\n? {' + leadingWs + '}', 'g'), '\n')
    }

    return text
  }

  /**
   * Given a markdown slide section element, this will
   * return all arguments that aren't related to markdown
   * parsing. Used to forward any other user-defined arguments
   * to the output markdown slide.
   */
  function getForwardedAttributes (section) {
    var attributes = section.attributes
    var result = []

    for (var i = 0, len = attributes.length; i < len; i++) {
      var name = attributes[i].name
      var value = attributes[i].value

      // disregard attributes that are used for markdown loading/parsing
      if (/data-(markdown|separator|vertical|notes)/gi.test(name)) continue

      if (value) {
        result.push(name + '="' + value + '"')
      } else {
        result.push(name)
      }
    }

    return result.join(' ')
  }

  /**
   * Inspects the given options and fills out default
   * values for what's not defined.
   */
  function getSlidifyOptions (options) {
    options = options || {}
    options.separator = options.separator || DEFAULT_SLIDE_SEPARATOR
    options.notesSeparator = options.notesSeparator || DEFAULT_NOTES_SEPARATOR
    options.attributes = options.attributes || ''

    return options
  }

  /**
   * Helper function for constructing a markdown slide.
   */
  function createMarkdownSlide (content, options) {
    options = getSlidifyOptions(options)

    var notesMatch = content.split(new RegExp(options.notesSeparator, 'mgi'))

    if (notesMatch.length === 2) {
      content = notesMatch[0] + '<aside class="notes" data-markdown>' + notesMatch[1].trim() + '</aside>'
    }

    // prevent script end tags in the content from interfering
    // with parsing
    content = content.replace(/<\/script>/g, SCRIPT_END_PLACEHOLDER)

    return '<script type="text/template">' + content + '</script>'
  }

  /**
   * Parses a data string into multiple slides based
   * on the passed in separator arguments.
   */
  function slidify (markdown, options) {
    options = getSlidifyOptions(options)

    var separatorRegex = new RegExp(options.separator + (options.verticalSeparator ? '|' + options.verticalSeparator : ''), 'mg')
    var horizontalSeparatorRegex = new RegExp(options.separator)

    var matches
    var lastIndex = 0
    var isHorizontal
    var wasHorizontal = true
    var content
    var sectionStack = []

    // iterate until all blocks between separators are stacked up
    while ((matches = separatorRegex.exec(markdown)) !== null) {
      // determine direction (horizontal by default)
      isHorizontal = horizontalSeparatorRegex.test(matches[0])

      if (!isHorizontal && wasHorizontal) {
        // create vertical stack
        sectionStack.push([])
      }

      // pluck slide content from markdown input
      content = markdown.substring(lastIndex, matches.index)

      if (isHorizontal && wasHorizontal) {
        // add to horizontal stack
        sectionStack.push(content)
      } else {
        // add to vertical stack
        sectionStack[sectionStack.length - 1].push(content)
      }

      lastIndex = separatorRegex.lastIndex
      wasHorizontal = isHorizontal
    }

    // add the remaining slide
    (wasHorizontal ? sectionStack : sectionStack[sectionStack.length - 1]).push(markdown.substring(lastIndex))

    var markdownSections = ''

    // flatten the hierarchical stack, and insert <section data-markdown> tags
    for (var i = 0, len = sectionStack.length; i < len; i++) {
      // vertical
      if (sectionStack[i] instanceof Array) {
        markdownSections += '<section ' + options.attributes + '>'

        sectionStack[i].forEach(function (child) {
          markdownSections += '<section data-markdown>' + createMarkdownSlide(child, options) + '</section>'
        })

        markdownSections += '</section>'
      } else {
        markdownSections += '<section ' + options.attributes + ' data-markdown>' + createMarkdownSlide(sectionStack[i], options) + '</section>'
      }
    }

    return markdownSections
  }

  /**
   * Parses any current data-markdown slides, splits
   * multi-slide markdown into separate sections and
   * handles loading of external markdown.
   */
  function processSlides () {
    var sections = document.querySelectorAll('[data-markdown]')
    var section

    for (var i = 0, len = sections.length; i < len; i++) {
      section = sections[i]

      if (section.getAttribute('data-markdown').length) {
        var xhr = new XMLHttpRequest()
        var url = section.getAttribute('data-markdown')

        var datacharset = section.getAttribute('data-charset')

        // see https://developer.mozilla.org/en-US/docs/Web/API/element.getAttribute#Notes
        if (datacharset !== null && datacharset !== '') {
          xhr.overrideMimeType('text/html; charset=' + datacharset)
        }

        xhr.onreadystatechange = function () {
          if (xhr.readyState === 4) {
            // file protocol yields status code 0 (useful for local debug, mobile applications etc.)
            if ((xhr.status >= 200 && xhr.status < 300) || xhr.status === 0) {
              section.outerHTML = slidify(xhr.responseText, {
                separator: section.getAttribute('data-separator'),
                verticalSeparator: section.getAttribute('data-separator-vertical'),
                notesSeparator: section.getAttribute('data-separator-notes'),
                attributes: getForwardedAttributes(section)
              })
            } else {
              section.outerHTML = '<section data-state="alert">' +
              'ERROR: The attempt to fetch ' + url + ' failed with HTTP status ' + xhr.status + '.' +
              'Check your browser\'s JavaScript console for more details.' +
              '<p>Remember that you need to serve the presentation HTML from a HTTP server.</p>' +
              '</section>'
            }
          }
        }

        xhr.open('GET', url, false)

        try {
          xhr.send()
        } catch (e) {
          alert('Failed to get the Markdown file ' + url + '. Make sure that the presentation and the file are served by a HTTP server and the file can be found there. ' + e)
        }
      } else if (section.getAttribute('data-separator') || section.getAttribute('data-separator-vertical') || section.getAttribute('data-separator-notes')) {
        section.outerHTML = slidify(getMarkdownFromSlide(section), {
          separator: section.getAttribute('data-separator'),
          verticalSeparator: section.getAttribute('data-separator-vertical'),
          notesSeparator: section.getAttribute('data-separator-notes'),
          attributes: getForwardedAttributes(section)
        })
      } else {
        section.innerHTML = createMarkdownSlide(getMarkdownFromSlide(section))
      }
    }
  }

  /**
   * Check if a node value has the attributes pattern.
   * If yes, extract it and add that value as one or several attributes
   * the the terget element.
   *
   * You need Cache Killer on Chrome to see the effect on any FOM transformation
   * directly on refresh (F5)
   * http://stackoverflow.com/questions/5690269/disabling-chrome-cache-for-website-development/7000899#answer-11786277
   */
  function addAttributeInElement (node, elementTarget, separator) {
    var mardownClassesInElementsRegex = new RegExp(separator, 'mg')
    var mardownClassRegex = new RegExp('([^"= ]+?)="([^"=]+?)"', 'mg')
    var nodeValue = node.nodeValue
    var matches
    var matchesClass
    if ((matches = mardownClassesInElementsRegex.exec(nodeValue))) {
      var classes = matches[1]
      nodeValue = nodeValue.substring(0, matches.index) + nodeValue.substring(mardownClassesInElementsRegex.lastIndex)
      node.nodeValue = nodeValue
      while ((matchesClass = mardownClassRegex.exec(classes))) {
        var name = matchesClass[1]
        var value = matchesClass[2]
        if (name.substr(0, 5) === 'data-' || window.whiteListAttr.indexOf(name) !== -1) { elementTarget.setAttribute(name, window.filterXSS.escapeAttrValue(value)) }
      }
      return true
    }
    return false
  }

  /**
   * Add attributes to the parent element of a text node,
   * or the element of an attribute node.
   */
  function addAttributes (section, element, previousElement, separatorElementAttributes, separatorSectionAttributes) {
    if (element != null && element.childNodes !== undefined && element.childNodes.length > 0) {
      var previousParentElement = element
      for (var i = 0; i < element.childNodes.length; i++) {
        var childElement = element.childNodes[i]
        if (i > 0) {
          let j = i - 1
          while (j >= 0) {
            var aPreviousChildElement = element.childNodes[j]
            if (typeof aPreviousChildElement.setAttribute === 'function' && aPreviousChildElement.tagName !== 'BR') {
              previousParentElement = aPreviousChildElement
              break
            }
            j = j - 1
          }
        }
        var parentSection = section
        if (childElement.nodeName === 'section') {
          parentSection = childElement
          previousParentElement = childElement
        }
        if (typeof childElement.setAttribute === 'function' || childElement.nodeType === Node.COMMENT_NODE) {
          addAttributes(parentSection, childElement, previousParentElement, separatorElementAttributes, separatorSectionAttributes)
        }
      }
    }

    if (element.nodeType === Node.COMMENT_NODE) {
      if (addAttributeInElement(element, previousElement, separatorElementAttributes) === false) {
        addAttributeInElement(element, section, separatorSectionAttributes)
      }
    }
  }

  /**
   * Converts any current data-markdown slides in the
   * DOM to HTML.
   */
  function convertSlides () {
    var sections = document.querySelectorAll('[data-markdown]')

    for (var i = 0, len = sections.length; i < len; i++) {
      var section = sections[i]

      // Only parse the same slide once
      if (!section.getAttribute('data-markdown-parsed')) {
        section.setAttribute('data-markdown-parsed', true)

        var notes = section.querySelector('aside.notes')
        var markdown = getMarkdownFromSlide(section)
        markdown = markdown.replace(/&lt;/g, '<').replace(/&gt;/g, '>')
        var rendered = md.render(markdown)
        rendered = preventXSS(rendered)
        var result = window.postProcess(rendered)
        section.innerHTML = result[0].outerHTML
        addAttributes(section, section, null, section.getAttribute('data-element-attributes') ||
        section.parentNode.getAttribute('data-element-attributes') ||
        DEFAULT_ELEMENT_ATTRIBUTES_SEPARATOR,
        section.getAttribute('data-attributes') ||
        section.parentNode.getAttribute('data-attributes') ||
        DEFAULT_SLIDE_ATTRIBUTES_SEPARATOR)

        // If there were notes, we need to re-add them after
        // having overwritten the section's HTML
        if (notes) {
          section.appendChild(notes)
        }
      }
    }
  }

  // API
  return {
    initialize: function () {
      processSlides()
      convertSlides()
    },
    // TODO: Do these belong in the API?
    processSlides: processSlides,
    convertSlides: convertSlides,
    slidify: slidify
  }
}))