// Utilities
import {
  computed,
  getCurrentScope,
  inject,
  onScopeDispose,
  provide,
  ref,
  shallowRef,
  toRef,
  watch,
  watchEffect,
} from 'vue'
import {
  consoleWarn,
  createRange,
  darken,
  deprecate,
  getCurrentInstance,
  getLuma,
  hasLightForeground,
  IN_BROWSER,
  lighten,
  mergeDeep,
  parseColor,
  PREFERS_REDUCED_MOTION,
  propsFactory,
  RGBtoHex,
  SUPPORTS_MATCH_MEDIA,
} from '@/util'
import { Box } from '@/util/box'

// Types
import type { VueHeadClient } from '@unhead/vue/client'
import type { HeadClient } from '@vueuse/head'
import type { App, DeepReadonly, InjectionKey, Ref } from 'vue'
import type { Color } from '@/util'

type DeepPartial<T> = T extends object ? { [P in keyof T]?: DeepPartial<T[P]> } : T

export interface ThemeTransitionOptions {
  origin?: string
  duration?: string
}

export type ThemeOptions = false | {
  cspNonce?: string
  defaultTheme?: 'light' | 'dark' | 'system' | string & {}
  variations?: false | VariationsOptions
  themes?: Record<string, ThemeDefinition>
  transition?: boolean | ThemeTransitionOptions
  stylesheetId?: string
  scope?: string
  utilities?: boolean
}
export type ThemeDefinition = DeepPartial<InternalThemeDefinition>

interface InternalThemeOptions {
  cspNonce?: string
  isDisabled: boolean
  defaultTheme: 'light' | 'dark' | 'system' | string & {}
  prefix: string
  variations: false | VariationsOptions
  themes: Record<string, InternalThemeDefinition>
  transition: boolean | ThemeTransitionOptions
  stylesheetId: string
  scope?: string
  scoped: boolean
  utilities: boolean
}

interface VariationsOptions {
  colors: string[]
  lighten: number
  darken: number
}

interface InternalThemeDefinition {
  dark: boolean
  colors: Colors
  variables: Record<string, string | number>
}

export interface Colors extends BaseColors, OnColors {
  [key: string]: Color
}

interface BaseColors {
  background: Color
  surface: Color
  primary: Color
  secondary: Color
  success: Color
  warning: Color
  error: Color
  info: Color
}

interface OnColors {
  'on-background': Color
  'on-surface': Color
  'on-primary': Color
  'on-secondary': Color
  'on-success': Color
  'on-warning': Color
  'on-error': Color
  'on-info': Color
}

export interface ThemeInstance {
  change: (themeName: string, transition?: boolean | ThemeTransitionOptions) => Promise<void>
  cycle: (themeArray?: string[], transition?: boolean | ThemeTransitionOptions) => Promise<void>
  toggle: (themeArray?: [string, string], transition?: boolean | ThemeTransitionOptions) => Promise<void>
  setTransitionOrigin: (e: PointerEvent | Element | null) => void

  readonly isDisabled: boolean
  readonly isSystem: Readonly<Ref<boolean>>
  readonly themes: Ref<Record<string, InternalThemeDefinition>>

  readonly name: Readonly<Ref<string>>
  readonly current: DeepReadonly<Ref<InternalThemeDefinition>>
  readonly computedThemes: DeepReadonly<Ref<Record<string, InternalThemeDefinition>>>
  readonly prefix: string

  readonly themeClasses: Readonly<Ref<string | undefined>>
  readonly styles: Readonly<Ref<string>>

  readonly global: {
    readonly name: Ref<string>
    readonly current: DeepReadonly<Ref<InternalThemeDefinition>>
  }
}

export const ThemeSymbol: InjectionKey<ThemeInstance> = Symbol.for('vuetify:theme')

export const makeThemeProps = propsFactory({
  theme: String,
}, 'theme')

function genDefaults () {
  return {
    defaultTheme: 'system',
    prefix: 'v-',
    variations: { colors: [], lighten: 0, darken: 0 },
    themes: {
      light: {
        dark: false,
        colors: {
          background: '#FFFFFF',
          surface: '#FFFFFF',
          'surface-bright': '#FFFFFF',
          'surface-light': '#EEEEEE',
          'surface-variant': '#424242',
          'on-surface-variant': '#EEEEEE',
          primary: '#1867C0',
          'primary-darken-1': '#1F5592',
          secondary: '#48A9A6',
          'secondary-darken-1': '#018786',
          error: '#B00020',
          info: '#2196F3',
          success: '#4CAF50',
          warning: '#FB8C00',
        },
        variables: {
          'border-color': '#000000',
          'border-opacity': 0.12,
          'shadow-color': '#000000',
          'high-emphasis-opacity': 0.87,
          'medium-emphasis-opacity': 0.60,
          'disabled-opacity': 0.38,
          'idle-opacity': 0.04,
          'hover-opacity': 0.04,
          'focus-opacity': 0.12,
          'selected-opacity': 0.08,
          'activated-opacity': 0.12,
          'pressed-opacity': 0.12,
          'dragged-opacity': 0.08,
          'theme-kbd': '#EEEEEE',
          'theme-on-kbd': '#000000',
          'theme-code': '#F5F5F5',
          'theme-on-code': '#000000',
          'theme-on-dark': '#FFF',
          'theme-on-light': '#000',
          'elevation-overlay-color': 'black',
          'elevation-overlay-opacity-step': '2%',
          'highlight-opacity': '8%',
        },
      },
      dark: {
        dark: true,
        colors: {
          background: '#121212',
          surface: '#212121',
          'surface-bright': '#ccbfd6',
          'surface-light': '#424242',
          'surface-variant': '#c8c8c8',
          'on-surface-variant': '#000000',
          primary: '#2196F3',
          'primary-darken-1': '#277CC1',
          secondary: '#54B6B2',
          'secondary-darken-1': '#48A9A6',
          error: '#CF6679',
          info: '#2196F3',
          success: '#4CAF50',
          warning: '#FB8C00',
        },
        variables: {
          'border-color': '#FFFFFF',
          'border-opacity': 0.12,
          'shadow-color': '#000000',
          'high-emphasis-opacity': 1,
          'medium-emphasis-opacity': 0.70,
          'disabled-opacity': 0.50,
          'idle-opacity': 0.10,
          'hover-opacity': 0.04,
          'focus-opacity': 0.12,
          'selected-opacity': 0.08,
          'activated-opacity': 0.12,
          'pressed-opacity': 0.16,
          'dragged-opacity': 0.08,
          'theme-kbd': '#424242',
          'theme-on-kbd': '#FFFFFF',
          'theme-code': '#343434',
          'theme-on-code': '#CCCCCC',
          'theme-on-dark': '#FFF',
          'theme-on-light': '#000',
          'elevation-overlay-color': 'white',
          'elevation-overlay-opacity-step': '2%',
          'highlight-opacity': '20%',
        },
      },
    },
    stylesheetId: 'vuetify-theme-stylesheet',
    scoped: false,
    utilities: true,
    transition: false,
  }
}

function parseThemeOptions (options: ThemeOptions = genDefaults()): InternalThemeOptions {
  const defaults = genDefaults()

  if (!options) return { ...defaults, isDisabled: true } as any

  return mergeDeep(defaults, options) as InternalThemeOptions
}

function createCssClass (lines: string[], selector: string, content: string[], scope?: string) {
  lines.push(
    `${getScopedSelector(selector, scope)} {\n`,
    ...content.map(line => `  ${line};\n`),
    '}\n',
  )
}

function genCssVariables (theme: InternalThemeDefinition, prefix: string) {
  const lightOverlay = theme.dark ? 2 : 1
  const darkOverlay = theme.dark ? 1 : 2

  const variables: string[] = []
  for (const [key, value] of Object.entries(theme.colors)) {
    const rgb = parseColor(value)
    variables.push(`--${prefix}theme-${key}: ${rgb.r},${rgb.g},${rgb.b}` + (rgb.a == null ? '' : `,${rgb.a}`))
    if (!key.startsWith('on-')) {
      variables.push(`--${prefix}theme-${key}-overlay-multiplier: ${getLuma(value) > 0.18 ? lightOverlay : darkOverlay}`)
    }
  }

  for (const [key, value] of Object.entries(theme.variables)) {
    const color = typeof value === 'string' && value.startsWith('#') ? parseColor(value) : undefined
    const rgb = color ? `${color.r}, ${color.g}, ${color.b}` + (color.a == null ? '' : `, ${color.a}`) : undefined
    variables.push(`--${prefix}${key}: ${rgb ?? value}`)
  }

  return variables
}

function genVariation (name: string, color: Color, variations: VariationsOptions | false) {
  const object: Record<string, string> = {}
  if (variations) {
    for (const variation of (['lighten', 'darken'] as const)) {
      const fn = variation === 'lighten' ? lighten : darken
      for (const amount of createRange(variations[variation], 1)) {
        object[`${name}-${variation}-${amount}`] = RGBtoHex(fn(parseColor(color), amount))
      }
    }
  }
  return object
}

function genVariations (colors: InternalThemeDefinition['colors'], variations: VariationsOptions | false) {
  if (!variations) return {}

  let variationColors = {}
  for (const name of variations.colors) {
    const color = colors[name]

    if (!color) continue

    variationColors = {
      ...variationColors,
      ...genVariation(name, color, variations),
    }
  }
  return variationColors
}

function genOnColors (colors: InternalThemeDefinition['colors'], variables: InternalThemeDefinition['variables']) {
  const onColors = {} as InternalThemeDefinition['colors']

  for (const color of Object.keys(colors)) {
    if (color.startsWith('on-') || colors[`on-${color}`]) continue

    const onColor = `on-${color}` as keyof OnColors
    const colorVal = parseColor(colors[color])

    onColors[onColor] = hasLightForeground(colorVal)
      ? variables['theme-on-dark']
      : variables['theme-on-light']
  }

  return onColors
}

function getScopedSelector (selector: string, scope?: string) {
  if (!scope) return selector

  const scopeSelector = `:where(${scope})`

  return selector === ':root' ? scopeSelector : `${scopeSelector} ${selector}`
}

function upsertStyles (id: string, cspNonce: string | undefined, styles: string) {
  const styleEl = getOrCreateStyleElement(id, cspNonce)

  if (!styleEl) return

  styleEl.innerHTML = styles
}

function getOrCreateStyleElement (id: string, cspNonce?: string) {
  if (!IN_BROWSER) return null

  let style = document.getElementById(id) as HTMLStyleElement | null

  if (!style) {
    style = document.createElement('style')
    style.id = id
    style.type = 'text/css'

    if (cspNonce) style.setAttribute('nonce', cspNonce)

    document.head.appendChild(style)
  }

  return style
}

// Composables
export function createTheme (options?: ThemeOptions): ThemeInstance & { install: (app: App) => void } {
  const parsedOptions = parseThemeOptions(options)
  const _name = shallowRef(parsedOptions.defaultTheme)
  const themes = ref(parsedOptions.themes)
  const systemName = shallowRef('light')

  const name = computed({
    get () {
      return _name.value === 'system' ? systemName.value : _name.value
    },
    set (val: string) {
      _name.value = val
    },
  })

  const computedThemes = computed(() => {
    const acc: Record<string, InternalThemeDefinition> = {}
    for (const [name, original] of Object.entries(themes.value)) {
      const defaultTheme = original.dark || name === 'dark'
        ? themes.value.dark
        : themes.value.light

      const merged = mergeDeep(defaultTheme, original) as InternalThemeDefinition

      const colors = {
        ...merged.colors,
        ...genVariations(merged.colors, parsedOptions.variations),
      }

      acc[name] = {
        ...merged,
        colors: {
          ...colors,
          ...genOnColors(colors, merged.variables),
        },
      }
    }
    return acc
  })

  const current = toRef(() => computedThemes.value[name.value])

  const isSystem = toRef(() => _name.value === 'system')

  const styles = computed(() => {
    const lines: string[] = []
    const scoped = parsedOptions.scoped ? parsedOptions.prefix : ''

    lines.push('@layer theme-base {\n')

    createCssClass(lines, ':root', [`color-scheme: ${current.value?.dark ? 'dark' : 'light'}`], parsedOptions.scope)

    createCssClass(lines, ':root', genCssVariables(current.value, parsedOptions.prefix), parsedOptions.scope)

    for (const [themeName, theme] of Object.entries(computedThemes.value)) {
      createCssClass(lines, `.${parsedOptions.prefix}theme--${themeName}`, [
        `color-scheme: ${theme.dark ? 'dark' : 'light'}`,
        ...genCssVariables(theme, parsedOptions.prefix),
      ], parsedOptions.scope)
    }

    lines.push('}\n')

    if (parsedOptions.utilities) {
      const bgLines: string[] = []
      const fgLines: string[] = []

      const colors = new Set(Object.values(computedThemes.value).flatMap(theme => Object.keys(theme.colors)))
      for (const key of colors) {
        if (key.startsWith('on-')) {
          createCssClass(fgLines, `.${key}`, [`color: rgb(var(--${parsedOptions.prefix}theme-${key}))`], parsedOptions.scope)
        } else {
          createCssClass(bgLines, `.${scoped}bg-${key}`, [
            `--${parsedOptions.prefix}theme-overlay-multiplier: var(--${parsedOptions.prefix}theme-${key}-overlay-multiplier)`,
            `background-color: rgb(var(--${parsedOptions.prefix}theme-${key}))`,
            `color: rgb(var(--${parsedOptions.prefix}theme-on-${key}))`,
          ], parsedOptions.scope)
          createCssClass(fgLines, `.${scoped}text-${key}`, [`color: rgb(var(--${parsedOptions.prefix}theme-${key}))`], parsedOptions.scope)
          createCssClass(fgLines, `.${scoped}border-${key}`, [`--${parsedOptions.prefix}border-color: var(--${parsedOptions.prefix}theme-${key})`], parsedOptions.scope)
        }
      }

      lines.push(
        '@layer theme-background {\n',
        ...bgLines.map(v => `  ${v}`),
        '}\n',
        '@layer theme-foreground {\n',
        ...fgLines.map(v => `  ${v}`),
        '}\n',
      )
    }

    return '@layer vuetify-utilities {\n' + lines.map(v => `  ${v}`).join('') + '\n}'
  })

  const themeClasses = toRef(() => parsedOptions.isDisabled ? undefined : `${parsedOptions.prefix}theme--${name.value}`)
  const themeNames = toRef(() => Object.keys(computedThemes.value))

  if (SUPPORTS_MATCH_MEDIA) {
    const media = window.matchMedia('(prefers-color-scheme: dark)')

    function updateSystemName () {
      systemName.value = media.matches ? 'dark' : 'light'
    }

    updateSystemName()

    media.addEventListener('change', updateSystemName, { passive: true })

    if (getCurrentScope()) {
      onScopeDispose(() => {
        media.removeEventListener('change', updateSystemName)
      })
    }
  }

  function install (app: App) {
    if (parsedOptions.isDisabled) return

    const head = app._context.provides.usehead as HeadClient & VueHeadClient<any> | undefined
    if (head) {
      function getHead () {
        return {
          style: [{
            textContent: styles.value,
            id: parsedOptions.stylesheetId,
            nonce: parsedOptions.cspNonce || false as never,
            tagPosition: 'bodyOpen' as const,
          }],
        }
      }

      if (head.push) {
        const entry = head.push(getHead)
        if (IN_BROWSER) {
          watch(styles, () => { entry.patch(getHead) })
        }
      } else {
        if (IN_BROWSER) {
          head.addHeadObjs(toRef(getHead))
          watchEffect(() => head.updateDOM())
        } else {
          head.addHeadObjs(getHead())
        }
      }
    } else {
      if (IN_BROWSER) {
        watch(styles, updateStyles, { immediate: true })
      } else {
        updateStyles()
      }

      function updateStyles () {
        upsertStyles(parsedOptions.stylesheetId, parsedOptions.cspNonce, styles.value)
      }
    }
  }

  let _transitionOrigin: string | null
  function setTransitionOrigin (e: PointerEvent | Element | null) {
    if (!e) {
      _transitionOrigin = null
      return
    }

    let x: number
    let y: number

    if (e instanceof Element) {
      const box = new Box(e)
      x = box.left + box.width / 2
      y = box.top + box.height / 2
    } else {
      x = e.clientX
      y = e.clientY
    }

    const originX = Math.min(98.9, 100 * x / window.innerWidth) // clip to avoid glitches
    const originY = 100 * y / window.innerHeight
    _transitionOrigin = `${originX.toFixed(2)}% ${originY.toFixed(2)}%`
  }

  function resolveTransitionOptions (
    transition?: boolean | ThemeTransitionOptions
  ): ThemeTransitionOptions | false {
    const opt = transition ?? parsedOptions.transition
    if (!opt && !_transitionOrigin) return false
    const global = typeof parsedOptions.transition === 'object' ? parsedOptions.transition : {}
    const local = typeof transition === 'object' ? transition : {}
    return {
      origin: local.origin ?? _transitionOrigin ?? global.origin,
      duration: local.duration ?? global.duration,
    }
  }

  function withPageTransition (callback: () => void, options: ThemeTransitionOptions): Promise<void> {
    if (!IN_BROWSER || !document.startViewTransition || PREFERS_REDUCED_MOTION()) {
      callback()
      return Promise.resolve()
    }

    const origin = options.origin ?? '50% 0%'
    const duration = options.duration ?? '500ms'

    const style = document.createElement('style')
    style.textContent =
      `html:active-view-transition-type(vuetify-theme)::view-transition-old(root) { animation: none; }` +
      `html:active-view-transition-type(vuetify-theme)::view-transition-new(root) { animation: v-circle-in ${duration} ease-in-out; }` +
      `@keyframes v-circle-in {` +
      `  from { clip-path: circle(0% at ${origin}); }` +
      `  to { clip-path: circle(150% at ${origin}); }` +
      `}`
    document.head.appendChild(style)

    const transition = document.startViewTransition({ update: () => callback(), types: ['vuetify-theme'] })
    transition.finished.then(() => {
      _transitionOrigin = null
      style.remove()
    })

    return transition.updateCallbackDone
  }

  function change (themeName: string, transition?: boolean | ThemeTransitionOptions): Promise<void> {
    if (themeName !== 'system' && !themeNames.value.includes(themeName)) {
      consoleWarn(`Theme "${themeName}" not found on the Vuetify theme instance`)
      return Promise.resolve()
    }

    const apply = () => { name.value = themeName }
    const transitionOptions = resolveTransitionOptions(transition)

    if (transitionOptions) {
      return withPageTransition(apply, transitionOptions)
    }

    apply()
    return Promise.resolve()
  }

  function cycle (themeArray: string[] = themeNames.value, transition?: boolean | ThemeTransitionOptions): Promise<void> {
    const currentIndex = themeArray.indexOf(name.value)
    const nextIndex = currentIndex === -1 ? 0 : (currentIndex + 1) % themeArray.length

    return change(themeArray[nextIndex], transition)
  }

  function toggle (themeArray: [string, string] = ['light', 'dark'], transition?: boolean | ThemeTransitionOptions): Promise<void> {
    return cycle(themeArray, transition)
  }

  const globalName = new Proxy(name, {
    get (target, prop) {
      return Reflect.get(target, prop)
    },
    set (target, prop, val) {
      if (prop === 'value') {
        deprecate(`theme.global.name.value = ${val}`, `theme.change('${val}')`)
      }
      return Reflect.set(target, prop, val)
    },
  })

  return {
    install,
    change,
    cycle,
    toggle,
    setTransitionOrigin,
    isDisabled: parsedOptions.isDisabled,
    isSystem,
    name,
    themes,
    current,
    computedThemes,
    prefix: parsedOptions.prefix,
    themeClasses,
    styles,
    global: {
      name: globalName,
      current,
    },
  }
}

export function provideTheme (props: { theme?: string }) {
  getCurrentInstance('provideTheme')

  const theme = inject(ThemeSymbol, null)

  if (!theme) throw new Error('Could not find Vuetify theme injection')

  const name = toRef(() => props.theme ?? theme.name.value)
  const current = toRef(() => theme.computedThemes.value[name.value])

  const themeClasses = toRef(() => theme.isDisabled ? undefined : `${theme.prefix}theme--${name.value}`)

  const newTheme: ThemeInstance = {
    ...theme,
    name,
    current,
    themeClasses,
  }

  provide(ThemeSymbol, newTheme)

  return newTheme
}

export function useTheme () {
  getCurrentInstance('useTheme')

  const theme = inject(ThemeSymbol, null)

  if (!theme) throw new Error('Could not find Vuetify theme injection')

  return theme
}
