import _ from 'lodash'

function diff(before, after) {
  var cmds = []
  if (!after.components) {
    cmds.push({ command: 'destroy', args: [] })
    return cmds
  }
  if (!before.components) {
    cmds.push({ command: 'setConfig', args: [after] })
    return cmds
  }
  if (before.width !== after.width) {
    cmds.push({ command: 'setWidth', args: [after.width] })
  }
  if (before.height !== after.height) {
    cmds.push({ command: 'setHeight', args: [after.height] })
  }
  if (before.scale !== after.scale) {
    cmds.push({ command: 'setZoomMode', args: [after.scale] })
  }
  if (!_.isEqual(before.background, after.background)) {
    cmds.push({ command: 'setBackground', args: [after.background] })
  }
  if (before.name !== after.name) {
    cmds.push({ command: 'setName', args: [after.name] })
  }
  if (!_.isEqual(before.metadata, after.metadata)) {
    cmds.push({ command: 'setMetadata', args: [after.metadata] })
  }
  diffLayers(before.components, after.components, cmds)
  return cmds
}

function pluckId(layer) {
  return layer.id
}

function indexById(group, layer) {
  group[layer.id] = layer
  return group
}

function diffLayers(before, after, commands) {
  before = before || []
  after = after || []

  // order of layers by id
  var beforeOrder = before.map(pluckId)
  var afterOrder = after.map(pluckId)

  // index of layer by id
  var beforeIndex = before.reduce(indexById, {})
  var afterIndex = after.reduce(indexById, {})

  // track order of layers as if they have been mutated
  var tracker = beforeOrder.slice()

  // layers that have been added do not need to be diffed
  var clean = Object.create(null)

  var i, d, layerId, beforeLayer, afterLayer, insertBeforeLayerId

  // remove layers
  for (i = 0, d = 0; i < beforeOrder.length; i++) {
    layerId = beforeOrder[i]
    if (!afterIndex.hasOwnProperty(layerId)) {
      commands.push({ command: 'removeComponent', args: [layerId] })
      tracker.splice(tracker.indexOf(layerId, d), 1)
    } else {
      // limit where in tracker we need to look for a match
      d++
    }
  }

  // add/reorder layers
  for (i = 0, d = 0; i < afterOrder.length; i++) {
    // work backwards as insert is before an existing layer
    layerId = afterOrder[afterOrder.length - 1 - i]

    if (tracker[tracker.length - 1 - i] === layerId) continue

    if (beforeIndex.hasOwnProperty(layerId)) {
      // remove the layer before we insert at the correct position
      commands.push({ command: 'removeComponent', args: [layerId] })
      tracker.splice(tracker.lastIndexOf(layerId, tracker.length - d), 1)
    } else {
      // limit where in tracker we need to look for a match
      d++
    }

    // add layer at correct position
    insertBeforeLayerId = tracker[tracker.length - i]
    commands.push({
      command: 'addComponent',
      args: [afterIndex[layerId], insertBeforeLayerId]
    })
    tracker.splice(tracker.length - i, 0, layerId)
    clean[layerId] = true
  }

  // update layers
  for (i = 0; i < afterOrder.length; i++) {
    layerId = afterOrder[i]
    beforeLayer = beforeIndex[layerId]
    afterLayer = afterIndex[layerId]

    // no need to update if previously added (new or moved)
    if (clean[layerId] || _.isEqual(beforeLayer, afterLayer)) continue

    // If source, source-layer, or type have changes, then remove the layer
    // and add it back 'from scratch'.
    if (!_.isEqual(beforeLayer, afterLayer)) {
      let layoutEqual = _.isEqual(beforeLayer.layout, afterLayer.layout)
      let paintEqual = _.isEqual(beforeLayer.paint, afterLayer.paint)
      let sourceEqual = _.isEqual(beforeLayer.source, afterLayer.source)
      let eventsEqual = _.isEqual(beforeLayer.events, afterLayer.events)
      let metaEqual = _.isEqual(beforeLayer.metadata, afterLayer.metadata)
      if (!sourceEqual) {
        commands.push({
          command: 'setSource',
          args: [layerId, afterLayer.source]
        })
        continue
      }
      if (!layoutEqual) {
        commands.push({
          command: 'setLayout',
          args: [layerId, afterLayer.layout]
        })
      }
      if (!paintEqual) {
        commands.push({
          command: 'setPaint',
          args: [layerId, afterLayer.paint]
        })
      }
      if (!eventsEqual) {
        commands.push({
          command: 'setEvent',
          args: [layerId, afterLayer.events]
        })
      }

      if (!metaEqual) {
        commands.push({
          command: 'setMeta',
          args: [layerId, afterLayer.metadata]
        })
      }
    }
  }
}

export default diff
