import _ from 'lodash'
/* eslint-disable
    camelcase,
    max-len
 */
import App from '../../../base'
import UndoManager from './aceEditor/undo/UndoManager'
import AutoCompleteManager from './aceEditor/auto-complete/AutoCompleteManager'
import SpellCheckManager from './aceEditor/spell-check/SpellCheckManager'
import SpellCheckAdapter from './aceEditor/spell-check/SpellCheckAdapter'
import HighlightsManager from './aceEditor/highlights/HighlightsManager'
import CursorPositionManager from './aceEditor/cursor-position/CursorPositionManager'
import CursorPositionAdapter from './aceEditor/cursor-position/CursorPositionAdapter'
import TrackChangesManager from './aceEditor/track-changes/TrackChangesManager'
import TrackChangesAdapter from './aceEditor/track-changes/TrackChangesAdapter'
import MetadataManager from './aceEditor/metadata/MetadataManager'
import 'ace/ace'
import 'ace/ext-searchbox'
import 'ace/ext-modelist'
import 'ace/keybinding-vim'
import '../../metadata/services/metadata'
import '../../graphics/services/graphics'
import '../../preamble/services/preamble'
import '../../files/services/files'
let syntaxValidationEnabled
const { EditSession } = ace.require('ace/edit_session')
const ModeList = ace.require('ace/ext/modelist')
const { Vim } = ace.require('ace/keyboard/vim')
const SearchBox = ace.require('ace/ext/searchbox')

// Set the base path that ace will fetch modes/snippets/workers from
if (window.aceBasePath !== '') {
  syntaxValidationEnabled = true
  ace.config.set('basePath', `${window.aceBasePath}`)
} else {
  syntaxValidationEnabled = false
}

// By default, don't use workers - enable them per-session as required
ace.config.setDefaultValue('session', 'useWorker', false)

// Ace loads its script itself, so we need to hook in to be able to clear
// the cache.
if (ace.config._moduleUrl == null) {
  ace.config._moduleUrl = ace.config.moduleUrl
  ace.config.moduleUrl = function (...args) {
    const url = ace.config._moduleUrl(...Array.from(args || []))
    return url
  }
}

App.directive(
  'aceEditor',
  function (
    ide,
    $timeout,
    $compile,
    $rootScope,
    eventTracking,
    localStorage,
    $cacheFactory,
    metadata,
    graphics,
    preamble,
    files,
    $http,
    $q,
    $window
  ) {
    monkeyPatchSearch($rootScope, $compile)

    return {
      scope: {
        theme: '=',
        showPrintMargin: '=',
        keybindings: '=',
        fontSize: '=',
        autoComplete: '=',
        autoPairDelimiters: '=',
        sharejsDoc: '=',
        spellCheck: '=',
        spellCheckLanguage: '=',
        highlights: '=',
        text: '=',
        readOnly: '=',
        annotations: '=',
        navigateHighlights: '=',
        fileName: '=',
        onCtrlEnter: '=', // Compile
        onCtrlJ: '=', // Toggle the review panel
        onCtrlShiftC: '=', // Add a new comment
        onCtrlShiftA: '=', // Toggle track-changes on/off
        onSave: '=', // Cmd/Ctrl-S or :w in Vim
        syntaxValidation: '=',
        reviewPanel: '=',
        eventsBridge: '=',
        trackChanges: '=',
        docId: '=',
        rendererData: '=',
        lineHeight: '=',
        fontFamily: '='
      },
      link(scope, element, attrs) {
        // Don't freak out if we're already in an apply callback
        let spellCheckManager
        scope.$originalApply = scope.$apply
        scope.$apply = function (fn) {
          if (fn == null) {
            fn = function () {}
          }
          const phase = this.$root.$$phase
          if (phase === '$apply' || phase === '$digest') {
            return fn()
          } else {
            return this.$originalApply(fn)
          }
        }

        const editor = ace.edit(element.find('.ace-editor-body')[0])
        editor.$blockScrolling = Infinity

        // Besides the main editor, other elements will re-use this directive
        //  for displaying read-only content -- e.g. the history panes.
        const editorAcceptsChanges = attrs.aceEditor === 'editor'
        if (editorAcceptsChanges) {
          // end-to-end check for edits -> acks, globally on any doc
          // This may catch a missing attached ShareJsDoc that in turn bails out
          //  on missing acks.
          ide.globalEditorWatchdogManager.attachToEditor('Ace', editor)
        }

        // auto-insertion of braces, brackets, dollars
        editor.setOption('behavioursEnabled', scope.autoPairDelimiters || false)
        editor.setOption('wrapBehavioursEnabled', false)

        scope.$watch('autoPairDelimiters', autoPairDelimiters => {
          if (autoPairDelimiters) {
            return editor.setOption('behavioursEnabled', true)
          } else {
            return editor.setOption('behavioursEnabled', false)
          }
        })

        if (!window._debug_editors) {
          window._debug_editors = []
        }
        window._debug_editors.push(editor)

        scope.name = attrs.aceEditor

        if (scope.spellCheck) {
          // only enable spellcheck when explicitly required
          spellCheckManager = new SpellCheckManager(
            scope,
            $cacheFactory,
            $http,
            $q,
            new SpellCheckAdapter(editor)
          )
        }

        /* eslint-disable no-unused-vars */
        const undoManager = new UndoManager(editor)
        const highlightsManager = new HighlightsManager(scope, editor, element)
        const cursorPositionManager = new CursorPositionManager(
          scope,
          new CursorPositionAdapter(editor),
          localStorage
        )
        const trackChangesManager = new TrackChangesManager(
          scope,
          editor,
          element,
          new TrackChangesAdapter(editor)
        )

        const metadataManager = new MetadataManager(
          scope,
          editor,
          element,
          metadata
        )
        const autoCompleteManager = new AutoCompleteManager(
          scope,
          editor,
          element,
          metadataManager,
          graphics,
          preamble,
          files
        )

        // prevent user entering null and non-BMP unicode characters in Ace
        const BAD_CHARS_REGEXP = /[\0\uD800-\uDFFF]/g
        const BAD_CHARS_REPLACEMENT_CHAR = '\uFFFD'
        // the 'exec' event fires for ace functions before they are executed.
        // you can modify the input or reject the event with e.preventDefault()
        editor.commands.on('exec', function (e) {
          // replace bad characters in paste content
          if (e.command && e.command.name === 'paste') {
            BAD_CHARS_REGEXP.lastIndex = 0 // reset stateful regexp for this usage
            if (e.args && BAD_CHARS_REGEXP.test(e.args.text)) {
              e.args.text = e.args.text.replace(
                BAD_CHARS_REGEXP,
                BAD_CHARS_REPLACEMENT_CHAR
              )
            }
          }
          // replace bad characters in keyboard input
          if (e.command && e.command.name === 'insertstring') {
            BAD_CHARS_REGEXP.lastIndex = 0 // reset stateful regexp for this usage
            if (e.args && BAD_CHARS_REGEXP.test(e.args)) {
              e.args = e.args.replace(
                BAD_CHARS_REGEXP,
                BAD_CHARS_REPLACEMENT_CHAR
              )
            }
          }
        })

        /* eslint-enable no-unused-vars */

        scope.$watch('onSave', function (callback) {
          if (callback != null) {
            Vim.defineEx('write', 'w', callback)
            editor.commands.addCommand({
              name: 'save',
              bindKey: {
                win: 'Ctrl-S',
                mac: 'Command-S'
              },
              exec: callback,
              readOnly: true
            })
            // Not technically 'save', but Ctrl-. recompiles in OL v1
            // so maintain compatibility
            return editor.commands.addCommand({
              name: 'recompile_v1',
              bindKey: {
                win: 'Ctrl-.',
                mac: 'Ctrl-.'
              },
              exec: callback,
              readOnly: true
            })
          }
        })
        editor.commands.removeCommand('transposeletters')
        editor.commands.removeCommand('showSettingsMenu')
        editor.commands.removeCommand('foldall')

        // For European keyboards, the / is above 7 so needs Shift pressing.
        // This comes through as Command-Shift-/ on OS X, which is mapped to
        // toggleBlockComment.
        // This doesn't do anything for LaTeX, so remap this to togglecomment to
        // work for European keyboards as normal.
        // On Windows, the key combo comes as Ctrl-Shift-7.
        editor.commands.removeCommand('toggleBlockComment')
        editor.commands.removeCommand('togglecomment')

        editor.commands.addCommand({
          name: 'togglecomment',
          bindKey: {
            win: 'Ctrl-/|Ctrl-Shift-7',
            mac: 'Command-/|Command-Shift-/'
          },
          exec(editor) {
            return editor.toggleCommentLines()
          },
          multiSelectAction: 'forEachLine',
          scrollIntoView: 'selectionPart'
        })

        // Trigger search AND replace on CMD+F
        editor.commands.addCommand({
          name: 'find',
          bindKey: {
            win: 'Ctrl-F',
            mac: 'Command-F'
          },
          exec(editor) {
            return SearchBox.Search(editor, true)
          },
          readOnly: true
        })

        // Bold text on CMD+B
        editor.commands.addCommand({
          name: 'bold',
          bindKey: {
            win: 'Ctrl-B',
            mac: 'Command-B'
          },
          exec(editor) {
            const selection = editor.getSelection()
            if (selection.isEmpty()) {
              editor.insert('\\textbf{}')
              return editor.navigateLeft(1)
            } else {
              const text = editor.getCopyText()
              return editor.insert(`\\textbf{${text}}`)
            }
          },
          readOnly: false
        })

        // Italicise text on CMD+I
        editor.commands.addCommand({
          name: 'italics',
          bindKey: {
            win: 'Ctrl-I',
            mac: 'Command-I'
          },
          exec(editor) {
            const selection = editor.getSelection()
            if (selection.isEmpty()) {
              editor.insert('\\textit{}')
              return editor.navigateLeft(1)
            } else {
              const text = editor.getCopyText()
              return editor.insert(`\\textit{${text}}`)
            }
          },
          readOnly: false
        })

        scope.$watch('onCtrlEnter', function (callback) {
          if (callback != null) {
            return editor.commands.addCommand({
              name: 'compile',
              bindKey: {
                win: 'Ctrl-Enter',
                mac: 'Command-Enter'
              },
              exec: editor => {
                return callback()
              },
              readOnly: true
            })
          }
        })

        scope.$watch('onCtrlJ', function (callback) {
          if (callback != null) {
            return editor.commands.addCommand({
              name: 'toggle-review-panel',
              bindKey: {
                win: 'Ctrl-J',
                mac: 'Command-J'
              },
              exec: editor => {
                return callback()
              },
              readOnly: true
            })
          }
        })

        scope.$watch('onCtrlShiftC', function (callback) {
          if (callback != null) {
            return editor.commands.addCommand({
              name: 'add-new-comment',
              bindKey: {
                win: 'Ctrl-Shift-C',
                mac: 'Command-Shift-C'
              },
              exec: editor => {
                return callback()
              },
              readOnly: true
            })
          }
        })

        scope.$watch('onCtrlShiftA', function (callback) {
          if (callback != null) {
            return editor.commands.addCommand({
              name: 'toggle-track-changes',
              bindKey: {
                win: 'Ctrl-Shift-A',
                mac: 'Command-Shift-A'
              },
              exec: editor => {
                return callback()
              },
              readOnly: true
            })
          }
        })

        // Make '/' work for search in vim mode.
        editor.showCommandLine = arg => {
          if (arg === '/') {
            return SearchBox.Search(editor, true)
          }
        }

        const getCursorScreenPosition = function () {
          const session = editor.getSession()
          const cursorPosition = session.selection.getCursor()
          const sessionPos = session.documentToScreenPosition(
            cursorPosition.row,
            cursorPosition.column
          )
          return (
            sessionPos.row * editor.renderer.lineHeight - session.getScrollTop()
          )
        }

        if (attrs.resizeOn != null) {
          for (let event of Array.from(attrs.resizeOn.split(','))) {
            scope.$on(event, function () {
              scope.$applyAsync(() => {
                const previousScreenPosition = getCursorScreenPosition()
                editor.resize()
                // Put cursor back to same vertical position on screen
                const newScreenPosition = getCursorScreenPosition()
                const session = editor.getSession()
                return session.setScrollTop(
                  session.getScrollTop() +
                    newScreenPosition -
                    previousScreenPosition
                )
              })
            })
          }
        }

        scope.$on(`${scope.name}:set-scroll-size`, function (e, size) {
          // Make sure that the editor has enough scroll margin above and below
          // to scroll the review panel with the given size
          const marginTop = size.overflowTop
          const { maxHeight } = editor.renderer.layerConfig
          const marginBottom = Math.max(size.height - maxHeight, 0)
          return setScrollMargins(marginTop, marginBottom)
        })

        var setScrollMargins = function (marginTop, marginBottom) {
          let marginChanged = false
          if (editor.renderer.scrollMargin.top !== marginTop) {
            editor.renderer.scrollMargin.top = marginTop
            marginChanged = true
          }
          if (editor.renderer.scrollMargin.bottom !== marginBottom) {
            editor.renderer.scrollMargin.bottom = marginBottom
            marginChanged = true
          }
          if (marginChanged) {
            return editor.renderer.updateFull()
          }
        }

        const resetScrollMargins = () => setScrollMargins(0, 0)

        scope.$watch('theme', value => editor.setTheme(`ace/theme/${value}`))

        scope.$watch('showPrintMargin', value =>
          editor.setShowPrintMargin(value)
        )

        scope.$watch('keybindings', function (value) {
          if (['vim', 'emacs'].includes(value)) {
            return editor.setKeyboardHandler(`ace/keyboard/${value}`)
          } else {
            return editor.setKeyboardHandler(null)
          }
        })

        scope.$watch('fontSize', value =>
          element.find('.ace_editor, .ace_content').css({
            'font-size': value + 'px'
          })
        )

        scope.$watch('fontFamily', function (value) {
          const monospaceFamilies = [
            'Monaco',
            'Menlo',
            'Ubuntu Mono',
            'Consolas',
            'monospace'
          ]

          if (value != null) {
            switch (value) {
              case 'monaco':
                return editor.setOption(
                  'fontFamily',
                  monospaceFamilies.join(', ')
                )
              case 'lucida':
                return editor.setOption(
                  'fontFamily',
                  '"Lucida Console", "Source Code Pro", monospace'
                )
              default:
                return editor.setOption('fontFamily', null)
            }
          }
        })

        scope.$watch('lineHeight', function (value) {
          if (value != null) {
            switch (value) {
              case 'compact':
                editor.container.style.lineHeight = 1.33
                break
              case 'normal':
                editor.container.style.lineHeight = 1.6
                break
              case 'wide':
                editor.container.style.lineHeight = 2
                break
              default:
                editor.container.style.lineHeight = 1.6
            }
            return editor.renderer.updateFontSize()
          }
        })

        scope.$watch('sharejsDoc', function (sharejs_doc, old_sharejs_doc) {
          if (old_sharejs_doc != null) {
            scope.$broadcast('beforeChangeDocument')
            detachFromAce(old_sharejs_doc)
          }
          if (sharejs_doc != null) {
            attachToAce(sharejs_doc)
          }
          if (sharejs_doc != null && old_sharejs_doc != null) {
            return scope.$broadcast('afterChangeDocument')
          }
        })

        scope.$watch('text', function (text) {
          if (text != null) {
            editor.setValue(text, -1)
            const session = editor.getSession()
            return session.setUseWrapMode(true)
          }
        })

        scope.$watch('annotations', function (annotations) {
          const session = editor.getSession()
          return session.setAnnotations(annotations)
        })

        scope.$watch('readOnly', value => editor.setReadOnly(!!value))

        scope.$watch('syntaxValidation', function (value) {
          // ignore undefined settings here
          // only instances of ace with an explicit value should set useWorker
          // the history instance will have syntaxValidation undefined
          if (value != null && syntaxValidationEnabled) {
            const session = editor.getSession()
            return session.setOption('useWorker', value)
          }
        })

        editor.setOption('scrollPastEnd', true)

        let updateCount = 0
        const onChange = function () {
          updateCount++

          if (updateCount === 100) {
            eventTracking.send('editor-interaction', 'multi-doc-update')
          }
          return scope.$emit(`${scope.name}:change`)
        }

        let currentFirstVisibleRow = null
        const emitMiddleVisibleRowChanged = () => {
          const firstVisibleRow = editor.getFirstVisibleRow()
          if (firstVisibleRow === currentFirstVisibleRow) return

          currentFirstVisibleRow = firstVisibleRow
          const lastVisibleRow = editor.getLastVisibleRow()
          scope.$emit(
            `scroll:editor:update`,
            Math.floor((firstVisibleRow + lastVisibleRow) / 2)
          )
        }

        const onScroll = function (scrollTop) {
          if (scope.eventsBridge == null) {
            return
          }
          const height = editor.renderer.layerConfig.maxHeight
          emitMiddleVisibleRowChanged()
          return scope.eventsBridge.emit('aceScroll', scrollTop, height)
        }

        const onScrollbarVisibilityChanged = function (event, vRenderer) {
          if (scope.eventsBridge == null) {
            return
          }
          return scope.eventsBridge.emit(
            'aceScrollbarVisibilityChanged',
            vRenderer.scrollBarV.isVisible,
            vRenderer.scrollBarV.width
          )
        }

        if (scope.eventsBridge != null) {
          editor.renderer.on(
            'scrollbarVisibilityChanged',
            onScrollbarVisibilityChanged
          )

          scope.eventsBridge.on('externalScroll', position =>
            editor.getSession().setScrollTop(position)
          )
          scope.eventsBridge.on('refreshScrollPosition', function () {
            const session = editor.getSession()
            session.setScrollTop(session.getScrollTop() + 1)
            return session.setScrollTop(session.getScrollTop() - 1)
          })
        }

        const onSessionChangeForSpellCheck = function (e) {
          spellCheckManager.onSessionChange()
          if (e.oldSession != null) {
            e.oldSession.getDocument().off('change', spellCheckManager.onChange)
          }
          e.session.getDocument().on('change', spellCheckManager.onChange)
          if (e.oldSession != null) {
            e.oldSession.off('changeScrollTop', spellCheckManager.onScroll)
          }
          return e.session.on('changeScrollTop', spellCheckManager.onScroll)
        }

        const initSpellCheck = function () {
          if (!spellCheckManager) return
          spellCheckManager.init()
          editor.on('changeSession', onSessionChangeForSpellCheck)
          onSessionChangeForSpellCheck({
            session: editor.getSession()
          }) // Force initial setup
          return editor.on('nativecontextmenu', spellCheckManager.onContextMenu)
        }

        const tearDownSpellCheck = function () {
          if (!spellCheckManager) return
          editor.off('changeSession', onSessionChangeForSpellCheck)
          return editor.off(
            'nativecontextmenu',
            spellCheckManager.onContextMenu
          )
        }

        const initTrackChanges = function () {
          trackChangesManager.rangesTracker = scope.sharejsDoc.ranges

          // Force onChangeSession in order to set up highlights etc.
          trackChangesManager.onChangeSession()

          if (!trackChangesManager) return
          editor.on('changeSelection', trackChangesManager.onChangeSelection)

          // Selection also moves with updates elsewhere in the document
          editor.on('change', trackChangesManager.onChangeSelection)

          editor.on('changeSession', trackChangesManager.onChangeSession)
          editor.on('cut', trackChangesManager.onCut)
          editor.on('paste', trackChangesManager.onPaste)
          editor.renderer.on('resize', trackChangesManager.onResize)
        }

        const tearDownTrackChanges = function () {
          if (!trackChangesManager) return
          trackChangesManager.tearDown()
          editor.off('changeSelection', trackChangesManager.onChangeSelection)

          editor.off('change', trackChangesManager.onChangeSelection)
          editor.off('changeSession', trackChangesManager.onChangeSession)
          editor.off('cut', trackChangesManager.onCut)
          editor.off('paste', trackChangesManager.onPaste)
          editor.renderer.off('resize', trackChangesManager.onResize)
        }

        const initUndo = function () {
          // Emulate onChangeSession event. Note: listening to changeSession
          // event is unnecessary since this method is called when we switch
          // sessions (via ShareJS changing) anyway
          undoManager.onChangeSession(editor.getSession())
          editor.on('change', undoManager.onChange)
        }

        const tearDownUndo = function () {
          editor.off('change', undoManager.onChange)
        }

        const onSessionChangeForCursorPosition = function (e) {
          if (e.oldSession != null) {
            e.oldSession.selection.off(
              'changeCursor',
              cursorPositionManager.onCursorChange
            )
          }
          return e.session.selection.on(
            'changeCursor',
            cursorPositionManager.onCursorChange
          )
        }

        const onUnloadForCursorPosition = () =>
          cursorPositionManager.onUnload(editor.getSession())

        const initCursorPosition = function () {
          editor.on('changeSession', onSessionChangeForCursorPosition)

          // Force initial setup
          onSessionChangeForCursorPosition({ session: editor.getSession() })

          return $(window).on('unload', onUnloadForCursorPosition)
        }

        const tearDownCursorPosition = function () {
          editor.off('changeSession', onSessionChangeForCursorPosition)
          return $(window).off('unload', onUnloadForCursorPosition)
        }

        initCursorPosition()

        // Trigger the event once *only* - this is called after Ace is connected
        // to the ShareJs instance but this event should only be triggered the
        // first time the editor is opened. Not every time the docs opened
        const triggerEditorInitEvent = _.once(() =>
          scope.$broadcast('editorInit')
        )

        var attachToAce = function (sharejs_doc) {
          let mode
          const lines = sharejs_doc.getSnapshot().split('\n')
          let session = editor.getSession()
          if (session != null) {
            session.destroy()
          }

          // see if we can lookup a suitable mode from ace
          // but fall back to text by default
          try {
            if (/\.(Rtex|bbl|tikz)$/i.test(scope.fileName)) {
              // recognise Rtex and bbl as latex
              mode = 'ace/mode/latex'
            } else if (/\.(sty|cls|clo)$/.test(scope.fileName)) {
              // recognise some common files as tex
              mode = 'ace/mode/tex'
            } else {
              ;({ mode } = ModeList.getModeForPath(scope.fileName))
              // we prefer plain_text mode over text mode because ace's
              // text mode is actually for code and has unwanted
              // indenting (see wrapMethod in ace edit_session.js)
              if (mode === 'ace/mode/text') {
                mode = 'ace/mode/plain_text'
              }
            }
          } catch (error) {
            mode = 'ace/mode/plain_text'
          }

          // create our new session
          session = new EditSession(lines, mode)

          session.setUseWrapMode(true)
          // use syntax validation only when explicitly set
          if (
            scope.syntaxValidation != null &&
            syntaxValidationEnabled &&
            !/\.bib$/.test(scope.fileName)
          ) {
            session.setOption('useWorker', scope.syntaxValidation)
          }

          // set to readonly until document change handlers are attached
          editor.setReadOnly(true)

          // now attach session to editor
          editor.setSession(session)

          const doc = session.getDocument()
          doc.on('change', onChange)

          editor.initing = true
          sharejs_doc.attachToAce(editor)
          editor.initing = false

          // now ready to edit document
          // respect the readOnly setting, normally false
          editor.setReadOnly(scope.readOnly)
          triggerEditorInitEvent()

          if (!scope.readOnly) {
            initSpellCheck()
          }

          initTrackChanges()
          initUndo()

          resetScrollMargins()

          // need to set annotations after attaching because attaching
          // deletes and then inserts document content
          session.setAnnotations(scope.annotations)

          session.on('changeScrollTop', eventTracking.editingSessionHeartbeat)

          angular
            .element($window)
            .on('click', eventTracking.editingSessionHeartbeat)

          scope.$on('$destroy', () =>
            angular
              .element($window)
              .off('click', eventTracking.editingSessionHeartbeat)
          )

          if (scope.eventsBridge != null) {
            session.on('changeScrollTop', onScroll)
          }

          $rootScope.hasLintingError = false
          session.on('changeAnnotation', function () {
            // Both linter errors and compile logs are set as error annotations,
            // however when the user types something, the compile logs are
            // replaced with linter errors. When we check for lint errors before
            // autocompile we are guaranteed to get linter errors
            const hasErrors =
              session
                .getAnnotations()
                .filter(annotation => annotation.type !== 'info').length > 0

            if ($rootScope.hasLintingError !== hasErrors) {
              return ($rootScope.hasLintingError = hasErrors)
            }
          })

          setTimeout(() =>
            // Let any listeners init themselves
            onScroll(editor.renderer.getScrollTop())
          )

          return editor.focus()
        }

        var detachFromAce = function (sharejs_doc) {
          tearDownSpellCheck()
          tearDownTrackChanges()
          tearDownUndo()
          sharejs_doc.detachFromAce()
          sharejs_doc.off('remoteop.recordRemote')

          const session = editor.getSession()
          session.off('changeScrollTop')

          const doc = session.getDocument()
          return doc.off('change', onChange)
        }

        if (scope.rendererData != null) {
          editor.renderer.on('changeCharacterSize', () => {
            scope.$apply(
              () => (scope.rendererData.lineHeight = editor.renderer.lineHeight)
            )
          })
        }

        scope.$watch('rendererData', function (rendererData) {
          if (rendererData != null) {
            return (rendererData.lineHeight = editor.renderer.lineHeight)
          }
        })

        scope.$on('$destroy', function () {
          if (scope.sharejsDoc != null) {
            scope.$broadcast('changeEditor')
            tearDownSpellCheck()
            tearDownCursorPosition()
            tearDownUndo()
            detachFromAce(scope.sharejsDoc)
            const session = editor.getSession()
            if (session != null) {
              session.destroy()
            }
            return scope.eventsBridge.emit(
              'aceScrollbarVisibilityChanged',
              false,
              0
            )
          }
        })

        return scope.$emit(`${scope.name}:inited`, editor)
      },

      template: `\
<div class="ace-editor-wrapper">
<div
  class="undo-conflict-warning alert alert-danger small"
  ng-show="undo.show_remote_warning"
>
  <strong>Watch out!</strong>
  We had to undo some of your collaborators changes before we could undo yours.
  <a
    href="#"
    class="pull-right"
    ng-click="undo.show_remote_warning = false"
  >Dismiss</a>
</div>
<div class="ace-editor-body"></div>
<spell-menu
  open="spellMenu.open"
  top="spellMenu.top"
  left="spellMenu.left"
  layout-from-bottom="spellMenu.layoutFromBottom"
  highlight="spellMenu.highlight"
  replace-word="replaceWord(highlight, suggestion)"
  learn-word="learnWord(highlight)"
></spell-menu>
<div
  class="annotation-label"
  ng-show="annotationLabel.show"
  ng-style="{
    position: 'absolute',
    left:     annotationLabel.left,
    right:    annotationLabel.right,
    bottom:   annotationLabel.bottom,
    top:      annotationLabel.top,
    'background-color': annotationLabel.backgroundColor
  }"
>
  {{ annotationLabel.text }}
</div>

<a
  href
  class="highlights-before-label btn btn-info btn-xs"
  ng-show="updateLabels.highlightsBefore > 0"
  ng-click="gotoHighlightAbove()"
>
  <i class="fa fa-fw fa-arrow-up"></i>
  {{ updateLabels.highlightsBefore }} more update{{ updateLabels.highlightsBefore > 1 && "" || "s" }} above
</a>

<a
  href
  class="highlights-after-label btn btn-info btn-xs"
  ng-show="updateLabels.highlightsAfter > 0"
  ng-click="gotoHighlightBelow()"
>
  <i class="fa fa-fw fa-arrow-down"></i>
  {{ updateLabels.highlightsAfter }} more update{{ updateLabels.highlightsAfter > 1 && "" || "s" }} below

</a>
</div>\
`
    }
  }
)

function monkeyPatchSearch($rootScope, $compile) {
  const searchHtml = `\
<div class="ace_search right">
<a href type="button" action="hide" class="ace_searchbtn_close">
  <i class="fa fa-fw fa-times"></i>
</a>
<div class="ace_search_form">
  <input class="ace_search_field form-control input-sm" placeholder="Search for" spellcheck="false"></input>
  <div class="btn-group">
    <button type="button" action="findNext" class="ace_searchbtn next btn btn-default btn-sm">
      <i class="fa fa-chevron-down fa-fw"></i>
    </button>
    <button type="button" action="findPrev" class="ace_searchbtn prev btn btn-default btn-sm">
      <i class="fa fa-chevron-up fa-fw"></i>
    </button>
  </div>
</div>
<div class="ace_replace_form">
  <input class="ace_search_field form-control input-sm" placeholder="Replace with" spellcheck="false"></input>
  <div class="btn-group">
    <button type="button" action="replaceAndFindNext" class="ace_replacebtn btn btn-default btn-sm">Replace</button>
    <button type="button" action="replaceAll" class="ace_replacebtn btn btn-default btn-sm">All</button>
  </div>
</div>
<div class="ace_search_options">
  <div class="btn-group">
    <button action="toggleRegexpMode" class="btn btn-default btn-sm" tooltip-placement="bottom" tooltip-append-to-body="true" tooltip="RegExp Search">.*</button>
    <button action="toggleCaseSensitive" class="btn btn-default btn-sm" tooltip-placement="bottom" tooltip-append-to-body="true" tooltip="CaseSensitive Search">Aa</button>
    <button action="toggleWholeWords" class="btn btn-default btn-sm" tooltip-placement="bottom" tooltip-append-to-body="true" tooltip="Whole Word Search">"..."</button>
    <button action="searchInSelection" class="btn btn-default btn-sm" tooltip-placement="bottom" tooltip-append-to-body="true" tooltip="Search Within Selection"><i class="fa fa-align-left"></i></button>
  </div>
  <span class="ace_search_counter"></span>
</div>
<div action="toggleReplace" class="hidden"></div>
</div>\
`

  // Remove Ace CSS
  $('#ace_searchbox').remove()

  const SB = SearchBox.SearchBox
  const { $init } = SB.prototype
  SB.prototype.$init = function () {
    this.element = $compile(searchHtml)($rootScope.$new())[0]
    return $init.apply(this)
  }
}
