import { isServer, isWeb } from '@tamagui/constants'
import { useRef, useSyncExternalStore } from 'react'
import { getSetting } from '../config'
import { resetMediaStyleCache } from '../helpers/createMediaStyle'
import { matchMedia } from '../helpers/matchMedia'
import { mediaObjectToString } from '../helpers/mediaObjectToString'
import {
  getMedia,
  mediaKeys,
  mediaQueryConfig,
  setMediaState,
} from '../helpers/mediaState'
import type {
  ComponentContextI,
  DebugProp,
  GetStyleState,
  IsMediaType,
  MediaQueryState,
  TamaguiInternalConfig,
  UseMediaState,
  WidthHeight,
} from '../types'
import { defaultMediaImportance } from '../helpers/pseudoDescriptors'

const mediaKeyRegex = /\$(platform|theme|group)-/

export const isMediaKey = (key: string): boolean => {
  if (key[0] !== '$') return false
  if (mediaKeys.has(key)) return true
  if (mediaKeyRegex.test(key)) return true
  return false
}

export const getMediaKey = (key: string): IsMediaType => {
  if (key[0] !== '$') return false
  if (mediaKeys.has(key)) return true
  const match = key.match(mediaKeyRegex)
  if (match) return match[1] as 'platform' | 'theme' | 'group'
  return false
}

// for SSR capture it at time of startup
let initState: MediaQueryState

let mediaKeysOrdered: string[]

export const getMediaKeyImportance = (key: string) => {
  if (process.env.NODE_ENV === 'development' && key[0] === '$') {
    throw new Error('use short key')
  }

  // + 100 because we set base usedKeys=1, pseudos are 2-N (however many we have)
  // all media go above all pseudos so we need to pad it based on that
  // right now theres 5 pseudos but in the future could be a few more
  return mediaKeysOrdered.indexOf(key) + 100
}

const dispose = new Set<Function>()

let mediaVersion = 0

export const configureMedia = (config: TamaguiInternalConfig) => {
  const { media } = config
  const mediaQueryDefaultActive = getSetting('mediaQueryDefaultActive')
  if (!media) return
  mediaVersion++
  // reset cached media style prefixes/selectors so they get recalculated with new key order
  resetMediaStyleCache()
  for (const key in media) {
    getMedia()[key] = mediaQueryDefaultActive?.[key] || false
    mediaKeys.add(`$${key}`)
  }
  Object.assign(mediaQueryConfig, media)
  initState = { ...getMedia() }
  mediaKeysOrdered = Object.keys(media)
  setupMediaListeners()
}

function unlisten() {
  dispose.forEach((cb) => cb())
  dispose.clear()
}

/**
 * Note: This should *not* set the state on the first render!
 * Because to avoid hydration issues SSR must match the server
 * *and then* re-render with the actual media query state.
 */
let setupVersion = -1
export function setupMediaListeners() {
  if (isWeb && isServer) return
  if (process.env.IS_STATIC) return

  // avoid setting up more than once per config
  if (setupVersion === mediaVersion) return
  setupVersion = mediaVersion

  // hmr, undo existing before re-binding
  unlisten()

  for (const key in mediaQueryConfig) {
    const str = mediaObjectToString(mediaQueryConfig[key])
    const getMatch = () => matchMedia(str)
    const match = getMatch()
    if (!match) {
      throw new Error('⚠️ No match')
    }

    // react native needs these deprecated apis for now
    match.addListener(update)
    dispose.add(() => {
      match.removeListener(update)
    })

    function update() {
      const next = !!getMatch().matches
      if (next === getMedia()[key]) return
      setMediaState({ ...getMedia(), [key]: next })
      updateMediaListeners()
    }

    update()
  }
}

const listeners = new Set<any>()

export function updateMediaListeners() {
  listeners.forEach((cb) => cb(getMedia()))
}

type MediaState = {
  enabled?: boolean
  keys?: Set<string> | null
}

const States = new WeakMap<any, MediaState>()

export function setMediaShouldUpdate(
  ref: any,
  enabled?: boolean,
  keys?: MediaState['keys']
) {
  const cur = States.get(ref)

  if (!cur || cur.enabled !== enabled || keys) {
    States.set(ref, {
      ...cur,
      enabled,
      keys,
    })
  }
}

function subscribe(subscriber: () => void) {
  listeners.add(subscriber)
  return () => {
    listeners.delete(subscriber)
  }
}

export function useMedia(
  componentContext?: ComponentContextI,
  debug?: DebugProp
): UseMediaState {
  'use no memo'

  const componentState = componentContext ? States.get(componentContext) : null

  const internalRef = useRef<{
    keys: Set<string>
    lastState: MediaQueryState
    pendingState?: MediaQueryState
  }>(null)
  if (!internalRef.current) {
    internalRef.current = {
      keys: new Set(),
      lastState: getMedia(),
    }
  }

  // reset on next render
  if (internalRef.current.pendingState) {
    internalRef.current.lastState = internalRef.current.pendingState
    internalRef.current.pendingState = undefined
  }

  const { keys } = internalRef.current

  // clear each render to track only rendered touched keys
  if (keys.size) {
    keys.clear()
  }

  const state = useSyncExternalStore(
    subscribe,
    () => {
      const curKeys = componentState?.keys || keys
      const { lastState, pendingState } = internalRef.current!

      if (!curKeys.size) {
        return lastState
      }

      const ms = getMedia()
      for (const key of curKeys) {
        if (ms[key] !== (pendingState || lastState)[key]) {
          if (process.env.NODE_ENV === 'development' && debug) {
            console.warn(`useMedia() ✍️`, key, lastState[key], '=>', ms[key])
          }

          // in emitter mode (no-rerender) avoid changing state, instead emit
          if (componentContext?.mediaEmit) {
            componentContext.mediaEmit(ms)
            internalRef.current!.pendingState = ms
            return lastState
          }

          internalRef.current!.lastState = ms

          return ms
        }
      }

      return lastState
    },
    getServerSnapshot
  )

  return new Proxy(state, {
    get(_, key) {
      if (!disableMediaTouch && typeof key === 'string') {
        keys.add(key)
      }
      return Reflect.get(state, key)
    },
  })
}

const getServerSnapshot = () => initState

let disableMediaTouch = false
export function _disableMediaTouch(val: boolean) {
  disableMediaTouch = val
}

export function getMediaState(mediaGroups: Set<string>, layout: WidthHeight) {
  disableMediaTouch = true
  let res: Record<string, boolean>
  try {
    res = Object.fromEntries(
      [...mediaGroups].map((mediaKey) => {
        return [mediaKey, mediaKeyMatch(mediaKey, layout as any)]
      })
    )
  } finally {
    disableMediaTouch = false
  }
  return res
}

export const getMediaImportanceIfMoreImportant = (
  mediaKey: string,
  key: string,
  styleState: GetStyleState,
  isSizeMedia: boolean
) => {
  const importance = isSizeMedia
    ? getMediaKeyImportance(mediaKey)
    : defaultMediaImportance
  const usedKeys = styleState.usedKeys
  return !usedKeys[key] || importance > usedKeys[key] ? importance : null
}

const cachedMediaKeyToQuery: Record<string, string> = {}

export function mediaKeyToQuery(key: string) {
  return (
    cachedMediaKeyToQuery[key] ||
    (cachedMediaKeyToQuery[key] = mediaObjectToString(mediaQueryConfig[key]))
  )
}

export function mediaKeyMatch(
  key: string,
  dimensions: { width: number; height: number }
) {
  const mediaQueries = mediaQueryConfig[key]
  const result = Object.keys(mediaQueries).every((query) => {
    const expectedVal = +mediaQueries[query]
    const isMax = query.startsWith('max')
    const isWidth = query.endsWith('Width')
    const givenVal = dimensions[isWidth ? 'width' : 'height']
    // if not max then min
    return isMax ? givenVal < expectedVal : givenVal > expectedVal
  })
  return result
}
