import cc from 'color-convert'
import { RGB } from 'color-convert/conversions'
import create from '../dom/create'
import onFind from '../dom/onFind'

/*
Contrast ratios

105/95 = 1.105
 95/86 = 1.105
 86/78 = 1.103

 78/38 = 2.053
 38/19 = 2.000

 19/13 = 1.462
 13/ 9 = 1.444
  9/ 6 = 1.500
*/
const defaultLuminances = [90, 81, 72, 38, 14, 8, 4]

/*
Contrast ratios

105/70 = 1.500
 70/47 = 1.489
 47/31 = 1.516

 31/19 = 1.632
 19/12 = 1.583

 12/10 = 1.200
 10/ 8 = 1.250
  8/ 6 = 1.333
*/
const defaultDarkLuminances = [3, 5, 7, 14, 26, 42, 65]

type ThemeType = {
  appearance?: 'dark' | 'light'
  boxedInput?: boolean
  compact?: boolean
  compactRails?: boolean
  highContrast?: boolean
  contrastChoices?: 'more' | 'normal'
  headerBackground?: string
  luminances?: number[]
  darkLuminances?: number[]
  colors: Colors
}

type Colors = {
  dominant: string
  gray: string
  red: string
  orange: string
  yellow: string
  green: string
  blue: string
  purple: string
  accent?: string
}

type PresetThemeName = 'Bold' | 'Calm' | 'Brightspot'

const presetThemes: Record<PresetThemeName, ThemeType> = {
  Bold: {
    luminances: defaultLuminances,
    darkLuminances: defaultDarkLuminances,
    colors: {
      dominant: '#93a7c6',
      gray: '#7d7f88',
      red: '#ff1e3c',
      orange: '#f28d00',
      yellow: '#baa700',
      green: '#51bd00',
      blue: '#13315c',
      purple: '#af95ff',
    },
  },
  Calm: {
    luminances: defaultLuminances,
    darkLuminances: defaultDarkLuminances,
    colors: {
      dominant: '#908f8b',
      gray: '#908f8b',
      red: '#c14234',
      orange: '#ff9505',
      yellow: '#e2c222',
      green: '#90b52d',
      blue: '#2aa3bf',
      purple: '#7700ff',
    },
  },
  Brightspot: {
    luminances: defaultLuminances,
    darkLuminances: defaultDarkLuminances,
    colors: {
      dominant: '#4b4545',
      gray: '#958f8f',
      red: '#ee0120',
      orange: '#f98948',
      yellow: '#f9dc5c',
      green: '#15655b',
      blue: '#225b7d',
      purple: '#3a4697',
    },
  },
}

const getDefaultTheme = () => getPresetTheme('Bold')

function getPresetTheme(name: PresetThemeName) {
  const t = presetThemes[name]
  return {
    ...t,
    luminances: t.luminances,
    darkLuminances: t.darkLuminances,
    colors: { ...t.colors },
  }
}

function relativeLuminance(rgb: RGB) {
  const [r, g, b] = rgb
    .map((c) => c / 255)
    .map((c) => (c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4)))
  return (0.2126 * r + 0.7152 * g + 0.0722 * b) * 100
}

export default class Theme {
  _element: HTMLElement
  _dark = false
  _theme: ThemeType

  constructor(element: HTMLElement) {
    this._element = element

    // Appends Theme Color Table in Settings
    onFind(
      this._element,
      '.themeColorContainer',
      (themeColorContainer: HTMLElement) => {
        const updatedColors = themeColorContainer.dataset.colors
        const updatedLuminances = themeColorContainer.dataset.luminances
        const updatedDarkLuminances = themeColorContainer.dataset.darkLuminances

        const appearanceInput =
          themeColorContainer.querySelector<HTMLInputElement>(
            'input.appearance',
          )
        if (appearanceInput) appearanceInput.value = this._dark.toString()

        const colorTable = this.createTable(
          updatedColors,
          updatedLuminances,
          updatedDarkLuminances,
        )
        themeColorContainer.appendChild(colorTable)
      },
    )

    // Adds event listener to Header Background
    onFind(
      this._element,
      '.headerBackgroundContainer .color',
      (headerBackgroundInput) => {
        headerBackgroundInput.addEventListener('change', (e) => {
          if (e.target instanceof HTMLInputElement) {
            this._theme.headerBackground = e.target.value
          }
          this._updateColorAccent()
          this._updateUI()
        })
      },
    )

    // Only display color table if custom is chosen as preset
    onFind(
      this._element,
      ".CIG-row[data-field-name='cms.presetTheme'] .ComboInput-option[data-value='CUSTOM']",
      (customPreset) => {
        const presetCig = customPreset.closest('.CIG-row')
        if (!presetCig) return
        const colorsField = presetCig.nextElementSibling
        if (!colorsField) return

        if (customPreset.getAttribute('aria-selected') == 'true') {
          colorsField.classList.remove('is-hidden')
        } else {
          colorsField.classList.add('is-hidden')
        }

        const observer = new MutationObserver((mutations) => {
          mutations.forEach((mutation) => {
            if (mutation.type === 'attributes') {
              const customPresetNode = mutation.target

              if (
                customPresetNode instanceof Element &&
                customPresetNode.getAttribute('aria-selected') == 'true'
              ) {
                colorsField.classList.remove('is-hidden')
              } else {
                colorsField.classList.add('is-hidden')
              }
            }
          })
        })

        observer.observe(customPreset, {
          attributes: true,
        })
      },
    )

    const storedTheme = window.localStorage.getItem('BSP.Theme')
    let validSavedTheme = false
    const savedTheme = document.documentElement.dataset.theme ?? ''

    try {
      JSON.parse(savedTheme)
      validSavedTheme = true
    } catch (e) {
      validSavedTheme = false
    }

    this._theme = validSavedTheme
      ? JSON.parse(savedTheme)
      : storedTheme
        ? JSON.parse(storedTheme)
        : getDefaultTheme()

    if (!this._theme.luminances) {
      this._theme.luminances = defaultLuminances
    }

    if (!this._theme.darkLuminances) {
      this._theme.darkLuminances = defaultDarkLuminances
    }

    if (document.readyState === 'loading') {
      window.addEventListener('DOMContentLoaded', () => {
        this._updateUI()
      })
    } else {
      this._updateUI()
    }

    this._setAppearance()

    if (window.matchMedia) {
      window
        .matchMedia('(prefers-color-scheme: dark)')
        .addEventListener('change', () => {
          this._setAppearance()
          this._updateUI()
        })
    }

    window.addEventListener('message', async ({ data }) => {
      if (data.type === 'BSP.Theme.UpdateFrameUI') {
        this._dark = data.isDark
        this._theme = data.theme
        this._updateColorAccent()
        this._updateUI()
      }
    })
  }

  _setAppearance() {
    const appearance = this._theme.appearance

    if (this._element.dataset.previewOverlay) {
      // Always set the appearance of preview overlay frame to light mode
      this._dark = false
      document.documentElement.style.colorScheme = 'light'
      this._updateUI()
    } else {
      if (appearance) {
        this._dark = appearance === 'dark'
      } else {
        this._dark =
          window.matchMedia &&
          window.matchMedia('(prefers-color-scheme: dark)').matches
      }

      document.documentElement.style.colorScheme = this._dark ? 'dark' : 'light'
    }
  }

  _updateUI() {
    const elements =
      this._element === document.documentElement
        ? [this._element, document.body]
        : [this._element]

    for (const e of elements) {
      e.classList.toggle('is-dark', this._dark)
      e.classList.toggle('is-boxedInput', !!this._theme.boxedInput)
      e.classList.toggle('is-compact', !!this._theme.compact)
      e.classList.toggle('is-compactRails', !!this._theme.compactRails)
      e.classList.toggle('is-highContrast', !!this._theme.highContrast)
      if (this._theme.contrastChoices === 'more') {
        e.classList.add('is-moreContrast')
      } else if (this._theme.contrastChoices === 'normal') {
        e.classList.add('is-normalContrast')
      }
    }

    const style = this._element.style
    const headerBackground = this._theme.headerBackground
    const blackLuminance = 1
    const whiteLuminance = 100

    if (headerBackground) {
      style.setProperty('--Page-headerBackground', headerBackground)

      for (const e of elements) {
        e.classList.toggle(
          'is-lightHeader',
          relativeLuminance(cc.hex.rgb(headerBackground)) >
            Math.sqrt(blackLuminance * whiteLuminance) !=
            this._dark,
        )
      }
    } else if (this._dark) {
      style.setProperty('--Page-headerBackground', 'var(--color-dominant1)')

      for (const e of elements) {
        e.classList.add('is-lightHeader')
      }
    } else {
      style.setProperty('--Page-headerBackground', 'var(--color-dominant7)')

      for (const e of elements) {
        e.classList.remove('is-lightHeader')
      }
    }

    const gray = cc.hex.hsl(this._theme.colors.gray)

    const black = this._guessColor(gray[0], gray[1], blackLuminance)
    const white = this._guessColor(gray[0], gray[1], whiteLuminance)

    if (this._dark) {
      this._setColorProperty(style, 'black', white)
      this._setColorProperty(style, 'white', black)
      style.setProperty(
        '--color-iconHoverBackground',
        'rgba(255, 255, 255, 0.08)',
      )
    } else {
      this._setColorProperty(style, 'black', black)
      this._setColorProperty(style, 'white', white)
      style.setProperty('--color-iconHoverBackground', 'rgba(0, 0, 0, 0.08)')
    }

    const luminances = this._theme[this._dark ? 'darkLuminances' : 'luminances']

    let name: keyof Colors
    for (name in this._theme.colors) {
      if (name === 'accent') continue
      const [h, s] = cc.hex.hsl(this._theme.colors[name])

      luminances?.forEach((luminance, i) => {
        this._setColorProperty(
          style,
          `${name}${i + 1}`,
          this._guessColor(
            h,
            s,
            this._theme.highContrast && i == 3
              ? this._dark
                ? (luminance + 5) * 4.5 - 5
                : (luminance + 5) / 4.5 - 5
              : luminance,
          ),
        )
      })
    }
  }

  _guessColor(hue: number, saturation: number, luminance: number) {
    let minDiff = 100
    let lightness = 0

    for (let l = 0; l <= 100; l += 0.1) {
      const diff = Math.abs(
        luminance - relativeLuminance(cc.hsl.rgb([hue, saturation, l])),
      )

      if (minDiff > diff) {
        minDiff = diff
        lightness = l
      }
    }

    return cc.hsl.rgb([hue, saturation, lightness])
  }

  _setColorProperty(style: CSSStyleDeclaration, name: string, color: RGB) {
    style.setProperty(`--color-${name}`, '#' + cc.rgb.hex(color))
    style.setProperty(`--rgb-${name}`, color.join(' '))
  }

  _updateColorAccent() {
    this._theme.colors.accent = this._theme.colors.dominant
  }

  getColor(name: string, luminance: number) {
    return this._element.style.getPropertyValue(`--color-${name}${luminance}`)
  }

  createTable = (
    updatedColors?: string,
    updatedLuminances?: string,
    updatedDarkLuminances?: string,
  ) => {
    const table = create('table', { className: 'Theme-colors' })
    const lr = create('tr')

    table.appendChild(lr)
    lr.appendChild(create('td', { colspan: 2 }))

    const newLuminances = this._dark ? updatedDarkLuminances : updatedLuminances

    const luminances = newLuminances
      ? JSON.parse(newLuminances)
      : this._theme[this._dark ? 'darkLuminances' : 'luminances']

    const colors = updatedColors
      ? JSON.parse(updatedColors)
      : this._theme.colors

    luminances.forEach((l: number, i: number) => {
      lr.appendChild(
        create(
          'td',
          create('input', {
            type: 'text',
            name: 'luminance-' + i,
            value: l,
            oninput: (e: Event) => {
              if (e.target instanceof HTMLInputElement) {
                luminances[i] = parseInt(e.target.value, 10)
              }
              this._updateColorAccent()
              this._updateUI()
            },
            onkeydown: (e: KeyboardEvent) => {
              if (!(e.target instanceof HTMLInputElement)) return
              let v = parseInt(e.target.value, 10)

              switch (e.key) {
                case 'ArrowUp':
                  v++
                  e.target.value = v.toString()
                  luminances[i] = v
                  this._updateColorAccent()
                  this._updateUI()
                  break

                case 'ArrowDown':
                  v--
                  e.target.value = v.toString()
                  luminances[i] = v
                  this._updateColorAccent()
                  this._updateUI()
                  break
              }
            },
          }),
        ),
      )
    })

    Object.keys(colors).forEach((name) => {
      if (name === 'accent') {
        return
      }

      const row = create(
        'tr',
        create('td', name.charAt(0).toUpperCase() + name.slice(1)),
      )

      row.appendChild(
        create(
          'td',
          create('input', {
            className: 'color',
            type: 'text',
            name: 'color-' + name,
            value: colors[name],
            onchange: (e: Event) => {
              if (e.target instanceof HTMLInputElement) {
                colors[name] = e.target.value
              }
              this._updateColorAccent()
              this._updateUI()
            },
          }),
        ),
      )

      for (let i = 0; i < luminances.length; i++) {
        row.appendChild(
          create('td', { style: `background:var(--color-${name}${i + 1});` }),
        )
      }

      table.appendChild(row)
    })

    return table
  }
}
