import {
  rgb2hex,
  hex2rgb,
  getContrastRatioLayers
} from 'src/services/color_convert/color_convert.js'
import {
  DEFAULT_SHADOWS,
  generateColors,
  generateShadows,
  generateRadii,
  generateFonts,
  composePreset,
  getThemes,
  shadows2to3,
  colors2to3
} from 'src/services/style_setter/style_setter.js'
import {
  newImporter,
  newExporter
} from 'src/services/export_import/export_import.js'
import {
  SLOT_INHERITANCE
} from 'src/services/theme_data/pleromafe.js'
import {
  CURRENT_VERSION,
  OPACITIES,
  getLayers,
  getOpacitySlot
} from 'src/services/theme_data/theme_data.service.js'
import ColorInput from 'src/components/color_input/color_input.vue'
import RangeInput from 'src/components/range_input/range_input.vue'
import OpacityInput from 'src/components/opacity_input/opacity_input.vue'
import ShadowControl from 'src/components/shadow_control/shadow_control.vue'
import FontControl from 'src/components/font_control/font_control.vue'
import ContrastRatio from 'src/components/contrast_ratio/contrast_ratio.vue'
import TabSwitcher from 'src/components/tab_switcher/tab_switcher.jsx'
import Checkbox from 'src/components/checkbox/checkbox.vue'
import Select from 'src/components/select/select.vue'

import Preview from './preview.vue'

// List of color values used in v1
const v1OnlyNames = [
  'bg',
  'fg',
  'text',
  'link',
  'cRed',
  'cGreen',
  'cBlue',
  'cOrange'
].map(_ => _ + 'ColorLocal')

// if color doesn't match regex, it's not a color
const isColor = (color) => /^#[0-9a-f]{6}$/i.test(color)

const colorConvert = (color) => {
  if (color.startsWith('--') || color === 'transparent' || !isColor(color)) {
    return color
  } else {
    return hex2rgb(color)
  }
}

export default {
  data () {
    return {
      themeImporter: newImporter({
        validator: this.importValidator,
        onImport: this.onImport,
        onImportFailure: this.onImportFailure
      }),
      themeExporter: newExporter({
        filename: 'pleroma_theme',
        getExportedObject: () => this.exportedTheme
      }),
      availableStyles: [],
      selected: '',
      selectedTheme: this.$store.getters.mergedConfig.theme,
      themeWarning: undefined,
      tempImportFile: undefined,
      engineVersion: 0,

      previewShadows: {},
      previewColors: {},
      previewRadii: {},
      previewFonts: {},

      shadowsInvalid: true,
      colorsInvalid: true,
      radiiInvalid: true,

      keepColor: false,
      keepShadows: false,
      keepOpacity: false,
      keepRoundness: false,
      keepFonts: false,

      ...Object.keys(SLOT_INHERITANCE)
        .map(key => [key, ''])
        .reduce((acc, [key, val]) => ({ ...acc, [ key + 'ColorLocal' ]: val }), {}),

      ...Object.keys(OPACITIES)
        .map(key => [key, ''])
        .reduce((acc, [key, val]) => ({ ...acc, [ key + 'OpacityLocal' ]: val }), {}),

      shadowSelected: undefined,
      shadowsLocal: {},
      fontsLocal: {},

      btnRadiusLocal: '',
      inputRadiusLocal: '',
      checkboxRadiusLocal: '',
      panelRadiusLocal: '',
      avatarRadiusLocal: '',
      avatarAltRadiusLocal: '',
      attachmentRadiusLocal: '',
      tooltipRadiusLocal: ''
    }
  },
  created () {
    const self = this

    getThemes()
      .then((promises) => {
        return Promise.all(
          Object.entries(promises)
            .map(([k, v]) => v.then(res => [k, res]))
        )
      })
      .then(themes => themes.reduce((acc, [k, v]) => {
        if (v) {
          return {
            ...acc,
            [k]: v
          }
        } else {
          return acc
        }
      }, {}))
      .then((themesComplete) => {
        self.availableStyles = themesComplete
      })
  },
  mounted () {
    this.loadThemeFromLocalStorage()
    if (typeof this.shadowSelected === 'undefined') {
      this.shadowSelected = this.shadowsAvailable[0]
    }
  },
  computed: {
    themeWarningHelp () {
      if (!this.themeWarning) return
      const t = this.$t
      const pre = 'settings.style.switcher.help.'
      const {
        origin,
        themeEngineVersion,
        type,
        noActionsPossible
      } = this.themeWarning
      if (origin === 'file') {
        // Loaded v2 theme from file
        if (themeEngineVersion === 2 && type === 'wrong_version') {
          return t(pre + 'v2_imported')
        }
        if (themeEngineVersion > CURRENT_VERSION) {
          return t(pre + 'future_version_imported') + ' ' +
            (
              noActionsPossible
                ? t(pre + 'snapshot_missing')
                : t(pre + 'snapshot_present')
            )
        }
        if (themeEngineVersion < CURRENT_VERSION) {
          return t(pre + 'future_version_imported') + ' ' +
            (
              noActionsPossible
                ? t(pre + 'snapshot_missing')
                : t(pre + 'snapshot_present')
            )
        }
      } else if (origin === 'localStorage') {
        if (type === 'snapshot_source_mismatch') {
          return t(pre + 'snapshot_source_mismatch')
        }
        // FE upgraded from v2
        if (themeEngineVersion === 2) {
          return t(pre + 'upgraded_from_v2')
        }
        // Admin downgraded FE
        if (themeEngineVersion > CURRENT_VERSION) {
          return t(pre + 'fe_downgraded') + ' ' +
            (
              noActionsPossible
                ? t(pre + 'migration_snapshot_ok')
                : t(pre + 'migration_snapshot_gone')
            )
        }
        // Admin upgraded FE
        if (themeEngineVersion < CURRENT_VERSION) {
          return t(pre + 'fe_upgraded') + ' ' +
            (
              noActionsPossible
                ? t(pre + 'migration_snapshot_ok')
                : t(pre + 'migration_snapshot_gone')
            )
        }
      }
    },
    selectedVersion () {
      return Array.isArray(this.selectedTheme) ? 1 : 2
    },
    currentColors () {
      return Object.keys(SLOT_INHERITANCE)
        .map(key => [key, this[key + 'ColorLocal']])
        .reduce((acc, [key, val]) => ({ ...acc, [ key ]: val }), {})
    },
    currentOpacity () {
      return Object.keys(OPACITIES)
        .map(key => [key, this[key + 'OpacityLocal']])
        .reduce((acc, [key, val]) => ({ ...acc, [ key ]: val }), {})
    },
    currentRadii () {
      return {
        btn: this.btnRadiusLocal,
        input: this.inputRadiusLocal,
        checkbox: this.checkboxRadiusLocal,
        panel: this.panelRadiusLocal,
        avatar: this.avatarRadiusLocal,
        avatarAlt: this.avatarAltRadiusLocal,
        tooltip: this.tooltipRadiusLocal,
        attachment: this.attachmentRadiusLocal
      }
    },
    preview () {
      return composePreset(this.previewColors, this.previewRadii, this.previewShadows, this.previewFonts)
    },
    previewTheme () {
      if (!this.preview.theme.colors) return { colors: {}, opacity: {}, radii: {}, shadows: {}, fonts: {} }
      return this.preview.theme
    },
    // This needs optimization maybe
    previewContrast () {
      try {
        if (!this.previewTheme.colors.bg) return {}
        const colors = this.previewTheme.colors
        const opacity = this.previewTheme.opacity
        if (!colors.bg) return {}
        if (!isColor(colors.bg)) return {}
        const hints = (ratio) => ({
          text: ratio.toPrecision(3) + ':1',
          // AA level, AAA level
          aa: ratio >= 4.5,
          aaa: ratio >= 7,
          // same but for 18pt+ texts
          laa: ratio >= 3,
          laaa: ratio >= 4.5
        })
        const colorsConverted = Object.entries(colors).reduce((acc, [key, value]) => ({ ...acc, [key]: colorConvert(value) }), {})

        const ratios = Object.entries(SLOT_INHERITANCE).reduce((acc, [key, value]) => {
          const slotIsBaseText = key === 'text' || key === 'link'
          const slotIsText = slotIsBaseText || (
            typeof value === 'object' && value !== null && value.textColor
          )
          if (!slotIsText) return acc
          const { layer, variant } = slotIsBaseText ? { layer: 'bg' } : value
          const background = variant || layer
          const opacitySlot = getOpacitySlot(background)
          const textColors = [
            key,
            ...(background === 'bg' ? ['cRed', 'cGreen', 'cBlue', 'cOrange'] : [])
          ]

          const layers = getLayers(
            layer,
            variant || layer,
            opacitySlot,
            colorsConverted,
            opacity
          )

          return {
            ...acc,
            ...textColors.reduce((acc, textColorKey) => {
              const newKey = slotIsBaseText
                ? 'bg' + textColorKey[0].toUpperCase() + textColorKey.slice(1)
                : textColorKey
              return {
                ...acc,
                [newKey]: getContrastRatioLayers(
                  colorsConverted[textColorKey],
                  layers,
                  colorsConverted[textColorKey]
                )
              }
            }, {})
          }
        }, {})

        return Object.entries(ratios).reduce((acc, [k, v]) => { acc[k] = hints(v); return acc }, {})
      } catch (e) {
        console.warn('Failure computing contrasts', e)
      }
    },
    previewRules () {
      if (!this.preview.rules) return ''
      return [
        ...Object.values(this.preview.rules),
        'color: var(--text)',
        'font-family: var(--interfaceFont, sans-serif)'
      ].join(';')
    },
    shadowsAvailable () {
      return Object.keys(DEFAULT_SHADOWS).sort()
    },
    currentShadowOverriden: {
      get () {
        return !!this.currentShadow
      },
      set (val) {
        if (val) {
          this.shadowsLocal[this.shadowSelected] = this.currentShadowFallback.map(_ => Object.assign({}, _))
        } else {
          delete this.shadowsLocal[this.shadowSelected]
        }
      }
    },
    currentShadowFallback () {
      return (this.previewTheme.shadows || {})[this.shadowSelected]
    },
    currentShadow: {
      get () {
        return this.shadowsLocal[this.shadowSelected]
      },
      set (v) {
        this.shadowsLocal[this.shadowSelected] = v
      }
    },
    themeValid () {
      return !this.shadowsInvalid && !this.colorsInvalid && !this.radiiInvalid
    },
    exportedTheme () {
      const saveEverything = (
        !this.keepFonts &&
        !this.keepShadows &&
        !this.keepOpacity &&
        !this.keepRoundness &&
        !this.keepColor
      )

      const source = {
        themeEngineVersion: CURRENT_VERSION
      }

      if (this.keepFonts || saveEverything) {
        source.fonts = this.fontsLocal
      }
      if (this.keepShadows || saveEverything) {
        source.shadows = this.shadowsLocal
      }
      if (this.keepOpacity || saveEverything) {
        source.opacity = this.currentOpacity
      }
      if (this.keepColor || saveEverything) {
        source.colors = this.currentColors
      }
      if (this.keepRoundness || saveEverything) {
        source.radii = this.currentRadii
      }

      const theme = {
        themeEngineVersion: CURRENT_VERSION,
        ...this.previewTheme
      }

      return {
        // To separate from other random JSON files and possible future source formats
        _pleroma_theme_version: 2, theme, source
      }
    },
    isActive () {
      const tabSwitcher = this.$parent
      return tabSwitcher ? tabSwitcher.isActive('theme') : false
    }
  },
  components: {
    ColorInput,
    OpacityInput,
    RangeInput,
    ContrastRatio,
    ShadowControl,
    FontControl,
    TabSwitcher,
    Preview,
    Checkbox,
    Select
  },
  methods: {
    loadTheme (
      {
        theme,
        source,
        _pleroma_theme_version: fileVersion
      },
      origin,
      forceUseSource = false
    ) {
      this.dismissWarning()
      if (!source && !theme) {
        throw new Error('Can\'t load theme: empty')
      }
      const version = (origin === 'localStorage' && !theme.colors)
        ? 'l1'
        : fileVersion
      const snapshotEngineVersion = (theme || {}).themeEngineVersion
      const themeEngineVersion = (source || {}).themeEngineVersion || 2
      const versionsMatch = themeEngineVersion === CURRENT_VERSION
      const sourceSnapshotMismatch = (
        theme !== undefined &&
          source !== undefined &&
          themeEngineVersion !== snapshotEngineVersion
      )
      // Force loading of source if user requested it or if snapshot
      // is unavailable
      const forcedSourceLoad = (source && forceUseSource) || !theme
      if (!(versionsMatch && !sourceSnapshotMismatch) &&
          !forcedSourceLoad &&
          version !== 'l1' &&
          origin !== 'defaults'
      ) {
        if (sourceSnapshotMismatch && origin === 'localStorage') {
          this.themeWarning = {
            origin,
            themeEngineVersion,
            type: 'snapshot_source_mismatch'
          }
        } else if (!theme) {
          this.themeWarning = {
            origin,
            noActionsPossible: true,
            themeEngineVersion,
            type: 'no_snapshot_old_version'
          }
        } else if (!versionsMatch) {
          this.themeWarning = {
            origin,
            noActionsPossible: !source,
            themeEngineVersion,
            type: 'wrong_version'
          }
        }
      }
      this.normalizeLocalState(theme, version, source, forcedSourceLoad)
    },
    forceLoadLocalStorage () {
      this.loadThemeFromLocalStorage(true)
    },
    dismissWarning () {
      this.themeWarning = undefined
      this.tempImportFile = undefined
    },
    forceLoad () {
      const { origin } = this.themeWarning
      switch (origin) {
        case 'localStorage':
          this.loadThemeFromLocalStorage(true)
          break
        case 'file':
          this.onImport(this.tempImportFile, true)
          break
      }
      this.dismissWarning()
    },
    forceSnapshot () {
      const { origin } = this.themeWarning
      switch (origin) {
        case 'localStorage':
          this.loadThemeFromLocalStorage(false, true)
          break
        case 'file':
          console.error('Forcing snapshot from file is not supported yet')
          break
      }
      this.dismissWarning()
    },
    loadThemeFromLocalStorage (confirmLoadSource = false, forceSnapshot = false) {
      const {
        customTheme: theme,
        customThemeSource: source
      } = this.$store.getters.mergedConfig
      if (!theme && !source) {
        // Anon user or never touched themes
        this.loadTheme(
          this.$store.state.instance.themeData,
          'defaults',
          confirmLoadSource
        )
      } else {
        this.loadTheme(
          {
            theme,
            source: forceSnapshot ? theme : source
          },
          'localStorage',
          confirmLoadSource
        )
      }
    },
    setCustomTheme () {
      this.$store.dispatch('setOption', {
        name: 'customTheme',
        value: {
          themeEngineVersion: CURRENT_VERSION,
          ...this.previewTheme
        }
      })
      this.$store.dispatch('setOption', {
        name: 'customThemeSource',
        value: {
          themeEngineVersion: CURRENT_VERSION,
          shadows: this.shadowsLocal,
          fonts: this.fontsLocal,
          opacity: this.currentOpacity,
          colors: this.currentColors,
          radii: this.currentRadii
        }
      })
    },
    updatePreviewColorsAndShadows () {
      this.previewColors = generateColors({
        opacity: this.currentOpacity,
        colors: this.currentColors
      })
      this.previewShadows = generateShadows(
        { shadows: this.shadowsLocal, opacity: this.previewTheme.opacity, themeEngineVersion: this.engineVersion },
        this.previewColors.theme.colors,
        this.previewColors.mod
      )
    },
    importTheme () { this.themeImporter.importData() },
    exportTheme () { this.themeExporter.exportData() },
    onImport (parsed, forceSource = false) {
      this.tempImportFile = parsed
      this.loadTheme(parsed, 'file', forceSource)
    },
    onImportFailure (result) {
      this.$store.dispatch('pushGlobalNotice', { messageKey: 'settings.invalid_theme_imported', level: 'error' })
    },
    importValidator (parsed) {
      const version = parsed._pleroma_theme_version
      return version >= 1 || version <= 2
    },
    clearAll () {
      this.loadThemeFromLocalStorage()
    },

    // Clears all the extra stuff when loading V1 theme
    clearV1 () {
      Object.keys(this.$data)
        .filter(_ => _.endsWith('ColorLocal') || _.endsWith('OpacityLocal'))
        .filter(_ => !v1OnlyNames.includes(_))
        .forEach(key => {
          this.$data[key] = undefined
        })
    },

    clearRoundness () {
      Object.keys(this.$data)
        .filter(_ => _.endsWith('RadiusLocal'))
        .forEach(key => {
          this.$data[key] = undefined
        })
    },

    clearOpacity () {
      Object.keys(this.$data)
        .filter(_ => _.endsWith('OpacityLocal'))
        .forEach(key => {
          this.$data[key] = undefined
        })
    },

    clearShadows () {
      this.shadowsLocal = {}
    },

    clearFonts () {
      this.fontsLocal = {}
    },

    /**
     * This applies stored theme data onto form. Supports three versions of data:
     * v3 (version >= 3) - newest version of themes which supports snapshots for better compatiblity
     * v2 (version = 2) - newer version of themes.
     * v1 (version = 1) - older version of themes (import from file)
     * v1l (version = l1) - older version of theme (load from local storage)
     * v1 and v1l differ because of way themes were stored/exported.
     * @param {Object} theme - theme data (snapshot)
     * @param {Number} version - version of data. 0 means try to guess based on data. "l1" means v1, locastorage type
     * @param {Object} source - theme source - this will be used if compatible
     * @param {Boolean} source - by default source won't be used if version doesn't match since it might render differently
     *                           this allows importing source anyway
     */
    normalizeLocalState (theme, version = 0, source, forceSource = false) {
      let input
      if (typeof source !== 'undefined') {
        if (forceSource || source.themeEngineVersion === CURRENT_VERSION) {
          input = source
          version = source.themeEngineVersion
        } else {
          input = theme
        }
      } else {
        input = theme
      }

      const radii = input.radii || input
      const opacity = input.opacity
      const shadows = input.shadows || {}
      const fonts = input.fonts || {}
      const colors = !input.themeEngineVersion
        ? colors2to3(input.colors || input)
        : input.colors || input

      if (version === 0) {
        if (input.version) version = input.version
        // Old v1 naming: fg is text, btn is foreground
        if (typeof colors.text === 'undefined' && typeof colors.fg !== 'undefined') {
          version = 1
        }
        // New v2 naming: text is text, fg is foreground
        if (typeof colors.text !== 'undefined' && typeof colors.fg !== 'undefined') {
          version = 2
        }
      }

      this.engineVersion = version

      // Stuff that differs between V1 and V2
      if (version === 1) {
        this.fgColorLocal = rgb2hex(colors.btn)
        this.textColorLocal = rgb2hex(colors.fg)
      }

      if (!this.keepColor) {
        this.clearV1()
        const keys = new Set(version !== 1 ? Object.keys(SLOT_INHERITANCE) : [])
        if (version === 1 || version === 'l1') {
          keys
            .add('bg')
            .add('link')
            .add('cRed')
            .add('cBlue')
            .add('cGreen')
            .add('cOrange')
        }

        keys.forEach(key => {
          const color = colors[key]
          const hex = rgb2hex(colors[key])
          this[key + 'ColorLocal'] = hex === '#aN' ? color : hex
        })
      }

      if (opacity && !this.keepOpacity) {
        this.clearOpacity()
        Object.entries(opacity).forEach(([k, v]) => {
          if (typeof v === 'undefined' || v === null || Number.isNaN(v)) return
          this[k + 'OpacityLocal'] = v
        })
      }

      if (!this.keepRoundness) {
        this.clearRoundness()
        Object.entries(radii).forEach(([k, v]) => {
          // 'Radius' is kept mostly for v1->v2 localstorage transition
          const key = k.endsWith('Radius') ? k.split('Radius')[0] : k
          this[key + 'RadiusLocal'] = v
        })
      }

      if (!this.keepShadows) {
        this.clearShadows()
        if (version === 2) {
          this.shadowsLocal = shadows2to3(shadows, this.previewTheme.opacity)
        } else {
          this.shadowsLocal = shadows
        }
        this.shadowSelected = this.shadowsAvailable[0]
      }

      if (!this.keepFonts) {
        this.clearFonts()
        this.fontsLocal = fonts
      }
    }
  },
  watch: {
    currentRadii () {
      try {
        this.previewRadii = generateRadii({ radii: this.currentRadii })
        this.radiiInvalid = false
      } catch (e) {
        this.radiiInvalid = true
        console.warn(e)
      }
    },
    shadowsLocal: {
      handler () {
        if (Object.getOwnPropertyNames(this.previewColors).length === 1) return
        try {
          this.updatePreviewColorsAndShadows()
          this.shadowsInvalid = false
        } catch (e) {
          this.shadowsInvalid = true
          console.warn(e)
        }
      },
      deep: true
    },
    fontsLocal: {
      handler () {
        try {
          this.previewFonts = generateFonts({ fonts: this.fontsLocal })
          this.fontsInvalid = false
        } catch (e) {
          this.fontsInvalid = true
          console.warn(e)
        }
      },
      deep: true
    },
    currentColors () {
      try {
        this.updatePreviewColorsAndShadows()
        this.colorsInvalid = false
        this.shadowsInvalid = false
      } catch (e) {
        this.colorsInvalid = true
        this.shadowsInvalid = true
        console.warn(e)
      }
    },
    currentOpacity () {
      try {
        this.updatePreviewColorsAndShadows()
      } catch (e) {
        console.warn(e)
      }
    },
    selected () {
      this.selectedTheme = Object.entries(this.availableStyles).find(([k, s]) => {
        if (Array.isArray(s)) {
          return s[0] === this.selected
        } else {
          return s.name === this.selected
        }
      })[1]
    },
    selectedTheme () {
      this.dismissWarning()
      if (this.selectedVersion === 1) {
        if (!this.keepRoundness) {
          this.clearRoundness()
        }

        if (!this.keepShadows) {
          this.clearShadows()
        }

        if (!this.keepOpacity) {
          this.clearOpacity()
        }

        if (!this.keepColor) {
          this.clearV1()

          this.bgColorLocal = this.selectedTheme[1]
          this.fgColorLocal = this.selectedTheme[2]
          this.textColorLocal = this.selectedTheme[3]
          this.linkColorLocal = this.selectedTheme[4]
          this.cRedColorLocal = this.selectedTheme[5]
          this.cGreenColorLocal = this.selectedTheme[6]
          this.cBlueColorLocal = this.selectedTheme[7]
          this.cOrangeColorLocal = this.selectedTheme[8]
        }
      } else if (this.selectedVersion >= 2) {
        this.normalizeLocalState(this.selectedTheme.theme, 2, this.selectedTheme.source)
      }
    }
  }
}
