/* @flow */
import isInBrowser from 'is-in-browser'
import StyleSheet from './StyleSheet'
import PluginsRegistry from './PluginsRegistry'
import sheets from './sheets'
import {plugins as internalPlugins} from './plugins/index'
import createGenerateIdDefault from './utils/createGenerateId'
import createRule from './utils/createRule'
import DomRenderer from './DomRenderer'
import type {
  Rule,
  RuleFactoryOptions,
  RuleOptions,
  StyleSheetFactoryOptions,
  Plugin,
  JssOptions,
  InternalJssOptions,
  JssStyle
} from './types'
import type {GenerateId} from './utils/createGenerateId'

let instanceCounter = 0

export default class Jss {
  id = instanceCounter++

  version = process.env.VERSION

  plugins = new PluginsRegistry()

  options: InternalJssOptions = {
    id: {minify: false},
    createGenerateId: createGenerateIdDefault,
    Renderer: isInBrowser ? DomRenderer : null,
    plugins: []
  }

  generateId: GenerateId = createGenerateIdDefault({minify: false})

  constructor(options?: JssOptions) {
    for (let i = 0; i < internalPlugins.length; i++) {
      this.plugins.use(internalPlugins[i], {queue: 'internal'})
    }
    this.setup(options)
  }

  /**
   * Prepares various options, applies plugins.
   * Should not be used twice on the same instance, because there is no plugins
   * deduplication logic.
   */
  setup(options?: JssOptions = {}): this {
    if (options.createGenerateId) {
      this.options.createGenerateId = options.createGenerateId
    }

    if (options.id) {
      this.options.id = {
        ...this.options.id,
        ...options.id
      }
    }

    if (options.createGenerateId || options.id) {
      this.generateId = this.options.createGenerateId(this.options.id)
    }

    if (options.insertionPoint != null) this.options.insertionPoint = options.insertionPoint
    if ('Renderer' in options) {
      this.options.Renderer = options.Renderer
    }

    // eslint-disable-next-line prefer-spread
    if (options.plugins) this.use.apply(this, options.plugins)

    return this
  }

  /**
   * Create a Style Sheet.
   */
  createStyleSheet(styles: Object, options: StyleSheetFactoryOptions = {}): StyleSheet {
    let {index} = options
    if (typeof index !== 'number') {
      index = sheets.index === 0 ? 0 : sheets.index + 1
    }
    const sheet = new StyleSheet(styles, {
      ...options,
      jss: this,
      generateId: options.generateId || this.generateId,
      insertionPoint: this.options.insertionPoint,
      Renderer: this.options.Renderer,
      index
    })
    this.plugins.onProcessSheet(sheet)

    return sheet
  }

  /**
   * Detach the Style Sheet and remove it from the registry.
   */
  removeStyleSheet(sheet: StyleSheet): this {
    sheet.detach()
    sheets.remove(sheet)
    return this
  }

  /**
   * Create a rule without a Style Sheet.
   * [Deprecated] will be removed in the next major version.
   */
  createRule(name?: string, style?: JssStyle = {}, options?: RuleFactoryOptions = {}): Rule | null {
    // Enable rule without name for inline styles.
    if (typeof name === 'object') {
      return this.createRule(undefined, name, style)
    }

    const ruleOptions: RuleOptions = {...options, name, jss: this, Renderer: this.options.Renderer}

    if (!ruleOptions.generateId) ruleOptions.generateId = this.generateId
    if (!ruleOptions.classes) ruleOptions.classes = {}
    if (!ruleOptions.keyframes) ruleOptions.keyframes = {}

    const rule = createRule(name, style, ruleOptions)

    if (rule) this.plugins.onProcessRule(rule)

    return rule
  }

  /**
   * Register plugin. Passed function will be invoked with a rule instance.
   */
  use(...plugins: Array<Plugin>): this {
    plugins.forEach(plugin => {
      this.plugins.use(plugin)
    })

    return this
  }
}
