text: 'use strict'
  var escapeHTML = require('../Utils').escapeHTML
  
  var cmdHelper = require('../helper/CmdHelper')
  
  var domQuery = require('min-dom').query,
    domAttr = require('min-dom').attr,
    domClosest = require('min-dom').closest
  
  var filter = require('lodash/filter'),
    forEach = require('lodash/forEach'),
    keys = require('lodash/keys')
  
  var domify = require('min-dom').domify
  
  var entryFieldDescription = require('./EntryFieldDescription')
  
  var updateSelection = require('selection-update')
  
  var TABLE_ROW_DIV_SNIPPET = '<div class="bpp-field-wrapper bpp-table-row">'
  var DELETE_ROW_BUTTON_SNIPPET = '<button class="clear" data-action="deleteElement">' + '<span>X</span>' + '</button>'
  
  function createInputRowTemplate(properties, canRemove) {
    var template = TABLE_ROW_DIV_SNIPPET
    template += createInputTemplate(properties, canRemove)
    template += canRemove ? DELETE_ROW_BUTTON_SNIPPET : ''
    template += '</div>'
  
    return template
  }
  
  function createInputTemplate(properties, canRemove) {
    var columns = properties.length
    var template = ''
    forEach(properties, function (prop) {
      template +=
        '<input class="bpp-table-row-columns-' +
        columns +
        ' ' +
        (canRemove ? 'bpp-table-row-removable' : '') +
        '" ' +
        'id="activiti-table-row-cell-input-value" ' +
        'type="text" ' +
        'name="' +
        escapeHTML(prop) +
        '" />'
    })
    return template
  }
  
  function createLabelRowTemplate(labels) {
    var template = TABLE_ROW_DIV_SNIPPET
    template += createLabelTemplate(labels)
    template += '</div>'
  
    return template
  }
  
  function createLabelTemplate(labels) {
    var columns = labels.length
    var template = ''
    forEach(labels, function (label) {
      template += '<label class="bpp-table-row-columns-' + columns + '">' + escapeHTML(label) + '</label>'
    })
    return template
  }
  
  function pick(elements, properties) {
    return (elements || []).map(function (elem) {
      var newElement = {}
      forEach(properties, function (prop) {
        newElement[prop] = elem[prop] || ''
      })
      return newElement
    })
  }
  
  function diff(element, node, values, oldValues, editable) {
    return filter(values, function (value, idx) {
      return !valueEqual(element, node, value, oldValues[idx], editable, idx)
    })
  }
  
  function valueEqual(element, node, value, oldValue, editable, idx) {
    if (value && !oldValue) {
      return false
    }
    var allKeys = keys(value).concat(keys(oldValue))
  
    return allKeys.every(function (key) {
      var n = value[key] || undefined
      var o = oldValue[key] || undefined
      return !editable(element, node, key, idx) || n === o
    })
  }
  
  function getEntryNode(node) {
    return domClosest(node, '[data-entry]', true)
  }
  
  function getContainer(node) {
    return domQuery('div[data-list-entry-container]', node)
  }
  
  function getSelection(node) {
    return {
      start: node.selectionStart,
      end: node.selectionEnd
    }
  }
  
  function setSelection(node, selection) {
    node.selectionStart = selection.start
    node.selectionEnd = selection.end
  }
  
  /**
   * @param  {Object} options
   * @param  {string} options.id
   * @param  {string} options.description
   * @param  {Array<string>} options.modelProperties
   * @param  {Array<string>} options.labels
   * @param  {Function} options.getElements - this callback function must return a list of business object items
   * @param  {Function} options.removeElement
   * @param  {Function} options.addElement
   * @param  {Function} options.updateElement
   * @param  {Function} options.editable
   * @param  {Function} options.setControlValue
   * @param  {Function} options.show
   *
   * @return {Object}
   */
  module.exports = function (options) {
    var id = options.id,
      modelProperties = options.modelProperties,
      labels = options.labels,
      description = options.description
  
    var labelRow = createLabelRowTemplate(labels)
  
    var getElements = options.getElements
  
    var removeElement = options.removeElement,
      canRemove = typeof removeElement === 'function'
  
    var addElement = options.addElement,
      canAdd = typeof addElement === 'function',
      addLabel = options.addLabel || 'Add Value'
  
    var updateElement = options.updateElement,
      canUpdate = typeof updateElement === 'function'
  
    var editable =
        options.editable ||
        function () {
          return true
        },
      setControlValue = options.setControlValue
  
    var show = options.show,
      canBeShown = typeof show === 'function'
  
    var elements = function (element, node) {
      return pick(getElements(element, node), modelProperties)
    }
  
    var factory = {
      id: id,
      html:
        (canAdd
          ? '<div class="bpp-table-add-row" ' +
            (canBeShown ? 'data-show="show"' : '') +
            '>' +
            '<label>' +
            escapeHTML(addLabel) +
            '</label>' +
            '<button class="add" data-action="addElement"><span>+</span></button>' +
            '</div>'
          : '') +
        '<div class="bpp-table" data-show="showTable">' +
        '<div class="bpp-field-wrapper bpp-table-row">' +
        labelRow +
        '</div>' +
        '<div data-list-entry-container>' +
        '</div>' +
        '</div>' +
        // add description below table entry field
        (description ? entryFieldDescription(description) : ''),
  
      get: function (element, node) {
        var boElements = elements(element, node, this.__invalidValues)
  
        var invalidValues = this.__invalidValues
  
        delete this.__invalidValues
  
        forEach(invalidValues, function (value, idx) {
          var element = boElements[idx]
  
          forEach(modelProperties, function (prop) {
            element[prop] = value[prop]
          })
        })
  
        return boElements
      },
  
      set: function (element, values, node) {
        var action = this.__action || {}
        delete this.__action
  
        if (action.id === 'delete-element') {
          return removeElement(element, node, action.idx)
        } else if (action.id === 'add-element') {
          return addElement(element, node)
        } else if (canUpdate) {
          var commands = [],
            valuesToValidate = values
  
          if (typeof options.validate !== 'function') {
            valuesToValidate = diff(element, node, values, elements(element, node), editable)
          }
  
          var self = this
  
          forEach(valuesToValidate, function (value) {
            var validationError,
              idx = values.indexOf(value)
  
            if (typeof options.validate === 'function') {
              validationError = options.validate(element, value, node, idx)
            }
  
            if (!validationError) {
              var cmd = updateElement(element, value, node, idx)
  
              if (cmd) {
                commands.push(cmd)
              }
            } else {
              // cache invalid value in an object by index as key
              self.__invalidValues = self.__invalidValues || {}
              self.__invalidValues[idx] = value
  
              // execute a command, which does not do anything
              commands.push(cmdHelper.updateProperties(element, {}))
            }
          })
  
          return commands
        }
      },
      createListEntryTemplate: function (value, index, selectBox) {
        return createInputRowTemplate(modelProperties, canRemove)
      },
  
      addElement: function (element, node, event, scopeNode) {
        var template = domify(createInputRowTemplate(modelProperties, canRemove))
  
        var container = getContainer(node)
        container.appendChild(template)
  
        this.__action = {
          id: 'add-element'
        }
  
        return true
      },
  
      deleteElement: function (element, node, event, scopeNode) {
        var container = getContainer(node)
        var rowToDelete = event.delegateTarget.parentNode
        var idx = parseInt(domAttr(rowToDelete, 'data-index'), 10)
  
        container.removeChild(rowToDelete)
  
        this.__action = {
          id: 'delete-element',
          idx: idx
        }
  
        return true
      },
  
      editable: function (element, rowNode, input, prop, value, idx) {
        var entryNode = domClosest(rowNode, '[data-entry]')
        return editable(element, entryNode, prop, idx)
      },
  
      show: function (element, entryNode, node, scopeNode) {
        entryNode = getEntryNode(entryNode)
        return show(element, entryNode, node, scopeNode)
      },
  
      showTable: function (element, entryNode, node, scopeNode) {
        entryNode = getEntryNode(entryNode)
        var elems = elements(element, entryNode)
        return elems && elems.length && (!canBeShown || show(element, entryNode, node, scopeNode))
      },
  
      validateListItem: function (element, value, node, idx) {
        if (typeof options.validate === 'function') {
          return options.validate(element, value, node, idx)
        }
      }
    }
  
    // Update/set the selection on the correct position.
    // It's the same code like for an input value in the PropertiesPanel.js.
    if (setControlValue) {
      factory.setControlValue = function (element, rowNode, input, prop, value, idx) {
        var entryNode = getEntryNode(rowNode)
  
        var isReadOnly = domAttr(input, 'readonly')
        var oldValue = input.value
  
        var selection
  
        // prevents input fields from having the value 'undefined'
        if (value === undefined) {
          value = ''
        }
  
        // when the attribute 'readonly' exists, ignore the comparison
        // with 'oldValue' and 'value'
        if (!!isReadOnly && oldValue === value) {
          return
        }
  
        // update selection on undo/redo
        if (document.activeElement === input) {
          selection = updateSelection(getSelection(input), oldValue, value)
        }
  
        setControlValue(element, entryNode, input, prop, value, idx)
  
        if (selection) {
          setSelection(input, selection)
        }
      }
    }
  
    return factory
  }
  