'use strict'

const cloneDeep = require('lodash.clonedeep')
const merge = require('lodash.merge')

const pluginRegistry = require('./pluginRegistry')

function freezeTheme (theme) {
  const queue = [theme]
  while (queue.length > 0) {
    const object = queue.shift()
    Object.freeze(object)

    for (const key of Object.keys(object)) {
      const value = object[key]
      if (value !== null && typeof value === 'object') {
        queue.push(value)
      }
    }
  }

  return theme
}

const defaultTheme = freezeTheme({
  boolean: { open: '', close: '' },
  circular: '[Circular]',
  date: {
    invalid: 'invalid',
    value: { open: '', close: '' }
  },
  diffGutters: {
    actual: '- ',
    expected: '+ ',
    padding: '  '
  },
  error: {
    ctor: { open: '(', close: ')' },
    name: { open: '', close: '' }
  },
  function: {
    name: { open: '', close: '' },
    stringTag: { open: '', close: '' }
  },
  global: { open: '', close: '' },
  item: {
    after: ',',
    customFormat: null,
    increaseValueIndent: false
  },
  list: { openBracket: '[', closeBracket: ']' },
  mapEntry: {
    after: ',',
    separator: ' => '
  },
  maxDepth: '…',
  null: { open: '', close: '' },
  number: { open: '', close: '' },
  object: {
    openBracket: '{',
    closeBracket: '}',
    ctor: { open: '', close: '' },
    stringTag: { open: '@', close: '' },
    secondaryStringTag: { open: '@', close: '' }
  },
  property: {
    after: ',',
    customFormat: null,
    keyBracket: { open: '[', close: ']' },
    separator: ': ',
    increaseValueIndent: false
  },
  regexp: {
    source: { open: '/', close: '/' },
    flags: { open: '', close: '' },
    separator: '---'
  },
  stats: { separator: '---' },
  string: {
    open: '',
    close: '',
    line: { open: "'", close: "'", escapeQuote: "'" },
    multiline: { start: '`', end: '`', escapeQuote: '``' },
    controlPicture: { open: '', close: '' },
    diff: {
      insert: { open: '', close: '' },
      delete: { open: '', close: '' },
      equal: { open: '', close: '' },
      insertLine: { open: '', close: '' },
      deleteLine: { open: '', close: '' }
    }
  },
  symbol: { open: '', close: '' },
  typedArray: {
    bytes: { open: '', close: '' }
  },
  undefined: { open: '', close: '' }
})

const pluginRefs = new Map()
pluginRefs.count = 0
const normalizedPluginThemes = new Map()
function normalizePlugins (plugins) {
  if (!Array.isArray(plugins) || plugins.length === 0) return null

  const refs = []
  const themes = []
  for (const fromPlugin of pluginRegistry.getThemes(plugins)) {
    if (!pluginRefs.has(fromPlugin.name)) {
      pluginRefs.set(fromPlugin.name, pluginRefs.count++)
    }

    refs.push(pluginRefs.get(fromPlugin.name))
    themes.push(fromPlugin.theme)
  }

  const ref = refs.join('.')
  if (normalizedPluginThemes.has(ref)) {
    return {
      ref,
      theme: normalizedPluginThemes.get(ref)
    }
  }

  const theme = freezeTheme(themes.reduce((acc, pluginTheme) => {
    return merge(acc, pluginTheme)
  }, cloneDeep(defaultTheme)))
  normalizedPluginThemes.set(ref, theme)
  return {ref, theme}
}

const normalizedCache = new WeakMap()
function normalize (options) {
  options = Object.assign({plugins: [], theme: null}, options)

  const normalizedPlugins = normalizePlugins(options.plugins)
  if (!options.theme) {
    return normalizedPlugins ? normalizedPlugins.theme : defaultTheme
  }

  const entry = normalizedCache.get(options.theme) || {theme: null, withPlugins: new Map()}
  if (!normalizedCache.has(options.theme)) normalizedCache.set(options.theme, entry)

  if (normalizedPlugins) {
    if (entry.withPlugins.has(normalizedPlugins.ref)) {
      return entry.withPlugins.get(normalizedPlugins.ref)
    }

    const theme = freezeTheme(merge(cloneDeep(normalizedPlugins.theme), options.theme))
    entry.withPlugins.set(normalizedPlugins.ref, theme)
    return theme
  }

  if (!entry.theme) {
    entry.theme = freezeTheme(merge(cloneDeep(defaultTheme), options.theme))
  }
  return entry.theme
}
exports.normalize = normalize

const modifiers = new WeakMap()
function addModifier (descriptor, modifier) {
  if (modifiers.has(descriptor)) {
    modifiers.get(descriptor).add(modifier)
  } else {
    modifiers.set(descriptor, new Set([modifier]))
  }
}
exports.addModifier = addModifier

const modifierCache = new WeakMap()
const originalCache = new WeakMap()
function applyModifiers (descriptor, theme) {
  if (!modifiers.has(descriptor)) return theme

  return Array.from(modifiers.get(descriptor)).reduce((prev, modifier) => {
    const cache = modifierCache.get(modifier) || new WeakMap()
    if (!modifierCache.has(modifier)) modifierCache.set(modifier, cache)

    if (cache.has(prev)) return cache.get(prev)

    const modifiedTheme = cloneDeep(prev)
    modifier(modifiedTheme)
    freezeTheme(modifiedTheme)
    cache.set(prev, modifiedTheme)
    originalCache.set(modifiedTheme, theme)
    return modifiedTheme
  }, theme)
}
exports.applyModifiers = applyModifiers

function applyModifiersToOriginal (descriptor, theme) {
  return applyModifiers(descriptor, originalCache.get(theme) || theme)
}
exports.applyModifiersToOriginal = applyModifiersToOriginal