import fs from 'node:fs'
import { colord, extend } from 'colord'
import mixPlugin from 'colord/plugins/mix'

import json from './variables.json' assert { type: 'json' }
import {
  FALLBACK_THEME,
  getThemeForToken,
  ignoreStyles,
  kebabify,
} from './utils'

extend([mixPlugin])

type TokenName = string
type TokenValue = { alias?: string; resolved: string | number }
type ThemeTokenValues = Map<TokenName, TokenValue>
type ThemeKey = string

// Holds a lookup table of all the themes (signature hotels, etc.)
const themes = new Map<ThemeKey, ThemeTokenValues>()

// Set the fallback theme that holds all properties that are used by all themes
themes.set(FALLBACK_THEME, new Map())

// Collect all real themes
json.collections
  .find((collection) => {
    return collection.name === 'Color Theming'
  })
  ?.modes.forEach((mode) => {
    themes.set(mode.name, new Map())
  })

// Some tokens aliases values that have a responsive nature.
// The responsiveness cannot be encoded into this distribution as we do not
// distribute CSS selectors, only CSS properties and JSON objects.
// We mark these properties with a prefix of "Impl-" in order to convey that
// their responsiveness must be implemented in their target environment.
// This also means that we must revisit this in order to support React Native.
const responsiveTokens = new Set()

// Collect responsive tokens
json.collections
  .find((collection) => collection.name === 'Responsive')
  ?.modes.forEach((mode) => {
    mode.variables.forEach((variable) => {
      responsiveTokens.add(variable.name)
    })
  })

// Collect non-alias tokens
json.collections.forEach((collection) => {
  collection.modes.filter(ignoreStyles).forEach((mode) => {
    mode.variables.forEach((variable) => {
      if (!variable.isAlias) {
        let token = variable.name
        let value = variable.value

        const tokenTheme = getThemeForToken(token, mode, themes)

        switch (variable.type) {
          case 'boolean':
            if (typeof variable.value === 'boolean') {
              // Handle text transform
              if (/text transform/i.test(variable.name)) {
                token = variable.name.replace(
                  /text transform/i,
                  'Text-Transform'
                )
                value = variable.value ? 'uppercase' : 'unset'
              }
              // Handle text decoration
              else if (/text decoration/i.test(variable.name)) {
                token = variable.name.replace(
                  /text decoration/i,
                  'Text-Decoration'
                )
                value = variable.value ? 'underline' : 'unset'
              }
            } else {
              throw new Error(
                `Bad variable value input type. Expected 'boolean' got '${variable.type}'`
              )
            }
            break
          case 'number':
            if (typeof variable.value === 'number') {
              // Only use two decimals for all numbers
              value = Number(variable.value.toFixed(2))
            } else {
              throw new Error(
                `Bad variable value input type. Expected 'number' got '${variable.type}'`
              )
            }
            break
        }

        if (typeof value === 'string' || typeof value === 'number') {
          const theme = themes.get(tokenTheme)
          if (theme) {
            theme.set(token, {
              // primitives will not have aliases
              resolved: value,
            })
          }
        } else {
          throw new Error(
            `Unsupported value type: ${typeof value} (${token}: ${value})`
          )
        }
      }
    })
  })
})

// Collect alias tokens
json.collections.forEach((collection) => {
  collection.modes.filter(ignoreStyles).forEach((mode) => {
    mode.variables.forEach((variable) => {
      if (variable.isAlias) {
        const token = variable.name
        let value = variable.value

        const tokenTheme = getThemeForToken(token, mode, themes)

        switch (variable.type) {
          case 'color':
          case 'string':
          case 'number':
            // Some values are defined as objects, so we need to dig out the real value
            if (typeof value === 'object' && 'name' in value) {
              value = value.name
            }

            // Some values are alises to tokens that have a responsive nature.
            // TODO: Revisit for RN support
            if (responsiveTokens.has(value)) {
              value = `Impl-${value}`
            }

            if (typeof value === 'string') {
              const theme = themes.get(tokenTheme)
              if (theme) {
                theme.set(token, {
                  // We can only resolve aliases after we have collected all
                  // primitives and aliases.
                  resolved: '',
                  alias: value,
                })
              }
            }
            break
          default:
            console.warn(
              `Unsupported variable type: ${variable.type} (${variable.name}: ${variable.value})`
            )
        }
      }
    })
  })
})

// Collect effects tokens
json.collections.forEach((collection) => {
  collection.modes.forEach((mode) => {
    mode.variables.forEach((variable) => {
      if (variable.type === 'effect') {
        if (typeof variable.value === 'object' && 'effects' in variable.value) {
          if (variable.value.effects.length > 1) {
            console.warn(
              `Unsupported effect declaration with multiple effects.`,
              variable.value.effects
            )
          }
          // We only support one effect declaration per variable
          const effect = variable.value.effects[0]
          switch (effect.type) {
            case 'DROP_SHADOW': {
              const { r, g, b, a } = effect.color
              const { x, y } = effect.offset
              const value = `${x}px ${y}px ${effect.radius}px ${effect.spread}px rgba(${r}, ${g}, ${b}, ${a})`
              const token = `BoxShadow-${variable.name}`

              const theme = themes.get(FALLBACK_THEME)
              if (theme) {
                theme.set(token, {
                  // primitives will not have aliases
                  resolved: value,
                })
              }
              break
            }
          }
        }
      }
    })
  })
})

// Resolve all aliases to primitives
themes.forEach((themeTokenValues) => {
  themeTokenValues.forEach((value, token) => {
    // Default the resolved value to the original value. Meaning if no value
    // could be resolved the token value does not change.
    let resolveValue = value

    // For each loop the while loop checks if the resolved value is an alias
    // without a resolved value. If so it continues to resolve the value until a
    // primitive value is found.
    while (resolveValue.alias && !resolveValue.resolved) {
      // Used for breaking the while loop. The while loop continues to
      // recursively resolve alias that have aliases as their value.
      // If we have loop through all the themes and not found any primitive
      // value, then we stop looking by breaking the while loop.
      let found = false

      Array.from(themes).some(([, innerTheme]) => {
        return Array.from(innerTheme).some(([innerToken, innerValue]) => {
          if (innerToken === value.alias && innerValue.resolved) {
            resolveValue = { ...value, resolved: innerValue.resolved }
            found = true
            return true
          }
        })
      })

      // We have search through all the themes and nothing matched, stop looking
      if (!found) {
        break
      }
    }

    // Update the token value with the result of the above.
    // Either nothing matches and the original is set, meaning nothing changed.
    // Or we did get a match and the new value is set.
    themeTokenValues.set(token, resolveValue)
  })
})

// Process hover alpha colors
// All */Hover* styles will blend with */Default
themes.forEach((themeTokenValue) => {
  themeTokenValue.forEach((value, token) => {
    if (token.toLowerCase().indexOf('hover') >= 0) {
      const hoverValue = value.resolved

      if (typeof hoverValue === 'string') {
        if (hoverValue.startsWith('#') && hoverValue.length === 9) {
          // The color that the hover color with alpha mixes with.
          const baseToken = token
            .split('/')
            .map((tokenPart) => {
              if (tokenPart.toLowerCase().indexOf('hover') >= 0) {
                return 'Default'
              }
              return tokenPart
            })
            .join('/')

          const baseValue =
            themeTokenValue.get(baseToken) ??
            themes.get(FALLBACK_THEME)?.get(baseToken)
          if (baseValue) {
            if (typeof baseValue.resolved === 'string' && baseValue.resolved) {
              const baseColor = colord(baseValue.resolved)
              const hoverColor = colord(hoverValue)

              if (baseColor.alpha() > 0 && hoverColor.alpha() > 0) {
                const computedHoverValue = baseColor
                  .mix(hoverColor.alpha(1), hoverColor.alpha())
                  .toHex()

                themeTokenValue.set(token, {
                  // no alias for the computed hover color
                  resolved: computedHoverValue,
                })
              }
            } else {
              throw new Error(
                `Base token ${baseToken} is unresolved: ${baseValue}`
              )
            }
          } else {
            throw new Error(
              `Unable to find base token (${baseToken}) for hover token ${token}`
            )
          }
        }
      }
    }
  })
})

// Prepare for output
const variablesJsOutput = [
  '/* This file is generated, do not edit manually! */',
]
themes.forEach((themeTokenValues, themeKey) => {
  const cssContentPrimitives: string[] = []
  const cssContentAliases: string[] = []

  themeTokenValues.forEach((value, token) => {
    const outputToken = kebabify(token)
    let outputValue = value.alias ? value.alias : value.resolved

    if (value.alias) {
      cssContentAliases.push(
        `  --${outputToken}: var(--${kebabify(outputValue.toString())});`
      )
    } else {
      if (typeof outputValue === 'string') {
        // Check for properties that need quotes
        if (/font(-| )family/gi.test(token)) {
          outputValue = `"${outputValue}"`
        }
      } else if (typeof outputValue === 'number') {
        // font-weight is unitless
        if (!/font(-| )weight/gi.test(token)) {
          outputValue = `${outputValue}px`
        }
      } else {
        throw new Error(
          `Unsupported value type: ${typeof value} (${token}: ${value})`
        )
      }

      cssContentPrimitives.push(`  --${outputToken}: ${outputValue};`)
    }
  })

  const filename = kebabify(themeKey).toLowerCase()

  const cssOutput = [
    '/* This file is generated, do not edit manually! */',
    // The base styles target the :root.
    // All themes require the use of their scoping classname to be used.
    themeKey === FALLBACK_THEME ? ':root {' : `.${filename} {`,
    '  /* Values */',
    ...cssContentPrimitives.sort(),
    '',
    '  /* Aliases */',
    ...cssContentAliases.sort(),
    '}',
    '', // New line at end of file
  ]

  fs.writeFileSync(`../lib/styles/${filename}.css`, cssOutput.join('\n'), {
    encoding: 'utf-8',
  })

  const resolvedJsOutput = [
    '/* This file is generated, do not edit manually! */',
    `export const theme = ${JSON.stringify(
      Array.from(themeTokenValues).reduce((acc, [token, value]) => {
        if (value.resolved) {
          acc[token] = value.resolved
        }
        return acc
      }, {}),
      null,
      2
    )}`,
  ]
  fs.writeFileSync(
    `../lib/styles/${filename}.js`,
    resolvedJsOutput.join('\n'),
    {
      encoding: 'utf-8',
    }
  )

  // This output is only meant for the `Variables` component.
  // It is mainly used for the Storybook to show all the variables.
  variablesJsOutput.push(
    `export const ${themeKey
      .split(' ')
      .map((v, i) => (i === 0 ? v.toLowerCase() : v))
      .join(
        ''
      )} = ${JSON.stringify(Object.fromEntries(themeTokenValues.entries()))} as const`
  )
})

fs.writeFileSync(`../lib/tokens/index.ts`, variablesJsOutput.join('\n'), {
  encoding: 'utf-8',
})
