_ = require 'underscore-plus'
scrollbarStyle = require 'scrollbar-style'
{Range, Point} = require 'text-buffer'
{CompositeDisposable} = require 'event-kit'
{ipcRenderer} = require 'electron'

TextEditorPresenter = require './text-editor-presenter'
GutterContainerComponent = require './gutter-container-component'
InputComponent = require './input-component'
LinesComponent = require './lines-component'
ScrollbarComponent = require './scrollbar-component'
ScrollbarCornerComponent = require './scrollbar-corner-component'
OverlayManager = require './overlay-manager'
DOMElementPool = require './dom-element-pool'
LinesYardstick = require './lines-yardstick'
BlockDecorationsComponent = require './block-decorations-component'
LineTopIndex = require 'line-top-index'

module.exports =
class TextEditorComponent
  scrollSensitivity: 0.4
  cursorBlinkPeriod: 800
  cursorBlinkResumeDelay: 100
  tileSize: 12

  pendingScrollTop: null
  pendingScrollLeft: null
  updateRequested: false
  updatesPaused: false
  updateRequestedWhilePaused: false
  heightAndWidthMeasurementRequested: false
  inputEnabled: true
  measureScrollbarsWhenShown: true
  measureLineHeightAndDefaultCharWidthWhenShown: true
  stylingChangeAnimationFrameRequested: false
  gutterComponent: null
  mounted: true
  initialized: false

  Object.defineProperty @prototype, "domNode",
    get: -> @domNodeValue
    set: (domNode) ->
      @assert domNode?, "TextEditorComponent::domNode was set to null."
      @domNodeValue = domNode

  constructor: ({@editor, @hostElement, @rootElement, @stylesElement, @useShadowDOM, tileSize, @views, @themes, @config, @workspace, @assert, @grammars}) ->
    @tileSize = tileSize if tileSize?
    @disposables = new CompositeDisposable

    @observeConfig()
    @setScrollSensitivity(@config.get('editor.scrollSensitivity'))

    lineTopIndex = new LineTopIndex({
      defaultLineHeight: @editor.getLineHeightInPixels()
    })
    @presenter = new TextEditorPresenter
      model: @editor
      tileSize: tileSize
      cursorBlinkPeriod: @cursorBlinkPeriod
      cursorBlinkResumeDelay: @cursorBlinkResumeDelay
      stoppedScrollingDelay: 200
      config: @config
      lineTopIndex: lineTopIndex

    @presenter.onDidUpdateState(@requestUpdate)

    @domElementPool = new DOMElementPool
    @domNode = document.createElement('div')
    if @useShadowDOM
      @domNode.classList.add('editor-contents--private')

      insertionPoint = document.createElement('content')
      insertionPoint.setAttribute('select', 'atom-overlay')
      @domNode.appendChild(insertionPoint)
      @overlayManager = new OverlayManager(@presenter, @hostElement, @views)
      @blockDecorationsComponent = new BlockDecorationsComponent(@hostElement, @views, @presenter, @domElementPool)
    else
      @domNode.classList.add('editor-contents')
      @overlayManager = new OverlayManager(@presenter, @domNode, @views)

    @scrollViewNode = document.createElement('div')
    @scrollViewNode.classList.add('scroll-view')
    @domNode.appendChild(@scrollViewNode)

    @hiddenInputComponent = new InputComponent
    @scrollViewNode.appendChild(@hiddenInputComponent.getDomNode())

    @linesComponent = new LinesComponent({@presenter, @hostElement, @useShadowDOM, @domElementPool, @assert, @grammars})
    @scrollViewNode.appendChild(@linesComponent.getDomNode())

    if @blockDecorationsComponent?
      @linesComponent.getDomNode().appendChild(@blockDecorationsComponent.getDomNode())

    @linesYardstick = new LinesYardstick(@editor, @linesComponent, lineTopIndex, @grammars)
    @presenter.setLinesYardstick(@linesYardstick)

    @horizontalScrollbarComponent = new ScrollbarComponent({orientation: 'horizontal', onScroll: @onHorizontalScroll})
    @scrollViewNode.appendChild(@horizontalScrollbarComponent.getDomNode())

    @verticalScrollbarComponent = new ScrollbarComponent({orientation: 'vertical', onScroll: @onVerticalScroll})
    @domNode.appendChild(@verticalScrollbarComponent.getDomNode())

    @scrollbarCornerComponent = new ScrollbarCornerComponent
    @domNode.appendChild(@scrollbarCornerComponent.getDomNode())

    @observeEditor()
    @listenForDOMEvents()

    @disposables.add @stylesElement.onDidAddStyleElement @onStylesheetsChanged
    @disposables.add @stylesElement.onDidUpdateStyleElement @onStylesheetsChanged
    @disposables.add @stylesElement.onDidRemoveStyleElement @onStylesheetsChanged
    unless @themes.isInitialLoadComplete()
      @disposables.add @themes.onDidChangeActiveThemes @onAllThemesLoaded
    @disposables.add scrollbarStyle.onDidChangePreferredScrollbarStyle @refreshScrollbars

    @disposables.add @views.pollDocument(@pollDOM)

    @updateSync()
    @checkForVisibilityChange()
    @initialized = true

  destroy: ->
    @mounted = false
    @disposables.dispose()
    @presenter.destroy()
    @gutterContainerComponent?.destroy()
    @domElementPool.clear()

    @verticalScrollbarComponent.destroy()
    @horizontalScrollbarComponent.destroy()

    @onVerticalScroll = null
    @onHorizontalScroll = null

  getDomNode: ->
    @domNode

  updateSync: ->
    @updateSyncPreMeasurement()

    @oldState ?= {}
    @newState = @presenter.getPostMeasurementState()

    if @editor.getLastSelection()? and not @editor.getLastSelection().isEmpty()
      @domNode.classList.add('has-selection')
    else
      @domNode.classList.remove('has-selection')

    if @newState.focused isnt @oldState.focused
      @domNode.classList.toggle('is-focused', @newState.focused)

    @performedInitialMeasurement = false if @editor.isDestroyed()

    if @performedInitialMeasurement
      if @newState.height isnt @oldState.height
        if @newState.height?
          @domNode.style.height = @newState.height + 'px'
        else
          @domNode.style.height = ''

    if @newState.gutters.length
      @mountGutterContainerComponent() unless @gutterContainerComponent?
      @gutterContainerComponent.updateSync(@newState)
    else
      @gutterContainerComponent?.getDomNode()?.remove()
      @gutterContainerComponent = null

    @hiddenInputComponent.updateSync(@newState)
    @linesComponent.updateSync(@newState)
    @blockDecorationsComponent?.updateSync(@newState)
    @horizontalScrollbarComponent.updateSync(@newState)
    @verticalScrollbarComponent.updateSync(@newState)
    @scrollbarCornerComponent.updateSync(@newState)

    @overlayManager?.render(@newState)

    if @clearPoolAfterUpdate
      @domElementPool.clear()
      @clearPoolAfterUpdate = false

    if @editor.isAlive()
      @updateParentViewFocusedClassIfNeeded()
      @updateParentViewMiniClass()

  updateSyncPreMeasurement: ->
    @linesComponent.updateSync(@presenter.getPreMeasurementState())

  readAfterUpdateSync: =>
    @overlayManager?.measureOverlays()
    @blockDecorationsComponent?.measureBlockDecorations() if @isVisible()

  mountGutterContainerComponent: ->
    @gutterContainerComponent = new GutterContainerComponent({@editor, @onLineNumberGutterMouseDown, @domElementPool, @views})
    @domNode.insertBefore(@gutterContainerComponent.getDomNode(), @domNode.firstChild)

  becameVisible: ->
    @updatesPaused = true
    @measureScrollbars() if @measureScrollbarsWhenShown
    @sampleFontStyling()
    @sampleBackgroundColors()
    @measureWindowSize()
    @measureDimensions()
    @measureLineHeightAndDefaultCharWidth() if @measureLineHeightAndDefaultCharWidthWhenShown
    @editor.setVisible(true)
    @performedInitialMeasurement = true
    @updatesPaused = false
    @updateSync() if @canUpdate()

  requestUpdate: =>
    return unless @canUpdate()

    if @updatesPaused
      @updateRequestedWhilePaused = true
      return

    if @hostElement.isUpdatedSynchronously()
      @updateSync()
    else unless @updateRequested
      @updateRequested = true
      @views.updateDocument =>
        @updateRequested = false
        @updateSync() if @canUpdate()
      @views.readDocument(@readAfterUpdateSync)

  canUpdate: ->
    @mounted and @editor.isAlive()

  requestAnimationFrame: (fn) ->
    @updatesPaused = true
    requestAnimationFrame =>
      fn()
      @updatesPaused = false
      if @updateRequestedWhilePaused and @canUpdate()
        @updateRequestedWhilePaused = false
        @requestUpdate()

  getTopmostDOMNode: ->
    @hostElement

  observeEditor: ->
    @disposables.add @editor.observeGrammar(@onGrammarChanged)

  listenForDOMEvents: ->
    @domNode.addEventListener 'mousewheel', @onMouseWheel
    @domNode.addEventListener 'textInput', @onTextInput
    @scrollViewNode.addEventListener 'mousedown', @onMouseDown
    @scrollViewNode.addEventListener 'scroll', @onScrollViewScroll

    @listenForIMEEvents()
    @trackSelectionClipboard() if process.platform is 'linux'

  listenForIMEEvents: ->
    # The IME composition events work like this:
    #
    # User types 's', chromium pops up the completion helper
    #   1. compositionstart fired
    #   2. compositionupdate fired; event.data == 's'
    # User hits arrow keys to move around in completion helper
    #   3. compositionupdate fired; event.data == 's' for each arry key press
    # User escape to cancel
    #   4. compositionend fired
    # OR User chooses a completion
    #   4. compositionend fired
    #   5. textInput fired; event.data == the completion string

    checkpoint = null
    @domNode.addEventListener 'compositionstart', =>
      checkpoint = @editor.createCheckpoint()
    @domNode.addEventListener 'compositionupdate', (event) =>
      @editor.insertText(event.data, select: true)
    @domNode.addEventListener 'compositionend', (event) =>
      @editor.revertToCheckpoint(checkpoint)
      event.target.value = ''

  # Listen for selection changes and store the currently selected text
  # in the selection clipboard. This is only applicable on Linux.
  trackSelectionClipboard: ->
    timeoutId = null
    writeSelectedTextToSelectionClipboard = =>
      return if @editor.isDestroyed()
      if selectedText = @editor.getSelectedText()
        # This uses ipcRenderer.send instead of clipboard.writeText because
        # clipboard.writeText is a sync ipcRenderer call on Linux and that
        # will slow down selections.
        ipcRenderer.send('write-text-to-selection-clipboard', selectedText)
    @disposables.add @editor.onDidChangeSelectionRange ->
      clearTimeout(timeoutId)
      timeoutId = setTimeout(writeSelectedTextToSelectionClipboard)

  observeConfig: ->
    @disposables.add @config.onDidChange 'editor.fontSize', =>
      @sampleFontStyling()
      @invalidateMeasurements()
    @disposables.add @config.onDidChange 'editor.fontFamily', =>
      @sampleFontStyling()
      @invalidateMeasurements()
    @disposables.add @config.onDidChange 'editor.lineHeight', =>
      @sampleFontStyling()
      @invalidateMeasurements()

  onGrammarChanged: =>
    if @scopedConfigDisposables?
      @scopedConfigDisposables.dispose()
      @disposables.remove(@scopedConfigDisposables)

    @scopedConfigDisposables = new CompositeDisposable
    @disposables.add(@scopedConfigDisposables)

    scope = @editor.getRootScopeDescriptor()
    @scopedConfigDisposables.add @config.observe 'editor.scrollSensitivity', {scope}, @setScrollSensitivity

  focused: ->
    if @mounted
      @presenter.setFocused(true)
      @hiddenInputComponent.getDomNode().focus()

  blurred: ->
    if @mounted
      @presenter.setFocused(false)

  onTextInput: (event) =>
    event.stopPropagation()

    # If we prevent the insertion of a space character, then the browser
    # interprets the spacebar keypress as a page-down command.
    event.preventDefault() unless event.data is ' '

    return unless @isInputEnabled()

    inputNode = event.target

    # Work around of the accented character suggestion feature in OS X.
    # Text input fires before a character is inserted, and if the browser is
    # replacing the previous un-accented character with an accented variant, it
    # will select backward over it.
    selectedLength = inputNode.selectionEnd - inputNode.selectionStart
    @editor.selectLeft() if selectedLength is 1

    insertedRange = @editor.insertText(event.data, groupUndo: true)
    inputNode.value = event.data if insertedRange

  onVerticalScroll: (scrollTop) =>
    return if @updateRequested or scrollTop is @presenter.getScrollTop()

    animationFramePending = @pendingScrollTop?
    @pendingScrollTop = scrollTop
    unless animationFramePending
      @requestAnimationFrame =>
        pendingScrollTop = @pendingScrollTop
        @pendingScrollTop = null
        @presenter.setScrollTop(pendingScrollTop)
        @presenter.commitPendingScrollTopPosition()

  onHorizontalScroll: (scrollLeft) =>
    return if @updateRequested or scrollLeft is @presenter.getScrollLeft()

    animationFramePending = @pendingScrollLeft?
    @pendingScrollLeft = scrollLeft
    unless animationFramePending
      @requestAnimationFrame =>
        @presenter.setScrollLeft(@pendingScrollLeft)
        @presenter.commitPendingScrollLeftPosition()
        @pendingScrollLeft = null

  onMouseWheel: (event) =>
    # Only scroll in one direction at a time
    {wheelDeltaX, wheelDeltaY} = event

    # Ctrl+MouseWheel adjusts font size.
    if event.ctrlKey and @config.get('editor.zoomFontWhenCtrlScrolling')
      if wheelDeltaY > 0
        @workspace.increaseFontSize()
      else if wheelDeltaY < 0
        @workspace.decreaseFontSize()
      event.preventDefault()
      return

    if Math.abs(wheelDeltaX) > Math.abs(wheelDeltaY)
      # Scrolling horizontally
      previousScrollLeft = @presenter.getScrollLeft()
      updatedScrollLeft = previousScrollLeft - Math.round(wheelDeltaX * @scrollSensitivity)

      event.preventDefault() if @presenter.canScrollLeftTo(updatedScrollLeft)
      @presenter.setScrollLeft(updatedScrollLeft)
    else
      # Scrolling vertically
      @presenter.setMouseWheelScreenRow(@screenRowForNode(event.target))
      previousScrollTop = @presenter.getScrollTop()
      updatedScrollTop = previousScrollTop - Math.round(wheelDeltaY * @scrollSensitivity)

      event.preventDefault() if @presenter.canScrollTopTo(updatedScrollTop)
      @presenter.setScrollTop(updatedScrollTop)

  onScrollViewScroll: =>
    if @mounted
      console.warn "TextEditorScrollView scrolled when it shouldn't have."
      @scrollViewNode.scrollTop = 0
      @scrollViewNode.scrollLeft = 0

  onDidChangeScrollTop: (callback) ->
    @presenter.onDidChangeScrollTop(callback)

  onDidChangeScrollLeft: (callback) ->
    @presenter.onDidChangeScrollLeft(callback)

  setScrollLeft: (scrollLeft) ->
    @presenter.setScrollLeft(scrollLeft)

  setScrollRight: (scrollRight) ->
    @presenter.setScrollRight(scrollRight)

  setScrollTop: (scrollTop) ->
    @presenter.setScrollTop(scrollTop)

  setScrollBottom: (scrollBottom) ->
    @presenter.setScrollBottom(scrollBottom)

  getScrollTop: ->
    @presenter.getScrollTop()

  getScrollLeft: ->
    @presenter.getScrollLeft()

  getScrollRight: ->
    @presenter.getScrollRight()

  getScrollBottom: ->
    @presenter.getScrollBottom()

  getScrollHeight: ->
    @presenter.getScrollHeight()

  getScrollWidth: ->
    @presenter.getScrollWidth()

  getMaxScrollTop: ->
    @presenter.getMaxScrollTop()

  getVerticalScrollbarWidth: ->
    @presenter.getVerticalScrollbarWidth()

  getHorizontalScrollbarHeight: ->
    @presenter.getHorizontalScrollbarHeight()

  getVisibleRowRange: ->
    @presenter.getVisibleRowRange()

  pixelPositionForScreenPosition: (screenPosition, clip=true) ->
    screenPosition = Point.fromObject(screenPosition)
    screenPosition = @editor.clipScreenPosition(screenPosition) if clip

    unless @presenter.isRowVisible(screenPosition.row)
      @presenter.setScreenRowsToMeasure([screenPosition.row])

    unless @linesComponent.lineNodeForLineIdAndScreenRow(@presenter.lineIdForScreenRow(screenPosition.row), screenPosition.row)?
      @updateSyncPreMeasurement()

    pixelPosition = @linesYardstick.pixelPositionForScreenPosition(screenPosition)
    @presenter.clearScreenRowsToMeasure()
    pixelPosition

  screenPositionForPixelPosition: (pixelPosition) ->
    row = @linesYardstick.measuredRowForPixelPosition(pixelPosition)
    if row? and not @presenter.isRowVisible(row)
      @presenter.setScreenRowsToMeasure([row])
      @updateSyncPreMeasurement()

    position = @linesYardstick.screenPositionForPixelPosition(pixelPosition)
    @presenter.clearScreenRowsToMeasure()
    position

  pixelRectForScreenRange: (screenRange) ->
    rowsToMeasure = []
    unless @presenter.isRowVisible(screenRange.start.row)
      rowsToMeasure.push(screenRange.start.row)
    unless @presenter.isRowVisible(screenRange.end.row)
      rowsToMeasure.push(screenRange.end.row)

    if rowsToMeasure.length > 0
      @presenter.setScreenRowsToMeasure(rowsToMeasure)
      @updateSyncPreMeasurement()

    rect = @presenter.absolutePixelRectForScreenRange(screenRange)

    if rowsToMeasure.length > 0
      @presenter.clearScreenRowsToMeasure()

    rect

  pixelRangeForScreenRange: (screenRange, clip=true) ->
    {start, end} = Range.fromObject(screenRange)
    {start: @pixelPositionForScreenPosition(start, clip), end: @pixelPositionForScreenPosition(end, clip)}

  pixelPositionForBufferPosition: (bufferPosition) ->
    @pixelPositionForScreenPosition(
      @editor.screenPositionForBufferPosition(bufferPosition)
    )

  invalidateBlockDecorationDimensions: ->
    @presenter.invalidateBlockDecorationDimensions(arguments...)

  onMouseDown: (event) =>
    unless event.button is 0 or (event.button is 1 and process.platform is 'linux')
      # Only handle mouse down events for left mouse button on all platforms
      # and middle mouse button on Linux since it pastes the selection clipboard
      return

    return if event.target?.classList.contains('horizontal-scrollbar')

    {detail, shiftKey, metaKey, ctrlKey} = event

    # CTRL+click brings up the context menu on OSX, so don't handle those either
    return if ctrlKey and process.platform is 'darwin'

    # Prevent focusout event on hidden input if editor is already focused
    event.preventDefault() if @oldState.focused

    screenPosition = @screenPositionForMouseEvent(event)

    if event.target?.classList.contains('fold-marker')
      bufferRow = @editor.bufferRowForScreenRow(screenPosition.row)
      @editor.unfoldBufferRow(bufferRow)
      return

    switch detail
      when 1
        if shiftKey
          @editor.selectToScreenPosition(screenPosition)
        else if metaKey or (ctrlKey and process.platform isnt 'darwin')
          cursorAtScreenPosition = @editor.getCursorAtScreenPosition(screenPosition)
          if cursorAtScreenPosition and @editor.hasMultipleCursors()
            cursorAtScreenPosition.destroy()
          else
            @editor.addCursorAtScreenPosition(screenPosition, autoscroll: false)
        else
          @editor.setCursorScreenPosition(screenPosition, autoscroll: false)
      when 2
        @editor.getLastSelection().selectWord(autoscroll: false)
      when 3
        @editor.getLastSelection().selectLine(null, autoscroll: false)

    @handleDragUntilMouseUp (screenPosition) =>
      @editor.selectToScreenPosition(screenPosition, suppressSelectionMerge: true, autoscroll: false)

  onLineNumberGutterMouseDown: (event) =>
    return unless event.button is 0 # only handle the left mouse button

    {shiftKey, metaKey, ctrlKey} = event

    if shiftKey
      @onGutterShiftClick(event)
    else if metaKey or (ctrlKey and process.platform isnt 'darwin')
      @onGutterMetaClick(event)
    else
      @onGutterClick(event)

  onGutterClick: (event) =>
    clickedScreenRow = @screenPositionForMouseEvent(event).row
    clickedBufferRow = @editor.bufferRowForScreenRow(clickedScreenRow)
    initialScreenRange = @editor.screenRangeForBufferRange([[clickedBufferRow, 0], [clickedBufferRow + 1, 0]])
    @editor.setSelectedScreenRange(initialScreenRange, preserveFolds: true, autoscroll: false)
    @handleGutterDrag(initialScreenRange)

  onGutterMetaClick: (event) =>
    clickedScreenRow = @screenPositionForMouseEvent(event).row
    clickedBufferRow = @editor.bufferRowForScreenRow(clickedScreenRow)
    initialScreenRange = @editor.screenRangeForBufferRange([[clickedBufferRow, 0], [clickedBufferRow + 1, 0]])
    @editor.addSelectionForScreenRange(initialScreenRange, preserveFolds: true, autoscroll: false)
    @handleGutterDrag(initialScreenRange)

  onGutterShiftClick: (event) =>
    tailScreenPosition = @editor.getLastSelection().getTailScreenPosition()
    clickedScreenRow = @screenPositionForMouseEvent(event).row
    clickedBufferRow = @editor.bufferRowForScreenRow(clickedScreenRow)
    clickedLineScreenRange = @editor.screenRangeForBufferRange([[clickedBufferRow, 0], [clickedBufferRow + 1, 0]])

    if clickedScreenRow < tailScreenPosition.row
      @editor.selectToScreenPosition(clickedLineScreenRange.start, suppressSelectionMerge: true, autoscroll: false)
    else
      @editor.selectToScreenPosition(clickedLineScreenRange.end, suppressSelectionMerge: true, autoscroll: false)

    @handleGutterDrag(new Range(tailScreenPosition, tailScreenPosition))

  handleGutterDrag: (initialRange) ->
    @handleDragUntilMouseUp (screenPosition) =>
      dragRow = screenPosition.row
      if dragRow < initialRange.start.row
        startPosition = @editor.clipScreenPosition([dragRow, 0], skipSoftWrapIndentation: true)
        screenRange = new Range(startPosition, startPosition).union(initialRange)
        @editor.getLastSelection().setScreenRange(screenRange, reversed: true, autoscroll: false, preserveFolds: true)
      else
        endPosition = [dragRow + 1, 0]
        screenRange = new Range(endPosition, endPosition).union(initialRange)
        @editor.getLastSelection().setScreenRange(screenRange, reversed: false, autoscroll: false, preserveFolds: true)

  onStylesheetsChanged: (styleElement) =>
    return unless @performedInitialMeasurement
    return unless @themes.isInitialLoadComplete()

    # This delay prevents the styling from going haywire when stylesheets are
    # reloaded in dev mode. It seems like a workaround for a browser bug, but
    # not totally sure.

    unless @stylingChangeAnimationFrameRequested
      @stylingChangeAnimationFrameRequested = true
      requestAnimationFrame =>
        @stylingChangeAnimationFrameRequested = false
        if @mounted
          @refreshScrollbars() if not styleElement.sheet? or @containsScrollbarSelector(styleElement.sheet)
          @handleStylingChange()

  onAllThemesLoaded: =>
    @refreshScrollbars()
    @handleStylingChange()

  handleStylingChange: =>
    @sampleFontStyling()
    @sampleBackgroundColors()
    @invalidateMeasurements()

  handleDragUntilMouseUp: (dragHandler) ->
    dragging = false
    lastMousePosition = {}
    animationLoop = =>
      @requestAnimationFrame =>
        if dragging and @mounted
          linesClientRect = @linesComponent.getDomNode().getBoundingClientRect()
          autoscroll(lastMousePosition, linesClientRect)
          screenPosition = @screenPositionForMouseEvent(lastMousePosition, linesClientRect)
          dragHandler(screenPosition)
          animationLoop()
        else if not @mounted
          stopDragging()

    onMouseMove = (event) ->
      lastMousePosition.clientX = event.clientX
      lastMousePosition.clientY = event.clientY

      # Start the animation loop when the mouse moves prior to a mouseup event
      unless dragging
        dragging = true
        animationLoop()

      # Stop dragging when cursor enters dev tools because we can't detect mouseup
      onMouseUp() if event.which is 0

    onMouseUp = (event) =>
      if dragging
        stopDragging()
        @editor.finalizeSelections()
        @editor.mergeIntersectingSelections()
      pasteSelectionClipboard(event)

    stopDragging = ->
      dragging = false
      window.removeEventListener('mousemove', onMouseMove)
      window.removeEventListener('mouseup', onMouseUp)
      disposables.dispose()

    autoscroll = (mouseClientPosition) =>
      {top, bottom, left, right} = @scrollViewNode.getBoundingClientRect()
      top += 30
      bottom -= 30
      left += 30
      right -= 30

      if mouseClientPosition.clientY < top
        mouseYDelta = top - mouseClientPosition.clientY
        yDirection = -1
      else if mouseClientPosition.clientY > bottom
        mouseYDelta = mouseClientPosition.clientY - bottom
        yDirection = 1

      if mouseClientPosition.clientX < left
        mouseXDelta = left - mouseClientPosition.clientX
        xDirection = -1
      else if mouseClientPosition.clientX > right
        mouseXDelta = mouseClientPosition.clientX - right
        xDirection = 1

      if mouseYDelta?
        @presenter.setScrollTop(@presenter.getScrollTop() + yDirection * scaleScrollDelta(mouseYDelta))
        @presenter.commitPendingScrollTopPosition()

      if mouseXDelta?
        @presenter.setScrollLeft(@presenter.getScrollLeft() + xDirection * scaleScrollDelta(mouseXDelta))
        @presenter.commitPendingScrollLeftPosition()

    scaleScrollDelta = (scrollDelta) ->
      Math.pow(scrollDelta / 2, 3) / 280

    pasteSelectionClipboard = (event) =>
      if event?.which is 2 and process.platform is 'linux'
        if selection = require('./safe-clipboard').readText('selection')
          @editor.insertText(selection)

    window.addEventListener('mousemove', onMouseMove)
    window.addEventListener('mouseup', onMouseUp)
    disposables = new CompositeDisposable
    disposables.add(@editor.getBuffer().onWillChange(onMouseUp))
    disposables.add(@editor.onDidDestroy(stopDragging))

  isVisible: ->
    # Investigating an exception that occurs here due to ::domNode being null.
    @assert @domNode?, "TextEditorComponent::domNode was null.", (error) =>
      error.metadata = {@initialized}

    @domNode? and (@domNode.offsetHeight > 0 or @domNode.offsetWidth > 0)

  pollDOM: =>
    unless @checkForVisibilityChange()
      @sampleBackgroundColors()
      @measureDimensions()
      @sampleFontStyling()
      @overlayManager?.measureOverlays()

  checkForVisibilityChange: ->
    if @isVisible()
      if @wasVisible
        false
      else
        @becameVisible()
        @wasVisible = true
    else
      @wasVisible = false

  # Measure explicitly-styled height and width and relay them to the model. If
  # these values aren't explicitly styled, we assume the editor is unconstrained
  # and use the scrollHeight / scrollWidth as its height and width in
  # calculations.
  measureDimensions: ->
    return unless @mounted

    {position} = getComputedStyle(@hostElement)
    {height} = @hostElement.style

    if position is 'absolute' or height
      @presenter.setAutoHeight(false)
      height =  @hostElement.offsetHeight
      if height > 0
        @presenter.setExplicitHeight(height)
    else
      @presenter.setAutoHeight(true)
      @presenter.setExplicitHeight(null)

    clientWidth = @scrollViewNode.clientWidth
    paddingLeft = parseInt(getComputedStyle(@scrollViewNode).paddingLeft)
    clientWidth -= paddingLeft
    if clientWidth > 0
      @presenter.setContentFrameWidth(clientWidth)

    @presenter.setGutterWidth(@gutterContainerComponent?.getDomNode().offsetWidth ? 0)
    @presenter.setBoundingClientRect(@hostElement.getBoundingClientRect())

  measureWindowSize: ->
    return unless @mounted

    # FIXME: on Ubuntu (via xvfb) `window.innerWidth` reports an incorrect value
    # when window gets resized through `atom.setWindowDimensions({width:
    # windowWidth, height: windowHeight})`.
    @presenter.setWindowSize(window.innerWidth, window.innerHeight)

  sampleFontStyling: =>
    oldFontSize = @fontSize
    oldFontFamily = @fontFamily
    oldLineHeight = @lineHeight

    {@fontSize, @fontFamily, @lineHeight} = getComputedStyle(@getTopmostDOMNode())

    if @fontSize isnt oldFontSize or @fontFamily isnt oldFontFamily or @lineHeight isnt oldLineHeight
      @clearPoolAfterUpdate = true
      @measureLineHeightAndDefaultCharWidth()
      @invalidateMeasurements()

  sampleBackgroundColors: (suppressUpdate) ->
    {backgroundColor} = getComputedStyle(@hostElement)

    @presenter.setBackgroundColor(backgroundColor)

    lineNumberGutter = @gutterContainerComponent?.getLineNumberGutterComponent()
    if lineNumberGutter
      gutterBackgroundColor = getComputedStyle(lineNumberGutter.getDomNode()).backgroundColor
      @presenter.setGutterBackgroundColor(gutterBackgroundColor)

  measureLineHeightAndDefaultCharWidth: ->
    if @isVisible()
      @measureLineHeightAndDefaultCharWidthWhenShown = false
      @linesComponent.measureLineHeightAndDefaultCharWidth()
    else
      @measureLineHeightAndDefaultCharWidthWhenShown = true

  measureScrollbars: ->
    @measureScrollbarsWhenShown = false

    cornerNode = @scrollbarCornerComponent.getDomNode()
    originalDisplayValue = cornerNode.style.display

    cornerNode.style.display = 'block'

    width = (cornerNode.offsetWidth - cornerNode.clientWidth) or 15
    height = (cornerNode.offsetHeight - cornerNode.clientHeight) or 15

    @presenter.setVerticalScrollbarWidth(width)
    @presenter.setHorizontalScrollbarHeight(height)

    cornerNode.style.display = originalDisplayValue

  containsScrollbarSelector: (stylesheet) ->
    for rule in stylesheet.cssRules
      if rule.selectorText?.indexOf('scrollbar') > -1
        return true
    false

  refreshScrollbars: =>
    if @isVisible()
      @measureScrollbarsWhenShown = false
    else
      @measureScrollbarsWhenShown = true
      return

    verticalNode = @verticalScrollbarComponent.getDomNode()
    horizontalNode = @horizontalScrollbarComponent.getDomNode()
    cornerNode = @scrollbarCornerComponent.getDomNode()

    originalVerticalDisplayValue = verticalNode.style.display
    originalHorizontalDisplayValue = horizontalNode.style.display
    originalCornerDisplayValue = cornerNode.style.display

    # First, hide all scrollbars in case they are visible so they take on new
    # styles when they are shown again.
    verticalNode.style.display = 'none'
    horizontalNode.style.display = 'none'
    cornerNode.style.display = 'none'

    # Force a reflow
    cornerNode.offsetWidth

    # Now measure the new scrollbar dimensions
    @measureScrollbars()

    # Now restore the display value for all scrollbars, since they were
    # previously hidden
    verticalNode.style.display = originalVerticalDisplayValue
    horizontalNode.style.display = originalHorizontalDisplayValue
    cornerNode.style.display = originalCornerDisplayValue

  consolidateSelections: (e) ->
    e.abortKeyBinding() unless @editor.consolidateSelections()

  lineNodeForScreenRow: (screenRow) ->
    tileRow = @presenter.tileForRow(screenRow)
    tileComponent = @linesComponent.getComponentForTile(tileRow)

    tileComponent?.lineNodeForScreenRow(screenRow)

  lineNumberNodeForScreenRow: (screenRow) ->
    tileRow = @presenter.tileForRow(screenRow)
    gutterComponent = @gutterContainerComponent.getLineNumberGutterComponent()
    tileComponent = gutterComponent.getComponentForTile(tileRow)

    tileComponent?.lineNumberNodeForScreenRow(screenRow)

  tileNodesForLines: ->
    @linesComponent.getTiles()

  tileNodesForLineNumbers: ->
    gutterComponent = @gutterContainerComponent.getLineNumberGutterComponent()
    gutterComponent.getTiles()

  screenRowForNode: (node) ->
    while node?
      if screenRow = node.dataset.screenRow
        return parseInt(screenRow)
      node = node.parentElement
    null

  getFontSize: ->
    parseInt(getComputedStyle(@getTopmostDOMNode()).fontSize)

  setFontSize: (fontSize) ->
    @getTopmostDOMNode().style.fontSize = fontSize + 'px'
    @sampleFontStyling()
    @invalidateMeasurements()

  getFontFamily: ->
    getComputedStyle(@getTopmostDOMNode()).fontFamily

  setFontFamily: (fontFamily) ->
    @getTopmostDOMNode().style.fontFamily = fontFamily
    @sampleFontStyling()
    @invalidateMeasurements()

  setLineHeight: (lineHeight) ->
    @getTopmostDOMNode().style.lineHeight = lineHeight
    @sampleFontStyling()
    @invalidateMeasurements()

  invalidateMeasurements: ->
    @linesYardstick.invalidateCache()
    @presenter.measurementsChanged()

  setShowIndentGuide: (showIndentGuide) ->
    @config.set("editor.showIndentGuide", showIndentGuide)

  setScrollSensitivity: (scrollSensitivity) =>
    if scrollSensitivity = parseInt(scrollSensitivity)
      @scrollSensitivity = Math.abs(scrollSensitivity) / 100

  screenPositionForMouseEvent: (event, linesClientRect) ->
    pixelPosition = @pixelPositionForMouseEvent(event, linesClientRect)
    @screenPositionForPixelPosition(pixelPosition, true)

  pixelPositionForMouseEvent: (event, linesClientRect) ->
    {clientX, clientY} = event

    linesClientRect ?= @linesComponent.getDomNode().getBoundingClientRect()
    top = clientY - linesClientRect.top + @presenter.getRealScrollTop()
    left = clientX - linesClientRect.left + @presenter.getRealScrollLeft()
    bottom = linesClientRect.top + @presenter.getRealScrollTop() + linesClientRect.height - clientY
    right = linesClientRect.left + @presenter.getRealScrollLeft() + linesClientRect.width - clientX

    {top, left, bottom, right}

  getGutterWidth: ->
    @presenter.getGutterWidth()

  getModel: ->
    @editor

  isInputEnabled: -> @inputEnabled

  setInputEnabled: (@inputEnabled) -> @inputEnabled

  setContinuousReflow: (continuousReflow) ->
    @presenter.setContinuousReflow(continuousReflow)

  updateParentViewFocusedClassIfNeeded: ->
    if @oldState.focused isnt @newState.focused
      @hostElement.classList.toggle('is-focused', @newState.focused)
      @rootElement.classList.toggle('is-focused', @newState.focused)
      @oldState.focused = @newState.focused

  updateParentViewMiniClass: ->
    @hostElement.classList.toggle('mini', @editor.isMini())
    @rootElement.classList.toggle('mini', @editor.isMini())
