import {
  clone,
  each,
  filter,
  find,
  keys,
  last,
  maxBy,
  minBy
} from 'lodash'

import {
  createAnnotation,
  deleteCharacter as deleteChar,
  deleteNode,
  deleteSelection as deleteSel,
  expandAnnotation,
  truncateAnnotation
} from 'substance'

class TrackChangesProvider {
  constructor (config) {
    this.config = config
    this.config.documentSession.on('didUpdate', this.handleUndoRedo, this)
  }

  /*

    HANDLERS

  */

  handleTransaction (options) {
    options.selection = this.getSelection()
    this.chooseHanlder(options)
  }

  chooseHanlder (options) {
    if (options.status === 'add') return this.handleAdd(options)
    if (options.status === 'delete') return this.handleDelete(options)
  }

  handleAdd (options) {
    const isSelectionCollapsed = this.isSelectionCollapsed()

    if (isSelectionCollapsed) return this.handleAddCollapsed(options)
    if (!isSelectionCollapsed) return this.handleAddNonCollapsed(options)
  }

  handleAddCollapsed (options) {
    const { event } = options

    const notOnTrack = this.isNotOnTrackAnnotation()
    const isOnAdd = this.isOnAnnotation('add')
    const isOnDelete = this.isOnAnnotation('delete')

    if (notOnTrack) return this.insertCharacterWithAddAnnotation(options)

    if (isOnAdd) {
      // annotation gets expanded automatically, unless the selection is on its left edge
      const annotation = this.getAnnotationByStatus('add')
      const isOnLeftEdge = this.isOnLeftEdge(annotation)

      this.insertText(event)
      if (isOnLeftEdge) this.expandAnnotationToDirection(annotation)
      return
    }

    if (isOnDelete) {
      const annotation = this.getAnnotationByStatus('delete')

      const isOnLeftEdge = this.isOnLeftEdge(annotation)
      const isOnRightEdge = this.isOnRightEdge(annotation)
      const withinAnnotation = this.isSelectionContainedWithin(annotation, true)

      // if contained within the delete annotation, move it to the edge,
      // insert character, set event to null so that the character does
      // not get inserted twice, and handle again
      if (withinAnnotation) {
        this.moveCursorTo(annotation.endOffset)
        this.insertCharacterWithoutExpandingAnnotation(annotation, options)
        options.selection = this.getSelection()
        return this.handleAdd(options)
      }

      if (isOnLeftEdge) return this.insertCharacterWithAddAnnotation(options)

      if (isOnRightEdge) {
        this.insertCharacterWithoutExpandingAnnotation(annotation, options)
        return this.handleAdd(options)
      }
    }
  }

  handleAddNonCollapsed (options) {
    let { selection } = options

    const notOnTrack = this.isNotOnTrackAnnotation()
    const isOnDelete = this.isOnAnnotation('delete')

    if (notOnTrack) return this.deleteSelectedAndCreateAddition(options)

    // delete all additions of the same user and
    // shorten selection by the number of deleted characters
    const shortenBy = this.deleteAllOwnAdditions(selection)
    const startOffset = selection.startOffset
    const endOffset = selection.endOffset - shortenBy
    selection = this.updateSelection(selection, startOffset, endOffset)
    options.selection = selection

    if (isOnDelete) {
      const annotation = this.getAnnotationByStatus('delete')
      const withinAnnotation = this.isSelectionContainedWithin(annotation)

      if (withinAnnotation) {
        // if selection is wholly contained within a delete annotation,
        // move to the end of the annotation and handle again
        this.moveCursorTo(annotation.endOffset)
        options.selection = this.getSelection()
        return this.handleAddCollapsed(options)
      }
    }

    // after deleting all own additions, there is still text selected
    // mark it as deleted and add new addition annotation at the end
    if (selection.endOffset > selection.startOffset) {
      return this.deleteSelectedAndCreateAddition(options)
    }

    // if you got here, deleting all own additions left a collapsed selection
    // (ie. the whole selection was on an own addition annotation)
    // since selection is collapsed, handle it as such
    return this.handleAddCollapsed(options)
  }

  handleDelete (options) {
    const { key, move } = options
    const isSelectionCollapsed = this.isSelectionCollapsed()

    options.direction = {
      cursorTo: (move === 'left') ? 'start' : 'end',
      key: key,
      move: move
    }

    if (isSelectionCollapsed) return this.handleDeleteCollapsed(options)
    if (!isSelectionCollapsed) return this.handleDeleteNonCollapsed(options)
  }

  handleDeleteCollapsed (options) {
    const { direction } = options

    const notOnTrack = this.isNotOnTrackAnnotation()
    const isOnAdd = this.isOnAnnotation('add')
    const isOnDelete = this.isOnAnnotation('delete')

    if (notOnTrack) return this.selectCharacterAndMarkDeleted(options)

    if (isOnAdd) {
      console.log('delete collapsed on add')
      const annotation = this.getAnnotationByStatus('add')
      const isOnLeftEdge = this.isOnLeftEdge(annotation)
      const isOnRightEdge = this.isOnRightEdge(annotation)
      const key = direction.key

      // when on own additions, simply delete the character
      // unless it is on the edge: then make a deletion annotation

      // TODO -- watch it for different users
      if (
        (isOnLeftEdge && key === 'BACKSPACE') ||
        (isOnRightEdge && key === 'DELETE')
      ) {
        return this.selectCharacterAndMarkDeleted(options)
      }

      return this.deleteCharacter(direction.move)
    }

    if (isOnDelete) {
      const annotation = this.getAnnotationByStatus('delete')
      const isOnLeftEdge = this.isOnLeftEdge(annotation)
      const isOnRightEdge = this.isOnRightEdge(annotation)
      const key = direction.key

      let moveOnly, point

      if (!isOnLeftEdge && !isOnRightEdge) {
        point = annotation[direction.cursorTo + 'Offset']
        moveOnly = true
      } else if (isOnLeftEdge && key === 'DELETE') {
        point = annotation.endOffset
        moveOnly = true
      } else if (isOnRightEdge && key === 'BACKSPACE') {
        point = annotation.startOffset
        moveOnly = true
      }

      if (moveOnly) return this.moveCursorTo(point)
      return this.expandAnnotationToDirection(annotation, direction)
    }
  }

  handleDeleteNonCollapsed (options) {
    const { direction } = options
    let { selection } = options

    const notOnTrack = this.isNotOnTrackAnnotation()
    const isOnDelete = this.isOnAnnotation('delete')

    if (notOnTrack) return this.markSelectionAsDeleted(options)

    const shortenBy = this.deleteAllOwnAdditions(selection)
    const startOffset = selection.startOffset
    const endOffset = selection.endOffset - shortenBy
    this.updateSelection(selection, startOffset, endOffset)

    console.log('collapsed', selection.isCollapsed())
    if (selection.isCollapsed()) return this.handleDeleteCollapsed(options)

    if (isOnDelete) {
      // console.log
      const annotation = this.getAnnotationByStatus('delete')
      const containedWithin = this.isSelectionContainedWithin(annotation)

      if (containedWithin) {
        const point = annotation[direction.cursorTo + 'Offset']
        return this.moveCursorTo(point)
      }

      // const selection = this.getSelection()
      // this.expandTrackAnnotation(selection, annotation)
      //
      // const key = direction.key
      //
      // let point
      // if (key === 'BACKSPACE') point = selection.startOffset
      // if (key === 'DELETE') point = selection.endOffset
      // this.moveCursorTo(point)
    }

    options.selection = this.deleteOrMergeAllOwnDeletions(selection)
    // console.log(this.getSelection())
    // options.selection = this.getSelection()
    this.markSelectionAsDeleted(options)
  }

  /*
    HANDLER COMMON FUNCTIONS
  */

  createAdditionAnnotationOnLastChar () {
    const selection = this.setSelectionPlusOne('left')
    this.createAddAnnotation(selection)
    this.moveCursorTo('end')
  }

  deleteAllOwnAdditions (selection) {
    // TODO -- for same user
    const additions = this.getAllAnnotationsByStatus('add')
    const originalSelection = selection || this.getSelection()

    let shortenBy = 0

    each(additions, (annotation) => {
      const selection = annotation.getSelection()

      // make sure only part of annotation that is selected is deleted
      if (annotation.startOffset < originalSelection.startOffset) {
        selection.startOffset = originalSelection.startOffset
      }
      if (annotation.endOffset > originalSelection.endOffset) {
        selection.endOffset = originalSelection.endOffset
      }

      shortenBy += (selection.endOffset - selection.startOffset)
      this.deleteSelection(selection)
    })

    return shortenBy    // return how much shorter the selection should now be
  }

  deleteOrMergeAllOwnDeletions (selection) {
    const deletions = clone(this.getAllAnnotationsByStatus('delete'))
    // filter by own deletions
    console.log(selection, deletions)

    // const originalSelection = clone(selection)
    const selectionArray = [selection]

    each(deletions, (annotation) => {
      const annotationSelection = annotation.getSelection()
      const contained = selection.contains(annotationSelection)

      if (!contained) {
        selectionArray.push(annotationSelection)
      }
      console.log('fkdjsjf')
      this.removeTrackAnnotation(annotation)
    })

    selection.startOffset = minBy(selectionArray, 'startOffset').startOffset
    selection.endOffset = maxBy(selectionArray, 'endOffset').endOffset

    return selection
    // TODO
    // this.updateSelection(selection, startOffset, endOffset)
  }

  deleteSelectedAndCreateAddition (options) {
    let { selection } = options

    this.createDeleteAnnotation(selection)
    this.moveCursorTo('end', selection)

    // selection is now collapsed, so handle it as collapsed
    this.handleAddCollapsed(options)
  }

  expandAnnotationToDirection (annotation, options) {
    if (!options) options = {}
    const move = options.move || 'left'
    const cursorTo = options.cursorTo || 'end'

    const selection = this.setSelectionPlusOne(move)
    this.expandTrackAnnotation(selection, annotation)
    this.moveCursorTo(cursorTo)
  }

  insertCharacterWithAddAnnotation (options) {
    const { event } = options
    this.insertText(event)

    // TODO -- watch it with additions by other users
    this.createAdditionAnnotationOnLastChar()
  }

  insertCharacterWithoutExpandingAnnotation (annotation, options) {
    const { event } = options
    let selection = options

    this.insertText(event)
    selection = this.setSelectionPlusOne('left')
    this.truncateTrackAnnotation(selection, annotation)
    this.moveCursorTo('end')

    options.event = null
  }

  markSelectionAsDeleted (options) {
    const { direction, selection } = options
    this.createDeleteAnnotation(selection)
    this.moveCursorTo(direction.cursorTo)
  }

  selectCharacterAndMarkDeleted (options) {
    const { direction } = options
    const selection = this.setSelectionPlusOne(direction.move)

    this.createDeleteAnnotation(selection)
    this.moveCursorTo(direction.cursorTo)
  }

  /*

    TRANSFORMATIONS

  */

  createAddAnnotation (selection) {
    this.createTrackAnnotation(selection, 'add')
  }

  createDeleteAnnotation (selection) {
    this.createTrackAnnotation(selection, 'delete')
  }

  // TODO -- selection could default to current selection
  createTrackAnnotation (selection, status) {
    const surface = this.getSurface()
    const info = this.getInfo()
    const { user } = this.config

    if (selection.isContainerSelection()) {
      return console.warn('Cannot delete a container')
    }

    const transformation = (tx, args) => {
      const newNode = {
        selection: selection,
        node: {
          status: status,
          type: 'track-change',
          user: {
            roles: user.roles,
            username: user.username
          }
        }
      }
      createAnnotation(tx, newNode)
    }

    surface.transaction(transformation, info)
  }

  deleteCharacter (direction) {
    const surface = this.getSurface()
    const info = { action: 'delete' }

    const transformation = (tx, args) => {
      args.direction = direction
      return deleteChar(tx, args)
    }

    surface.transaction(transformation, info)
  }

  deleteSelection (selection) {
    const surface = this.getSurface()
    const info = { action: 'delete' }

    const transformation = (tx, args) => {
      args.selection = selection
      return deleteSel(tx, args)
    }

    surface.transaction(transformation, info)
  }

  expandTrackAnnotation (selection, annotation) {
    const surface = this.getSurface()
    const info = this.getInfo()

    const transformation = (tx, args) => {
      args.selection = selection
      args.anno = annotation

      expandAnnotation(tx, args)
    }

    surface.transaction(transformation, info)
  }

  insertText (event) {
    if (!event) return
    const surface = this.getSurface()

    surface.transaction(function (tx, args) {
      if (surface.domSelection) surface.domSelection.clear()
      args.text = event.data || ' '    // if no data, it's a space key
      return surface.insertText(tx, args)
    }, { action: 'type' })
  }

  removeTrackAnnotation (annotation) {
    const surface = this.getSurface()

    const transformation = (tx, args) => {
      args.nodeId = annotation.id
      deleteNode(tx, args)
    }

    surface.transaction(transformation)
  }

  truncateTrackAnnotation (selection, annotation) {
    const surface = this.getSurface()
    const info = this.getInfo()

    const transformation = (tx, args) => {
      args.anno = annotation
      args.selection = selection
      truncateAnnotation(tx, args)
    }

    surface.transaction(transformation, info)
  }

  /*

    ANNOTATION HELPERS

  */

  // is annotation from the same user

  getAllAnnotationsByStatus (status) {
    const annotations = this.getAllExistingTrackAnnotations()
    const annotationsByStatus = filter(annotations, (annotation) => {
      return annotation.status === status
    })
    return annotationsByStatus
  }

  // TODO -- handle multiple delete and add annotations
  getAnnotationByStatus (status) {
    const annotationsForStatus = this.getAllAnnotationsByStatus(status)
    return annotationsForStatus[0]
  }

  // getExistingAnnotation () {
  //   const documentSession = this.getDocumentSession()
  //   const selectionState = documentSession.getSelectionState()
  //   const annotations = selectionState.getAnnotationsForType('track-change')
  //   return annotations[0]
  // }

  getAllExistingTrackAnnotations () {
    const documentSession = this.getDocumentSession()
    const selectionState = documentSession.getSelectionState()
    const annotations = selectionState.getAnnotationsForType('track-change')
    return annotations
  }

  // getDistinctAnnotationsForSelection () {
  //   const selection = this.getSelection()
  //   const annotations = this.getAllExistingTrackAnnotations()
  //
  //   console.log(selection)
  //   console.log(annotations)
  //
  //   console.log(selection.getFragments())
  //
  //   // get all annotations by status (add / delete)
  //   // get all pieces of free text
  // }

  // prevent substance from running getBoundingRectangle,
  // as we will unset the selection manually
  getInfo () {
    return { skipSelection: true }
  }

  isNotOnTrackAnnotation () {
    const annotations = this.getAllExistingTrackAnnotations()
    return (annotations.length === 0)
  }

  // returns whether the selection is on an add / delete tracked change
  isOnAnnotation (status) {
    const annotations = this.getAllExistingTrackAnnotations()
    const annotation = find(annotations, (annotation) => {
      return annotation.status === status
    })

    if (!annotation) return false
    return true
  }

  isOnLeftEdge (annotation) {
    const selection = this.getSelection()
    return (selection.startOffset === annotation.startOffset)
  }

  isOnRightEdge (annotation) {
    const selection = this.getSelection()
    return (selection.endOffset === annotation.endOffset)
  }

  /**

    HISTORY HANDLERS

  */

  // TODO -- shouldn't run both all the time
  handleUndoRedo (update, info) {
    if (!info.replay) return
    // console.log(update)
    // console.log(info)
    this.handleUndo(update)
    // this.handleRedo(update)
  }

  handleUndo (update) {
    const deleted = update.change.deleted
    const deletedLength = keys(deleted).length

    // console.log(keys(deleted))

    if (deletedLength === 0) return
    if (deletedLength > 1) {
      return console.warn('FIXME: Multiple operations in track changes replay!')
    }

    const deletedOp = deleted[keys(deleted)[0]]
    if (!deletedOp.type === 'track-change') return

    const documentSession = this.getDocumentSession()
    documentSession.undo()
  }

  handleRedo () {
    const documentSession = this.getDocumentSession()
    const undoneChanges = documentSession.undoneChanges
    const lastChange = last(undoneChanges)
    const op = last(lastChange.ops)

    const isTrack = op.path[0].split('-').slice(0, -1).join('-') === 'track-change'
    console.log(isTrack)
  }

  /*

    SELECTION HANDLERS

  */

  isAnnotationContainedWithinSelection (annotation, strict) {
    const selection = this.getSelection()
    const annotationSelection = annotation.getSelection()

    return selection.contains(annotationSelection, strict)
  }

  isSelectionCollapsed () {
    const selection = this.getSelection()
    const isCollapsed = selection.isCollapsed()
    return isCollapsed
  }

  // TODO -- refactor this and isAnnotationContainedWithinSelection into one
  isSelectionContainedWithin (annotation, strict) {
    const selection = this.getSelection()
    // console.trace()
    const annotationSelection = annotation.getSelection()

    return annotationSelection.contains(selection, strict)

    // const leftSide = (selection.startOffset < annotation.startOffset)
    // const rightSide = (selection.endOffset > annotation.endOffset)
    //
    // if (leftSide || rightSide) return false
    // return true
  }

  moveCursorTo (point, sel) {
    const selection = sel || this.getSelection()
    const surface = this.getSurface()

    // TODO -- use substance's selection.collapse(direction)
    if (point === 'start') {
      selection.endOffset = selection.startOffset
    } else if (point === 'end') {
      selection.startOffset = selection.endOffset
    } else {
      selection.startOffset = point
      selection.endOffset = point
    }

    surface.setSelection(selection)
  }

  setSelectionPlusOne (direction) {
    const selection = this.getSelection()

    if (direction === 'left') selection.startOffset -= 1
    if (direction === 'right') selection.endOffset += 1

    return selection
  }

  updateSelection (selection, startOffset, endOffset) {
    const surface = this.getSurface()

    selection.startOffset = startOffset
    selection.endOffset = endOffset

    surface.setSelection(selection)
    return selection
  }

  /*

    GETTERS

  */

  getDocumentSession () {
    return this.config.documentSession
  }

  getSelection () {
    const surface = this.getSurface()
    return surface.getSelection()
  }

  getSurface () {
    const surfaceManager = this.config.surfaceManager
    const id = this.config.containerId

    return surfaceManager.getSurface(id)
  }
}

export default TrackChangesProvider