crypto = require 'crypto'
ipc = require 'ipc'
os = require 'os'
path = require 'path'
remote = require 'remote'
shell = require 'shell'

_ = require 'underscore'
{deprecate} = require 'grim'
{Emitter} = require 'event-kit'
{Model} = require 'theorist'
fs = require 'fs-plus'
{convertStackTrace, convertLine} = require 'coffeestack'

WindowEventHandler = require './window-event-handler'
StylesElement = require './styles-element'

Utils = require './flux/models/utils'
{APIError} = require './flux/errors'

ensureInteger = (f, fallback) ->
  if f is NaN or f is undefined or f is null
    f = fallback
  return Math.round(f)

# Essential: Atom global for dealing with packages, themes, menus, and the window.
#
# The singleton of this class is always available as the `atom` global.
module.exports =
class Atom extends Model
  @version: 1  # Increment this when the serialization format changes

  # Load or create the application environment
  # Returns an Atom instance, fully initialized
  @loadOrCreate: ->
    startTime = Date.now()

    savedState = @_loadSavedState()
    if savedState and savedState?.version is @version
      app = new this(savedState)
    else
      app = new this({@version})

    return app

  # Loads and returns the serialized state corresponding to this window
  # if it exists; otherwise returns undefined.
  @_loadSavedState: ->
    statePath = @getStatePath()

    if fs.existsSync(statePath)
      try
        stateString = fs.readFileSync(statePath, 'utf8')
      catch error
        console.warn "Error reading window state: #{statePath}", error.stack, error
    else
      stateString = @getLoadSettings().windowState

    try
      JSON.parse(stateString) if stateString?
    catch error
      console.warn "Error parsing window state: #{statePath} #{error.stack}", error

  # Returns the path where the state for the current window will be
  # located if it exists.
  @getStatePath: ->
    {isSpec, mainWindow} = @getLoadSettings()
    if isSpec
      filename = 'spec-saved-state.json'
    else if mainWindow
      path.join(@getConfigDirPath(), 'main-window-state.json')
    else
      null

  # Get the directory path to Atom's configuration area.
  #
  # Returns the absolute path to ~/.nylas
  @getConfigDirPath: ->
    @configDirPath ?= fs.absolute('~/.nylas')

  # Returns the load settings hash associated with the current window.
  @getLoadSettings: ->
    @loadSettings ?= JSON.parse(decodeURIComponent(location.search.substr(14)))

    cloned = Utils.deepClone(@loadSettings)
    # The loadSettings.windowState could be large, request it only when needed.
    cloned.__defineGetter__ 'windowState', =>
      @getCurrentWindow().loadSettings.windowState
    cloned.__defineSetter__ 'windowState', (value) =>
      @getCurrentWindow().loadSettings.windowState = value
    cloned

  @getCurrentWindow: ->
    remote.getCurrentWindow()

  workspaceViewParentSelector: 'body'
  lastUncaughtError: null

  ###
  Section: Properties
  ###

  # Public: A {CommandRegistry} instance
  commands: null

  # Public: A {Config} instance
  config: null

  # Public: A {Clipboard} instance
  clipboard: null

  # Public: A {MenuManager} instance
  menu: null

  # Public: A {KeymapManager} instance
  keymaps: null

  # Public: A {PackageManager} instance
  packages: null

  # Public: A {ThemeManager} instance
  themes: null

  # Public: A {StyleManager} instance
  styles: null

  ###
  Section: Construction and Destruction
  ###

  # Call .loadOrCreate instead
  constructor: (@savedState={}) ->
    {@version} = @savedState
    @emitter = new Emitter

  # Sets up the basic services that should be available in all modes
  # (both spec and application).
  #
  # Call after this instance has been assigned to the `atom` global.
  initialize: ->
    # Disable deprecations unless in dev mode or spec mode so that regular
    # editor performance isn't impacted by generating stack traces for
    # deprecated calls.
    unless @inDevMode() or @inSpecMode()
      require('grim').deprecate = ->

    @setupErrorHandling()

    @unsubscribe()

    @loadTime = null

    Config = require './config'
    KeymapManager = require './keymap-manager'
    CommandRegistry = require './command-registry'
    PackageManager = require './package-manager'
    Clipboard = require './clipboard'
    ThemeManager = require './theme-manager'
    StyleManager = require './style-manager'
    ActionBridge = require './flux/action-bridge'
    MenuManager = require './menu-manager'
    configDirPath = @getConfigDirPath()

    {devMode, safeMode, resourcePath, windowType} = @getLoadSettings()

    document.body.classList.add("platform-#{process.platform}")
    document.body.classList.add("window-type-#{windowType}")

    # Add 'src/global' to module search path.
    globalPath = path.join(resourcePath, 'src', 'global')
    require('module').globalPaths.push(globalPath)

    # Still set NODE_PATH since tasks may need it.
    process.env.NODE_PATH = globalPath

    # Make react.js faster
    process.env.NODE_ENV ?= 'production' unless devMode

    # Set Atom's home so packages don't have to guess it
    process.env.ATOM_HOME = configDirPath

    # Setup config and load it immediately so it's available to our singletons
    @config = new Config({configDirPath, resourcePath})

    @keymaps = new KeymapManager({configDirPath, resourcePath})
    @keymaps.onDidMatchBinding (event) ->
      # If the user fired a command with the application: prefix bound to
      # the body, re-fire it up into the browser process. This prevents us
      # from needing this crap, which has to be updated every time a new
      # application: command is added:
      # https://github.com/atom/atom/blob/master/src/workspace-element.coffee#L119
      if event.binding.command.indexOf('application:') is 0 and event.binding.selector.indexOf("body") is 0
        ipc.send('command', event.binding.command)

    unless @inSpecMode()
      @actionBridge = new ActionBridge(ipc)

    @commands = new CommandRegistry
    specMode = @inSpecMode()
    @packages = new PackageManager({devMode, configDirPath, resourcePath, safeMode, specMode})
    @styles = new StyleManager
    document.head.appendChild(new StylesElement)
    @themes = new ThemeManager({packageManager: @packages, configDirPath, resourcePath, safeMode})
    @clipboard = new Clipboard()

    @menu = new MenuManager({resourcePath})
    if process.platform is 'win32'
      @getCurrentWindow().setMenuBarVisibility(false)

    # initialize spell checking
    require('web-frame').setSpellCheckProvider("en-US", false, {
      spellCheck: (text) ->
        !(require('spellchecker').isMisspelled(text))
    })

    @subscribe @packages.onDidActivateInitialPackages => @watchThemes()
    @windowEventHandler = new WindowEventHandler

    window.onbeforeunload = => @_unloading()
    @_unloadCallbacks = []

  # Start our error reporting to the backend and attach error handlers
  # to the window and the Bluebird Promise library, converting things
  # back through the sourcemap as necessary.
  setupErrorHandling: ->
    ErrorReporter = require './error-reporter'
    @errorReporter = new ErrorReporter
      inSpecMode: @inSpecMode()
      inDevMode: @inDevMode()

    sourceMapCache = {}

    window.onerror = =>
      @lastUncaughtError = Array::slice.call(arguments)
      [message, url, line, column, originalError] = @lastUncaughtError

      # Convert the javascript error back into a Coffeescript error
      convertedLine = convertLine(url, line, column, sourceMapCache)
      {line, column} = convertedLine if convertedLine?
      originalError.stack = convertStackTrace(originalError.stack, sourceMapCache) if originalError

      eventObject = {message, url, line, column, originalError}

      openDevTools = true
      eventObject.preventDefault = -> openDevTools = false

      # Announce that we will display the error. Recipients can call preventDefault
      # to prevent the developer tools from being shown
      @emitter.emit('will-throw-error', eventObject)

      if openDevTools and @inDevMode()
        @openDevTools()
        @executeJavaScriptInDevTools('InspectorFrontendAPI.showConsole()')

      # Announce that the error was uncaught
      @emit('uncaught-error', arguments...)
      @emitter.emit('did-throw-error', eventObject)

    # Since Bluebird is the promise library, we can properly report
    # unhandled errors from business logic inside promises.
    Promise.longStackTraces() unless @inSpecMode()

    Promise.onPossiblyUnhandledRejection (error) =>
      error.stack = convertStackTrace(error.stack, sourceMapCache)

      # API Errors are logged to Sentry only under certain circumstances,
      # and are logged directly from the NylasAPI class.
      if error instanceof APIError
        return

      if @inSpecMode()
        console.error(error.stack)
      else if @inDevMode()
        console.error(error.message, error.stack, error)
        @openDevTools()
        @executeJavaScriptInDevTools('InspectorFrontendAPI.showConsole()')
      else
        console.warn(error)
        console.warn(error.stack)

      @emitError(error)

  emitError: (error) ->
    console.error(error) unless @inSpecMode()
    eventObject = {message: error.message, originalError: error}
    @emitter.emit('will-throw-error', eventObject)
    @emit('uncaught-error', error.message, null, null, null, error)
    @emitter.emit('did-throw-error', eventObject)

  ###
  Section: Event Subscription
  ###

  # Extended: Invoke the given callback whenever {::beep} is called.
  #
  # * `callback` {Function} to be called whenever {::beep} is called.
  #
  # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
  onDidBeep: (callback) ->
    @emitter.on 'did-beep', callback

  # Extended: Invoke the given callback when there is an unhandled error, but
  # before the devtools pop open
  #
  # * `callback` {Function} to be called whenever there is an unhandled error
  #   * `event` {Object}
  #     * `originalError` {Object} the original error object
  #     * `message` {String} the original error object
  #     * `url` {String} Url to the file where the error originated.
  #     * `line` {Number}
  #     * `column` {Number}
  #     * `preventDefault` {Function} call this to avoid popping up the dev tools.
  #
  # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
  onWillThrowError: (callback) ->
    @emitter.on 'will-throw-error', callback

  # Extended: Invoke the given callback whenever there is an unhandled error.
  #
  # * `callback` {Function} to be called whenever there is an unhandled error
  #   * `event` {Object}
  #     * `originalError` {Object} the original error object
  #     * `message` {String} the original error object
  #     * `url` {String} Url to the file where the error originated.
  #     * `line` {Number}
  #     * `column` {Number}
  #
  # Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
  onDidThrowError: (callback) ->
    @emitter.on 'did-throw-error', callback

  # Extended: Run the Chromium content-tracing module for five seconds, and save
  # the output to a file which is printed to the command-line output of the app.
  # You can take the file exported by this function and load it into Chrome's
  # content trace visualizer (chrome://tracing). It's like Chromium Developer
  # Tools Profiler, but for all processes and threads.
  trace: ->
    tracing = remote.require('content-tracing')
    tracing.startRecording '*', 'record-until-full,enable-sampling,enable-systrace', ->
      console.log('Tracing started')
      setTimeout ->
        tracing.stopRecording '', (path) ->
          console.log('Tracing data recorded to ' + path)
      , 5000

  ###
  Section: Atom Details
  ###
  isMainWindow: ->
    !!@getLoadSettings().mainWindow

  isWorkWindow: ->
    @getWindowType() is 'work'

  getWindowType: ->
    @getLoadSettings().windowType

  # Public: Is the current window in development mode?
  inDevMode: ->
    @getLoadSettings().devMode

  # Public: Is the current window in safe mode?
  inSafeMode: ->
    @getLoadSettings().safeMode

  # Public: Is the current window running specs?
  inSpecMode: ->
    @getLoadSettings().isSpec

  # Public: Get the version of the Atom application.
  #
  # Returns the version text {String}.
  getVersion: ->
    @appVersion ?= @getLoadSettings().appVersion

  # Public: Determine whether the current version is an official release.
  isReleasedVersion: ->
    not /\w{7}/.test(@getVersion()) # Check if the release is a 7-character SHA prefix

  # Public: Get the directory path to N1's configuration area.
  #
  # Returns the absolute path to `~/.nylas`.
  getConfigDirPath: ->
    @constructor.getConfigDirPath()

  # Public: Get the time taken to completely load the current window.
  #
  # This time include things like loading and activating packages, creating
  # DOM elements for the editor, and reading the config.
  #
  # Returns the {Number} of milliseconds taken to load the window or null
  # if the window hasn't finished loading yet.
  getWindowLoadTime: ->
    @loadTime

  # Public: Get the load settings for the current window.
  #
  # Returns an {Object} containing all the load setting key/value pairs.
  getLoadSettings: ->
    @constructor.getLoadSettings()

  ###
  Section: Managing The Atom Window
  ###

  # Essential: Close the current window.
  close: ->
    @getCurrentWindow().close()

  quit: ->
    remote.require('app').quit()

  # Essential: Get the size of current window.
  #
  # Returns an {Object} in the format `{width: 1000, height: 700}`
  getSize: ->
    [width, height] = @getCurrentWindow().getSize()
    {width, height}

  # Essential: Set the size of current window.
  #
  # * `width` The {Number} of pixels.
  # * `height` The {Number} of pixels.
  setSize: (width, height) ->
    width = ensureInteger(width, 100)
    height = ensureInteger(height, 100)
    @getCurrentWindow().setSize(width, height)

  # Essential: Transition and set the size of the current window.
  #
  # * `width` The {Number} of pixels.
  # * `height` The {Number} of pixels.
  # * `duration` The {Number} of pixels.
  setSizeAnimated: (width, height, duration=400) ->
    # On Windows, the native window resizing code isn't fast enough to "animate"
    # by resizing over and over again. Just turn off animation for now.
    if process.platform is 'win32'
      duration = 1

    # Avoid divide by zero errors below
    duration = Math.max(1, duration)

    # Keep track of the number of times this method has been invoked, and ensure
    # that we only `tick` for the last invocation. This prevents two resizes from
    # running at the same time.
    @_setSizeAnimatedCallCount ?= 0
    @_setSizeAnimatedCallCount += 1
    call = @_setSizeAnimatedCallCount

    cubicInOut = (t) -> if t<.5 then 4*t**3 else (t-1)*(2*t-2)**2+1
    win = @getCurrentWindow()
    width = Math.round(width)
    height = Math.round(height)

    startBounds = win.getBounds()
    startTime = Date.now() - 1 # - 1 so that if duration is 1, t = 1 on the first frame

    boundsForI = (i) ->
      # It's very important this function never return undefined for any of the
      # keys which blows up setBounds.
      x: ensureInteger(startBounds.x + (width-startBounds.width) * -0.5 * i, 0)
      y: ensureInteger(startBounds.y + (height-startBounds.height) * -0.5 * i, 0)
      width: ensureInteger(startBounds.width + (width-startBounds.width) * i, width)
      height: ensureInteger(startBounds.height + (height-startBounds.height) * i, height)

    tick = =>
      return unless call is @_setSizeAnimatedCallCount
      t = Math.min(1, (Date.now() - startTime) / (duration))
      i = cubicInOut(t)
      win.setBounds(boundsForI(i))
      unless t is 1
        _.defer(tick)
    tick()

  setMinimumWidth: (minWidth) ->
    win = @getCurrentWindow()
    minWidth = ensureInteger(minWidth, 0)
    minHeight = win.getMinimumSize()[1]
    win.setMinimumSize(minWidth, minHeight)

    [currWidth, currHeight] = win.getSize()
    win.setSize(minWidth, currHeight) if minWidth > currWidth

  # Essential: Get the position of current window.
  #
  # Returns an {Object} in the format `{x: 10, y: 20}`
  getPosition: ->
    [x, y] = @getCurrentWindow().getPosition()
    {x, y}

  # Essential: Set the position of current window.
  #
  # * `x` The {Number} of pixels.
  # * `y` The {Number} of pixels.
  setPosition: (x, y) ->
    x = ensureInteger(x, 0)
    y = ensureInteger(y, 0)
    ipc.send('call-window-method', 'setPosition', x, y)

  # Extended: Get the current window
  getCurrentWindow: ->
    @constructor.getCurrentWindow()

  # Extended: Move current window to the center of the screen.
  center: ->
    ipc.send('call-window-method', 'center')

  # Extended: Focus the current window.
  focus: ->
    ipc.send('call-window-method', 'focus')
    window.focus()

  # Extended: Show the current window.
  show: ->
    ipc.send('call-window-method', 'show')

  isVisible: ->
    @getCurrentWindow().isVisible()

  # Extended: Hide the current window.
  hide: ->
    ipc.send('call-window-method', 'hide')

  # Extended: Reload the current window.
  reload: ->
    ipc.send('call-window-method', 'restart')

  # Updates the window load settings - called when the app is ready to display
  # a hot-loaded window. Causes listeners registered with `onWindowPropsReceived`
  # to receive new window props.
  loadSettingsChanged: (loadSettings) =>
    @loadSettings = loadSettings
    @constructor.loadSettings = loadSettings
    {width, height, windowProps} = loadSettings

    @emitter.emit('window-props-received', windowProps ? {})

    if width and height
      @setWindowDimensions({width, height})

  # Public: The windowProps passed when creating the window via `newWindow`.
  #
  getWindowProps: ->
    @getLoadSettings().windowProps ? {}

  # Public: If your package declares hot-loaded window types, `onWindowPropsReceived`
  # fires when your hot-loaded window is about to be shown so you can update
  # components to reflect the new window props.
  #
  # - callback: A function to call when window props are received, just before
  #   the hot window is shown. The first parameter is the new windowProps.
  #
  onWindowPropsReceived: (callback) ->
    @emitter.on('window-props-received', callback)

  # Extended: Is the current window maximized?
  isMaximixed: ->
    @getCurrentWindow().isMaximized()

  maximize: ->
    ipc.send('call-window-method', 'maximize')

  minimize: ->
    ipc.send('call-window-method', 'minimize')

  # Extended: Is the current window in full screen mode?
  isFullScreen: ->
    @getCurrentWindow().isFullScreen()

  # Extended: Set the full screen state of the current window.
  setFullScreen: (fullScreen=false) ->
    ipc.send('call-window-method', 'setFullScreen', fullScreen)
    if fullScreen then document.body.classList.add("fullscreen") else document.body.classList.remove("fullscreen")

  # Extended: Toggle the full screen state of the current window.
  toggleFullScreen: ->
    @setFullScreen(!@isFullScreen())

  # Get the dimensions of this window.
  #
  # Returns an {Object} with the following keys:
  #   * `x`      The window's x-position {Number}.
  #   * `y`      The window's y-position {Number}.
  #   * `width`  The window's width {Number}.
  #   * `height` The window's height {Number}.
  getWindowDimensions: ->
    browserWindow = @getCurrentWindow()
    {x, y, width, height} = browserWindow.getBounds()
    maximized = browserWindow.isMaximized()
    {x, y, width, height, maximized}

  # Set the dimensions of the window.
  #
  # The window will be centered if either the x or y coordinate is not set
  # in the dimensions parameter. If x or y are omitted the window will be
  # centered. If height or width are omitted only the position will be changed.
  #
  # * `dimensions` An {Object} with the following keys:
  #   * `x` The new x coordinate.
  #   * `y` The new y coordinate.
  #   * `width` The new width.
  #   * `height` The new height.
  setWindowDimensions: ({x, y, width, height}) ->
    if x? and y? and width? and height?
      @getCurrentWindow().setBounds({x, y, width, height})
    else if width? and height?
      @setSize(width, height)
    else if x? and y?
      @setPosition(x, y)
    else
      @center()

  # Returns true if the dimensions are useable, false if they should be ignored.
  # Work around for https://github.com/atom/electron/issues/473
  isValidDimensions: ({x, y, width, height}={}) ->
    width > 0 and height > 0 and x + width > 0 and y + height > 0

  getDefaultWindowDimensions: ->
    screen = remote.require('screen')
    {width, height} = screen.getPrimaryDisplay().workAreaSize
    x = 0
    y = 0

    MAX_WIDTH = 1440
    if width > MAX_WIDTH
      x = Math.floor((width - MAX_WIDTH) / 2)
      width = MAX_WIDTH

    MAX_HEIGHT = 900
    if height > MAX_HEIGHT
      y = Math.floor((height - MAX_HEIGHT) / 2)
      height = MAX_HEIGHT

    {x, y, width, height}

  restoreWindowDimensions: ->
    dimensions = @savedState.windowDimensions
    unless @isValidDimensions(dimensions)
      dimensions = @getDefaultWindowDimensions()
    @setWindowDimensions(dimensions)
    @maximize() if dimensions.maximized and process.platform isnt 'darwin'

  storeWindowDimensions: ->
    dimensions = @getWindowDimensions()
    @savedState.windowDimensions = dimensions if @isValidDimensions(dimensions)

  # Call this method when establishing a real application window.
  startRootWindow: ->
    @displayWindow()

    {safeMode, windowType} = @getLoadSettings()
    @registerCommands()
    @loadConfig()
    @keymaps.loadBundledKeymaps()
    @themes.loadBaseStylesheets()
    @packages.loadPackages(windowType)
    @deserializePackageStates()
    @deserializeSheetContainer()
    @packages.activate()
    @keymaps.loadUserKeymap()
    @requireUserInitScript() unless safeMode
    @menu.update()

    @showRootWindow()

    ipc.sendChannel('window-command', 'window:loaded')

  showRootWindow: ->
    cover = document.getElementById("application-loading-cover")
    cover.classList.add('visible')
    @restoreWindowDimensions()
    @getCurrentWindow().setMinimumSize(875, 500)

  registerCommands: ->
    {resourcePath} = @getLoadSettings()
    CommandInstaller = require './command-installer'
    CommandInstaller.installAtomCommand resourcePath, false, (error) ->
      console.warn error.message if error?
    CommandInstaller.installApmCommand resourcePath, false, (error) ->
      console.warn error.message if error?

  # Call this method when establishing a secondary application window
  # displaying a specific set of packages.
  #
  startSecondaryWindow: ->
    {width,
     height,
     windowType,
     windowPackages} = @getLoadSettings()

    cover = document.getElementById("application-loading-cover")
    cover.remove()

    @loadConfig()

    @keymaps.loadBundledKeymaps()
    @themes.loadBaseStylesheets()

    @packages.loadPackages(windowType)
    @packages.loadPackage(pack) for pack in (windowPackages ? [])
    @deserializeSheetContainer()
    @packages.activate()
    @keymaps.loadUserKeymap()

    ipc.on("load-settings-changed", @loadSettingsChanged)

    @setWindowDimensions({width, height}) if width and height

    @menu.update()

    ipc.sendChannel('window-command', 'window:loaded')

  # Requests that the backend browser bootup a new window with the given
  # options.
  # See the valid option types in Application::newWindow in
  # src/browser/application.coffee
  newWindow: (options={}) -> ipc.send('new-window', options)

  # Registers a hot window for certain packages
  # See the valid option types in Application::registerHotWindow in
  # src/browser/application.coffee
  registerHotWindow: (options={}) -> ipc.send('register-hot-window', options)

  # Unregisters a hot window with the given windowType
  unregisterHotWindow: (windowType) -> ipc.send('unregister-hot-window', windowType)

  saveStateAndUnloadWindow: ->
    @packages.deactivatePackages()
    @savedState.packageStates = @packages.packageStates
    @saveSync()
    @windowState = null

  ###
  Section: Messaging the User
  ###

  displayWindow: ({maximize} = {}) ->
    @show()
    @focus()
    @maximize() if maximize

  # Essential: Visually and audibly trigger a beep.
  beep: ->
    shell.beep() if @config.get('core.audioBeep')
    @emitter.emit 'did-beep'

  # Essential: A flexible way to open a dialog akin to an alert dialog.
  #
  # ## Examples
  #
  # ```coffee
  # atom.confirm
  #   message: 'How you feeling?'
  #   detailedMessage: 'Be honest.'
  #   buttons:
  #     Good: -> window.alert('good to hear')
  #     Bad: -> window.alert('bummer')
  # ```
  #
  # * `options` An {Object} with the following keys:
  #   * `message` The {String} message to display.
  #   * `detailedMessage` (optional) The {String} detailed message to display.
  #   * `buttons` (optional) Either an array of strings or an object where keys are
  #     button names and the values are callbacks to invoke when clicked.
  #
  # Returns the chosen button index {Number} if the buttons option was an array.
  confirm: ({message, detailedMessage, buttons}={}) ->
    buttons ?= {}
    if _.isArray(buttons)
      buttonLabels = buttons
    else
      buttonLabels = Object.keys(buttons)

    dialog = remote.require('dialog')
    chosen = dialog.showMessageBox @getCurrentWindow(),
      type: 'info'
      message: message
      detail: detailedMessage
      buttons: buttonLabels

    if _.isArray(buttons)
      chosen
    else
      callback = buttons[buttonLabels[chosen]]
      callback?()

  ###
  Section: Managing the Dev Tools
  ###

  # Extended: Open the dev tools for the current window.
  openDevTools: ->
    ipc.send('call-window-method', 'openDevTools')

  # Extended: Toggle the visibility of the dev tools for the current window.
  toggleDevTools: ->
    ipc.send('call-window-method', 'toggleDevTools')

  # Extended: Execute code in dev tools.
  executeJavaScriptInDevTools: (code) ->
    ipc.send('call-window-method', 'executeJavaScriptInDevTools', code)

  ###
  Section: Private
  ###

  deserializeSheetContainer: ->
    startTime = Date.now()
    # Put state back into sheet-container? Restore app state here
    @item = document.createElement("atom-workspace")
    @item.setAttribute("id", "sheet-container")
    @item.setAttribute("class", "sheet-container")
    @item.setAttribute("tabIndex", "-1")

    React = require "react"
    SheetContainer = require './sheet-container'
    React.render(React.createElement(SheetContainer), @item)
    document.querySelector(@workspaceViewParentSelector).appendChild(@item)

  deserializePackageStates: ->
    @packages.packageStates = @savedState.packageStates ? {}
    delete @savedState.packageStates

  loadThemes: ->
    @themes.load()

  loadConfig: ->
    @config.setSchema null, {type: 'object', properties: _.clone(require('./config-schema'))}
    @config.load()

  watchThemes: ->
    @themes.onDidChangeActiveThemes =>
      # Only reload stylesheets from non-theme packages
      for pack in @packages.getActivePackages() when pack.getType() isnt 'theme'
        pack.reloadStylesheets?()
      null

  exit: (status) ->
    app = remote.require('app')
    app.emit('will-exit')
    remote.process.exit(status)

  showOpenDialog: (options, callback) ->
    dialog = remote.require('dialog')
    dialog.showOpenDialog(@getCurrentWindow(), options, callback)

  showSaveDialog: (defaultPath, callback) ->
    dialog = remote.require('dialog')
    dialog.showSaveDialog(@getCurrentWindow(), {title: 'Save File', defaultPath}, callback)

  saveSync: ->
    stateString = JSON.stringify(@savedState)
    if statePath = @constructor.getStatePath()
      fs.writeFileSync(statePath, stateString, 'utf8')
    else
      @getCurrentWindow().loadSettings.windowState = stateString

  crashMainProcess: ->
    remote.process.crash()

  crashRenderProcess: ->
    process.crash()

  getUserInitScriptPath: ->
    initScriptPath = fs.resolve(@getConfigDirPath(), 'init', ['js', 'coffee'])
    initScriptPath ? path.join(@getConfigDirPath(), 'init.coffee')

  requireUserInitScript: ->
    if userInitScriptPath = @getUserInitScriptPath()
      try
        require(userInitScriptPath) if fs.isFileSync(userInitScriptPath)
      catch error
        console.log(error)

  # Require the module with the given globals.
  #
  # The globals will be set on the `window` object and removed after the
  # require completes.
  #
  # * `id` The {String} module name or path.
  # * `globals` An optinal {Object} to set as globals during require.
  requireWithGlobals: (id, globals={}) ->
    existingGlobals = {}
    for key, value of globals
      existingGlobals[key] = window[key]
      window[key] = value

    require(id)

    for key, value of existingGlobals
      if value is undefined
        delete window[key]
      else
        window[key] = value

  onUpdateAvailable: (callback) ->
    @emitter.on 'update-available', callback

  updateAvailable: (details) ->
    @emitter.emit 'update-available', details

  # Lets multiple components register beforeUnload callbacks.
  # The callbacks are expected to return either true or false.
  #
  # Note: If you return false to cancel the window close, you /must/ perform
  # work and then call finishUnload. We do not support cancelling quit!
  # https://phab.nylas.com/D1932#inline-11722
  #
  onBeforeUnload: (callback) -> @_unloadCallbacks.push(callback)

  _unloading: ->
    continueUnload = true
    for callback in @_unloadCallbacks
      returnValue = callback()
      if returnValue is true
        continue
      else if returnValue is false
        continueUnload = false
      else
        console.warn "You registered an `onBeforeUnload` callback that does not return either exactly `true` or `false`. It returned #{returnValue}", callback
    return continueUnload

  # Call this method to resume the close / quit process if you returned
  # false from a onBeforeUnload handler.
  #
  finishUnload: ->
    _.defer =>
      if remote.getGlobal('application').quitting
        remote.require('app').quit()
      else
        @close()
