'use strict'

const {ipcRenderer} = require('electron')
const path = require('path')
const fs = require('fs-plus')
const {CompositeDisposable, Disposable} = require('event-kit')
const scrollbarStyle = require('scrollbar-style')
const _ = require('underscore-plus')

class WorkspaceElement extends HTMLElement {
  attachedCallback () {
    this.focus()
    this.htmlElement = document.querySelector('html')
    this.htmlElement.addEventListener('mouseleave', this.handleCenterLeave)
  }

  detachedCallback () {
    this.subscriptions.dispose()
    this.htmlElement.removeEventListener('mouseleave', this.handleCenterLeave)
  }

  initializeContent () {
    this.classList.add('workspace')
    this.setAttribute('tabindex', -1)

    this.verticalAxis = document.createElement('atom-workspace-axis')
    this.verticalAxis.classList.add('vertical')

    this.horizontalAxis = document.createElement('atom-workspace-axis')
    this.horizontalAxis.classList.add('horizontal')
    this.horizontalAxis.appendChild(this.verticalAxis)

    this.appendChild(this.horizontalAxis)
  }

  observeScrollbarStyle () {
    this.subscriptions.add(scrollbarStyle.observePreferredScrollbarStyle(style => {
      switch (style) {
        case 'legacy':
          this.classList.remove('scrollbars-visible-when-scrolling')
          this.classList.add('scrollbars-visible-always')
          break
        case 'overlay':
          this.classList.remove('scrollbars-visible-always')
          this.classList.add('scrollbars-visible-when-scrolling')
          break
      }
    }))
  }

  observeTextEditorFontConfig () {
    this.updateGlobalTextEditorStyleSheet()
    this.subscriptions.add(this.config.onDidChange('editor.fontSize', this.updateGlobalTextEditorStyleSheet.bind(this)))
    this.subscriptions.add(this.config.onDidChange('editor.fontFamily', this.updateGlobalTextEditorStyleSheet.bind(this)))
    this.subscriptions.add(this.config.onDidChange('editor.lineHeight', this.updateGlobalTextEditorStyleSheet.bind(this)))
  }

  updateGlobalTextEditorStyleSheet () {
    const styleSheetSource = `atom-text-editor {
  font-size: ${this.config.get('editor.fontSize')}px;
  font-family: ${this.config.get('editor.fontFamily')};
  line-height: ${this.config.get('editor.lineHeight')};
}`
    this.styleManager.addStyleSheet(styleSheetSource, {sourcePath: 'global-text-editor-styles', priority: -1})
  }

  initialize (model, {config, project, styleManager, viewRegistry}) {
    this.handleCenterEnter = this.handleCenterEnter.bind(this)
    this.handleCenterLeave = this.handleCenterLeave.bind(this)
    this.handleEdgesMouseMove = _.throttle(this.handleEdgesMouseMove.bind(this), 100)
    this.handleDockDragEnd = this.handleDockDragEnd.bind(this)
    this.handleDragStart = this.handleDragStart.bind(this)
    this.handleDragEnd = this.handleDragEnd.bind(this)
    this.handleDrop = this.handleDrop.bind(this)

    this.model = model
    this.viewRegistry = viewRegistry
    this.project = project
    this.config = config
    this.styleManager = styleManager
    if (this.viewRegistry == null) { throw new Error('Must pass a viewRegistry parameter when initializing WorkspaceElements') }
    if (this.project == null) { throw new Error('Must pass a project parameter when initializing WorkspaceElements') }
    if (this.config == null) { throw new Error('Must pass a config parameter when initializing WorkspaceElements') }
    if (this.styleManager == null) { throw new Error('Must pass a styleManager parameter when initializing WorkspaceElements') }

    this.subscriptions = new CompositeDisposable(
      new Disposable(() => {
        this.paneContainer.removeEventListener('mouseenter', this.handleCenterEnter)
        this.paneContainer.removeEventListener('mouseleave', this.handleCenterLeave)
        window.removeEventListener('mousemove', this.handleEdgesMouseMove)
        window.removeEventListener('dragend', this.handleDockDragEnd)
        window.removeEventListener('dragstart', this.handleDragStart)
        window.removeEventListener('dragend', this.handleDragEnd, true)
        window.removeEventListener('drop', this.handleDrop, true)
      })
    )
    this.initializeContent()
    this.observeScrollbarStyle()
    this.observeTextEditorFontConfig()

    this.paneContainer = this.model.getCenter().paneContainer.getElement()
    this.verticalAxis.appendChild(this.paneContainer)
    this.addEventListener('focus', this.handleFocus.bind(this))

    this.addEventListener('mousewheel', this.handleMousewheel.bind(this), true)
    window.addEventListener('dragstart', this.handleDragStart)

    this.panelContainers = {
      top: this.model.panelContainers.top.getElement(),
      left: this.model.panelContainers.left.getElement(),
      right: this.model.panelContainers.right.getElement(),
      bottom: this.model.panelContainers.bottom.getElement(),
      header: this.model.panelContainers.header.getElement(),
      footer: this.model.panelContainers.footer.getElement(),
      modal: this.model.panelContainers.modal.getElement()
    }

    this.horizontalAxis.insertBefore(this.panelContainers.left, this.verticalAxis)
    this.horizontalAxis.appendChild(this.panelContainers.right)

    this.verticalAxis.insertBefore(this.panelContainers.top, this.paneContainer)
    this.verticalAxis.appendChild(this.panelContainers.bottom)

    this.insertBefore(this.panelContainers.header, this.horizontalAxis)
    this.appendChild(this.panelContainers.footer)

    this.appendChild(this.panelContainers.modal)

    this.paneContainer.addEventListener('mouseenter', this.handleCenterEnter)
    this.paneContainer.addEventListener('mouseleave', this.handleCenterLeave)

    return this
  }

  getModel () { return this.model }

  handleDragStart (event) {
    if (!isTab(event.target)) return
    const {item} = event.target
    if (!item) return
    this.model.setDraggingItem(item)
    window.addEventListener('dragend', this.handleDragEnd, true)
    window.addEventListener('drop', this.handleDrop, true)
  }

  handleDragEnd (event) {
    this.dragEnded()
  }

  handleDrop (event) {
    this.dragEnded()
  }

  dragEnded () {
    this.model.setDraggingItem(null)
    window.removeEventListener('dragend', this.handleDragEnd, true)
    window.removeEventListener('drop', this.handleDrop, true)
  }

  handleCenterEnter (event) {
    // Just re-entering the center isn't enough to hide the dock toggle buttons, since they poke
    // into the center and we want to give an affordance.
    this.cursorInCenter = true
    this.checkCleanupDockHoverEvents()
  }

  handleCenterLeave (event) {
    // If the cursor leaves the center, we start listening to determine whether one of the docs is
    // being hovered.
    this.cursorInCenter = false
    this.updateHoveredDock({x: event.pageX, y: event.pageY})
    window.addEventListener('mousemove', this.handleEdgesMouseMove)
    window.addEventListener('dragend', this.handleDockDragEnd)
  }

  handleEdgesMouseMove (event) {
    this.updateHoveredDock({x: event.pageX, y: event.pageY})
  }

  handleDockDragEnd (event) {
    this.updateHoveredDock({x: event.pageX, y: event.pageY})
  }

  updateHoveredDock (mousePosition) {
    this.hoveredDock = null
    for (let location in this.model.paneContainers) {
      if (location !== 'center') {
        const dock = this.model.paneContainers[location]
        if (!this.hoveredDock && dock.pointWithinHoverArea(mousePosition)) {
          this.hoveredDock = dock
          dock.setHovered(true)
        } else {
          dock.setHovered(false)
        }
      }
    }
    this.checkCleanupDockHoverEvents()
  }

  checkCleanupDockHoverEvents () {
    if (this.cursorInCenter && !this.hoveredDock) {
      window.removeEventListener('mousemove', this.handleEdgesMouseMove)
      window.removeEventListener('dragend', this.handleDockDragEnd)
    }
  }

  handleMousewheel (event) {
    if (event.ctrlKey && this.config.get('editor.zoomFontWhenCtrlScrolling') && (event.target.closest('atom-text-editor') != null)) {
      if (event.wheelDeltaY > 0) {
        this.model.increaseFontSize()
      } else if (event.wheelDeltaY < 0) {
        this.model.decreaseFontSize()
      }
      event.preventDefault()
      event.stopPropagation()
    }
  }

  handleFocus (event) {
    this.model.getActivePane().activate()
  }

  focusPaneViewAbove () { this.focusPaneViewInDirection('above') }

  focusPaneViewBelow () { this.focusPaneViewInDirection('below') }

  focusPaneViewOnLeft () { this.focusPaneViewInDirection('left') }

  focusPaneViewOnRight () { this.focusPaneViewInDirection('right') }

  focusPaneViewInDirection (direction, pane) {
    const activePane = this.model.getActivePane()
    const paneToFocus = this.nearestVisiblePaneInDirection(direction, activePane)
    paneToFocus && paneToFocus.focus()
  }

  moveActiveItemToPaneAbove (params) {
    this.moveActiveItemToNearestPaneInDirection('above', params)
  }

  moveActiveItemToPaneBelow (params) {
    this.moveActiveItemToNearestPaneInDirection('below', params)
  }

  moveActiveItemToPaneOnLeft (params) {
    this.moveActiveItemToNearestPaneInDirection('left', params)
  }

  moveActiveItemToPaneOnRight (params) {
    this.moveActiveItemToNearestPaneInDirection('right', params)
  }

  moveActiveItemToNearestPaneInDirection (direction, params) {
    const activePane = this.model.getActivePane()
    const nearestPaneView = this.nearestVisiblePaneInDirection(direction, activePane)
    if (nearestPaneView == null) { return }
    if (params && params.keepOriginal) {
      activePane.getContainer().copyActiveItemToPane(nearestPaneView.getModel())
    } else {
      activePane.getContainer().moveActiveItemToPane(nearestPaneView.getModel())
    }
    nearestPaneView.focus()
  }

  nearestVisiblePaneInDirection (direction, pane) {
    const distance = function (pointA, pointB) {
      const x = pointB.x - pointA.x
      const y = pointB.y - pointA.y
      return Math.sqrt(Math.pow(x, 2) + Math.pow(y, 2))
    }

    const paneView = pane.getElement()
    const box = this.boundingBoxForPaneView(paneView)

    const paneViews = atom.workspace.getVisiblePanes()
      .map(otherPane => otherPane.getElement())
      .filter(otherPaneView => {
        const otherBox = this.boundingBoxForPaneView(otherPaneView)
        switch (direction) {
          case 'left': return otherBox.right.x <= box.left.x
          case 'right': return otherBox.left.x >= box.right.x
          case 'above': return otherBox.bottom.y <= box.top.y
          case 'below': return otherBox.top.y >= box.bottom.y
        }
      }).sort((paneViewA, paneViewB) => {
        const boxA = this.boundingBoxForPaneView(paneViewA)
        const boxB = this.boundingBoxForPaneView(paneViewB)
        switch (direction) {
          case 'left': return distance(box.left, boxA.right) - distance(box.left, boxB.right)
          case 'right': return distance(box.right, boxA.left) - distance(box.right, boxB.left)
          case 'above': return distance(box.top, boxA.bottom) - distance(box.top, boxB.bottom)
          case 'below': return distance(box.bottom, boxA.top) - distance(box.bottom, boxB.top)
        }
      })

    return paneViews[0]
  }

  boundingBoxForPaneView (paneView) {
    const boundingBox = paneView.getBoundingClientRect()

    return {
      left: {x: boundingBox.left, y: boundingBox.top},
      right: {x: boundingBox.right, y: boundingBox.top},
      top: {x: boundingBox.left, y: boundingBox.top},
      bottom: {x: boundingBox.left, y: boundingBox.bottom}
    }
  }

  runPackageSpecs () {
    const activePaneItem = this.model.getActivePaneItem()
    const activePath = activePaneItem && typeof activePaneItem.getPath === 'function' ? activePaneItem.getPath() : null
    let projectPath
    if (activePath != null) {
      [projectPath] = this.project.relativizePath(activePath)
    } else {
      [projectPath] = this.project.getPaths()
    }
    if (projectPath) {
      let specPath = path.join(projectPath, 'spec')
      const testPath = path.join(projectPath, 'test')
      if (!fs.existsSync(specPath) && fs.existsSync(testPath)) {
        specPath = testPath
      }

      ipcRenderer.send('run-package-specs', specPath)
    }
  }

  runBenchmarks () {
    const activePaneItem = this.model.getActivePaneItem()
    const activePath = activePaneItem && typeof activePaneItem.getPath === 'function' ? activePaneItem.getPath() : null
    let projectPath
    if (activePath) {
      [projectPath] = this.project.relativizePath(activePath)
    } else {
      [projectPath] = this.project.getPaths()
    }

    if (projectPath) {
      ipcRenderer.send('run-benchmarks', path.join(projectPath, 'benchmarks'))
    }
  }
}

module.exports = document.registerElement('atom-workspace', {prototype: WorkspaceElement.prototype})

function isTab (element) {
  let el = element
  while (el != null) {
    if (el.getAttribute && el.getAttribute('is') === 'tabs-tab') return true
    el = el.parentElement
  }
  return false
}
