SystemNameLoader = require './../core/SystemNameLoader'
###
  Good-to-knows:
    dataPath: an array of keys that walks you up a JSON object that's being patched
      ex: ['scripts', 0, 'description']
    deltaPath: an array of keys that walks you up a JSON Diff Patch object.
      ex: ['scripts', '_0', 'description']
###

module.exports.expandDelta = (delta, left, schema) ->
  flattenedDeltas = flattenDelta(delta)
  (expandFlattenedDelta(fd, left, schema) for fd in flattenedDeltas)


module.exports.flattenDelta = flattenDelta = (delta, dataPath=null, deltaPath=null) ->
  # takes a single jsondiffpatch delta and returns an array of objects with
  return [] unless delta
  dataPath ?= []
  deltaPath ?= []
  return [{dataPath: dataPath, deltaPath: deltaPath, o: delta}] if _.isArray delta

  results = []
  affectingArray = delta._t is 'a'
  for deltaIndex, childDelta of delta
    continue if deltaIndex is '_t'
    dataIndex = if affectingArray then parseInt(deltaIndex.replace('_', '')) else deltaIndex
    results = results.concat flattenDelta(
      childDelta, dataPath.concat([dataIndex]), deltaPath.concat([deltaIndex]))
  results


expandFlattenedDelta = (delta, left, schema) ->
  # takes a single flattened delta and converts into an object that can be
  # easily formatted into something human readable.

  delta.action = '???'
  o = delta.o # the raw jsondiffpatch delta

  if _.isArray(o) and o.length is 1
    delta.action = 'added'
    delta.newValue = o[0]

  if _.isArray(o) and o.length is 2
    delta.action = 'modified'
    delta.oldValue = o[0]
    delta.newValue = o[1]

  if _.isArray(o) and o.length is 3 and o[1] is 0 and o[2] is 0
    delta.action = 'deleted'
    delta.oldValue = o[0]

  if _.isPlainObject(o) and o._t is 'a'
    delta.action = 'modified-array'

  if _.isPlainObject(o) and o._t isnt 'a'
    delta.action = 'modified-object'

  if _.isArray(o) and o.length is 3 and o[2] is 3
    delta.action = 'moved-index'
    delta.destinationIndex = o[1]
    delta.originalIndex = delta.dataPath[delta.dataPath.length-1]

  if _.isArray(o) and o.length is 3 and o[1] is 0 and o[2] is 2
    delta.action = 'text-diff'
    delta.unidiff = o[0]

  humanPath = []
  parentLeft = left
  parentSchema = schema
  for key, i in delta.dataPath
    # TODO: Better schema/json walking
    childSchema = parentSchema?.items or parentSchema?.properties?[key] or {}
    childLeft = parentLeft?[key]
    humanKey = null
    childData = if i is delta.dataPath.length-1 and delta.action is 'added' then o[0] else childLeft
    humanKey ?= childData.name or childData.id if childData
    humanKey ?= SystemNameLoader.getName(childData?.original)
    humanKey ?= "#{childSchema.title}" if childSchema.title
    humanKey ?= _.string.titleize key
    humanPath.push humanKey
    parentLeft = childLeft
    parentSchema = childSchema

  delta.humanPath = humanPath.join(' :: ')
  delta.schema = childSchema
  delta.left = childLeft
  delta.right = jsondiffpatch.patch childLeft, delta.o unless delta.action is 'moved-index'

  delta

module.exports.makeJSONDiffer = ->
  hasher = (obj) -> if obj? then obj.name or obj.id or obj._id or JSON.stringify(_.keys(obj)) else 'null'
  jsondiffpatch.create({objectHash: hasher})

module.exports.getConflicts = (headDeltas, pendingDeltas) ->
  # headDeltas and pendingDeltas should be lists of deltas returned by expandDelta
  # Returns a list of conflict objects with properties:
  #   headDelta
  #   pendingDelta
  # The deltas that have conflicts also have conflict properties pointing to one another.

  headPathMap = groupDeltasByAffectingPaths(headDeltas)
  pendingPathMap = groupDeltasByAffectingPaths(pendingDeltas)
  paths = _.keys(headPathMap).concat(_.keys(pendingPathMap))

  # Here's my thinking: conflicts happen when one delta path is a substring of another delta path
  # So, sort paths from both deltas together, which will naturally make conflicts adjacent,
  # and if one is identified AND one path is from the headDeltas AND the other is from pendingDeltas
  # This is all to avoid an O(nm) brute force search.

  conflicts = []
  paths.sort()
  for path, i in paths
    offset = 1
    while i + offset < paths.length
      # Look at the neighbor
      nextPath = paths[i+offset]
      offset += 1

      # these stop being substrings of each other? Then conflict DNE
      if not (_.string.startsWith nextPath, path) then break

      # check if these two are from the same group, but we still need to check for more beyond
      unless headPathMap[path] or headPathMap[nextPath] then continue
      unless pendingPathMap[path] or pendingPathMap[nextPath] then continue

      # Okay, we found two deltas from different groups which conflict
      for headMetaDelta in (headPathMap[path] or headPathMap[nextPath])
        headDelta = headMetaDelta.delta
        for pendingMetaDelta in (pendingPathMap[path] or pendingPathMap[nextPath])
          pendingDelta = pendingMetaDelta.delta
          conflicts.push({headDelta: headDelta, pendingDelta: pendingDelta})
          pendingDelta.conflict = headDelta
          headDelta.conflict = pendingDelta

  return conflicts if conflicts.length

groupDeltasByAffectingPaths = (deltas) ->
  metaDeltas = []
  for delta in deltas
    conflictPaths = []
    # We're being fairly liberal with what's a conflict, because the alternative is worse
    if delta.action is 'moved-index'
      # If you moved items around in an array, mark the whole array as a gonner
      conflictPaths.push delta.dataPath.slice(0, delta.dataPath.length-1)
    else if delta.action in ['deleted', 'added'] and _.isNumber(delta.dataPath[delta.dataPath.length-1])
      # If you remove or add items in an array, mark the whole thing as a gonner
      conflictPaths.push delta.dataPath.slice(0, delta.dataPath.length-1)
    else
      conflictPaths.push delta.dataPath
    for path in conflictPaths
      metaDeltas.push {
        delta: delta
        path: (item.toString() for item in path).join('/')
      }

  map = _.groupBy metaDeltas, 'path'
  return map

module.exports.pruneConflictsFromDelta = (delta, conflicts) ->
  expandedDeltas = (conflict.pendingDelta for conflict in conflicts)
  module.exports.pruneExpandedDeltasFromDelta delta, expandedDeltas

module.exports.pruneExpandedDeltasFromDelta = (delta, expandedDeltas) ->
  # the jsondiffpatch delta mustn't include any dangling nodes,
  # or else things will get removed which shouldn't be, or errors will occur
  for expandedDelta in expandedDeltas
    prunePath delta, expandedDelta.deltaPath
  if _.isEmpty delta then undefined else delta

prunePath = (delta, path) ->
  if path.length is 1
    delete delta[path] unless delta[path] is undefined
  else
    prunePath delta[path[0]], path.slice(1) unless delta[path[0]] is undefined
    keys = (k for k in _.keys(delta[path[0]]) when k isnt '_t')
    delete delta[path[0]] if keys.length is 0

module.exports.DOC_SKIP_PATHS = [
  '_id','version', 'commitMessage', 'parent', 'created',
  'slug', 'index', '__v', 'patches', 'creator', 'js', 'watchers', 'levelsUpdated'
]