const {BrowserWindow, app, dialog, ipcMain} = require('electron')
const path = require('path')
const fs = require('fs')
const url = require('url')
const {EventEmitter} = require('events')

const ICON_PATH = path.resolve(__dirname, '..', '..', 'resources', 'atom.png')

let includeShellLoadTime = true
let nextId = 0

module.exports =
class AtomWindow extends EventEmitter {
  constructor (atomApplication, fileRecoveryService, settings = {}) {
    super()

    this.id = nextId++
    this.atomApplication = atomApplication
    this.fileRecoveryService = fileRecoveryService
    this.isSpec = settings.isSpec
    this.headless = settings.headless
    this.safeMode = settings.safeMode
    this.devMode = settings.devMode
    this.resourcePath = settings.resourcePath

    let {pathToOpen, locationsToOpen} = settings
    if (!locationsToOpen && pathToOpen) locationsToOpen = [{pathToOpen}]
    if (!locationsToOpen) locationsToOpen = []

    this.loadedPromise = new Promise(resolve => { this.resolveLoadedPromise = resolve })
    this.closedPromise = new Promise(resolve => { this.resolveClosedPromise = resolve })

    const options = {
      show: false,
      title: 'Atom',
      tabbingIdentifier: 'atom',
      webPreferences: {
        // Prevent specs from throttling when the window is in the background:
        // this should result in faster CI builds, and an improvement in the
        // local development experience when running specs through the UI (which
        // now won't pause when e.g. minimizing the window).
        backgroundThrottling: !this.isSpec,
        // Disable the `auxclick` feature so that `click` events are triggered in
        // response to a middle-click.
        // (Ref: https://github.com/atom/atom/pull/12696#issuecomment-290496960)
        disableBlinkFeatures: 'Auxclick'
      }
    }

    // Don't set icon on Windows so the exe's ico will be used as window and
    // taskbar's icon. See https://github.com/atom/atom/issues/4811 for more.
    if (process.platform === 'linux') options.icon = ICON_PATH
    if (this.shouldAddCustomTitleBar()) options.titleBarStyle = 'hidden'
    if (this.shouldAddCustomInsetTitleBar()) options.titleBarStyle = 'hiddenInset'
    if (this.shouldHideTitleBar()) options.frame = false
    this.browserWindow = new BrowserWindow(options)

    Object.defineProperty(this.browserWindow, 'loadSettingsJSON', {
      get: () => JSON.stringify(Object.assign({
        userSettings: !this.isSpec
          ? this.atomApplication.configFile.get()
          : null
      }, this.loadSettings))
    })

    this.handleEvents()

    this.loadSettings = Object.assign({}, settings)
    this.loadSettings.appVersion = app.getVersion()
    this.loadSettings.resourcePath = this.resourcePath
    this.loadSettings.atomHome = process.env.ATOM_HOME
    if (this.loadSettings.devMode == null) this.loadSettings.devMode = false
    if (this.loadSettings.safeMode == null) this.loadSettings.safeMode = false
    if (this.loadSettings.clearWindowState == null) this.loadSettings.clearWindowState = false

    if (!this.loadSettings.initialPaths) {
      this.loadSettings.initialPaths = []
      for (const {pathToOpen, stat} of locationsToOpen) {
        if (!pathToOpen) continue
        if (stat && stat.isDirectory()) {
          this.loadSettings.initialPaths.push(pathToOpen)
        } else {
          const parentDirectory = path.dirname(pathToOpen)
          if (stat && stat.isFile() || fs.existsSync(parentDirectory)) {
            this.loadSettings.initialPaths.push(parentDirectory)
          } else {
            this.loadSettings.initialPaths.push(pathToOpen)
          }
        }
      }
    }

    this.loadSettings.initialPaths.sort()

    // Only send to the first non-spec window created
    if (includeShellLoadTime && !this.isSpec) {
      includeShellLoadTime = false
      if (!this.loadSettings.shellLoadTime) {
        this.loadSettings.shellLoadTime = Date.now() - global.shellStartTime
      }
    }

    this.representedDirectoryPaths = this.loadSettings.initialPaths
    if (!this.loadSettings.env) this.env = this.loadSettings.env

    this.browserWindow.on('window:loaded', () => {
      this.disableZoom()
      this.emit('window:loaded')
      this.resolveLoadedPromise()
    })

    this.browserWindow.on('window:locations-opened', () => {
      this.emit('window:locations-opened')
    })

    this.browserWindow.on('enter-full-screen', () => {
      this.browserWindow.webContents.send('did-enter-full-screen')
    })

    this.browserWindow.on('leave-full-screen', () => {
      this.browserWindow.webContents.send('did-leave-full-screen')
    })

    this.browserWindow.loadURL(
      url.format({
        protocol: 'file',
        pathname: `${this.resourcePath}/static/index.html`,
        slashes: true
      })
    )

    this.browserWindow.showSaveDialog = this.showSaveDialog.bind(this)

    if (this.isSpec) this.browserWindow.focusOnWebView()

    const hasPathToOpen = !(locationsToOpen.length === 1 && locationsToOpen[0].pathToOpen == null)
    if (hasPathToOpen && !this.isSpecWindow()) this.openLocations(locationsToOpen)
  }

  hasProjectPath () {
    return this.representedDirectoryPaths.length > 0
  }

  setupContextMenu () {
    const ContextMenu = require('./context-menu')

    this.browserWindow.on('context-menu', menuTemplate => {
      return new ContextMenu(menuTemplate, this)
    })
  }

  containsPaths (paths) {
    return paths.every(p => this.containsPath(p))
  }

  containsPath (pathToCheck) {
    if (!pathToCheck) return false
    let stat
    return this.representedDirectoryPaths.some(projectPath => {
      if (pathToCheck === projectPath) return true
      if (!pathToCheck.startsWith(path.join(projectPath, path.sep))) return false
      if (stat === undefined) stat = fs.statSyncNoException(pathToCheck)
      return !stat || !stat.isDirectory()
    })
  }

  handleEvents () {
    this.browserWindow.on('close', async event => {
      if (!this.atomApplication.quitting && !this.unloading) {
        event.preventDefault()
        this.unloading = true
        this.atomApplication.saveCurrentWindowOptions(false)
        if (await this.prepareToUnload()) this.close()
      }
    })

    this.browserWindow.on('closed', () => {
      this.fileRecoveryService.didCloseWindow(this)
      this.atomApplication.removeWindow(this)
      this.resolveClosedPromise()
    })

    this.browserWindow.on('unresponsive', () => {
      if (this.isSpec) return
      dialog.showMessageBox(this.browserWindow, {
        type: 'warning',
        buttons: ['Force Close', 'Keep Waiting'],
        cancelId: 1, // Canceling should be the least destructive action
        message: 'Editor is not responding',
        detail:
          'The editor is not responding. Would you like to force close it or just keep waiting?'
      }, response => { if (response === 0) this.browserWindow.destroy() })
    })

    this.browserWindow.webContents.on('crashed', async () => {
      if (this.headless) {
        console.log('Renderer process crashed, exiting')
        this.atomApplication.exit(100)
        return
      }

      await this.fileRecoveryService.didCrashWindow(this)
      dialog.showMessageBox(this.browserWindow, {
        type: 'warning',
        buttons: ['Close Window', 'Reload', 'Keep It Open'],
        cancelId: 2, // Canceling should be the least destructive action
        message: 'The editor has crashed',
        detail: 'Please report this issue to https://github.com/atom/atom'
      }, response => {
        switch (response) {
          case 0: return this.browserWindow.destroy()
          case 1: return this.browserWindow.reload()
        }
      })
    })

    this.browserWindow.webContents.on('will-navigate', (event, url) => {
      if (url !== this.browserWindow.webContents.getURL()) event.preventDefault()
    })

    this.setupContextMenu()

    // Spec window's web view should always have focus
    if (this.isSpec) this.browserWindow.on('blur', () => this.browserWindow.focusOnWebView())
  }

  async prepareToUnload () {
    if (this.isSpecWindow()) return true

    this.lastPrepareToUnloadPromise = new Promise(resolve => {
      const callback = (event, result) => {
        if (BrowserWindow.fromWebContents(event.sender) === this.browserWindow) {
          ipcMain.removeListener('did-prepare-to-unload', callback)
          if (!result) {
            this.unloading = false
            this.atomApplication.quitting = false
          }
          resolve(result)
        }
      }
      ipcMain.on('did-prepare-to-unload', callback)
      this.browserWindow.webContents.send('prepare-to-unload')
    })

    return this.lastPrepareToUnloadPromise
  }

  openPath (pathToOpen, initialLine, initialColumn) {
    return this.openLocations([{pathToOpen, initialLine, initialColumn}])
  }

  async openLocations (locationsToOpen) {
    await this.loadedPromise
    this.sendMessage('open-locations', locationsToOpen)
  }

  didChangeUserSettings (settings) {
    this.sendMessage('did-change-user-settings', settings)
  }

  didFailToReadUserSettings (message) {
    this.sendMessage('did-fail-to-read-user-settings', message)
  }

  replaceEnvironment (env) {
    this.browserWindow.webContents.send('environment', env)
  }

  sendMessage (message, detail) {
    this.browserWindow.webContents.send('message', message, detail)
  }

  sendCommand (command, ...args) {
    if (this.isSpecWindow()) {
      if (!this.atomApplication.sendCommandToFirstResponder(command)) {
        switch (command) {
          case 'window:reload': return this.reload()
          case 'window:toggle-dev-tools': return this.toggleDevTools()
          case 'window:close': return this.close()
        }
      }
    } else if (this.isWebViewFocused()) {
      this.sendCommandToBrowserWindow(command, ...args)
    } else if (!this.atomApplication.sendCommandToFirstResponder(command)) {
      this.sendCommandToBrowserWindow(command, ...args)
    }
  }

  sendURIMessage (uri) {
    this.browserWindow.webContents.send('uri-message', uri)
  }

  sendCommandToBrowserWindow (command, ...args) {
    const action = args[0] && args[0].contextCommand
      ? 'context-command'
      : 'command'
    this.browserWindow.webContents.send(action, command, ...args)
  }

  getDimensions () {
    const [x, y] = Array.from(this.browserWindow.getPosition())
    const [width, height] = Array.from(this.browserWindow.getSize())
    return {x, y, width, height}
  }

  shouldAddCustomTitleBar () {
    return (
      !this.isSpec &&
      process.platform === 'darwin' &&
      this.atomApplication.config.get('core.titleBar') === 'custom'
    )
  }

  shouldAddCustomInsetTitleBar () {
    return (
      !this.isSpec &&
      process.platform === 'darwin' &&
      this.atomApplication.config.get('core.titleBar') === 'custom-inset'
    )
  }

  shouldHideTitleBar () {
    return (
      !this.isSpec &&
      process.platform === 'darwin' &&
      this.atomApplication.config.get('core.titleBar') === 'hidden'
    )
  }

  close () {
    return this.browserWindow.close()
  }

  focus () {
    return this.browserWindow.focus()
  }

  minimize () {
    return this.browserWindow.minimize()
  }

  maximize () {
    return this.browserWindow.maximize()
  }

  unmaximize () {
    return this.browserWindow.unmaximize()
  }

  restore () {
    return this.browserWindow.restore()
  }

  setFullScreen (fullScreen) {
    return this.browserWindow.setFullScreen(fullScreen)
  }

  setAutoHideMenuBar (autoHideMenuBar) {
    return this.browserWindow.setAutoHideMenuBar(autoHideMenuBar)
  }

  handlesAtomCommands () {
    return !this.isSpecWindow() && this.isWebViewFocused()
  }

  isFocused () {
    return this.browserWindow.isFocused()
  }

  isMaximized () {
    return this.browserWindow.isMaximized()
  }

  isMinimized () {
    return this.browserWindow.isMinimized()
  }

  isWebViewFocused () {
    return this.browserWindow.isWebViewFocused()
  }

  isSpecWindow () {
    return this.isSpec
  }

  reload () {
    this.loadedPromise = new Promise(resolve => { this.resolveLoadedPromise = resolve })
    this.prepareToUnload().then(canUnload => {
      if (canUnload) this.browserWindow.reload()
    })
    return this.loadedPromise
  }

  showSaveDialog (options, callback) {
    options = Object.assign({
      title: 'Save File',
      defaultPath: this.representedDirectoryPaths[0]
    }, options)

    if (typeof callback === 'function') {
      // Async
      dialog.showSaveDialog(this.browserWindow, options, callback)
    } else {
      // Sync
      return dialog.showSaveDialog(this.browserWindow, options)
    }
  }

  toggleDevTools () {
    return this.browserWindow.toggleDevTools()
  }

  openDevTools () {
    return this.browserWindow.openDevTools()
  }

  closeDevTools () {
    return this.browserWindow.closeDevTools()
  }

  setDocumentEdited (documentEdited) {
    return this.browserWindow.setDocumentEdited(documentEdited)
  }

  setRepresentedFilename (representedFilename) {
    return this.browserWindow.setRepresentedFilename(representedFilename)
  }

  setRepresentedDirectoryPaths (representedDirectoryPaths) {
    this.representedDirectoryPaths = representedDirectoryPaths
    this.representedDirectoryPaths.sort()
    this.loadSettings.initialPaths = this.representedDirectoryPaths
    return this.atomApplication.saveCurrentWindowOptions()
  }

  didClosePathWithWaitSession (path) {
    this.atomApplication.windowDidClosePathWithWaitSession(this, path)
  }

  copy () {
    return this.browserWindow.copy()
  }

  disableZoom () {
    return this.browserWindow.webContents.setVisualZoomLevelLimits(1, 1)
  }
}
