{Emitter, CompositeDisposable} = require 'event-kit'
Path = require 'path'
{defaults} = require 'underscore-plus'
TextBuffer = require 'text-buffer'
TextEditor = require './text-editor'
TextEditorComponent = require './text-editor-component'
StylesElement = require './styles-element'

ShadowStyleSheet = null

class TextEditorElement extends HTMLElement
  model: null
  componentDescriptor: null
  component: null
  attached: false
  tileSize: null
  focusOnAttach: false
  hasTiledRendering: true
  logicalDisplayBuffer: true

  createdCallback: ->
    # Use globals when the following instance variables aren't set.
    @config = atom.config
    @themes = atom.themes
    @workspace = atom.workspace
    @assert = atom.assert
    @views = atom.views
    @styles = atom.styles
    @grammars = atom.grammars

    @emitter = new Emitter
    @subscriptions = new CompositeDisposable

    @addEventListener 'focus', @focused.bind(this)
    @addEventListener 'blur', @blurred.bind(this)

    @classList.add('editor')
    @setAttribute('tabindex', -1)

  initializeContent: (attributes) ->
    if @config.get('editor.useShadowDOM')
      @useShadowDOM = true

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

      @createShadowRoot()

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

      @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

  attachedCallback: ->
    @buildModel() unless @getModel()?
    @assert(@model.isAlive(), "Attaching a view for a destroyed editor")
    @mountComponent() unless @component?
    @listenForComponentEvents()
    @component.checkForVisibilityChange()
    if this is document.activeElement
      @focused()
    @emitter.emit("did-attach")

  detachedCallback: ->
    @unmountComponent()
    @subscriptions.dispose()
    @subscriptions = new CompositeDisposable
    @emitter.emit("did-detach")

  listenForComponentEvents: ->
    @subscriptions.add @component.onDidChangeScrollTop =>
      @emitter.emit("did-change-scroll-top", arguments...)
    @subscriptions.add @component.onDidChangeScrollLeft =>
      @emitter.emit("did-change-scroll-left", arguments...)

  initialize: (model, {@views, @config, @themes, @workspace, @assert, @styles, @grammars}) ->
    throw new Error("Must pass a config parameter when initializing TextEditorElements") unless @views?
    throw new Error("Must pass a config parameter when initializing TextEditorElements") unless @config?
    throw new Error("Must pass a themes parameter when initializing TextEditorElements") unless @themes?
    throw new Error("Must pass a workspace parameter when initializing TextEditorElements") unless @workspace?
    throw new Error("Must pass a assert parameter when initializing TextEditorElements") unless @assert?
    throw new Error("Must pass a styles parameter when initializing TextEditorElements") unless @styles?
    throw new Error("Must pass a grammars parameter when initializing TextEditorElements") unless @grammars?

    @setModel(model)
    this

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

    @model = model
    @model.setUpdatedSynchronously(@isUpdatedSynchronously())
    @initializeContent()
    @mountComponent()
    @addGrammarScopeAttribute()
    @addMiniAttribute() if @model.isMini()
    @addEncodingAttribute()
    @model.onDidChangeGrammar => @addGrammarScopeAttribute()
    @model.onDidChangeEncoding => @addEncodingAttribute()
    @model.onDidDestroy => @unmountComponent()
    @model.onDidChangeMini (mini) => if mini then @addMiniAttribute() else @removeMiniAttribute()
    @model

  getModel: ->
    @model ? @buildModel()

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

  mountComponent: ->
    @component = new TextEditorComponent(
      hostElement: this
      rootElement: @rootElement
      stylesElement: @stylesElement
      editor: @model
      tileSize: @tileSize
      useShadowDOM: @useShadowDOM
      views: @views
      themes: @themes
      config: @config
      workspace: @workspace
      assert: @assert
      grammars: @grammars
    )
    @rootElement.appendChild(@component.getDomNode())

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

  unmountComponent: ->
    if @component?
      @component.destroy()
      @component.getDomNode().remove()
      @component = null

  focused: ->
    @component?.focused()

  blurred: (event) ->
    unless @useShadowDOM
      if event.relatedTarget is @component.hiddenInputComponent.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: ->
    @dataset.grammar = @model.getGrammar()?.scopeName?.replace(/\./g, ' ')

  addMiniAttribute: ->
    @setAttributeNode(document.createAttribute("mini"))

  removeMiniAttribute: ->
    @removeAttribute("mini")

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

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

  setUpdatedSynchronously: (@updatedSynchronously) ->
    @model?.setUpdatedSynchronously(@updatedSynchronously)
    @updatedSynchronously

  isUpdatedSynchronously: -> @updatedSynchronously

  # Extended: Continuously reflows lines and line numbers. (Has performance overhead)
  #
  # `continuousReflow` A {Boolean} indicating whether to keep reflowing or not.
  setContinuousReflow: (continuousReflow) ->
    @component?.setContinuousReflow(continuousReflow)

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

  # Extended: Get the maximum scroll top that can be applied to this element.
  #
  # Returns a {Number} of pixels.
  getMaxScrollTop: ->
    @component?.getMaxScrollTop()

  # 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) ->
    @component.pixelPositionForBufferPosition(bufferPosition)

  # 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) ->
    @component.pixelPositionForScreenPosition(screenPosition)

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

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

  # 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)

  onDidChangeScrollTop: (callback) ->
    @emitter.on("did-change-scroll-top", callback)

  onDidChangeScrollLeft: (callback) ->
    @emitter.on("did-change-scroll-left", callback)

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

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

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

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

  # Essential: Scrolls the editor to the top
  scrollToTop: ->
    @setScrollTop(0)

  # Essential: Scrolls the editor to the bottom
  scrollToBottom: ->
    @setScrollBottom(Infinity)

  getScrollTop: ->
    @component?.getScrollTop() or 0

  getScrollLeft: ->
    @component?.getScrollLeft() or 0

  getScrollRight: ->
    @component?.getScrollRight() or 0

  getScrollBottom: ->
    @component?.getScrollBottom() or 0

  getScrollHeight: ->
    @component?.getScrollHeight() or 0

  getScrollWidth: ->
    @component?.getScrollWidth() or 0

  getVerticalScrollbarWidth: ->
    @component?.getVerticalScrollbarWidth() or 0

  getHorizontalScrollbarHeight: ->
    @component?.getHorizontalScrollbarHeight() or 0

  getVisibleRowRange: ->
    @component?.getVisibleRowRange() or [0, 0]

  intersectsVisibleRowRange: (startRow, endRow) ->
    [visibleStart, visibleEnd] = @getVisibleRowRange()
    not (endRow <= visibleStart or visibleEnd <= startRow)

  selectionIntersectsVisibleRowRange: (selection) ->
    {start, end} = selection.getScreenRange()
    @intersectsVisibleRowRange(start.row, end.row + 1)

  screenPositionForPixelPosition: (pixelPosition) ->
    @component.screenPositionForPixelPosition(pixelPosition)

  pixelRectForScreenRange: (screenRange) ->
    @component.pixelRectForScreenRange(screenRange)

  pixelRangeForScreenRange: (screenRange) ->
    @component.pixelRangeForScreenRange(screenRange)

  setWidth: (width) ->
    @style.width = (@component.getGutterWidth() + width) + "px"
    @component.measureDimensions()

  getWidth: ->
    @offsetWidth - @component.getGutterWidth()

  setHeight: (height) ->
    @style.height = height + "px"
    @component.measureDimensions()

  getHeight: ->
    @offsetHeight

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