{Emitter} = require 'event-kit'
{View, $, callRemoveHooks} = require 'space-pen'
React = require 'react-atom-fork'
Path = require 'path'
{defaults} = require 'underscore-plus'
TextBuffer = require 'text-buffer'
TextEditor = require './text-editor'
TextEditorComponent = require './text-editor-component'
TextEditorView = null

ShadowStyleSheet = null

class TextEditorElement extends HTMLElement
  model: null
  componentDescriptor: null
  component: null
  attached: false
  lineOverdrawMargin: null
  focusOnAttach: false

  createdCallback: ->
    @emitter = new Emitter
    @initializeContent()
    @createSpacePenShim()
    @addEventListener 'focus', @focused.bind(this)
    @addEventListener 'blur', @blurred.bind(this)

  initializeContent: (attributes) ->
    @classList.add('editor')
    @setAttribute('tabindex', -1)

    if atom.config.get('editor.useShadowDOM')
      @useShadowDOM = true

      unless ShadowStyleSheet?
        ShadowStyleSheet = document.createElement('style')
        ShadowStyleSheet.textContent = atom.themes.loadLessStylesheet(require.resolve('../static/text-editor-shadow.less'))

      @createShadowRoot()

      @shadowRoot.appendChild(ShadowStyleSheet.cloneNode(true))
      @stylesElement = document.createElement('atom-styles')
      @stylesElement.setAttribute('context', 'atom-text-editor')
      @stylesElement.initialize()

      @rootElement = document.createElement('div')
      @rootElement.classList.add('editor--private')

      @shadowRoot.appendChild(@stylesElement)
      @shadowRoot.appendChild(@rootElement)
    else
      @useShadowDOM = false

      @classList.add('editor', 'editor-colors')
      @stylesElement = document.head.querySelector('atom-styles')
      @rootElement = this

  createSpacePenShim: ->
    TextEditorView ?= require './text-editor-view'
    @__spacePenView = new TextEditorView(this)

  attachedCallback: ->
    @buildModel() unless @getModel()?
    @mountComponent() unless @component?.isMounted()
    @component.checkForVisibilityChange()
    if this is document.activeElement
      @focused()
    @emitter.emit("did-attach")

  detachedCallback: ->
    @unmountComponent()
    @emitter.emit("did-detach")

  initialize: (model) ->
    @setModel(model)
    this

  setModel: (model) ->
    throw new Error("Model already assigned on TextEditorElement") if @model?
    return if model.isDestroyed()

    @model = model
    @mountComponent()
    @addGrammarScopeAttribute()
    @addMiniAttributeIfNeeded()
    @addEncodingAttribute()
    @model.onDidChangeGrammar => @addGrammarScopeAttribute()
    @model.onDidChangeEncoding => @addEncodingAttribute()
    @model.onDidDestroy => @unmountComponent()
    @__spacePenView.setModel(@model)
    @model

  getModel: ->
    @model ? @buildModel()

  buildModel: ->
    @setModel(new TextEditor(
      buffer: new TextBuffer(@textContent)
      softWrapped: false
      tabLength: 2
      softTabs: true
      mini: @hasAttribute('mini')
      gutterVisible: not @hasAttribute('gutter-hidden')
      placeholderText: @getAttribute('placeholder-text')
    ))

  mountComponent: ->
    @componentDescriptor ?= TextEditorComponent(
      hostElement: this
      rootElement: @rootElement
      stylesElement: @stylesElement
      editor: @model
      lineOverdrawMargin: @lineOverdrawMargin
      useShadowDOM: @useShadowDOM
    )
    @component = React.renderComponent(@componentDescriptor, @rootElement)

    if @useShadowDOM
      @shadowRoot.addEventListener('blur', @shadowRootBlurred.bind(this), true)
    else
      inputNode = @component.refs.input.getDOMNode()
      inputNode.addEventListener 'focus', @focused.bind(this)
      inputNode.addEventListener 'blur', => @dispatchEvent(new FocusEvent('blur', bubbles: false))

  unmountComponent: ->
    return unless @component?.isMounted()
    callRemoveHooks(this)
    React.unmountComponentAtNode(@rootElement)
    @component = null

  focused: ->
    @component?.focused()

  blurred: (event) ->
    unless @useShadowDOM
      if event.relatedTarget is @component?.refs.input?.getDOMNode()
        event.stopImmediatePropagation()
        return

    @component?.blurred()

  # Work around what seems to be a bug in Chromium. Focus can be stolen from the
  # hidden input when clicking on the gutter and transferred to the
  # already-focused host element. The host element never gets a 'focus' event
  # however, which leaves us in a limbo state where the text editor element is
  # focused but the hidden input isn't focused. This always refocuses the hidden
  # input if a blur event occurs in the shadow DOM that is transferring focus
  # back to the host element.
  shadowRootBlurred: (event) ->
    @component.focused() if event.relatedTarget is this

  addGrammarScopeAttribute: ->
    grammarScope = @model.getGrammar()?.scopeName?.replace(/\./g, ' ')
    @dataset.grammar = grammarScope

  addMiniAttributeIfNeeded: ->
    @setAttributeNode(document.createAttribute("mini")) if @model.isMini()

  addEncodingAttribute: ->
    @dataset.encoding = @model.getEncoding()

  hasFocus: ->
    this is document.activeElement or @contains(document.activeElement)

  setUpdatedSynchronously: (@updatedSynchronously) -> @updatedSynchronously

  isUpdatedSynchronously: -> @updatedSynchronously

  # Extended: get the width of a character of text displayed in this element.
  #
  # Returns a {Number} of pixels.
  getDefaultCharacterWidth: ->
    @getModel().getDefaultCharWidth()

  # Extended: Converts a buffer position to a pixel position.
  #
  # * `bufferPosition` An object that represents a buffer position. It can be either
  #   an {Object} (`{row, column}`), {Array} (`[row, column]`), or {Point}
  #
  # Returns an {Object} with two values: `top` and `left`, representing the pixel position.
  pixelPositionForBufferPosition: (bufferPosition) ->
    @getModel().pixelPositionForBufferPosition(bufferPosition, true)

  # Extended: Converts a screen position to a pixel position.
  #
  # * `screenPosition` An object that represents a screen position. It can be either
  #   an {Object} (`{row, column}`), {Array} (`[row, column]`), or {Point}
  #
  # Returns an {Object} with two values: `top` and `left`, representing the pixel positions.
  pixelPositionForScreenPosition: (screenPosition) ->
    @getModel().pixelPositionForScreenPosition(screenPosition, true)

  # Extended: Retrieves the number of the row that is visible and currently at the
  # top of the editor.
  #
  # Returns a {Number}.
  getFirstVisibleScreenRow: ->
    @getModel().getFirstVisibleScreenRow(true)

  # Extended: Retrieves the number of the row that is visible and currently at the
  # bottom of the editor.
  #
  # Returns a {Number}.
  getLastVisibleScreenRow: ->
    @getModel().getLastVisibleScreenRow(true)

  # Extended: call the given `callback` when the editor is attached to the DOM.
  #
  # * `callback` {Function}
  onDidAttach: (callback) ->
    @emitter.on("did-attach", callback)

  # Extended: call the given `callback` when the editor is detached from the DOM.
  #
  # * `callback` {Function}
  onDidDetach: (callback) ->
    @emitter.on("did-detach", callback)

stopEventPropagation = (commandListeners) ->
  newCommandListeners = {}
  for commandName, commandListener of commandListeners
    do (commandListener) ->
      newCommandListeners[commandName] = (event) ->
        event.stopPropagation()
        commandListener.call(@getModel(), event)
  newCommandListeners

stopEventPropagationAndGroupUndo = (commandListeners) ->
  newCommandListeners = {}
  for commandName, commandListener of commandListeners
    do (commandListener) ->
      newCommandListeners[commandName] = (event) ->
        event.stopPropagation()
        model = @getModel()
        model.transact atom.config.get('editor.undoGroupingInterval'), ->
          commandListener.call(model, event)
  newCommandListeners

atom.commands.add 'atom-text-editor', stopEventPropagation(
  'core:undo': -> @undo()
  'core:redo': -> @redo()
  'core:move-left': -> @moveLeft()
  'core:move-right': -> @moveRight()
  'core:select-left': -> @selectLeft()
  'core:select-right': -> @selectRight()
  'core:select-all': -> @selectAll()
  'editor:move-to-previous-word': -> @moveToPreviousWord()
  'editor:select-word': -> @selectWordsContainingCursors()
  'editor:consolidate-selections': (event) -> event.abortKeyBinding() unless @consolidateSelections()
  'editor:move-to-beginning-of-next-paragraph': -> @moveToBeginningOfNextParagraph()
  'editor:move-to-beginning-of-previous-paragraph': -> @moveToBeginningOfPreviousParagraph()
  'editor:move-to-beginning-of-screen-line': -> @moveToBeginningOfScreenLine()
  'editor:move-to-beginning-of-line': -> @moveToBeginningOfLine()
  'editor:move-to-end-of-screen-line': -> @moveToEndOfScreenLine()
  'editor:move-to-end-of-line': -> @moveToEndOfLine()
  'editor:move-to-first-character-of-line': -> @moveToFirstCharacterOfLine()
  'editor:move-to-beginning-of-word': -> @moveToBeginningOfWord()
  'editor:move-to-end-of-word': -> @moveToEndOfWord()
  'editor:move-to-beginning-of-next-word': -> @moveToBeginningOfNextWord()
  'editor:move-to-previous-word-boundary': -> @moveToPreviousWordBoundary()
  'editor:move-to-next-word-boundary': -> @moveToNextWordBoundary()
  'editor:select-to-beginning-of-next-paragraph': -> @selectToBeginningOfNextParagraph()
  'editor:select-to-beginning-of-previous-paragraph': -> @selectToBeginningOfPreviousParagraph()
  'editor:select-to-end-of-line': -> @selectToEndOfLine()
  'editor:select-to-beginning-of-line': -> @selectToBeginningOfLine()
  'editor:select-to-end-of-word': -> @selectToEndOfWord()
  'editor:select-to-beginning-of-word': -> @selectToBeginningOfWord()
  'editor:select-to-beginning-of-next-word': -> @selectToBeginningOfNextWord()
  'editor:select-to-next-word-boundary': -> @selectToNextWordBoundary()
  'editor:select-to-previous-word-boundary': -> @selectToPreviousWordBoundary()
  'editor:select-to-first-character-of-line': -> @selectToFirstCharacterOfLine()
  'editor:select-line': -> @selectLinesContainingCursors()
)

atom.commands.add 'atom-text-editor', stopEventPropagationAndGroupUndo(
  'core:backspace': -> @backspace()
  'core:delete': -> @delete()
  'core:cut': -> @cutSelectedText()
  'core:copy': -> @copySelectedText()
  'core:paste': -> @pasteText()
  'editor:delete-to-beginning-of-word': -> @deleteToBeginningOfWord()
  'editor:delete-to-beginning-of-line': -> @deleteToBeginningOfLine()
  'editor:delete-to-end-of-line': -> @deleteToEndOfLine()
  'editor:delete-to-end-of-word': -> @deleteToEndOfWord()
  'editor:delete-line': -> @deleteLine()
  'editor:cut-to-end-of-line': -> @cutToEndOfLine()
  'editor:transpose': -> @transpose()
  'editor:upper-case': -> @upperCase()
  'editor:lower-case': -> @lowerCase()
)

atom.commands.add 'atom-text-editor:not([mini])', stopEventPropagation(
  'core:move-up': -> @moveUp()
  'core:move-down': -> @moveDown()
  'core:move-to-top': -> @moveToTop()
  'core:move-to-bottom': -> @moveToBottom()
  'core:page-up': -> @pageUp()
  'core:page-down': -> @pageDown()
  'core:select-up': -> @selectUp()
  'core:select-down': -> @selectDown()
  'core:select-to-top': -> @selectToTop()
  'core:select-to-bottom': -> @selectToBottom()
  'core:select-page-up': -> @selectPageUp()
  'core:select-page-down': -> @selectPageDown()
  'editor:add-selection-below': -> @addSelectionBelow()
  'editor:add-selection-above': -> @addSelectionAbove()
  'editor:split-selections-into-lines': -> @splitSelectionsIntoLines()
  'editor:toggle-soft-tabs': -> @toggleSoftTabs()
  'editor:toggle-soft-wrap': -> @toggleSoftWrapped()
  'editor:fold-all': -> @foldAll()
  'editor:unfold-all': -> @unfoldAll()
  'editor:fold-current-row': -> @foldCurrentRow()
  'editor:unfold-current-row': -> @unfoldCurrentRow()
  'editor:fold-selection': -> @foldSelectedLines()
  'editor:fold-at-indent-level-1': -> @foldAllAtIndentLevel(0)
  'editor:fold-at-indent-level-2': -> @foldAllAtIndentLevel(1)
  'editor:fold-at-indent-level-3': -> @foldAllAtIndentLevel(2)
  'editor:fold-at-indent-level-4': -> @foldAllAtIndentLevel(3)
  'editor:fold-at-indent-level-5': -> @foldAllAtIndentLevel(4)
  'editor:fold-at-indent-level-6': -> @foldAllAtIndentLevel(5)
  'editor:fold-at-indent-level-7': -> @foldAllAtIndentLevel(6)
  'editor:fold-at-indent-level-8': -> @foldAllAtIndentLevel(7)
  'editor:fold-at-indent-level-9': -> @foldAllAtIndentLevel(8)
  'editor:log-cursor-scope': -> @logCursorScope()
  'editor:copy-path': -> @copyPathToClipboard()
  'editor:toggle-indent-guide': -> atom.config.set('editor.showIndentGuide', not atom.config.get('editor.showIndentGuide'))
  'editor:toggle-line-numbers': -> atom.config.set('editor.showLineNumbers', not atom.config.get('editor.showLineNumbers'))
  'editor:scroll-to-cursor': -> @scrollToCursorPosition()
)

atom.commands.add 'atom-text-editor:not([mini])', stopEventPropagationAndGroupUndo(
  'editor:indent': -> @indent()
  'editor:auto-indent': -> @autoIndentSelectedRows()
  'editor:indent-selected-rows': -> @indentSelectedRows()
  'editor:outdent-selected-rows': -> @outdentSelectedRows()
  'editor:newline': -> @insertNewline()
  'editor:newline-below': -> @insertNewlineBelow()
  'editor:newline-above': -> @insertNewlineAbove()
  'editor:toggle-line-comments': -> @toggleLineCommentsInSelection()
  'editor:checkout-head-revision': -> atom.project.getRepositories()[0]?.checkoutHeadForEditor(this)
  'editor:move-line-up': -> @moveLineUp()
  'editor:move-line-down': -> @moveLineDown()
  'editor:duplicate-lines': -> @duplicateLines()
  'editor:join-lines': -> @joinLines()
)

module.exports = TextEditorElement = document.registerElement 'atom-text-editor', prototype: TextEditorElement.prototype
