import extractClassNames from './extractClassNames'
import Hook from './hook'
import dlv from 'dlv'
import dset from 'dset'
import resolveFrom from 'resolve-from'
import importFrom from 'import-from'
import chokidar from 'chokidar'
import semver from 'semver'
import invariant from 'tiny-invariant'
import getPlugins from './getPlugins'
import getVariants from './getVariants'
import resolveConfig from './resolveConfig'
import * as path from 'path'
import * as fs from 'fs'
import { getUtilityConfigMap } from './getUtilityConfigMap'
import glob from 'fast-glob'
import normalizePath from 'normalize-path'
import execa from 'execa'

function arraysEqual(arr1, arr2) {
  return (
    JSON.stringify(arr1.concat([]).sort()) ===
    JSON.stringify(arr2.concat([]).sort())
  )
}

const CONFIG_GLOB =
  '**/{tailwind,tailwind.config,tailwind-config,.tailwindrc}.js'

export default async function getClassNames(
  cwd = process.cwd(),
  { onChange = () => {} } = {}
) {
  async function run() {
    let postcss
    let tailwindcss
    let version
    let featureFlags = { future: [], experimental: [] }

    const configPaths = (
      await glob(CONFIG_GLOB, {
        cwd,
        ignore: ['**/node_modules'],
        onlyFiles: true,
        absolute: true,
        suppressErrors: true,
      })
    )
      .map(normalizePath)
      .sort((a, b) => a.split('/').length - b.split('/').length)
      .map(path.normalize)

    invariant(configPaths.length > 0, 'No Tailwind CSS config found.')
    const configPath = configPaths[0]
    console.log(`Found Tailwind config file: ${configPath}`)
    const configDir = path.dirname(configPath)
    const tailwindBase = path.dirname(
      resolveFrom(configDir, 'tailwindcss/package.json')
    )
    postcss = importFrom(tailwindBase, 'postcss')
    tailwindcss = importFrom(configDir, 'tailwindcss')
    version = importFrom(configDir, 'tailwindcss/package.json').version
    console.log(`Found tailwindcss v${version}: ${tailwindBase}`)

    try {
      featureFlags = importFrom(tailwindBase, './lib/featureFlags.js').default
    } catch (_) {}

    const sepLocation = semver.gte(version, '0.99.0')
      ? ['separator']
      : ['options', 'separator']
    let userSeperator
    let userPurge
    let hook = Hook(fs.realpathSync(configPath), (exports) => {
      userSeperator = dlv(exports, sepLocation)
      userPurge = exports.purge
      dset(exports, sepLocation, '__TAILWIND_SEPARATOR__')
      exports.purge = {}
      return exports
    })

    hook.watch()
    let config
    try {
      config = __non_webpack_require__(configPath)
    } catch (error) {
      hook.unwatch()
      hook.unhook()
      throw error
    }

    hook.unwatch()

    let postcssResult

    try {
      postcssResult = await Promise.all(
        [
          semver.gte(version, '0.99.0') ? 'base' : 'preflight',
          'components',
          'utilities',
        ].map((group) =>
          postcss([tailwindcss(configPath)]).process(`@tailwind ${group};`, {
            from: undefined,
          })
        )
      )
    } catch (error) {
      throw error
    } finally {
      hook.unhook()
    }

    const [base, components, utilities] = postcssResult

    if (typeof userSeperator !== 'undefined') {
      dset(config, sepLocation, userSeperator)
    } else {
      delete config[sepLocation]
    }
    if (typeof userPurge !== 'undefined') {
      config.purge = userPurge
    } else {
      delete config.purge
    }

    const resolvedConfig = resolveConfig({ cwd: configDir, config })

    let browserslist = []
    try {
      const { stdout, stderr } = await execa('browserslist', [], {
        preferLocal: true,
        localDir: configDir,
        cwd: configDir,
      })
      if (stderr) {
        throw Error(stderr)
      }
      browserslist = stdout.split('\n')
    } catch (error) {
      console.error('Failed to load browserslist:', error)
    }

    return {
      version,
      configPath,
      config: resolvedConfig,
      separator: typeof userSeperator === 'undefined' ? ':' : userSeperator,
      classNames: await extractClassNames([
        { root: base.root, source: 'base' },
        { root: components.root, source: 'components' },
        { root: utilities.root, source: 'utilities' },
      ]),
      dependencies: hook.deps,
      plugins: getPlugins(config),
      variants: getVariants({ config, version, postcss, browserslist }),
      utilityConfigMap: await getUtilityConfigMap({
        cwd: configDir,
        resolvedConfig,
        postcss,
        browserslist,
      }),
      modules: {
        tailwindcss,
        postcss,
      },
      featureFlags,
    }
  }

  let watcher
  function watch(files = []) {
    unwatch()
    watcher = chokidar
      .watch(files, { cwd })
      .on('change', handleChange)
      .on('unlink', handleChange)
  }
  function unwatch() {
    if (watcher) {
      watcher.close()
    }
  }

  async function handleChange() {
    const prevDeps = result ? [result.configPath, ...result.dependencies] : []
    try {
      result = await run()
    } catch (error) {
      onChange({ error })
      return
    }
    const newDeps = [result.configPath, ...result.dependencies]
    if (!arraysEqual(prevDeps, newDeps)) {
      watch(newDeps)
    }
    onChange(result)
  }

  let result
  try {
    result = await run()
    console.log('Initialised successfully.')
  } catch (error) {
    console.error('Failed to initialise:', error)
    return null
  }

  watch([result.configPath, ...result.dependencies])

  return result
}