/* global snapshotAuxiliaryData */

const path = require('path')
const _ = require('underscore-plus')
const {Emitter, CompositeDisposable} = require('event-kit')
const {File} = require('pathwatcher')
const fs = require('fs-plus')
const LessCompileCache = require('./less-compile-cache')

// Extended: Handles loading and activating available themes.
//
// An instance of this class is always available as the `atom.themes` global.
module.exports =
class ThemeManager {
  constructor ({packageManager, config, styleManager, notificationManager, viewRegistry}) {
    this.packageManager = packageManager
    this.config = config
    this.styleManager = styleManager
    this.notificationManager = notificationManager
    this.viewRegistry = viewRegistry
    this.emitter = new Emitter()
    this.styleSheetDisposablesBySourcePath = {}
    this.lessCache = null
    this.initialLoadComplete = false
    this.packageManager.registerPackageActivator(this, ['theme'])
    this.packageManager.onDidActivateInitialPackages(() => {
      this.onDidChangeActiveThemes(() => this.packageManager.reloadActivePackageStyleSheets())
    })
  }

  initialize ({resourcePath, configDirPath, safeMode, devMode}) {
    this.resourcePath = resourcePath
    this.configDirPath = configDirPath
    this.safeMode = safeMode
    this.lessSourcesByRelativeFilePath = null
    if (devMode || (typeof snapshotAuxiliaryData === 'undefined')) {
      this.lessSourcesByRelativeFilePath = {}
      this.importedFilePathsByRelativeImportPath = {}
    } else {
      this.lessSourcesByRelativeFilePath = snapshotAuxiliaryData.lessSourcesByRelativeFilePath
      this.importedFilePathsByRelativeImportPath = snapshotAuxiliaryData.importedFilePathsByRelativeImportPath
    }
  }

  /*
  Section: Event Subscription
  */

  // Essential: Invoke `callback` when style sheet changes associated with
  // updating the list of active themes have completed.
  //
  // * `callback` {Function}
  //
  // Returns a {Disposable} on which `.dispose()` can be called to unsubscribe.
  onDidChangeActiveThemes (callback) {
    return this.emitter.on('did-change-active-themes', callback)
  }

  /*
  Section: Accessing Available Themes
  */

  getAvailableNames () {
    // TODO: Maybe should change to list all the available themes out there?
    return this.getLoadedNames()
  }

  /*
  Section: Accessing Loaded Themes
  */

  // Public: Returns an {Array} of {String}s of all the loaded theme names.
  getLoadedThemeNames () {
    return this.getLoadedThemes().map((theme) => theme.name)
  }

  // Public: Returns an {Array} of all the loaded themes.
  getLoadedThemes () {
    return this.packageManager.getLoadedPackages().filter((pack) => pack.isTheme())
  }

  /*
  Section: Accessing Active Themes
  */

  // Public: Returns an {Array} of {String}s of all the active theme names.
  getActiveThemeNames () {
    return this.getActiveThemes().map((theme) => theme.name)
  }

  // Public: Returns an {Array} of all the active themes.
  getActiveThemes () {
    return this.packageManager.getActivePackages().filter((pack) => pack.isTheme())
  }

  activatePackages () {
    return this.activateThemes()
  }

  /*
  Section: Managing Enabled Themes
  */

  warnForNonExistentThemes () {
    let themeNames = this.config.get('core.themes') || []
    if (!_.isArray(themeNames)) { themeNames = [themeNames] }
    for (let themeName of themeNames) {
      if (!themeName || (typeof themeName !== 'string') || !this.packageManager.resolvePackagePath(themeName)) {
        console.warn(`Enabled theme '${themeName}' is not installed.`)
      }
    }
  }

  // Public: Get the enabled theme names from the config.
  //
  // Returns an array of theme names in the order that they should be activated.
  getEnabledThemeNames () {
    let themeNames = this.config.get('core.themes') || []
    if (!_.isArray(themeNames)) { themeNames = [themeNames] }
    themeNames = themeNames.filter((themeName) =>
      (typeof themeName === 'string') && this.packageManager.resolvePackagePath(themeName)
    )

    // Use a built-in syntax and UI theme any time the configured themes are not
    // available.
    if (themeNames.length < 2) {
      const builtInThemeNames = [
        'atom-dark-syntax',
        'atom-dark-ui',
        'atom-light-syntax',
        'atom-light-ui',
        'base16-tomorrow-dark-theme',
        'base16-tomorrow-light-theme',
        'solarized-dark-syntax',
        'solarized-light-syntax'
      ]
      themeNames = _.intersection(themeNames, builtInThemeNames)
      if (themeNames.length === 0) {
        themeNames = ['one-dark-syntax', 'one-dark-ui']
      } else if (themeNames.length === 1) {
        if (_.endsWith(themeNames[0], '-ui')) {
          themeNames.unshift('one-dark-syntax')
        } else {
          themeNames.push('one-dark-ui')
        }
      }
    }

    // Reverse so the first (top) theme is loaded after the others. We want
    // the first/top theme to override later themes in the stack.
    return themeNames.reverse()
  }

  /*
  Section: Private
  */

  // Resolve and apply the stylesheet specified by the path.
  //
  // This supports both CSS and Less stylesheets.
  //
  // * `stylesheetPath` A {String} path to the stylesheet that can be an absolute
  //   path or a relative path that will be resolved against the load path.
  //
  // Returns a {Disposable} on which `.dispose()` can be called to remove the
  // required stylesheet.
  requireStylesheet (stylesheetPath, priority, skipDeprecatedSelectorsTransformation) {
    let fullPath = this.resolveStylesheet(stylesheetPath)
    if (fullPath) {
      const content = this.loadStylesheet(fullPath)
      return this.applyStylesheet(fullPath, content, priority, skipDeprecatedSelectorsTransformation)
    } else {
      throw new Error(`Could not find a file at path '${stylesheetPath}'`)
    }
  }

  unwatchUserStylesheet () {
    if (this.userStylesheetSubscriptions != null) this.userStylesheetSubscriptions.dispose()
    this.userStylesheetSubscriptions = null
    this.userStylesheetFile = null
    if (this.userStyleSheetDisposable != null) this.userStyleSheetDisposable.dispose()
    this.userStyleSheetDisposable = null
  }

  loadUserStylesheet () {
    this.unwatchUserStylesheet()

    const userStylesheetPath = this.styleManager.getUserStyleSheetPath()
    if (!fs.isFileSync(userStylesheetPath)) { return }

    try {
      this.userStylesheetFile = new File(userStylesheetPath)
      this.userStylesheetSubscriptions = new CompositeDisposable()
      const reloadStylesheet = () => this.loadUserStylesheet()
      this.userStylesheetSubscriptions.add(this.userStylesheetFile.onDidChange(reloadStylesheet))
      this.userStylesheetSubscriptions.add(this.userStylesheetFile.onDidRename(reloadStylesheet))
      this.userStylesheetSubscriptions.add(this.userStylesheetFile.onDidDelete(reloadStylesheet))
    } catch (error) {
      const message = `\
Unable to watch path: \`${path.basename(userStylesheetPath)}\`. Make sure
you have permissions to \`${userStylesheetPath}\`.

On linux there are currently problems with watch sizes. See
[this document][watches] for more info.
[watches]:https://github.com/atom/atom/blob/master/docs/build-instructions/linux.md#typeerror-unable-to-watch-path\
`
      this.notificationManager.addError(message, {dismissable: true})
    }

    let userStylesheetContents
    try {
      userStylesheetContents = this.loadStylesheet(userStylesheetPath, true)
    } catch (error) {
      return
    }

    this.userStyleSheetDisposable = this.styleManager.addStyleSheet(userStylesheetContents, {sourcePath: userStylesheetPath, priority: 2})
  }

  loadBaseStylesheets () {
    this.reloadBaseStylesheets()
  }

  reloadBaseStylesheets () {
    this.requireStylesheet('../static/atom', -2, true)
  }

  stylesheetElementForId (id) {
    const escapedId = id.replace(/\\/g, '\\\\')
    return document.head.querySelector(`atom-styles style[source-path="${escapedId}"]`)
  }

  resolveStylesheet (stylesheetPath) {
    if (path.extname(stylesheetPath).length > 0) {
      return fs.resolveOnLoadPath(stylesheetPath)
    } else {
      return fs.resolveOnLoadPath(stylesheetPath, ['css', 'less'])
    }
  }

  loadStylesheet (stylesheetPath, importFallbackVariables) {
    if (path.extname(stylesheetPath) === '.less') {
      return this.loadLessStylesheet(stylesheetPath, importFallbackVariables)
    } else {
      return fs.readFileSync(stylesheetPath, 'utf8')
    }
  }

  loadLessStylesheet (lessStylesheetPath, importFallbackVariables = false) {
    if (this.lessCache == null) {
      this.lessCache = new LessCompileCache({
        resourcePath: this.resourcePath,
        lessSourcesByRelativeFilePath: this.lessSourcesByRelativeFilePath,
        importedFilePathsByRelativeImportPath: this.importedFilePathsByRelativeImportPath,
        importPaths: this.getImportPaths()
      })
    }

    try {
      if (importFallbackVariables) {
        const baseVarImports = `\
@import "variables/ui-variables";
@import "variables/syntax-variables";\
`
        const relativeFilePath = path.relative(this.resourcePath, lessStylesheetPath)
        const lessSource = this.lessSourcesByRelativeFilePath[relativeFilePath]

        let content, digest
        if (lessSource != null) {
          ({ content } = lessSource);
          ({ digest } = lessSource)
        } else {
          content = baseVarImports + '\n' + fs.readFileSync(lessStylesheetPath, 'utf8')
          digest = null
        }

        return this.lessCache.cssForFile(lessStylesheetPath, content, digest)
      } else {
        return this.lessCache.read(lessStylesheetPath)
      }
    } catch (error) {
      let detail, message
      error.less = true
      if (error.line != null) {
        // Adjust line numbers for import fallbacks
        if (importFallbackVariables) { error.line -= 2 }

        message = `Error compiling Less stylesheet: \`${lessStylesheetPath}\``
        detail = `Line number: ${error.line}\n${error.message}`
      } else {
        message = `Error loading Less stylesheet: \`${lessStylesheetPath}\``
        detail = error.message
      }

      this.notificationManager.addError(message, {detail, dismissable: true})
      throw error
    }
  }

  removeStylesheet (stylesheetPath) {
    if (this.styleSheetDisposablesBySourcePath[stylesheetPath] != null) {
      this.styleSheetDisposablesBySourcePath[stylesheetPath].dispose()
    }
  }

  applyStylesheet (path, text, priority, skipDeprecatedSelectorsTransformation) {
    this.styleSheetDisposablesBySourcePath[path] = this.styleManager.addStyleSheet(
      text,
      {
        priority,
        skipDeprecatedSelectorsTransformation,
        sourcePath: path
      }
    )

    return this.styleSheetDisposablesBySourcePath[path]
  }

  activateThemes () {
    return new Promise(resolve => {
      // @config.observe runs the callback once, then on subsequent changes.
      this.config.observe('core.themes', () => {
        this.deactivateThemes().then(() => {
          this.warnForNonExistentThemes()
          this.refreshLessCache() // Update cache for packages in core.themes config

          const promises = []
          for (const themeName of this.getEnabledThemeNames()) {
            if (this.packageManager.resolvePackagePath(themeName)) {
              promises.push(this.packageManager.activatePackage(themeName))
            } else {
              console.warn(`Failed to activate theme '${themeName}' because it isn't installed.`)
            }
          }

          return Promise.all(promises).then(() => {
            this.addActiveThemeClasses()
            this.refreshLessCache() // Update cache again now that @getActiveThemes() is populated
            this.loadUserStylesheet()
            this.reloadBaseStylesheets()
            this.initialLoadComplete = true
            this.emitter.emit('did-change-active-themes')
            resolve()
          })
        })
      })
    })
  }

  deactivateThemes () {
    this.removeActiveThemeClasses()
    this.unwatchUserStylesheet()
    const results = this.getActiveThemes().map(pack => this.packageManager.deactivatePackage(pack.name))
    return Promise.all(results.filter((r) => (r != null) && (typeof r.then === 'function')))
  }

  isInitialLoadComplete () {
    return this.initialLoadComplete
  }

  addActiveThemeClasses () {
    const workspaceElement = this.viewRegistry.getView(this.workspace)
    if (workspaceElement) {
      for (const pack of this.getActiveThemes()) {
        workspaceElement.classList.add(`theme-${pack.name}`)
      }
    }
  }

  removeActiveThemeClasses () {
    const workspaceElement = this.viewRegistry.getView(this.workspace)
    for (const pack of this.getActiveThemes()) {
      workspaceElement.classList.remove(`theme-${pack.name}`)
    }
  }

  refreshLessCache () {
    if (this.lessCache) this.lessCache.setImportPaths(this.getImportPaths())
  }

  getImportPaths () {
    let themePaths
    const activeThemes = this.getActiveThemes()
    if (activeThemes.length > 0) {
      themePaths = (activeThemes.filter((theme) => theme).map((theme) => theme.getStylesheetsPath()))
    } else {
      themePaths = []
      for (const themeName of this.getEnabledThemeNames()) {
        const themePath = this.packageManager.resolvePackagePath(themeName)
        if (themePath) {
          const deprecatedPath = path.join(themePath, 'stylesheets')
          if (fs.isDirectorySync(deprecatedPath)) {
            themePaths.push(deprecatedPath)
          } else {
            themePaths.push(path.join(themePath, 'styles'))
          }
        }
      }
    }

    return themePaths.filter(themePath => fs.isDirectorySync(themePath))
  }
}
