text: "use strict"
  
  var wcwidth = require('./width')
  var utils = require('./utils')
  var padRight = utils.padRight
  var padCenter = utils.padCenter
  var padLeft = utils.padLeft
  var splitIntoLines = utils.splitIntoLines
  var splitLongWords = utils.splitLongWords
  var truncateString = utils.truncateString
  
  var DEFAULTS = {
    maxWidth: Infinity,
    minWidth: 0,
    columnSplitter: ' ',
    truncate: false,
    truncateMarker: '…',
    preserveNewLines: false,
    paddingChr: ' ',
    showHeaders: true,
    headingTransform: function(key) {
      return key.toUpperCase()
    },
    dataTransform: function(cell, column, index) {
      return cell
    }
  }
  
  module.exports = function(items, options) {
  
    options = options || {}
  
    var columnConfigs = options.config || {}
    delete options.config // remove config so doesn't appear on every column.
  
    var maxLineWidth = options.maxLineWidth || Infinity
    delete options.maxLineWidth // this is a line control option, don't pass it to column
  
    // Option defaults inheritance:
    // options.config[columnName] => options => DEFAULTS
    options = mixin(options, DEFAULTS)
    options.config = options.config || Object.create(null)
  
    options.spacing = options.spacing || '\n' // probably useless
    options.preserveNewLines = !!options.preserveNewLines
    options.showHeaders = !!options.showHeaders;
    options.columns = options.columns || options.include // alias include/columns, prefer columns if supplied
    var columnNames = options.columns || [] // optional user-supplied columns to include
  
    items = toArray(items, columnNames)
  
    // if not suppled column names, automatically determine columns from data keys
    if (!columnNames.length) {
      items.forEach(function(item) {
        for (var columnName in item) {
          if (columnNames.indexOf(columnName) === -1) columnNames.push(columnName)
        }
      })
    }
  
    // initialize column defaults (each column inherits from options.config)
    var columns = columnNames.reduce(function(columns, columnName) {
      var column = Object.create(options)
      columns[columnName] = mixin(column, columnConfigs[columnName])
      return columns
    }, Object.create(null))
  
    // sanitize column settings
    columnNames.forEach(function(columnName) {
      var column = columns[columnName]
      column.maxWidth = Math.ceil(column.maxWidth)
      column.minWidth = Math.ceil(column.minWidth)
      column.truncate = !!column.truncate
    })
  
    // sanitize data
    items = items.map(function(item) {
      var result = Object.create(null)
      columnNames.forEach(function(columnName) {
        // null/undefined -> ''
        result[columnName] = item[columnName] != null ? item[columnName] : ''
        // toString everything
        result[columnName] = '' + result[columnName]
        if (columns[columnName].preserveNewLines) {
          // merge non-newline whitespace chars
          result[columnName] = result[columnName].replace(/[^\S\n]/gmi, ' ')
        } else {
          // merge all whitespace chars
          result[columnName] = result[columnName].replace(/\s/gmi, ' ')
        }
      })
      return result
    })
  
    // transform data cells
    columnNames.forEach(function(columnName) {
      var column = columns[columnName]
      items = items.map(function(item, index) {
        item[columnName] = column.dataTransform(item[columnName], column, index)
        return item
      })
    })
  
    // add headers
    var headers = {}
    if(options.showHeaders) {
      columnNames.forEach(function(columnName) {
        var column = columns[columnName]
        headers[columnName] = column.headingTransform(columnName)
      })
      items.unshift(headers)
    }
    // get actual max-width between min & max
    // based on length of data in columns
    columnNames.forEach(function(columnName) {
      var column = columns[columnName]
      column.width = items.map(function(item) {
        return item[columnName]
      }).reduce(function(min, cur) {
        return Math.max(min, Math.min(column.maxWidth, Math.max(column.minWidth, wcwidth(cur))))
      }, 0)
    })
  
    // split long words so they can break onto multiple lines
    columnNames.forEach(function(columnName) {
      var column = columns[columnName]
      items = items.map(function(item) {
        item[columnName] = splitLongWords(item[columnName], column.width, column.truncateMarker)
        return item
      })
    })
  
    // wrap long lines. each item is now an array of lines.
    columnNames.forEach(function(columnName) {
      var column = columns[columnName]
      items = items.map(function(item, index) {
        var cell = item[columnName]
        item[columnName] = splitIntoLines(cell, column.width)
  
        // if truncating required, only include first line + add truncation char
        if (column.truncate && item[columnName].length > 1) {
            item[columnName] = splitIntoLines(cell, column.width - wcwidth(column.truncateMarker))
            var firstLine = item[columnName][0]
            if (!endsWith(firstLine, column.truncateMarker)) item[columnName][0] += column.truncateMarker
            item[columnName] = item[columnName].slice(0, 1)
        }
        return item
      })
    })
  
    // recalculate column widths from truncated output/lines
    columnNames.forEach(function(columnName) {
      var column = columns[columnName]
      column.width = items.map(function(item) {
        return item[columnName].reduce(function(min, cur) {
          return Math.max(min, Math.min(column.maxWidth, Math.max(column.minWidth, wcwidth(cur))))
        }, 0)
      }).reduce(function(min, cur) {
        return Math.max(min, Math.min(column.maxWidth, Math.max(column.minWidth, cur)))
      }, 0)
    })
  
  
    var rows = createRows(items, columns, columnNames, options.paddingChr) // merge lines into rows
    // conceive output
    return rows.reduce(function(output, row) {
      return output.concat(row.reduce(function(rowOut, line) {
        return rowOut.concat(line.join(options.columnSplitter))
      }, []))
    }, []).map(function(line) {
      return truncateString(line, maxLineWidth)
    }).join(options.spacing)
  }
  
  /**
   * Convert wrapped lines into rows with padded values.
   *
   * @param Array items data to process
   * @param Array columns column width settings for wrapping
   * @param Array columnNames column ordering
   * @return Array items wrapped in arrays, corresponding to lines
   */
  
  function createRows(items, columns, columnNames, paddingChr) {
    return items.map(function(item) {
      var row = []
      var numLines = 0
      columnNames.forEach(function(columnName) {
        numLines = Math.max(numLines, item[columnName].length)
      })
      // combine matching lines of each rows
      for (var i = 0; i < numLines; i++) {
        row[i] = row[i] || []
        columnNames.forEach(function(columnName) {
          var column = columns[columnName]
          var val = item[columnName][i] || '' // || '' ensures empty columns get padded
          if (column.align == 'right') row[i].push(padLeft(val, column.width, paddingChr))
          else if (column.align == 'center') row[i].push(padCenter(val, column.width, paddingChr))
          else row[i].push(padRight(val, column.width, paddingChr))
        })
      }
      return row
    })
  }
  
  /**
   * Generic source->target mixin.
   * Copy properties from `source` into `target` if target doesn't have them.
   * Destructive. Modifies `target`.
   *
   * @param target Object target for mixin properties.
   * @param source Object source of mixin properties.
   * @return Object `target` after mixin applied.
   */
  
  function mixin(target, source) {
    source = source || {}
    for (var key in source) {
      if (target.hasOwnProperty(key)) continue
      target[key] = source[key]
    }
    return target
  }
  
  /**
   * Adapted from String.prototype.endsWith polyfill.
   */
  
  function endsWith(target, searchString, position) {
    position = position || target.length;
    position = position - searchString.length;
    var lastIndex = target.lastIndexOf(searchString);
    return lastIndex !== -1 && lastIndex === position;
  }
  
  
  function toArray(items, columnNames) {
    if (Array.isArray(items)) return items
    var rows = []
    for (var key in items) {
      var item = {}
      item[columnNames[0] || 'key'] = key
      item[columnNames[1] || 'value'] = items[key]
      rows.push(item)
    }
    return rows
  }
  