import VBtn from '../components/VBtn'
import VIcon from '../components/VIcon'
import VSelect from '../components/VSelect'

import Filterable from './filterable'
import Themeable from './themeable'
import Loadable from './loadable'

import { getObjectValueByPath, isObject } from '../util/helpers'
import { consoleWarn } from '../util/console'

/**
 * DataIterable
 *
 * @mixin
 *
 * Base behavior for data table and data iterator
 * providing selection, pagination, sorting and filtering.
 *
 */
export default {
  name: 'data-iterable',

  data () {
    return {
      searchLength: 0,
      defaultPagination: {
        descending: false,
        page: 1,
        rowsPerPage: 5,
        sortBy: null,
        totalItems: 0
      },
      expanded: {},
      actionsClasses: 'v-data-iterator__actions',
      actionsRangeControlsClasses: 'v-data-iterator__actions__range-controls',
      actionsSelectClasses: 'v-data-iterator__actions__select',
      actionsPaginationClasses: 'v-data-iterator__actions__pagination'
    }
  },

  mixins: [Filterable, Loadable, Themeable],

  props: {
    expand: Boolean,
    hideActions: Boolean,
    disableInitialSort: Boolean,
    mustSort: Boolean,
    noResultsText: {
      type: String,
      default: '$vuetify.lang.dataIterator.noResultsText'
    },
    nextIcon: {
      type: String,
      default: '$vuetify.icons.next'
    },
    prevIcon: {
      type: String,
      default: '$vuetify.icons.prev'
    },
    rowsPerPageItems: {
      type: Array,
      default () {
        return [
          5,
          10,
          25,
          {
            text: '$vuetify.lang.dataIterator.rowsPerPageAll',
            value: -1
          }
        ]
      }
    },
    rowsPerPageText: {
      type: String,
      default: '$vuetify.lang.dataIterator.rowsPerPageText'
    },
    selectAll: [Boolean, String],
    search: {
      required: false
    },
    filter: {
      type: Function,
      default: (val, search) => {
        return val != null &&
          typeof val !== 'boolean' &&
          val.toString().toLowerCase().indexOf(search) !== -1
      }
    },
    customFilter: {
      type: Function,
      default: (items, search, filter) => {
        search = search.toString().toLowerCase()
        if (search.trim() === '') return items

        return items.filter(i => (
          Object.keys(i).some(j => filter(i[j], search))
        ))
      }
    },
    customSort: {
      type: Function,
      default: (items, index, isDescending) => {
        if (index === null) return items

        return items.sort((a, b) => {
          let sortA = getObjectValueByPath(a, index)
          let sortB = getObjectValueByPath(b, index)

          if (isDescending) {
            [sortA, sortB] = [sortB, sortA]
          }

          // Check if both are numbers
          if (!isNaN(sortA) && !isNaN(sortB)) {
            return sortA - sortB
          }

          // Check if both cannot be evaluated
          if (sortA === null && sortB === null) {
            return 0
          }

          [sortA, sortB] = [sortA, sortB]
            .map(s => (
              (s || '').toString().toLocaleLowerCase()
            ))

          if (sortA > sortB) return 1
          if (sortA < sortB) return -1

          return 0
        })
      }
    },
    value: {
      type: Array,
      default: () => []
    },
    items: {
      type: Array,
      required: true,
      default: () => []
    },
    totalItems: {
      type: Number,
      default: null
    },
    itemKey: {
      type: String,
      default: 'id'
    },
    pagination: {
      type: Object,
      default: () => {}
    }
  },

  computed: {
    computedPagination () {
      return this.hasPagination
        ? this.pagination
        : this.defaultPagination
    },
    computedRowsPerPageItems () {
      return this.rowsPerPageItems.map(item => {
        return isObject(item)
          ? Object.assign({}, item, {
            text: this.$vuetify.t(item.text)
          })
          : item
      })
    },
    hasPagination () {
      const pagination = this.pagination || {}

      return Object.keys(pagination).length > 0
    },
    hasSelectAll () {
      return this.selectAll !== undefined && this.selectAll !== false
    },
    itemsLength () {
      if (this.hasSearch) return this.searchLength
      return this.totalItems || this.items.length
    },
    indeterminate () {
      return this.hasSelectAll && this.someItems && !this.everyItem
    },
    everyItem () {
      return this.filteredItems.length &&
        this.filteredItems.every(i => this.isSelected(i))
    },
    someItems () {
      return this.filteredItems.some(i => this.isSelected(i))
    },
    getPage () {
      const { rowsPerPage } = this.computedPagination

      return rowsPerPage === Object(rowsPerPage)
        ? rowsPerPage.value
        : rowsPerPage
    },
    pageStart () {
      return this.getPage === -1
        ? 0
        : (this.computedPagination.page - 1) * this.getPage
    },
    pageStop () {
      return this.getPage === -1
        ? this.itemsLength
        : this.computedPagination.page * this.getPage
    },
    filteredItems () {
      return this.filteredItemsImpl()
    },
    selected () {
      const selected = {}
      for (let index = 0; index < this.value.length; index++) {
        const key = getObjectValueByPath(this.value[index], this.itemKey)
        selected[key] = true
      }
      return selected
    },
    hasSearch () {
      return this.search != null
    }
  },

  watch: {
    search () {
      this.$nextTick(() => {
        this.updatePagination({ page: 1, totalItems: this.itemsLength })
      })
    },
    'computedPagination.sortBy': 'resetPagination',
    'computedPagination.descending': 'resetPagination'
  },

  methods: {
    initPagination () {
      if (!this.rowsPerPageItems.length) {
        consoleWarn(`The prop 'rows-per-page-items' can not be empty`, this)
      } else {
        this.defaultPagination.rowsPerPage = this.rowsPerPageItems[0]
      }

      this.defaultPagination.totalItems = this.items.length

      this.updatePagination(
        Object.assign({}, this.defaultPagination, this.pagination)
      )
    },
    updatePagination (val) {
      const pagination = this.hasPagination
        ? this.pagination
        : this.defaultPagination
      const updatedPagination = Object.assign({}, pagination, val)
      this.$emit('update:pagination', updatedPagination)

      if (!this.hasPagination) {
        this.defaultPagination = updatedPagination
      }
    },
    isSelected (item) {
      return this.selected[getObjectValueByPath(item, this.itemKey)]
    },
    isExpanded (item) {
      return this.expanded[getObjectValueByPath(item, this.itemKey)]
    },
    filteredItemsImpl (...additionalFilterArgs) {
      if (this.totalItems) return this.items

      let items = this.items.slice()

      if (this.hasSearch) {
        items = this.customFilter(items, this.search, this.filter, ...additionalFilterArgs)
        this.searchLength = items.length
      }

      items = this.customSort(
        items,
        this.computedPagination.sortBy,
        this.computedPagination.descending
      )

      return this.hideActions &&
        !this.hasPagination
        ? items
        : items.slice(this.pageStart, this.pageStop)
    },
    resetPagination () {
      this.computedPagination.page !== 1 &&
        this.updatePagination({ page: 1 })
    },
    sort (index) {
      const { sortBy, descending } = this.computedPagination
      if (sortBy === null) {
        this.updatePagination({ sortBy: index, descending: false })
      } else if (sortBy === index && !descending) {
        this.updatePagination({ descending: true })
      } else if (sortBy !== index) {
        this.updatePagination({ sortBy: index, descending: false })
      } else if (!this.mustSort) {
        this.updatePagination({ sortBy: null, descending: null })
      } else {
        this.updatePagination({ sortBy: index, descending: false })
      }
    },
    toggle (value) {
      const selected = Object.assign({}, this.selected)
      for (let index = 0; index < this.filteredItems.length; index++) {
        const key = getObjectValueByPath(this.filteredItems[index], this.itemKey)
        selected[key] = value
      }

      this.$emit('input', this.items.filter(i => {
        const key = getObjectValueByPath(i, this.itemKey)
        return selected[key]
      }))
    },
    createProps (item, index) {
      const props = { item, index }
      const keyProp = this.itemKey
      const itemKey = getObjectValueByPath(item, keyProp)

      Object.defineProperty(props, 'selected', {
        get: () => this.selected[itemKey],
        set: value => {
          if (itemKey == null) {
            consoleWarn(`"${keyProp}" attribute must be defined for item`, this)
          }

          let selected = this.value.slice()
          if (value) selected.push(item)
          else selected = selected.filter(i => getObjectValueByPath(i, keyProp) !== itemKey)
          this.$emit('input', selected)
        }
      })

      Object.defineProperty(props, 'expanded', {
        get: () => this.expanded[itemKey],
        set: value => {
          if (itemKey == null) {
            consoleWarn(`"${keyProp}" attribute must be defined for item`, this)
          }

          if (!this.expand) {
            for (const key in this.expanded) {
              this.expanded.hasOwnProperty(key) && this.$set(this.expanded, key, false)
            }
          }
          this.$set(this.expanded, itemKey, value)
        }
      })

      return props
    },
    genItems () {
      if (!this.itemsLength && !this.items.length) {
        const noData = this.$slots['no-data'] || this.$vuetify.t(this.noDataText)
        return [this.genEmptyItems(noData)]
      }

      if (!this.filteredItems.length) {
        const noResults = this.$slots['no-results'] || this.$vuetify.t(this.noResultsText)
        return [this.genEmptyItems(noResults)]
      }

      return this.genFilteredItems()
    },
    genPrevIcon () {
      return this.$createElement(VBtn, {
        props: {
          disabled: this.computedPagination.page === 1,
          icon: true,
          flat: true,
          dark: this.dark,
          light: this.light
        },
        on: {
          click: () => {
            const page = this.computedPagination.page
            this.updatePagination({ page: page - 1 })
          }
        },
        attrs: {
          'aria-label': this.$vuetify.t('$vuetify.lang.dataIterator.prevPage')
        }
      }, [this.$createElement(VIcon, this.prevIcon)])
    },
    genNextIcon () {
      const pagination = this.computedPagination
      const disabled = pagination.rowsPerPage < 0 ||
        pagination.page * pagination.rowsPerPage >= this.itemsLength ||
        this.pageStop < 0

      return this.$createElement(VBtn, {
        props: {
          disabled,
          icon: true,
          flat: true,
          dark: this.dark,
          light: this.light
        },
        on: {
          click: () => {
            const page = this.computedPagination.page
            this.updatePagination({ page: page + 1 })
          }
        },
        attrs: {
          'aria-label': this.$vuetify.t('$vuetify.lang.dataIterator.nextPage')
        }
      }, [this.$createElement(VIcon, this.nextIcon)])
    },
    genSelect () {
      return this.$createElement('div', {
        'class': this.actionsSelectClasses
      }, [
        this.$vuetify.t(this.rowsPerPageText),
        this.$createElement(VSelect, {
          attrs: {
            'aria-label': this.$vuetify.t(this.rowsPerPageText)
          },
          props: {
            items: this.computedRowsPerPageItems,
            value: this.computedPagination.rowsPerPage,
            hideDetails: true,
            auto: true,
            minWidth: '75px'
          },
          on: {
            input: val => {
              this.updatePagination({
                page: 1,
                rowsPerPage: val
              })
            }
          }
        })
      ])
    },
    genPagination () {
      let pagination = '–'

      if (this.itemsLength) {
        const stop = this.itemsLength < this.pageStop || this.pageStop < 0
          ? this.itemsLength
          : this.pageStop

        pagination = this.$scopedSlots.pageText
          ? this.$scopedSlots.pageText({
            pageStart: this.pageStart + 1,
            pageStop: stop,
            itemsLength: this.itemsLength
          })
          : this.$vuetify.t('$vuetify.lang.dataIterator.pageText', this.pageStart + 1, stop, this.itemsLength)
      }

      return this.$createElement('div', {
        'class': this.actionsPaginationClasses
      }, [pagination])
    },
    genActions () {
      const rangeControls = this.$createElement('div', {
        'class': this.actionsRangeControlsClasses
      }, [
        this.genPagination(),
        this.genPrevIcon(),
        this.genNextIcon()
      ])

      return [this.$createElement('div', {
        'class': this.actionsClasses
      }, [
        this.rowsPerPageItems.length > 1 ? this.genSelect() : null,
        rangeControls
      ])]
    }
  }
}


transition: color .2s cubic-bezier(.25,.8,.25,1), transform .3s cubic-bezier(.25,.8,.25,1)

keep keep keep keep replace keep keep keep keep

import '../../stylus/components/_small-dialog.styl'

// Mixins
import Returnable from '../../mixins/returnable'

// Utils
import { keyCodes } from '../../util/helpers'

import VBtn from '../VBtn'
import VMenu from '../VMenu'

export default {
  name: 'v-edit-dialog',

  mixins: [ Returnable ],

  data () {
    return {
      isActive: false,
      isSaving: false
    }
  },

  props: {
    cancelText: {
      default: 'Cancel'
    },
    large: Boolean,
    lazy: Boolean,
    persistent: Boolean,
    saveText: {
      default: 'Save'
    },
    transition: {
      type: String,
      default: 'slide-x-reverse-transition'
    }
  },

  watch: {
    isActive (val) {
      val && setTimeout(this.focus, 50) // Give DOM time to paint
    }
  },

  methods: {
    cancel () {
      this.isActive = false
    },
    focus () {
      const input = this.$refs.content.querySelector('input')
      input && input.focus()
    },
    genButton (fn, text) {
      return this.$createElement(VBtn, {
        props: {
          flat: true,
          color: 'primary',
          light: true
        },
        on: { click: fn }
      }, text)
    },
    genActions () {
      return this.$createElement('div', {
        'class': 'v-small-dialog__actions'
      }, [
        this.genButton(this.cancel, this.cancelText),
        this.genButton(() => this.save(this.returnValue), this.saveText)
      ])
    },
    genContent () {
      return this.$createElement('div', {
        on: {
          keydown: e => {
            const input = this.$refs.content.querySelector('input')
            e.keyCode === keyCodes.esc && this.cancel()
            e.keyCode === keyCodes.enter && input && this.save(input.value)
          }
        },
        ref: 'content'
      }, [this.$slots.input])
    }
  },

  render (h) {
    return h(VMenu, {
      'class': 'v-small-dialog',
      props: {
        contentClass: 'v-small-dialog__content',
        transition: this.transition,
        origin: 'top right',
        right: true,
        value: this.isActive,
        closeOnClick: !this.persistent,
        closeOnContentClick: false,
        lazy: this.lazy
      },
      on: {
        input: val => (this.isActive = val)
      }
    }, [
      h('a', {
        slot: 'activator'
      }, this.$slots.default),
      this.genContent(),
      this.large ? this.genActions() : null
    ])
  }
}


align-items: center
display: inline-flex
top: 0

@import '../bootstrap'
@import '../theme'

/** Theme */
selection-control($material)
  &.v-input--is-disabled
    .v-icon
      // needed for helper override
      color: $material.buttons.disabled !important

v-selection-control-rtl()
  .v-input--selection-controls__input
    margin-right: 0
    margin-left: 8px

theme(selection-control, "v-input--selection-controls")
rtl(v-selection-control-rtl, "v-input--selection-controls")

.v-input--selection-controls
  &__input
    color: inherit
    display: inline-flex
    flex: 0 1 auto
    height: 24px
    position: relative
    margin-right: 8px
    margin-top: 4px
    transition: color .2s cubic-bezier(.25,.8,.25,1), transform .3s cubic-bezier(.25,.8,.25,1)
    width: 24px

    input
      position: absolute
      opacity: 0

  &__ripple
    cursor: pointer
    height: 48px
    position: absolute
    transition: inherit
    width: 48px
    left: -12px
    top: calc(50% - 24px)

    &:before
      border-radius: 50%
      bottom: 0
      content: ''
      position: absolute
      opacity: .2
      left: 0
      right: 0
      top: 0
      transform-origin: center center
      transform: scale(0.2)
      transition: inherit

  .v-label
    top: 2px
    user-select: none

  &.v-input--is-focused,
  .v-radio--is-focused
    .v-input--selection-controls__ripple:before
      background: currentColor
      transform: scale(.8)


// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`VDatePickerTitle.js should render component and match snapshot 1`] = `

    1234

      2005-11-01

`;

exports[`VDatePickerTitle.js should render component when selecting year and match snapshot 1`] = `

    1234

      2005-11-01

`;

exports[`VDatePickerTitle.js should render year icon 1`] = `

  1234

    year

`;


import Bootable from './bootable'
import { consoleWarn } from '../util/console'

function validateAttachTarget (val) {
  const type = typeof val

  if (type === 'boolean' || type === 'string') return true

  return val.nodeType === Node.ELEMENT_NODE
}

export default {
  name: 'detachable',

  mixins: [Bootable],

  data: () => ({
    hasDetached: false
  }),

  props: {
    attach: {
      type: null,
      default: false,
      validator: validateAttachTarget
    },
    contentClass: {
      default: ''
    }
  },

  watch: {
    attach () {
      this.hasDetached = false
      this.initDetach()
    },
    hasContent: 'initDetach'
  },

  mounted () {
    !this.lazy && this.initDetach()
  },

  deactivated () {
    this.isActive = false
  },

  beforeDestroy () {
    if (!this.$refs.content) return

    // IE11 Fix
    try {
      this.$refs.content.parentNode.removeChild(this.$refs.content)
    } catch (e) { console.log(e) }
  },

  methods: {
    initDetach () {
      if (this._isDestroyed ||
        !this.$refs.content ||
        this.hasDetached ||
        // Leave menu in place if attached
        // and dev has not changed target
        this.attach === '' || // If used as a boolean prop (<v-menu attach>)
        this.attach === true || // If bound to a boolean (<v-menu :attach="true">)
        this.attach === 'attach' // If bound as boolean prop in pug (v-menu(attach))
      ) return

      let target
      if (this.attach === false) {
        // Default, detach to app
        target = document.querySelector('[data-app]')
      } else if (typeof this.attach === 'string') {
        // CSS selector
        target = document.querySelector(this.attach)
      } else {
        // DOM Element
        target = this.attach
      }

      if (!target) {
        consoleWarn(`Unable to locate target ${this.attach || '[data-app]'}`, this)
        return
      }

      target.insertBefore(
        this.$refs.content,
        target.firstChild
      )

      this.hasDetached = true
    }
  }
}


import Bootable from '../../mixins/bootable'

import {
  VTabTransition,
  VTabReverseTransition
} from '../transitions'

import {
  inject as RegistrableInject
} from '../../mixins/registrable'

import Touch from '../../directives/touch'

export default {
  name: 'v-tab-item',

  mixins: [
    Bootable,
    RegistrableInject('tabs', 'v-tab-item', 'v-tabs-items')
  ],

  components: {
    VTabTransition,
    VTabReverseTransition
  },

  directives: {
    Touch
  },

  data () {
    return {
      isActive: false,
      reverse: false
    }
  },

  props: {
    id: String,
    transition: {
      type: [Boolean, String],
      default: 'tab-transition'
    },
    reverseTransition: {
      type: [Boolean, String],
      default: 'tab-reverse-transition'
    }
  },

  computed: {
    computedTransition () {
      return this.reverse ? this.reverseTransition : this.transition
    }
  },

  methods: {
    toggle (target, reverse, showTransition, index) {
      this.$el.style.transition = !showTransition ? 'none' : null
      this.reverse = reverse
      this.isActive = (this.id || index.toString()) === target
    }
  },

  mounted () {
    this.tabs.register(this)
  },

  beforeDestroy () {
    this.tabs.unregister(this)
  },

  render (h) {
    const data = {
      staticClass: 'v-tabs__content',
      directives: [{
        name: 'show',
        value: this.isActive
      }],
      domProps: { id: this.id },
      on: this.$listeners
    }

    const div = h('div', data, this.showLazyContent(this.$slots.default))

    if (!this.computedTransition) return div

    return h('transition', {
      props: { name: this.computedTransition }
    }, [div])
  }
}


#= require underscore
#= require linked_list
#= require tandem/constants
#= require tandem/leaf
#= require tandem/utils

class TandemLine extends LinkedList.Node
  @CLASS_NAME : 'line'
  @DIRTY_CLASS: 'dirty'
  @ID_PREFIX  : 'line-'

  @applyRules: (root) ->
    Tandem.Utils.traversePreorder(root, 0, (node, index) =>
      if node.nodeType == node.ELEMENT_NODE
        rules = Tandem.Constants.LINE_RULES[node.tagName]
        if rules?
          _.each(rules, (data, rule) ->
            switch rule
              when 'rename' then node = Tandem.Utils.switchTag(node, data)
              else return
          )
        else
          node = Tandem.Utils.unwrap(node)
      return node
    )

  @isLineNode: (node) ->
    return node? && node.classList? && node.classList.contains(TandemLine.CLASS_NAME)

  @mergeAdjacent: (root) ->
    Tandem.Utils.traversePreorder(root, 0, (node) ->
      if node.nodeType == node.ELEMENT_NODE && !TandemLine.isLineNode(node)
        next = node.nextSibling
        if next?.tagName == node.tagName && node.tagName != 'LI' && !Tandem.Utils.isIgnoreNode(node) && !Tandem.Utils.isIgnoreNode(next)
          [nodeAttr, nodeValue] = Tandem.Utils.getAttributeForContainer(node)
          [nextAttr, nextValue] = Tandem.Utils.getAttributeForContainer(next)
          if nodeAttr == nextAttr && nodeValue == nextValue
            node = Tandem.Utils.mergeNodes(node, next)
      return node
    )

  @normalizeHtml: (root) ->
    return if root.childNodes.length == 1 && root.firstChild.tagName == 'BR'
    this.applyRules(root)
    this.removeRedundant(root)
    # TODO need wrapText before and after for these cases:
    # Before: TestWhat -> TestWhat
    # After: BoldTest -> BoldTest
    this.wrapText(root)
    this.mergeAdjacent(root)
    this.wrapText(root)
    root.appendChild(root.ownerDocument.createElement('br')) if root.firstChild == null

  @removeRedundant: (root) ->
    tandemKey = '_tandemAttributes' + Math.floor(Math.random() * 100000000)
    root[tandemKey] = {}

    isRedudant = (node) ->
      if node.nodeType == node.ELEMENT_NODE && !Tandem.Utils.isIgnoreNode(node)
        if node.textContent.length == 0
          return true
        [attrName, attrValue] = Tandem.Utils.getAttributeForContainer(node)
        if attrName?
          return node.parentNode[tandemKey][attrName]?     # Parent attribute value will overwrite child's so no need to check attrValue
        else if node.tagName == 'SPAN'
          # Check if children need us
          if node.childNodes.length == 0 || !_.any(node.childNodes, (child) -> child.nodeType != child.ELEMENT_NODE)
            return true
          # Check if parent needs us
          if node.previousSibling == null && node.nextSibling == null && !TandemLine.isLineNode(node.parentNode) && node.parentNode.tagName != 'LI'
            return true
      return false

    Tandem.Utils.traversePreorder(root, 0, (node) =>
      if isRedudant(node)
        node = Tandem.Utils.unwrap(node)
      if node?
        node[tandemKey] = _.clone(node.parentNode[tandemKey])
        [attrName, attrValue] = Tandem.Utils.getAttributeForContainer(node)
        node[tandemKey][attrName] = attrValue if attrName?
      return node
    )
    delete root[tandemKey]
    Tandem.Utils.traversePreorder(root, 0, (node) ->
      delete node[tandemKey]
      return node
    )

  @wrapText: (root) ->
    Tandem.Utils.traversePreorder(root, 0, (node) =>
      node.normalize()
      if node.nodeType == node.TEXT_NODE && (node.nextSibling? || node.previousSibling? || node.parentNode == root || node.parentNode.tagName == 'LI')
        span = node.ownerDocument.createElement('span')
        Tandem.Utils.wrap(span, node)
        node = span
      return node
    )

  constructor: (@doc, @node) ->
    @id = _.uniqueId(Tandem.Line.ID_PREFIX)
    @node.id = @id
    @node.classList.add(TandemLine.CLASS_NAME)
    this.rebuild()
    super(@node)

  buildLeaves: (node, attributes) ->
    _.each(_.clone(node.childNodes), (node) =>
      node.normalize()
      nodeAttributes = _.clone(attributes)
      [attrName, attrVal] = Tandem.Utils.getAttributeForContainer(node)
      nodeAttributes[attrName] = attrVal if attrName?
      if Tandem.Leaf.isLeafNode(node)
        @leaves.append(new Tandem.Leaf(this, node, nodeAttributes))
      else
        this.buildLeaves(node, nodeAttributes)
    )

  findLeaf: (leafNode) ->
    curLeaf = @leaves.first
    while curLeaf?
      return curLeaf if curLeaf.node == leafNode
      curLeaf = curLeaf.next
    return null

  rebuild: ->
    if @node.parentNode == @doc.root
      return false if @outerHTML == @node.outerHTML
      while @leaves? && @leaves.length > 0
        @leaves.remove(@leaves.first)
      @leaves = new LinkedList()
      TandemLine.normalizeHtml(@node)
      this.buildLeaves(@node, {})
      this.resetContent()
    else
      @doc.removeLine(this)
    return true

  resetContent: ->
    @length = _.reduce(@leaves.toArray(), ((length, leaf) -> leaf.length + length), 0)
    @outerHTML = @node.outerHTML
    @attributes = {}
    [attrName, attrVal] = Tandem.Utils.getAttributeForContainer(@node)
    @attributes[attrName] = attrVal if attrName?
    this.setDirty(false)
    @delta = this.toDelta()

  setDirty: (isDirty = true) ->
    if isDirty
      @node.classList.add(TandemLine.DIRTY_CLASS)
    else
      @node.classList.remove(TandemLine.DIRTY_CLASS)

  splitContents: (offset) ->
    this.setDirty()
    [node, offset] = Tandem.Utils.getChildAtOffset(@node, offset)
    if @node.tagName == 'OL' || @node.tagName == 'UL'
      [node, offset] = Tandem.Utils.getChildAtOffset(node, offset)
    return Tandem.Utils.splitNode(node, offset)

  toDelta: ->
    deltas = _.map(@leaves.toArray(), (leaf) ->
      return new JetInsert(leaf.text, leaf.getAttributes(true))
    )
    delta = new JetDelta(0, @length, deltas)
    delta.compact()
    return delta

window.Tandem ||= {}
window.Tandem.Line = TandemLine


notification :off

guard 'haml', :output => 'bin', :run_at_start => true do
  watch %r{(.+\.haml)}
end

guard 'coffeescript', :output => 'bin', :all_on_start => true do
  watch %r{(.+\.coffee)}
end

guard 'sass', :output => 'bin', :all_on_start => true do
  watch %r{(.+\.sass)}
end


$(document).ready( ->
  window.editor = editor = new Tandem.Editor('editor-container')
  window.toolbar = toolbar = new Tandem.Toolbar(editor)

  toolbar.on 'update', (attributes) ->
    $.post('/ios-message', {json: JSON.stringify(attributes)})

  editor.getText = -> ""
  editor.setText = ->
  editor.setCursor = ->
  editor.clearCursors = ->
  editor.clearMessages = ->
  editor.addMessage = ->
  editor.updateComposing = ->

  delegate = new Object()
  jetClient = new JetClient({delegate: delegate})
  textState = new JetTextState(editor, jetClient, "", Stypi.configs.sessionId)
  chatState = new JetChatState(editor, jetClient, [], Stypi.configs.sessionId)
  cursorState = new JetCursorState(editor, jetClient, {}, Stypi.configs.sessionId)
  jetClient.addState(textState)
  jetClient.addState(cursorState)
  jetClient.addState(chatState)
  jetClient.connect(Stypi.configs.docId, 0)

  textState.applyDeltaToText = (delta, authorId) ->   # Hacky overwrite
    editor.applyDelta(delta)
  textState.applyDeltaToCursors = ->
  Stypi.Presence = {
    setUsers: ->
  }

  editor.on(Tandem.Editor.events.API_TEXT_CHANGE, (delta) ->
    textState.localUpdate(delta)
    jetClient.checkRunwayReady()
  )
  editor.on(Tandem.Editor.events.USER_TEXT_CHANGE, (delta) ->
    textState.localUpdate(delta)
    jetClient.checkRunwayReady()
  )
)


body
  font-family: 'Helvetica'
  font-size: 13px
  padding: 0
  margin: 0

#content-container
  width: 100%
  height: 100%


#= require underscore
#= require linked_list
#= require tandem/line

class TandemDocument
  @INDENT_PREFIX: 'indent-'

  @fixListNumbering: (root) ->
    ols = root.querySelectorAll('ol')
    _.each(ols, (ol) ->
      indent = Tandem.Utils.getIndent(ol)
      previous = ol.previousSibling
      while true
        if !previous? || previous.tagName != 'OL'
          return ol.setAttribute('start', 1)
        else
          prevIndent = Tandem.Utils.getIndent(previous)
          if prevIndent < indent
            return ol.setAttribute('start', 1)
          else if prevIndent == indent
            return ol.setAttribute('start', parseInt(previous.getAttribute('start')) + 1)
          else
            previous = previous.previousSibling
    )

  @normalizeHtml: (root, options = {}) ->
    if root.childNodes.length == 0
      div = root.ownerDocument.createElement('div')
      return root.appendChild(div)
    _.each(_.clone(root.childNodes), (child) =>
      if child.nodeType != child.ELEMENT_NODE
        child.parentNode.removeChild(child)
      else
        # TODO editor.update should mark dirty lines
        if options.ignoreDirty || child.classList.contains(Tandem.Line.DIRTY_CLASS) || true
          _.each(Tandem.Constants.BREAK_TAGS, (tagName) ->
            tags = child.querySelectorAll(tagName)
            _.each(tags, (tag) ->
              tag.textContent = "\n"
            )
          )
          lineNodes = Tandem.Utils.breakBlocks(child)
          _.each(lineNodes, (lineNode) ->
            Tandem.Line.normalizeHtml(lineNode)
          )
    )
    TandemDocument.fixListNumbering(root)

  constructor: (@editor, @root) ->
    this.buildLines()

  appendLine: (lineNode) ->
    return this.insertLineBefore(lineNode, null)

  buildLines: ->
    this.reset()
    TandemDocument.normalizeHtml(@root, {ignoreDirty: true})
    this.rebuild()

  # Makes sure our data structures are consistent
  checkConsistency: (output = false) ->
    nodesByLine = _.map(@root.childNodes, (lineNode) ->
      nodes = lineNode.querySelectorAll('*')
      return _.filter(nodes, (node) ->
        return node.nodeType == node.ELEMENT_NODE && (node.nodeName == 'BR' || node.firstChild.nodeType == node.TEXT_NODE)
      )
    )
    lines = @lines.toArray()

    isConsistent = =>
      # @lines and @lineMap should match
      if lines.length != _.values(@lineMap).length
        console.error "@lines and @lineMap differ in length", lines.length, _.values(@lineMap).length
        return false
      return false if _.any(lines, (line) =>
        if !line? || @lineMap[line.id] != line
          if line?
            console.error line, "does not match", @lineMap[line.id]
          else
            console.error "null line"
          return true
        return false
      )
      # All leaves should still be in the DOM
      orphanedLeaf = orphanedLine = null
      if _.any(lines, (line) ->
        return _.any(line.leaves.toArray(), (leaf) ->
          if leaf.node.parentNode == null
            orphanedLeaf = leaf
            orphanedLine = line
            return true
          return false
        )
      )
        console.error 'Missing from DOM', orphanedLine, orphanedLeaf
        return false

      # All line lengths should be correct
      if _.any(lines, (line) ->
        lineLength = _.reduce(line.leaves.toArray(), (count, leaf) ->
          return leaf.length + count
        , 0)
        if lineLength != line.length
          console.error 'incorrect line length', lineLength, line.length
      )
        return false

      # Document length should be correct
      docLength = _.reduce(lines, (count, line) ->
        return line.length + count
      , lines.length - 1)
      if docLength != @length
        console.error "incorrect document length", docLength, @length
        return false

      # @lines should match nodesByLine
      if lines.length != nodesByLine.length
        console.error "@lines and nodesByLine differ in length"
        return false
      return false if _.any(lines, (line, index) =>
        if line.length != line.node.textContent.length
          console.error line, line.length, line.node.textContent.length, 'differ in length'
          return true
        leaves = line.leaves.toArray()
        leafNodes = _.map(leaves, (leaf) -> return leaf.node)
        if !_.isEqual(leafNodes, nodesByLine[index])
          console.error leafNodes, 'nodes differ from', nodesByLine[index]
          return true
        leaves = _.map(leaves, (leaf) ->
          return _.pick(leaf, 'attributes', 'length', 'line', 'node', 'text')
        )
        line.rebuild()
        rebuiltLeaves = _.map(line.leaves.toArray(), (leaf) ->
          return _.pick(leaf, 'attributes', 'length', 'line', 'node', 'text')
        )
        if !_.isEqual(leaves, rebuiltLeaves)
          console.error leaves, 'leaves differ from', rebuiltLeaves
          return true
        return false
      )
      return true

    if isConsistent()
      return true
    else
      if output
        console.error @root
        console.error nodesByLine
        console.error lines
        console.error @lineMap
      return false

  cleanNode: (lineNode) ->
    lineNode.classList.remove(Tandem.Line.DIRTY_CLASS)
    line = this.findLine(lineNode)
    if line?
      return this.updateLine(line)
    return false

  detectChange: ->
    # Go through HTML (which should be normalized)
    # Make sure non are different from their innerHTML, if so record change
    # Returns changes made

  findLeaf: (node) ->
    lineNode = node.parentNode
    while lineNode? && !Tandem.Line.isLineNode(lineNode)
      lineNode = lineNode.parentNode
    return null if !lineNode?
    line = this.findLine(lineNode)
    return line.findLeaf(node)

  findLine: (node) ->
    return @lineMap[node.id]

  findLineAtOffset: (index) ->
    [lineNode, offset] = Tandem.Utils.getChildAtOffset(@root, index)
    line = this.findLine(lineNode)
    console.warn "No line found at", index, this if !line?
    return [line, offset]

  findLineNode: (node) ->
    while node? && !Tandem.Line.isLineNode(node)
      node = node.parentNode
    return node

  insertLineBefore: (newLineNode, refLine) ->
    line = new Tandem.Line(this, newLineNode)
    if refLine != null
      @lines.insertAfter(refLine.prev, line)
    else
      @lines.append(line)
    @lineMap[line.id] = line
    @length += line.length
    @length += 1 if @lines.length > 1
    return line

  printLines: ->
    lines = @lines.toArray()
    console.info lines.length
    _.each(lines, (line) ->
      console.info line, line.id, line.node.textContent
    )

  rebuild: ->
    this.reset()
    _.each(@root.childNodes, (node) =>
      this.appendLine(node)
    )

  rebuildDirty: ->
    # First and last nodes are always dirty to handle edge cases
    @root.firstChild.classList.add(Tandem.Line.DIRTY_CLASS)
    @root.lastChild.classList.add(Tandem.Line.DIRTY_CLASS)
    dirtyNodes = _.clone(@root.getElementsByClassName(Tandem.Line.DIRTY_CLASS))
    _.each((dirtyNodes), (lineNode, index) =>
      this.cleanNode(lineNode)
      prevNode = lineNode.previousSibling
      nextNode = lineNode.nextSibling
      while prevNode? && prevNode != dirtyNodes[index - 1]
        this.cleanNode(prevNode)
        prevNode = prevNode.previousSibling
      while nextNode? && nextNode != dirtyNodes[index + 1]
        this.cleanNode(nextNode)
        nextNode = nextNode.nextSibling
    )

  removeLine: (line) ->
    delete @lineMap[line.id]
    @lines.remove(line)
    @length -= line.length + 1

  reset: ->
    @lines = new LinkedList()
    @lineMap = {}
    @length = 0

  splitLine: (line, offset) ->
    [lineNode1, lineNode2] = Tandem.Utils.splitNode(line.node, offset, true)
    line.node = lineNode1
    this.updateLine(line)
    return this.insertLineBefore(lineNode2, line.next)

  toDelta: ->
    lines = @lines.toArray()
    deltas = _.flatten(_.map(lines, (line, index) ->
      lineDeltas = JetDelta.copy(line.delta).deltas
      if index < lines.length - 1
        lineDeltas.push(new JetInsert("\n"))
      return lineDeltas
    ), true)
    delta = new JetDelta(0, @length, deltas)
    delta.compact()
    return delta

  updateDirty: ->
    dirtyNodes = @root.ownerDocument.getElementsByClassName(Tandem.Line.DIRTY_CLASS)
    fixLines = false
    _.each(dirtyNodes, (dirtyNode) =>
      line = this.findLine(dirtyNode)
      if line?
        this.updateLine(line)
        fixLines = true if line.attributes['list']?
    )
    TandemDocument.fixListNumbering(@root) if fixLines

  updateLine: (line) ->
    @length -= line.length
    didRebuild = line.rebuild()
    @length += line.length
    return didRebuild

window.Tandem ||= {}
window.Tandem.Document = TandemDocument


!!! 5
%html
  %head
    %title Stypi Editor Test
    %link{:rel => 'stylesheet', :type => 'text/css', :href => '../../../lib/mocha.css'}

%body
  #mocha
  #editor-container

  %script{:type => 'text/javascript', :src => '../../../lib/jquery.js'}
  %script{:type => 'text/javascript', :src => '../../../lib/underscore.js'}
  %script{:type => 'text/javascript', :src => '../../../lib/eventemitter2.js'}
  %script{:type => 'text/javascript', :src => '../../../lib/rangy/rangy-core.js'}
  %script{:type => 'text/javascript', :src => '../../../lib/jetsync.js'}
  %script{:type => 'text/javascript', :src => '../../../lib/linked_list.js'}
  %script{:type => 'text/javascript', :src => '../../src/debug.js'}
  %script{:type => 'text/javascript', :src => '../../src/constants.js'}
  %script{:type => 'text/javascript', :src => '../../src/utils.js'}
  %script{:type => 'text/javascript', :src => '../../src/leaf.js'}
  %script{:type => 'text/javascript', :src => '../../src/line.js'}
  %script{:type => 'text/javascript', :src => '../../src/document.js'}
  %script{:type => 'text/javascript', :src => '../../src/position.js'}
  %script{:type => 'text/javascript', :src => '../../src/range.js'}
  %script{:type => 'text/javascript', :src => '../../src/keyboard.js'}
  %script{:type => 'text/javascript', :src => '../../src/editor.js'}
  %script{:type => 'text/javascript', :src => '../../../lib/mocha.js'}
  %script{:type => 'text/javascript', :src => '../../../lib/chai.js'}
  :javascript
    mocha.setup('bdd');
    expect = chai.expect
  %script{:type => 'text/javascript', :src => 'scripts/editor.js'}
  :javascript
    mocha.run();


!!! 5
%html
  %head
    %title Stypi Dual Editor Demo
    %link{:rel => 'stylesheet', :type => 'text/css', :href => '../lib/reset.css'}
    %link{:rel => 'stylesheet', :type => 'text/css', :href => 'styles/style.css'}
    %link{:rel => 'stylesheet', :type => 'text/css', :href => 'styles/dual.css'}

  %body
    #content-container
      .editor-wrapper.first
        #formatting-container1.formatting-container
          %select.font-family
            %option San Serif
            %option Serif
            %option Monospace
          %select.font-size
            %option Small
            %option Normal
            %option Large
            %option Huge
          %button.format-button.bold B
          %button.format-button.italic I
          %button.format-button.underline U
          %button.format-button.link L
          %button.format-button.list 1.
          %button.format-button.bullet -
          %button.format-button.indent <-
          %button.format-button.outdent ->
        #editor-container1.editor-container
          %div<>
            %span<> Welcome to Stypi!
      .editor-wrapper
        #formatting-container2.formatting-container
          %select.font-family
            %option San Serif
            %option Serif
            %option Monospace
          %select.font-size
            %option Small
            %option Normal
            %option Large
            %option Huge
          %button.format-button.bold B
          %button.format-button.italic I
          %button.format-button.underline U
          %button.format-button.link L
          %button.format-button.list 1.
          %button.format-button.bullet -
          %button.format-button.outdent <-
          %button.format-button.indent ->
        #editor-container2.editor-container
          .line<>
            %span<> Welcome to Stypi!

    %script{:type => 'text/javascript', :src => '../lib/jquery.js'}
    %script{:type => 'text/javascript', :src => '../lib/underscore.js'}
    %script{:type => 'text/javascript', :src => '../lib/eventemitter2.js'}
    %script{:type => 'text/javascript', :src => '../lib/rangy/rangy-core.js'}
    %script{:type => 'text/javascript', :src => '../lib/linked_list.js'}
    %script{:type => 'text/javascript', :src => '../lib/jetsync.js'}
    %script{:type => 'text/javascript', :src => '../src/constants.js'}
    %script{:type => 'text/javascript', :src => '../src/utils.js'}
    %script{:type => 'text/javascript', :src => '../src/leaf.js'}
    %script{:type => 'text/javascript', :src => '../src/line.js'}
    %script{:type => 'text/javascript', :src => '../src/document.js'}
    %script{:type => 'text/javascript', :src => '../src/position.js'}
    %script{:type => 'text/javascript', :src => '../src/range.js'}
    %script{:type => 'text/javascript', :src => '../src/keyboard.js'}
    %script{:type => 'text/javascript', :src => '../src/editor.js'}
    %script{:type => 'text/javascript', :src => '../src/toolbar.js'}
    %script{:type => 'text/javascript', :src => '../src/debug.js'}
    %script{:type => 'text/javascript', :src => 'scripts/dual.js'}


@charset "UTF-8";
body {
  font: 20px/1.5 "Helvetica Neue", Helvetica, Arial, sans-serif;
  padding: 60px 50px;
}

#mocha ul, #mocha li {
  margin: 0;
  padding: 0;
}

#mocha ul {
  list-style: none;
}

#mocha h1, #mocha h2 {
  margin: 0;
}

#mocha h1 {
  margin-top: 15px;
  font-size: 1em;
  font-weight: 200;
}

#mocha h1 a {
  text-decoration: none;
  color: inherit;
}

#mocha h1 a:hover {
  text-decoration: underline;
}

#mocha .suite .suite h1 {
  margin-top: 0;
  font-size: .8em;
}

#mocha h2 {
  font-size: 12px;
  font-weight: normal;
  cursor: pointer;
}

#mocha .suite {
  margin-left: 15px;
}

#mocha .test {
  margin-left: 15px;
}

#mocha .test:hover h2::after {
  position: relative;
  top: 0;
  right: -10px;
  content: '(view source)';
  font-size: 12px;
  font-family: arial;
  color: #888;
}

#mocha .test.pending:hover h2::after {
  content: '(pending)';
  font-family: arial;
}

#mocha .test.pass.medium .duration {
  background: #C09853;
}

#mocha .test.pass.slow .duration {
  background: #B94A48;
}

#mocha .test.pass::before {
  content: '✓';
  font-size: 12px;
  display: block;
  float: left;
  margin-right: 5px;
  color: #00d6b2;
}

#mocha .test.pass .duration {
  font-size: 9px;
  margin-left: 5px;
  padding: 2px 5px;
  color: white;
  -webkit-box-shadow: inset 0 1px 1px rgba(0,0,0,.2);
  -moz-box-shadow: inset 0 1px 1px rgba(0,0,0,.2);
  box-shadow: inset 0 1px 1px rgba(0,0,0,.2);
  -webkit-border-radius: 5px;
  -moz-border-radius: 5px;
  -ms-border-radius: 5px;
  -o-border-radius: 5px;
  border-radius: 5px;
}

#mocha .test.pass.fast .duration {
  display: none;
}

#mocha .test.pending {
  color: #0b97c4;
}

#mocha .test.pending::before {
  content: '◦';
  color: #0b97c4;
}

#mocha .test.fail {
  color: #c00;
}

#mocha .test.fail pre {
  color: black;
}

#mocha .test.fail::before {
  content: '✖';
  font-size: 12px;
  display: block;
  float: left;
  margin-right: 5px;
  color: #c00;
}

#mocha .test pre.error {
  color: #c00;
}

#mocha .test pre {
  display: inline-block;
  font: 12px/1.5 monaco, monospace;
  margin: 5px;
  padding: 15px;
  border: 1px solid #eee;
  border-bottom-color: #ddd;
  -webkit-border-radius: 3px;
  -webkit-box-shadow: 0 1px 3px #eee;
}

#report.pass .test.fail {
  display: none;
}

#report.fail .test.pass {
  display: none;
}

#error {
  color: #c00;
  font-size: 1.5  em;
  font-weight: 100;
  letter-spacing: 1px;
}

#stats {
  position: fixed;
  top: 15px;
  right: 10px;
  font-size: 12px;
  margin: 0;
  color: #888;
}

#stats .progress {
  float: right;
  padding-top: 0;
}

#stats em {
  color: black;
}

#stats a {
  text-decoration: none;
  color: inherit;
}

#stats a:hover {
  border-bottom: 1px solid #eee;
}

#stats li {
  display: inline-block;
  margin: 0 5px;
  list-style: none;
  padding-top: 11px;
}

code .comment { color: #ddd }
code .init { color: #2F6FAD }
code .string { color: #5890AD }
code .keyword { color: #8A6343 }
code .number { color: #2F6FAD }


this.emit(TandemEditor.events.API_TEXT_CHANGE, delta) if emitEvent


body
  font-family: 'Helvetica', 'Arial', san-serif
  font-size: 13px
  padding: 25px

#content-container
  margin: auto
  width: 960px

.formatting-container
  background-color: #f5f5f5
  border-bottom: 1px solid #ccc
  height: 32px
  line-height: 32px
  padding: 0px 5px
  .active
    color: green
    font-weight: bold

.editor-container
  height: 600px

.editor-wrapper
  border: 1px solid #aaa
  box-shadow: 0 0 2px 2px #ddd


!!! 5
%html
  %head
    %title Stypi Editor Normalize Test
    %link{:rel => 'stylesheet', :type => 'text/css', :href => '../../../lib/mocha.css'}

%body
  #mocha
  #editor-container

  %script{:type => 'text/javascript', :src => '../../../lib/jquery.js'}
  %script{:type => 'text/javascript', :src => '../../../lib/underscore.js'}
  %script{:type => 'text/javascript', :src => '../../../lib/eventemitter2.js'}
  %script{:type => 'text/javascript', :src => '../../../lib/rangy/rangy-core.js'}
  %script{:type => 'text/javascript', :src => '../../../lib/jetsync.js'}
  %script{:type => 'text/javascript', :src => '../../../lib/linked_list.js'}
  %script{:type => 'text/javascript', :src => '../../src/debug.js'}
  %script{:type => 'text/javascript', :src => '../../src/constants.js'}
  %script{:type => 'text/javascript', :src => '../../src/utils.js'}
  %script{:type => 'text/javascript', :src => '../../src/leaf.js'}
  %script{:type => 'text/javascript', :src => '../../src/line.js'}
  %script{:type => 'text/javascript', :src => '../../src/document.js'}
  %script{:type => 'text/javascript', :src => '../../src/position.js'}
  %script{:type => 'text/javascript', :src => '../../src/range.js'}
  %script{:type => 'text/javascript', :src => '../../src/keyboard.js'}
  %script{:type => 'text/javascript', :src => '../../src/editor.js'}
  %script{:type => 'text/javascript', :src => '../../../lib/mocha.js'}
  %script{:type => 'text/javascript', :src => '../../../lib/chai.js'}
  :javascript
    mocha.setup('bdd');
    expect = chai.expect
  %script{:type => 'text/javascript', :src => 'scripts/normalize.js'}
  :javascript
    mocha.run();


#= require underscore
#= require rangy/rangy-core
#= require diff_match_patch
#= require eventemitter2
#= require tandem/document
#= require tandem/range
#= require tandem/keyboard

class TandemEditor extends EventEmitter2
  @editors: []

  @CONTAINER_ID: 'tandem-container'
  @ID_PREFIX: 'editor-'

  @events:
    API_TEXT_CHANGE       : 'api-text-change'
    USER_SELECTION_CHANGE : 'user-selection-change'
    USER_TEXT_CHANGE      : 'user-text-change'

  options:
    POLL_INTERVAL: 500

  constructor: (@container, enabled = true) ->
    @id = _.uniqueId(TandemEditor.ID_PREFIX)
    @container = document.getElementById(@container) if _.isString(@container)
    this.reset(true)
    this.enable() if enabled

  reset: (keepHTML = false) ->
    @ignoreDomChanges = true
    @iframe = this.createIframe(@container, keepHTML)
    @doc = new Tandem.Document(this, @iframe.contentWindow.document.getElementById(TandemEditor.CONTAINER_ID))
    @currentSelection = null
    @keyboard = new Tandem.Keyboard(this)
    this.initContentListeners()
    this.initSelectionListeners()
    @ignoreDomChanges = false
    TandemEditor.editors.push(this)

  createIframe: (parent, keepHTML) ->
    html = parent.innerHTML
    parent.innerHTML = ''
    iframe = document.createElement('iframe')
    iframe.frameborder = 0
    iframe.src = 'javascript:;'
    iframe.height = iframe.width = '100%'
    parent.appendChild(iframe)
    doc = iframe.contentWindow.document
    head = doc.getElementsByTagName('head')[0]
    style = doc.createElement('style')
    style.type = 'text/css'
    css = "
      body {
        font-family: 'Helvetica', 'Arial', san-serif;
        font-size: 13px;
        line-height: 15px;
        margin: 0px;
        padding: 10px 15px;
        white-space: pre;
      }

      ##{TandemEditor.CONTAINER_ID} { outline: none; }

      a { cursor: pointer; text-decoration: underline; }
      b { font-weight: bold; }
      i { font-style: italic; }
      s { text-decoration: line-through; }
      u { text-decoration: underline; }

      ol, ul { margin: 0px; padding: 0px; }
      ul { list-style-type: disc; }

      ol.indent-1, ol.indent-4, ol.indent-7 { list-style-type: decimal; }
      ol.indent-2, ol.indent-5, ol.indent-8 { list-style-type: lower-alpha; }
      ol.indent-3, ol.indent-6, ol.indent-9 { list-style-type: lower-roman; }

      .font-background.black  { background-color: black; }
      .font-background.red    { background-color: red; }
      .font-background.orange { background-color: orange; }
      .font-background.yellow { background-color: yellow; }
      .font-background.green  { background-color: green; }
      .font-background.blue   { background-color: blue; }
      .font-background.purple { background-color: purple; }
      .font-color.white       { color: white; }
      .font-color.red         { color: red; }
      .font-color.orange      { color: orange; }
      .font-color.yellow      { color: yellow; }
      .font-color.green       { color: green; }
      .font-color.blue        { color: blue; }
      .font-color.purple      { color: purple; }
      .font-family.monospace  { font-family: 'Courier New', monospace; }
      .font-family.serif      { font-family: 'Times New Roman', serif; }
      .font-size.huge         { font-size: 32px; line-height: 36px; }
      .font-size.large        { font-size: 18px; line-height: 22px; }
      .font-size.small        { font-size: 10px; line-height: 12px; }

      .indent-1 { margin-left: 2em; }
      .indent-2 { margin-left: 4em; }
      .indent-3 { margin-left: 6em; }
      .indent-4 { margin-left: 8em; }
      .indent-5 { margin-left: 10em; }
      .indent-6 { margin-left: 12em; }
      .indent-7 { margin-left: 14em; }
      .indent-8 { margin-left: 16em; }
      .indent-9 { margin-left: 18em; }

      .tab { display: inline-block; margin: 0px; }
    "
    if style.styleSheet?
      style.styleSheet.cssText = css
    else
      style.appendChild(doc.createTextNode(css))
    head.appendChild(style)
    contentContainer = doc.createElement('div')
    contentContainer.id = Tandem.Editor.CONTAINER_ID
    doc.body.appendChild(contentContainer)
    contentContainer.innerHTML = html if keepHTML
    return iframe

  disable: ->
    @doc.root.setAttribute('contenteditable', false)

  enable: ->
    if !@doc.root.getAttribute('contenteditable')
      @doc.root.setAttribute('contenteditable', true)
      @doc.root.focus()
      position = Tandem.Position.makePosition(this, 0)
      start = new Tandem.Range(this, position, position)
      this.setSelection(start)

  initContentListeners: ->
    onEdit = _.debounce( =>
      delta = this.update()
      if !delta.isIdentity()
        this.checkSelectionChange()
        this.setSelection(@currentSelection)
        this.emit(TandemEditor.events.USER_TEXT_CHANGE, delta)
    , 100)
    @doc.root.addEventListener('DOMSubtreeModified', =>
      return if @ignoreDomChanges
      onEdit()
    )

  initSelectionListeners: ->
    debounceCheckSelectionChange = _.debounce( =>
      this.checkSelectionChange()
    , 100)
    @doc.root.addEventListener('keyup', debounceCheckSelectionChange)
    @doc.root.addEventListener('mouseup', debounceCheckSelectionChange)
    setInterval((=> this.checkSelectionChange()), this.options.POLL_INTERVAL)

  # applyAttribute: (TandemRange range, String attr, Mixed value) ->
  # applyAttribute: (Number startIndex, Number length, String attr, Mixed value) ->
  applyAttribute: (startIndex, length, attr, value, emitEvent = true) ->
    delta = this.trackDelta( =>
      if !_.isNumber(startIndex)
        [range, attr, value] = [startIndex, length, attr]
        startIndex = range.start.getIndex()
        length = range.end.getIndex() - startIndex
      else
        range = new Tandem.Range(this, startIndex, startIndex + length)
      this.preserveSelection(range.start, 0, =>
        [startLine, startLineOffset] = Tandem.Utils.getChildAtOffset(@doc.root, startIndex)
        [endLine, endLineOffset] = Tandem.Utils.getChildAtOffset(@doc.root, startIndex + length)
        if startLine == endLine
          this.applyAttributeToLine(startLine, startLineOffset, endLineOffset, attr, value)
        else
          curLine = startLine.nextSibling
          while curLine? && curLine != endLine
            nextSibling = curLine.nextSibling
            this.applyAttributeToLine(curLine, 0, curLine.textContent.length, attr, value)
            curLine = nextSibling
          this.applyAttributeToLine(startLine, startLineOffset, startLine.textContent.length, attr, value)
          this.applyAttributeToLine(endLine, 0, endLineOffset, attr, value)
        @doc.rebuildDirty()
      )
    , emitEvent)
    this.emit(TandemEditor.events.API_TEXT_CHANGE, delta) if emitEvent

  applyAttributeToLine: (lineNode, startOffset, endOffset, attr, value) ->
    line = @doc.findLine(lineNode)
    if _.indexOf(Tandem.Constants.LINE_ATTRIBUTES, attr, true) > -1
      this.applyLineAttribute(line, attr, value)
    else
      return if startOffset == endOffset
      [prevNode, startNode] = line.splitContents(startOffset)
      [endNode, nextNode] = line.splitContents(endOffset)
      parentNode = startNode.parentNode
      if value && Tandem.Utils.getAttributeDefault(attr) != value
        fragment = @doc.root.ownerDocument.createDocumentFragment()
        Tandem.Utils.traverseSiblings(startNode, endNode, (node) ->
          node = Tandem.Utils.removeAttributeFromSubtree(node, attr)
          fragment.appendChild(node)
        )
        attrNode = Tandem.Utils.createContainerForAttribute(@doc.root.ownerDocument, attr, value)
        attrNode.appendChild(fragment)
        parentNode.insertBefore(attrNode, nextNode)
      else
        Tandem.Utils.traverseSiblings(startNode, endNode, (node) ->
          Tandem.Utils.removeAttributeFromSubtree(node, attr)
        )
    @doc.updateLine(line)
    Tandem.Document.fixListNumbering(@doc.root) if attr == 'list'

  applyDelta: (delta) ->
    console.assert(delta.startLength == @doc.length, "Trying to apply delta to incorrect doc length", delta, @doc, @doc.root)
    index = 0       # Stores where the last retain end was, so if we see another one, we know to delete
    offset = 0      # Tracks how many characters inserted to correctly offset new text
    oldDelta = @doc.toDelta()
    _.each(delta.deltas, (delta) =>
      if JetDelta.isInsert(delta)
        this.insertAt(index + offset, delta.text, false)
        _.each(delta.attributes, (value, attr) =>
          this.applyAttribute(index + offset, delta.text.length, attr, value, false)
        )
        offset += delta.getLength()
      else if JetDelta.isRetain(delta)
        if delta.start > index
          this.deleteAt(index + offset, delta.start - index, false)
          offset -= (delta.start - index)
        deltasInRange = oldDelta.getDeltasAt(delta)
        deltaOffset = 0
        _.each(deltasInRange, (deltaInRange) =>
          attributes = _.extend({}, deltaInRange.attributes, delta.attributes)
          _.each(attributes, (value, attr) =>
            this.applyAttribute(delta.start + offset + deltaOffset, deltaInRange.text.length, attr, value, false)
          )
          deltaOffset += deltaInRange.text.length
        )
        index = delta.end
      else
        console.warn('Unrecognized type in delta', delta)
    )
    # If end of text was deleted
    if delta.endLength < delta.startLength + offset
      this.deleteAt(delta.endLength, delta.startLength + offset - delta.endLength, false)
    newDelta = @doc.toDelta()
    composed = JetSync.compose(oldDelta, delta)
    composed.compact()
    console.assert(_.isEqual(composed, newDelta), oldDelta, delta, composed, newDelta)

  applyLineAttribute: (line, attr, value) ->
    indent = if _.isNumber(value) then value else (if value then 1 else 0)
    if attr == 'indent'
      Tandem.Utils.setIndent(line.node, indent)
    else if Tandem.Constants.INDENT_ATTRIBUTES[attr]?
      lineNode = line.node
      expectedTag = if value then (if attr == 'list' then 'OL' else 'UL') else 'DIV'
      if lineNode.tagName != expectedTag
        if value && lineNode.firstChild.tagName != 'LI'
          li = lineNode.ownerDocument.createElement('li')
          Tandem.Utils.wrapChildren(li, lineNode)
        else if !value && lineNode.firstChild.tagName == 'LI'
          Tandem.Utils.unwrap(lineNode.firstChild)
        line.node = Tandem.Utils.switchTag(lineNode, expectedTag)
      Tandem.Utils.setIndent(line.node, indent)
    line.setDirty()

  checkSelectionChange: ->
    return if @ignoreDomChanges
    selection = this.getSelection()
    if selection != @currentSelection && ((selection? && !selection.equals(@currentSelection)) || !@currentSelection.equals(selection))
      this.emit(TandemEditor.events.USER_SELECTION_CHANGE, selection)
      @currentSelection = selection

  deleteAt: (startIndex, length, emitEvent = true) ->
    delta = this.trackDelta( =>
      if !_.isNumber(startIndex)
        range = startIndex
        startPos = range.start
        endPos = range.end
        startIndex = range.start.getIndex()
        length = range.end.getIndex() - startIndex
      else
        startPos = Tandem.Position.makePosition(this, startIndex)
        endPos = Tandem.Position.makePosition(this, startIndex + length)
      startIndex = startPos.getIndex()
      endIndex = endPos.getIndex()
      this.preserveSelection(startPos, 0 - length, =>
        [startLineNode, startOffset] = Tandem.Utils.getChildAtOffset(@doc.root, startIndex)
        [endLineNode, endOffset] = Tandem.Utils.getChildAtOffset(@doc.root, endIndex)
        fragment = Tandem.Utils.extractNodes(startLineNode, startOffset, endLineNode, endOffset)
        lineNodes = _.values(fragment.childNodes).concat(_.uniq([startLineNode, endLineNode]))
        _.each(lineNodes, (lineNode) =>
          line = @doc.findLine(lineNode)
          @doc.updateLine(line) if line?
        )
      )
    , emitEvent)
    this.emit(TandemEditor.events.API_TEXT_CHANGE, delta) if emitEvent

  getAt: (startIndex, length) ->
    # - Returns array of {text: "", attr: {}}
    # 1. Get all nodes in the range
    # 2. For first and last, change the text
    # 3. Return array
    # - Helper to get nodes in given index range
    # - In the case of 0 lenght, text will always be "", but attributes should be properly applied

  getDelta: ->
    return @doc.toDelta()

  getSelection: ->
    return Tandem.Range.getSelection(this)

  insertAt: (startIndex, text, emitEvent = true) ->
    delta = this.trackDelta( =>
      position = Tandem.Position.makePosition(this, startIndex)
      index = startIndex = position.getIndex()
      startLine = @doc.findLineAtOffset(index)
      attr = if startLineNode? then startLine.attributes else {}
      this.preserveSelection(position, text.length, =>
        lines = text.split("\n")
        _.each(lines, (line, lineIndex) =>
          strings = line.split("\t")
          _.each(strings, (str, strIndex) =>
            this.insertTextAt(index, str)
            index += str.length
            if strIndex < strings.length - 1
              this.insertTabAt(index)
              index += 1
          )
          if lineIndex < lines.length - 1
            this.insertNewlineAt(index)
            index += 1
          else
            # TODO could be more clever about if we need to call this
            Tandem.Document.fixListNumbering(@doc.root)
        )
      )
    , emitEvent)
    this.emit(TandemEditor.events.API_TEXT_CHANGE, delta) if emitEvent

  insertNewlineAt: (startIndex) ->
    [line, offset] = @doc.findLineAtOffset(startIndex)
    newLine = @doc.splitLine(line, offset)
    # TODO we need to do this for each line attribute... find more elegant solution
    this.applyLineAttribute(newLine, 'list', false) if line.attributes.list
    this.applyLineAttribute(newLine, 'bullet', false) if line.attributes.bullet
    this.applyLineAttribute(newLine, 'indent', false) if line.attributes.indent

  insertTabAt: (startIndex) ->
    [startLineNode, startLineOffset] = Tandem.Utils.getChildAtOffset(@doc.root, startIndex)
    startLine = @doc.findLine(startLineNode)
    [prevNode, startNode] = startLine.splitContents(startLineOffset)
    tab = startLineNode.ownerDocument.createElement('span')
    tab.classList.add(Tandem.Leaf.TAB_NODE_CLASS)
    parentNode = prevNode?.parentNode || startNode?.parentNode
    parentNode.insertBefore(tab, startNode)
    @doc.updateLine(startLine)

  # insertTextAt: (Number startIndex, String text) ->
  # insertTextAt: (TandemRange startIndex, String text) ->
  insertTextAt: (startIndex, text) ->
    return if text.length <= 0
    position = Tandem.Position.makePosition(this, startIndex)
    startIndex = position.getIndex()
    leaf = position.getLeaf()
    if _.keys(leaf.attributes).length > 0 || Tandem.Utils.isIgnoreNode(leaf.node)
      [lineNode, lineOffset] = Tandem.Utils.getChildAtOffset(@doc.root, startIndex)
      [beforeNode, afterNode] = leaf.line.splitContents(lineOffset)
      parentNode = beforeNode?.parentNode || afterNode?.parentNode
      span = lineNode.ownerDocument.createElement('span')
      span.textContent = text
      parentNode.insertBefore(span, afterNode)
    else
      if leaf.node.nodeName == 'BR'
        parent = leaf.node.parentNode
        parent.removeChild(leaf.node)
        leaf.node = parent.ownerDocument.createElement('span')
        leaf.setText(text)
        parent.appendChild(leaf.node)
      else
        leaf.setText(leaf.node.textContent.substr(0, position.offset) + text + leaf.node.textContent.substr(position.offset))
    @doc.updateLine(leaf.line)

  preserveSelection: (modPosition, charAdditions, fn) ->
    selection = Tandem.Range.getSelection(this)
    if selection?
      isBodyChild = (node) -> return node.parentNode?.id == TandemEditor.CONTAINER_ID
      isBlockTag = (node) -> return _.indexOf(Tandem.Constants.BLOCK_TAGS, node.tagName, true) > -1
      startBlock = Tandem.Utils.findAncestor(selection.start.leafNode, isBlockTag)
      endBlock = Tandem.Utils.findAncestor(selection.end.leafNode, isBlockTag)
      return fn() if !startBlock? || !endBlock?
      startLine = Tandem.Utils.findAncestor(startBlock, isBodyChild)
      endLine = Tandem.Utils.findAncestor(endBlock, isBodyChild)
      startBlockOffset = Tandem.Position.getIndex(selection.start.leafNode, selection.start.offset, startBlock)
      endBlockOffset = Tandem.Position.getIndex(selection.end.leafNode, selection.end.offset, endBlock)
      startLineOffset = Tandem.Position.getIndex(startBlock, 0, startLine) + startBlockOffset
      endLineOffset = Tandem.Position.getIndex(endBlock, 0, endLine) + endBlockOffset
      fn()
      if startBlock.parentNode?
        startPos = new Tandem.Position(this, startBlock, startBlockOffset)
      else
        startLine = startLine.ownerDocument.getElementById(startLine.id) if startLine.parentNode == null
        startPos = new Tandem.Position(this, startLine, startLineOffset)
      if endBlock.parentNode?
        endPos = new Tandem.Position(this, startBlock, startBlockOffset)
      else
        endLine = endLine.ownerDocument.getElementById(endLine.id) if endLine.parentNode == null
        endPos = new Tandem.Position(this, endLine, endLineOffset)
      savedSelectionRange = new Tandem.Range(this, startPos, endPos)
      [selStart, selEnd] = this.transformSelection(modPosition, savedSelectionRange, charAdditions)
      savedSelectionRange = new Tandem.Range(this, Tandem.Position.makePosition(this, selStart), Tandem.Position.makePosition(this, selEnd))
      this.setSelection(savedSelectionRange)
      this.checkSelectionChange()
    else
      fn()

  setSelection: (range) ->
    Tandem.Range.setSelection(this, range)

  # TODO use node.compareDocumentPosition for efficiency
  transformSelection: (modPosition, selectionRange, charAdditions) ->
    modPos = if modPosition? then modPosition.getIndex() else 0
    selStart = selectionRange.start.getIndex()
    selEnd = selectionRange.end.getIndex()
    selStart = Math.max(selStart + charAdditions, modPos) if modPos <= selStart
    selEnd = Math.max(selEnd + charAdditions, modPos) if modPos < selEnd
    selEnd = Math.max(selStart, selEnd)
    return [selStart, selEnd]

  trackDelta: (fn, track = true) ->
    oldIgnoreDomChange = @ignoreDomChanges
    @ignoreDomChanges = true
    delta = null
    if track
      oldDelta = @doc.toDelta()
      fn()
      newDelta = @doc.toDelta()
      decompose = JetSync.decompose(oldDelta, newDelta)
      compose = JetSync.compose(oldDelta, decompose)
      console.assert(_.isEqual(compose, newDelta), oldDelta, newDelta, decompose, compose)
      delta = decompose
    else
      fn()
    @ignoreDomChanges = oldIgnoreDomChange
    return delta

  update: ->
    return this.trackDelta( =>
      this.preserveSelection(null, 0, =>
        Tandem.Document.normalizeHtml(@doc.root)
        lines = @doc.lines.toArray()
        lineNode = @doc.root.firstChild
        _.each(lines, (line, index) =>
          while line.node != lineNode
            if line.node.parentNode == @doc.root
              newLine = @doc.insertLineBefore(lineNode, line)
              lineNode = lineNode.nextSibling
            else
              @doc.removeLine(line)
              return
          @doc.updateLine(line)
          lineNode = lineNode.nextSibling
        )
        while lineNode != null
          newLine = @doc.appendLine(lineNode)
          lineNode = lineNode.nextSibling
      )
    , true)
    return

window.Tandem ||= {}
window.Tandem.Editor = TandemEditor


#= require underscore
#= require jquery

listenEditor = (source, target) ->
  source.on(Tandem.Editor.events.API_TEXT_CHANGE, (delta) ->
    target.applyDelta(delta)
    sourceHtml = Tandem.Utils.cleanHtml(source.doc.root.innerHTML)
    targetHtml = Tandem.Utils.cleanHtml(target.doc.root.innerHTML)
    console.assert(sourceHtml == targetHtml, "Editor diversion!", source, target)
  )
  source.on(Tandem.Editor.events.USER_TEXT_CHANGE, (delta) ->
    target.applyDelta(delta)
    sourceHtml = Tandem.Utils.cleanHtml(source.doc.root.innerHTML)
    targetHtml = Tandem.Utils.cleanHtml(target.doc.root.innerHTML)
    console.assert(sourceHtml == targetHtml, "Editor diversion!", source, target)
  )

editors = _.map([1, 2], (num) ->
  editor = new Tandem.Editor('editor-container' + num)
  toolbar = new Tandem.Toolbar(editor)
  _.each(['bold', 'italic', 'strike', 'underline', 'bullet', 'list', 'indent', 'outdent'], (format) ->
    $("#formatting-container#{num} .#{format}").click( ->
      toolbar.applyAttribute(format, !$(this).hasClass('active'))
    )
  )
  _.each(['font-family', 'font-size'], (format) ->
    $("#formatting-container#{num} .#{format}").change( ->
      toolbar.applyAttribute(format, $(this).val())
    )
  )
  $("#formatting-container#{num} .link").click( ->
    if ($(this).hasClass('active'))
      toolbar.applyAttribute('link', false)
    else
      toolbar.applyAttribute('link', 'http://www.google.com')
  )
  toolbar.on('update', (attributes) ->
    $("#formatting-container#{num} .format-button").removeClass('active')
    for key,value of attributes when value
      container = $("#formatting-container#{num} .#{key}")
      if container.is('button')
        container.addClass('active')
      else
        container.val(value)
  )
  return editor
)

listenEditor(editors[0], editors[1])
listenEditor(editors[1], editors[0])