// https://github.com/emotion-js/emotion/commit/f3b268f7c52103979402da919c9c0dd3f9e0e189
// MIT License. Copyright (c) Emotion team and other contributors
import { basename, normalize, parse, sep } from "node:path"
import { type Position, SourceMapGenerator } from "source-map"
import convert from "convert-source-map"
import findRoot from "find-root"
import memoize from "@emotion/memoize"
import { serializeStyles } from "@emotion/serialize"
import hashString from "@emotion/hash"
import { addDefault, addNamed } from "@babel/helper-module-imports"
import escapeRegexp from "escape-string-regexp"
import { compile } from "stylis"
import { createMacro } from "babel-plugin-macros"
import type { CallExpression, Node } from "@babel/types"
import type {
  BabelFile,
  NodePath,
  ParserOptions,
  PluginObj,
  PluginPass,
  types,
} from "@babel/core"

type Babel = typeof import("@babel/core")
type Types = typeof types

interface PluginOptions {
  sourceMap?: boolean
  autoLabel?: "dev-only" | "always" | "never"
  labelFormat?: string
  cssPropOptimization?: boolean
  importMap?: { [key: string]: string }
}

interface PluginStateOptions {
  cssPropOptimization: boolean
  autoLabel: PluginOptions["autoLabel"]
  labelFormat: string
  sourceMap: boolean
}

interface PluginState extends PluginPass {
  emotionSourceMap: boolean
  transformCssProp: boolean
  jsxReactImport: { importSource: string; export: string; cssExport: string }
  opts: PluginStateOptions
  pluginMacros: {
    [key: string]: Macro
  }
}

// src/utils/label.js
const invalidClassNameCharacters = /[!"#$%&'()*+,./:;<=>?@[\]^`{|}~]/g
const sanitizeLabelPart = (labelPart: string) =>
  labelPart.trim().replace(invalidClassNameCharacters, "-")

function getLabel(
  identifierName: string | null,
  labelFormat: string | ((label: { name: string; path: string }) => string) | undefined,
  filename: string,
) {
  if (!identifierName) return null
  const sanitizedName = sanitizeLabelPart(identifierName)
  if (!labelFormat) {
    return sanitizedName
  }
  if (typeof labelFormat === "function") {
    return labelFormat({
      name: sanitizedName,
      path: filename,
    })
  }
  const parsedPath = parse(filename)
  const localDirname = basename(parsedPath.dir)
  let localFilename = parsedPath.name
  if (localFilename === "index") {
    localFilename = localDirname
  }
  return labelFormat
    .replace(/\[local]/gi, sanitizedName)
    .replace(/\[filename]/gi, sanitizeLabelPart(localFilename))
    .replace(/\[dirname]/gi, sanitizeLabelPart(localDirname))
}

function getLabelFromPath(path: NodePath<CallExpression>, state: PluginState, t: Types) {
  return getLabel(
    getIdentifierName(path, t),
    state.opts.labelFormat,
    state.file.opts.filename!,
  )
}

const getObjectPropertyLikeName = (path: NodePath<Node>, t: Types): string | null => {
  if ((!t.isObjectProperty(path) && !t.isObjectMethod(path)) || path.node.computed) {
    return null
  }
  if (t.isIdentifier(path.node.key)) {
    return path.node.key.name
  }
  if (t.isStringLiteral(path.node.key)) {
    return path.node.key.value.replace(/\s+/g, "-")
  }
  return null
}

function getDeclaratorName(path: NodePath<Node>, t: Types): string {
  const parent = path.findParent(
    p =>
      p.isVariableDeclarator() ||
      p.isAssignmentExpression() ||
      p.isFunctionDeclaration() ||
      p.isFunctionExpression() ||
      p.isArrowFunctionExpression() ||
      p.isObjectProperty() ||
      p.isObjectMethod(),
  )
  if (!parent) {
    return ""
  }
  if (parent.isVariableDeclarator()) {
    if (t.isIdentifier(parent.node.id)) {
      return parent.node.id.name
    }
    return ""
  }
  if (parent.isAssignmentExpression()) {
    const { left } = parent.node
    if (t.isIdentifier(left)) {
      return left.name
    }
    if (t.isMemberExpression(left)) {
      let memberExpression = left
      let name = ""
      while (true) {
        if (!t.isIdentifier(memberExpression.property)) {
          return ""
        }
        name = `${memberExpression.property.name}${name ? `-${name}` : ""}`
        if (t.isIdentifier(memberExpression.object)) {
          return `${memberExpression.object.name}-${name}`
        }
        if (!t.isMemberExpression(memberExpression.object)) {
          return ""
        }
        memberExpression = memberExpression.object
      }
    }
    return ""
  }
  if (parent.isFunctionDeclaration()) {
    return parent.node.id.name || ""
  }
  if (parent.isFunctionExpression()) {
    if (parent.node.id) {
      return parent.node.id.name || ""
    }
    return getDeclaratorName(parent, t)
  }
  if (parent.isArrowFunctionExpression()) {
    return getDeclaratorName(parent, t)
  }
  const objectPropertyLikeName = getObjectPropertyLikeName(parent, t)
  if (objectPropertyLikeName) {
    return objectPropertyLikeName
  }
  const variableDeclarator = parent.findParent(p => p.isVariableDeclarator())
  if (!variableDeclarator || !variableDeclarator.get("id").isIdentifier()) {
    return ""
  }
  return variableDeclarator.node.id.name
}

function getIdentifierName(path: NodePath<CallExpression>, t: Types): string {
  const objPropertyLikeName = getObjectPropertyLikeName(path.parentPath, t)
  if (objPropertyLikeName) {
    return objPropertyLikeName
  }
  const classOrClassPropertyParent = path.findParent(
    p => t.isClassProperty(p) || t.isClass(p),
  )
  if (classOrClassPropertyParent) {
    if (
      t.isClassProperty(classOrClassPropertyParent) &&
      classOrClassPropertyParent.node.computed === false &&
      t.isIdentifier(classOrClassPropertyParent.node.key)
    ) {
      return classOrClassPropertyParent.node.key.name
    }
    if (t.isClass(classOrClassPropertyParent) && classOrClassPropertyParent.node.id) {
      return t.isIdentifier(classOrClassPropertyParent.node.id)
        ? classOrClassPropertyParent.node.id.name
        : ""
    }
  }
  const declaratorName = getDeclaratorName(path, t)
  if (declaratorName.charAt(0) === "_") {
    return ""
  }
  return declaratorName
}

// lib/utils/source-maps.js
function getGeneratorOptions(file: BabelFile) {
  return file.opts.generatorOpts ? file.opts.generatorOpts : file.opts
}

function makeSourceMapGenerator(file: BabelFile) {
  const generatorOptions = getGeneratorOptions(file)
  const filename = generatorOptions.sourceFileName!
  const generator = new SourceMapGenerator({
    file: filename,
    sourceRoot: generatorOptions.sourceRoot!,
  })
  generator.setSourceContent(filename, file.code)
  return generator
}

function getSourceMap(offset: Position, state: PluginState) {
  const generator = makeSourceMapGenerator(state.file)
  const generatorOpts = getGeneratorOptions(state.file)
  if (generatorOpts.sourceFileName && generatorOpts.sourceFileName !== "unknown") {
    generator.addMapping({
      generated: {
        line: 1,
        column: 0,
      },
      source: generatorOpts.sourceFileName,
      original: offset,
    })

    return convert.fromObject(generator).toComment({
      multiline: true,
    })
  }
  return ""
}

// lib/utils/get-target-class-name.js

const hashArray = (array: string[]) => hashString(array.join(""))
const getPackageRootPath = memoize(filename => findRoot(filename))
const separator = new RegExp(escapeRegexp(sep), "g")
const normalizePath = (path: string) => normalize(path).replace(separator, "/")

function getTargetClassName(state, t: Types) {
  if (state.emotionTargetClassNameCount === void 0) {
    state.emotionTargetClassNameCount = 0
  }
  const hasFilepath = state.file.opts.filename && state.file.opts.filename !== "unknown"
  const filename = hasFilepath ? state.file.opts.filename : ""
  let moduleName = ""
  let rootPath = filename
  try {
    rootPath = getPackageRootPath(filename)
    moduleName = require(rootPath + "/package.json").name
  } catch {}
  const finalPath = filename === rootPath ? "root" : filename.slice(rootPath.length)
  const positionInFile = state.emotionTargetClassNameCount++
  const stuffToHash = [moduleName]
  if (finalPath) {
    stuffToHash.push(normalizePath(finalPath))
  } else {
    stuffToHash.push(state.file.code)
  }
  const stableClassName = `e${hashArray(stuffToHash)}${positionInFile}`
  return stableClassName
}

// src/utils/object-to-string.js
function simplifyObject(node, t: Types) {
  let finalString = ""
  for (let i = 0; i < node.properties.length; i++) {
    const property = node.properties[i]
    if (
      !t.isObjectProperty(property) ||
      property.computed ||
      (!t.isIdentifier(property.key) && !t.isStringLiteral(property.key)) ||
      (!t.isStringLiteral(property.value) &&
        !t.isNumericLiteral(property.value) &&
        !t.isObjectExpression(property.value))
    ) {
      return node
    }
    const key = property.key.name || property.key.value
    if (key === "styles") {
      return node
    }
    if (t.isObjectExpression(property.value)) {
      const simplifiedChild = simplifyObject(property.value, t)
      if (!t.isStringLiteral(simplifiedChild)) {
        return node
      }
      finalString += `${key}{${simplifiedChild.value}}`
      continue
    }
    const value = property.value.value
    finalString += serializeStyles([{ [key]: value }]).styles
  }
  return t.stringLiteral(finalString)
}

// src/utils/transform-expression-with-styles.js

// src/utils/minify.js
const haveSameLocation = (element1, element2) =>
  element1.line === element2.line && element1.column === element2.column

const isAutoInsertedRule = element =>
  element.type === "rule" && element.parent && haveSameLocation(element, element.parent)

const toInputTree = (elements, tree) => {
  for (const element of elements) {
    const { parent, children } = element
    if (!parent) {
      tree.push(element)
    } else if (!isAutoInsertedRule(element)) {
      parent.children.push(element)
    }
    if (Array.isArray(children)) {
      element.children = []
      toInputTree(children, tree)
    }
  }
  return tree
}

const stringifyTree = elements =>
  elements
    .map(element => {
      switch (element.type) {
        case "import":
        case "decl": {
          return element.value
        }
        case "comm": {
          return element.props === "/" && element.value.includes("@") ? element.value : ""
        }
        case "rule": {
          return `${element.value.replace(/&\f/g, "&")}{${stringifyTree(
            element.children,
          )}}`
        }
        default: {
          return `${element.value}{${stringifyTree(element.children)}}`
        }
      }
    })
    .join("")

const interleave = (strings: string[], interpolations: string[]) =>
  interpolations.reduce(
    (array, interp, i) => [...array, interp, ...strings[i + 1]],
    [strings[0]],
  )

function getDynamicMatches(str: string) {
  const re = /xxx(\d+):xxx/gm
  let match
  const matches = []
  while ((match = re.exec(str)) !== null) {
    if (match !== null) {
      matches.push({
        value: match[0],
        p1: Number.parseInt(match[1], 10),
        index: match.index,
      })
    }
  }
  return matches
}

function replacePlaceholdersWithExpressions(string: string, expressions, t: Types) {
  const matches = getDynamicMatches(string)
  if (matches.length === 0) {
    if (string === "") {
      return []
    }
    return [t.stringLiteral(string)]
  }
  const strings = []
  const finalExpressions = []
  let cursor = 0
  for (const [i, { value, p1, index }] of matches.entries()) {
    const preMatch = string.substring(cursor, index)
    cursor = cursor + preMatch.length + value.length
    if (!preMatch && i === 0) {
      strings.push(t.stringLiteral(""))
    } else {
      strings.push(t.stringLiteral(preMatch))
    }
    finalExpressions.push(expressions[p1])
    if (i === matches.length - 1) {
      strings.push(t.stringLiteral(string.slice(Math.max(0, index + value.length))))
    }
  }
  return interleave(strings, finalExpressions).filter(node => node.value !== "")
}

function createRawStringFromTemplateLiteral(quasi) {
  const strs = quasi.quasis.map(x => x.value.cooked)
  const source = strs
    .reduce((array, str, i) => {
      array.push(str)
      if (i !== strs.length - 1) {
        array.push(`xxx${i}:xxx`)
      }
      return array
    }, [])
    .join("")
    .trim()
  return source
}

function minify(path, t: Types) {
  const quasi = path.node.quasi
  const raw = createRawStringFromTemplateLiteral(quasi)
  const minified = stringifyTree(toInputTree(compile(raw), []))
  const expressions = replacePlaceholdersWithExpressions(
    minified,
    quasi.expressions || [],
    t,
  )
  path.replaceWith(t.callExpression(path.node.tag, expressions))
}

// src/utils/transpiled-output-utils.js
function getTypeScriptMakeTemplateObjectPath(path) {
  if (path.node.arguments.length === 0) {
    return null
  }
  const firstArgPath = path.get("arguments")[0]
  if (
    firstArgPath.isLogicalExpression() &&
    firstArgPath.get("left").isIdentifier() &&
    firstArgPath.get("right").isAssignmentExpression() &&
    firstArgPath.get("right.right").isCallExpression() &&
    firstArgPath.get("right.right.callee").isIdentifier() &&
    firstArgPath.node.right.right.callee.name.includes("makeTemplateObject") &&
    firstArgPath.node.right.right.arguments.length === 2
  ) {
    return firstArgPath.get("right.right")
  }
  return null
}
function isTaggedTemplateTranspiledByBabel(path) {
  if (path.node.arguments.length === 0) {
    return false
  }
  const firstArgPath = path.get("arguments")[0]
  if (!firstArgPath.isCallExpression() || !firstArgPath.get("callee").isIdentifier()) {
    return false
  }
  const calleeName = firstArgPath.node.callee.name
  if (!calleeName.includes("templateObject")) {
    return false
  }
  const bindingPath = path.scope.getBinding(calleeName).path
  if (!bindingPath.isFunction()) {
    return false
  }
  const functionBody = bindingPath.get("body.body")
  if (!functionBody[0].isVariableDeclaration()) {
    return false
  }
  const declarationInit = functionBody[0].get("declarations")[0].get("init")
  if (!declarationInit.isCallExpression()) {
    return false
  }
  const declarationInitArguments = declarationInit.get("arguments")
  if (
    declarationInitArguments.length === 0 ||
    declarationInitArguments.length > 2 ||
    declarationInitArguments.some(argPath => !argPath.isArrayExpression())
  ) {
    return false
  }
  return true
}

// lib/utils/strings.js
const appendStringReturningExpressionToArguments = (t: Types, path, expression) => {
  const lastIndex = path.node.arguments.length - 1
  const last = path.node.arguments[lastIndex]
  if (t.isStringLiteral(last)) {
    if (typeof expression === "string") {
      path.node.arguments[lastIndex].value += expression
    } else {
      path.node.arguments[lastIndex] = t.binaryExpression("+", last, expression)
    }
  } else {
    const makeTemplateObjectCallPath = getTypeScriptMakeTemplateObjectPath(path)
    if (makeTemplateObjectCallPath) {
      for (const argPath of makeTemplateObjectCallPath.get("arguments")) {
        const elements = argPath.get("elements")
        const lastElement = elements.at(-1)
        if (typeof expression === "string") {
          lastElement.replaceWith(t.stringLiteral(lastElement.node.value + expression))
        } else {
          lastElement.replaceWith(
            t.binaryExpression("+", lastElement.node, t.cloneNode(expression)),
          )
        }
      }
    } else if (!isTaggedTemplateTranspiledByBabel(path)) {
      if (typeof expression === "string") {
        path.node.arguments.push(t.stringLiteral(expression))
      } else {
        path.node.arguments.push(expression)
      }
    }
  }
}

const joinStringLiterals = (expressions, t) =>
  expressions.reduce((finalExpressions, currentExpression, i) => {
    if (!t.isStringLiteral(currentExpression)) {
      finalExpressions.push(currentExpression)
    } else if (t.isStringLiteral(finalExpressions.at(-1))) {
      finalExpressions.at(-1).value += currentExpression.value
    } else {
      finalExpressions.push(currentExpression)
    }
    return finalExpressions
  }, [])

// src/utils/create-node-env-conditional.js
function createNodeEnvConditional(t, production, development) {
  return t.conditionalExpression(
    t.binaryExpression(
      "===",
      t.memberExpression(
        t.memberExpression(t.identifier("process"), t.identifier("env")),
        t.identifier("NODE_ENV"),
      ),
      t.stringLiteral("production"),
    ),
    production,
    development,
  )
}

// src/utils/transform-expression-with-styles.js
const CSS_OBJECT_STRINGIFIED_ERROR =
  "You have tried to stringify object returned from `css` function. It isn't supposed to be used directly (e.g. as value of the `className` prop), but rather handed to emotion so it can handle it (e.g. as value of `css` prop)."

const transformExpressionWithStyles = ({
  babel,
  state,
  path,
  shouldLabel,
  sourceMap = "",
}: {
  babel: Babel
  state: PluginState
}) => {
  const autoLabel = state.opts.autoLabel || "dev-only"
  const t = babel.types

  if (t.isTaggedTemplateExpression(path)) {
    if (!sourceMap && state.emotionSourceMap && path.node.quasi.loc !== void 0) {
      sourceMap = getSourceMap(path.node.quasi.loc.start, state)
    }
    minify(path, t)
  }

  if (t.isCallExpression(path)) {
    const canAppendStrings = path.node.arguments.every(
      arg => arg.type !== "SpreadElement",
    )
    for (const node of path.get("arguments")) {
      if (t.isObjectExpression(node)) {
        node.replaceWith(simplifyObject(node.node, t))
      }
    }
    path.node.arguments = joinStringLiterals(path.node.arguments, t)

    if (
      !sourceMap &&
      canAppendStrings &&
      state.emotionSourceMap &&
      path.node.loc !== void 0
    ) {
      sourceMap = getSourceMap(path.node.loc.start, state)
    }
    const label =
      shouldLabel && autoLabel !== "never" ? getLabelFromPath(path, state, t) : null

    if (path.node.arguments.length === 1 && t.isStringLiteral(path.node.arguments[0])) {
      const cssString = path.node.arguments[0].value.replace(/;$/, "")
      let res = serializeStyles([
        `${cssString}${label && autoLabel === "always" ? `;label:${label};` : ""}`,
      ])
      const prodNode = t.objectExpression([
        t.objectProperty(t.identifier("name"), t.stringLiteral(res.name)),
        t.objectProperty(t.identifier("styles"), t.stringLiteral(res.styles)),
      ])
      if (!state.emotionStringifiedCssId) {
        const uid = state.file.scope.generateUidIdentifier(
          "__EMOTION_STRINGIFIED_CSS_ERROR__",
        )
        state.emotionStringifiedCssId = uid
        const cssObjectToString = t.functionDeclaration(
          uid,
          [],
          t.blockStatement([
            t.returnStatement(t.stringLiteral(CSS_OBJECT_STRINGIFIED_ERROR)),
          ]),
        )
        cssObjectToString._compact = true
        state.file.path.unshiftContainer("body", [cssObjectToString])
      }
      if (label && autoLabel === "dev-only") {
        res = serializeStyles([`${cssString};label:${label};`])
      }
      const devNode = t.objectExpression(
        [
          t.objectProperty(t.identifier("name"), t.stringLiteral(res.name)),
          t.objectProperty(t.identifier("styles"), t.stringLiteral(res.styles)),
          sourceMap && t.objectProperty(t.identifier("map"), t.stringLiteral(sourceMap)),
          t.objectProperty(
            t.identifier("toString"),
            t.cloneNode(state.emotionStringifiedCssId),
          ),
        ].filter(Boolean),
      )
      return createNodeEnvConditional(t, prodNode, devNode)
    }
    if (canAppendStrings && label) {
      const labelString = `;label:${label};`
      switch (autoLabel) {
        case "dev-only": {
          const labelConditional = createNodeEnvConditional(
            t,
            t.stringLiteral(""),
            t.stringLiteral(labelString),
          )
          appendStringReturningExpressionToArguments(t, path, labelConditional)
          break
        }
        case "always": {
          appendStringReturningExpressionToArguments(t, path, labelString)
          break
        }
      }
    }

    if (sourceMap) {
      const sourceMapConditional = createNodeEnvConditional(
        t,
        t.stringLiteral(""),
        t.stringLiteral(sourceMap),
      )
      appendStringReturningExpressionToArguments(t, path, sourceMapConditional)
    }
  }
}

// lib/utils/get-styled-options.js
const getKnownProperties = (t, node) =>
  new Set(
    node.properties
      .filter(n => t.isObjectProperty(n) && !n.computed)
      .map(n => (t.isIdentifier(n.key) ? n.key.name : n.key.value)),
  )
const createObjectSpreadLike = (t, file, ...objs) =>
  t.callExpression(file.addHelper("extends"), [t.objectExpression([]), ...objs])
const getStyledOptions = (t, path, state) => {
  const autoLabel = state.opts.autoLabel || "dev-only"
  const args = path.node.arguments
  const optionsArgument = args.length >= 2 ? args[1] : null
  const prodProperties = []
  let devProperties = null
  const knownProperties =
    optionsArgument && t.isObjectExpression(optionsArgument)
      ? getKnownProperties(t, optionsArgument)
      : /* @__PURE__ */ new Set()
  if (!knownProperties.has("target")) {
    prodProperties.push(
      t.objectProperty(
        t.identifier("target"),
        t.stringLiteral(getTargetClassName(state, t)),
      ),
    )
  }
  const label =
    autoLabel !== "never" && !knownProperties.has("label")
      ? getLabelFromPath(path, state, t)
      : null
  if (label) {
    const labelNode = t.objectProperty(t.identifier("label"), t.stringLiteral(label))
    switch (autoLabel) {
      case "always": {
        prodProperties.push(labelNode)
        break
      }
      case "dev-only": {
        devProperties = [labelNode]
        break
      }
    }
  }
  if (optionsArgument) {
    if (
      t.isConditionalExpression(optionsArgument) &&
      t.isBinaryExpression(optionsArgument.test) &&
      t.buildMatchMemberExpression("process.env.NODE_ENV")(optionsArgument.test.left)
    ) {
      return optionsArgument
    }
    if (!t.isObjectExpression(optionsArgument)) {
      const prodNode = createObjectSpreadLike(
        t,
        state.file,
        t.objectExpression(prodProperties),
        optionsArgument,
      )
      return devProperties
        ? createNodeEnvConditional(
            t,
            prodNode,
            t.cloneNode(
              createObjectSpreadLike(
                t,
                state.file,
                t.objectExpression(prodProperties.concat(devProperties)),
                optionsArgument,
              ),
            ),
          )
        : prodNode
    }
    prodProperties.unshift(...optionsArgument.properties)
  }
  return devProperties
    ? createNodeEnvConditional(
        t,
        t.objectExpression(prodProperties),
        t.cloneNode(t.objectExpression(prodProperties.concat(devProperties))),
      )
    : t.objectExpression(prodProperties)
}

// lib/utils/add-import.js
function addImport(state, importSource, importedSpecifier, nameHint) {
  const cacheKey = ["import", importSource, importedSpecifier].join(":")
  if (state[cacheKey] === void 0) {
    let importIdentifier
    importIdentifier =
      importedSpecifier === "default"
        ? addDefault(state.file.path, importSource, {
            nameHint,
          })
        : addNamed(state.file.path, importedSpecifier, importSource, {
            nameHint,
          })
    state[cacheKey] = importIdentifier.name
  }
  return {
    type: "Identifier",
    name: state[cacheKey],
  }
}

// lib/utils/transformer-macro.js
type Macro = ReturnType<typeof createTransformerMacro>

function createTransformerMacro(transformers3, { importSource }) {
  const macro = createMacro(
    ({ path, source, references, state, babel, isEmotionCall }) => {
      if (!path) {
        path = state.file.scope.path
          .get("body")
          .find(p => p.isImportDeclaration() && p.node.source.value === source)
      }
      if (/\/macro$/.test(source)) {
        path
          .get("source")
          .replaceWith(babel.types.stringLiteral(source.replace(/\/macro$/, "")))
      }
      if (!isEmotionCall) {
        state.emotionSourceMap = true
      }
      for (const importSpecifierName of Object.keys(references)) {
        if (transformers3[importSpecifierName]) {
          for (const reference of references[importSpecifierName].reverse()) {
            let options
            let transformer
            if (Array.isArray(transformers3[importSpecifierName])) {
              transformer = transformers3[importSpecifierName][0]
              options = transformers3[importSpecifierName][1]
            } else {
              transformer = transformers3[importSpecifierName]
              options = {}
            }
            transformer({
              state,
              babel,
              path,
              importSource,
              importSpecifierName,
              options,
              reference,
            })
          }
        }
      }
      return {
        keepImports: true,
      }
    },
  )
  macro.transformers = transformers3
  return macro
}

// lib/emotion-macro.js
const isAlreadyTranspiled = path => {
  if (!path.isCallExpression()) {
    return false
  }
  const firstArgPath = path.get("arguments.0")
  if (!firstArgPath) {
    return false
  }
  if (!firstArgPath.isConditionalExpression()) {
    return false
  }
  const alternatePath = firstArgPath.get("alternate")
  if (!alternatePath.isObjectExpression()) {
    return false
  }
  const properties = new Set(alternatePath.get("properties").map(p => p.node.key.name))
  return ["name", "styles"].every(p => properties.has(p))
}
const createEmotionTransformer =
  (isPure: boolean) =>
  ({ state, babel, reference }: { state: PluginState; babel: Babel }) => {
    const path = reference.parentPath
    if (isAlreadyTranspiled(path)) {
      return
    }
    if (isPure) {
      path.addComment("leading", "#__PURE__")
    }
    const node = transformExpressionWithStyles({
      babel,
      state,
      path,
      shouldLabel: true,
    })
    if (node) {
      path.node.arguments[0] = node
    }
  }
const cssTransformers = {
  css: createEmotionTransformer(true),
  injectGlobal: createEmotionTransformer(false),
  keyframes: createEmotionTransformer(true),
}
const createEmotionMacro = (importSource: string) =>
  createTransformerMacro(cssTransformers, {
    importSource,
  })

// lib/styled-macro.js
const getReferencedSpecifier = (path, specifierName) => {
  const specifiers = path.get("specifiers")
  return specifierName === "default"
    ? specifiers.find(p => p.isImportDefaultSpecifier())
    : specifiers.find(p => p.node.local.name === specifierName)
}

type StyledTransformer = typeof styledTransformer

const styledTransformer = ({
  state,
  babel,
  path,
  importSource,
  reference,
  importSpecifierName,
  options: { styledBaseImport, isWeb },
}: {
  state: PluginState
  babel: Babel
  importSource: string
  importSpecifierName: string
  options: {
    styledBaseImport: [string, string] | null
    isWeb: boolean
  }
}) => {
  const t = babel.types
  const getStyledIdentifier = () => {
    if (
      !styledBaseImport ||
      (styledBaseImport[0] === importSource &&
        styledBaseImport[1] === importSpecifierName)
    ) {
      return t.cloneNode(reference.node)
    }
    if (path.node) {
      const referencedSpecifier = getReferencedSpecifier(path, importSpecifierName)
      if (referencedSpecifier) {
        referencedSpecifier.remove()
      }
      if (path.get("specifiers").length === 0) {
        path.remove()
      }
    }
    const [baseImportSource, baseSpecifierName] = styledBaseImport
    return addImport(state, baseImportSource, baseSpecifierName, "styled")
  }
  let createStyledComponentPath = null
  if (t.isMemberExpression(reference.parent) && reference.parent.computed === false) {
    if (
      // checks if the first character is lowercase
      // becasue we don't want to transform the member expression if
      // it's in primitives/native
      reference.parent.property.name.charCodeAt(0) > 96
    ) {
      reference.parentPath.replaceWith(
        t.callExpression(getStyledIdentifier(), [
          t.stringLiteral(reference.parent.property.name),
        ]),
      )
    } else {
      reference.replaceWith(getStyledIdentifier())
    }
    createStyledComponentPath = reference.parentPath
  } else if (
    reference.parentPath &&
    t.isCallExpression(reference.parentPath) &&
    reference.parent.callee === reference.node
  ) {
    reference.replaceWith(getStyledIdentifier())
    createStyledComponentPath = reference.parentPath
  }
  if (!createStyledComponentPath) {
    return
  }
  const styledCallLikeWithStylesPath = createStyledComponentPath.parentPath
  const node = transformExpressionWithStyles({
    path: styledCallLikeWithStylesPath,
    state,
    babel,
    shouldLabel: false,
  })
  if (node && isWeb) {
    styledCallLikeWithStylesPath.node.arguments[0] = node
  }
  styledCallLikeWithStylesPath.addComment("leading", "#__PURE__")
  if (isWeb) {
    createStyledComponentPath.node.arguments[1] = getStyledOptions(
      t,
      createStyledComponentPath,
      state,
    )
  }
}

const createStyledMacro = ({
  importSource,
  originalImportSource = importSource,
  baseImportName = "default",
  isWeb,
}: {
  importSource: string
  originalImportSource?: string
  baseImportName?: string
  isWeb: boolean
}) =>
  createTransformerMacro(
    {
      default: [
        styledTransformer,
        {
          styledBaseImport: [importSource, baseImportName],
          isWeb,
        },
      ],
    },
    {
      importSource: originalImportSource,
    },
  )

// lib/core-macro.js
const transformCssCallExpression = ({
  state,
  babel,
  path,
  sourceMap,
  annotateAsPure = true,
}) => {
  const node = transformExpressionWithStyles({
    babel,
    state,
    path,
    shouldLabel: true,
    sourceMap,
  })
  if (node) {
    path.replaceWith(node)
    path.hoist()
  } else if (annotateAsPure && path.isCallExpression()) {
    path.addComment("leading", "#__PURE__")
  }
}
const transformCsslessArrayExpression = ({ state, babel, path }) => {
  const t = babel.types
  const expressionPath = path.get("value.expression")
  const sourceMap =
    state.emotionSourceMap && path.node.loc !== void 0
      ? getSourceMap(path.node.loc.start, state)
      : ""
  expressionPath.replaceWith(
    t.callExpression(
      // the name of this identifier doesn't really matter at all
      // it'll never appear in generated code
      t.identifier("___shouldNeverAppearCSS"),
      path.node.value.expression.elements,
    ),
  )
  transformCssCallExpression({
    babel,
    state,
    path: expressionPath,
    sourceMap,
    annotateAsPure: false,
  })
  if (t.isCallExpression(expressionPath)) {
    expressionPath.replaceWith(t.arrayExpression(expressionPath.node.arguments))
  }
}

const transformCsslessObjectExpression = ({ state, babel, path, cssImport }) => {
  const t = babel.types
  const expressionPath = path.get("value.expression")
  const sourceMap =
    state.emotionSourceMap && path.node.loc !== void 0
      ? getSourceMap(path.node.loc.start, state)
      : ""
  expressionPath.replaceWith(
    t.callExpression(
      // the name of this identifier doesn't really matter at all
      // it'll never appear in generated code
      t.identifier("___shouldNeverAppearCSS"),
      [path.node.value.expression],
    ),
  )
  transformCssCallExpression({
    babel,
    state,
    path: expressionPath,
    sourceMap,
  })
  if (t.isCallExpression(expressionPath)) {
    expressionPath
      .get("callee")
      .replaceWith(addImport(state, cssImport.importSource, cssImport.cssExport, "css"))
  }
}
const cssTransformer = ({
  state,
  babel,
  reference,
}: {
  state: PluginState
  babel: Babel
}) => {
  transformCssCallExpression({
    babel,
    state,
    path: reference.parentPath,
  })
}

const globalTransformer = ({
  state,
  babel,
  reference,
  importSource,
  options,
}: {
  state: PluginState
  babel: Babel
  importSource: string
  options: PluginOptions
}) => {
  const t = babel.types
  if (
    !t.isJSXIdentifier(reference.node) ||
    !t.isJSXOpeningElement(reference.parentPath.node)
  ) {
    return
  }
  const stylesPropPath = reference.parentPath
    .get("attributes")
    .find(p => t.isJSXAttribute(p.node) && p.node.name.name === "styles")
  if (!stylesPropPath) {
    return
  }
  if (t.isJSXExpressionContainer(stylesPropPath.node.value)) {
    if (t.isArrayExpression(stylesPropPath.node.value.expression)) {
      transformCsslessArrayExpression({
        state,
        babel,
        path: stylesPropPath,
      })
    } else if (t.isObjectExpression(stylesPropPath.node.value.expression)) {
      transformCsslessObjectExpression({
        state,
        babel,
        path: stylesPropPath,
        cssImport:
          options.cssExport === void 0
            ? {
                importSource: "@emotion/react",
                cssExport: "css",
              }
            : {
                importSource,
                cssExport: options.cssExport,
              },
      })
    }
  }
}
const reactTransformers = {
  // this is an empty function because this transformer is never called
  // we don't run any transforms on `jsx` directly
  // instead we use it as a hint to enable css prop optimization
  jsx: () => {},
  css: cssTransformer,
  Global: globalTransformer,
}
const core_macro_default = createTransformerMacro(reactTransformers, {
  importSource: "@emotion/react",
})

// lib/index.js
const getCssExport = (reexported, importSource, mapping) => {
  const cssExport = Object.keys(mapping).find(localExportName => {
    const [packageName, exportName] = mapping[localExportName].canonicalImport
    return packageName === "@emotion/react" && exportName === "css"
  })
  if (!cssExport) {
    throw new Error(
      `You have specified that '${importSource}' re-exports '${reexported}' from '@emotion/react' but it doesn't also re-export 'css' from '@emotion/react', 'css' is necessary for certain optimisations, please re-export it from '${importSource}'`,
    )
  }
  return cssExport
}
const webStyledMacro = createStyledMacro({
  importSource: "@emotion/styled/base",
  originalImportSource: "@emotion/styled",
  isWeb: true,
})
const nativeStyledMacro = createStyledMacro({
  importSource: "@emotion/native",
  originalImportSource: "@emotion/native",
  isWeb: false,
})
const primitivesStyledMacro = createStyledMacro({
  importSource: "@emotion/primitives",
  originalImportSource: "@emotion/primitives",
  isWeb: false,
})

const vanillaEmotionMacro = createEmotionMacro("@emotion/css")

const transformersSource = {
  "@emotion/css": cssTransformers,
  "@emotion/react": reactTransformers,
  "@emotion/styled": {
    default: [
      styledTransformer,
      {
        styledBaseImport: ["@emotion/styled/base", "default"],
        isWeb: true,
      },
    ],
  },
  "@emotion/primitives": {
    default: [
      styledTransformer,
      {
        isWeb: false,
      },
    ],
  },
  "@emotion/native": {
    default: [
      styledTransformer,
      {
        isWeb: false,
      },
    ],
  },
}

const AUTO_LABEL_VALUES = ["dev-only", "never", "always"]

export default function (babel: Babel, options: PluginOptions): PluginObj<PluginState> {
  if (options.autoLabel !== void 0 && !AUTO_LABEL_VALUES.includes(options.autoLabel)) {
    throw new Error(
      `The 'autoLabel' option must be undefined, or one of the following: ${AUTO_LABEL_VALUES.map(
        s => `"${s}"`,
      ).join(", ")}`,
    )
  }

  const t = babel.types
  return {
    name: "@emotion",
    // https://github.com/babel/babel/blob/0c97749e0fe8ad845b902e0b23a24b308b0bf05d/packages/babel-plugin-syntax-jsx/src/index.ts#L9-L18
    manipulateOptions(_opts, parserOpts: ParserOptions) {
      const { plugins } = parserOpts
      if (
        plugins!.some(p => {
          const plugin = Array.isArray(p) ? p[0] : p
          return plugin === "typescript" || plugin === "jsx"
        })
      ) {
        return
      }
      plugins!.push("jsx")
    },
    visitor: {
      ImportDeclaration(path, state) {
        const macro = state.pluginMacros[path.node.source.value]
        if (macro === void 0) {
          return
        }
        if (t.isImportNamespaceSpecifier(path.node.specifiers[0])) {
          return
        }
        const imports = path.node.specifiers.map(s => ({
          localName: s.local.name,
          importedName: s.type === "ImportDefaultSpecifier" ? "default" : s.imported.name,
        }))
        let shouldExit = false
        let hasReferences = false
        const referencePathsByImportName = imports.reduce(
          (byName, { importedName, localName }) => {
            const binding = path.scope.getBinding(localName)
            if (!binding) {
              shouldExit = true
              return byName
            }
            byName[importedName] = binding.referencePaths
            hasReferences = hasReferences || byName[importedName].length > 0
            return byName
          },
          {} as Record<string, NodePath<Node>[]>,
        )
        if (!hasReferences || shouldExit) {
          return
        }
        state.file.scope.path.traverse({
          Identifier() {},
        })
        macro({
          path,
          references: referencePathsByImportName,
          state,
          babel,
          isEmotionCall: true,
          isBabelMacrosCall: true,
        })
      },

      Program(path, state) {
        const macros2 = {}
        const jsxReactImports = [
          {
            importSource: "@emotion/react",
            export: "jsx",
            cssExport: "css",
          },
        ]
        state.jsxReactImport = jsxReactImports[0]
        for (const importSource of Object.keys(state.opts.importMap || {})) {
          const value = state.opts.importMap[importSource]
          const transformers = {}
          for (const localExportName of Object.keys(value)) {
            const { canonicalImport, ...options2 } = value[localExportName]
            const [packageName, exportName] = canonicalImport
            if (packageName === "@emotion/react" && exportName === "jsx") {
              jsxReactImports.push({
                importSource,
                export: localExportName,
                cssExport: getCssExport("jsx", importSource, value),
              })
              continue
            }
            const packageTransformers = transformersSource[packageName]
            if (packageTransformers === undefined) {
              throw new Error(
                `There is no transformer for the export '${exportName}' in '${packageName}'`,
              )
            }
            let extraOptions
            if (packageName === "@emotion/react" && exportName === "Global") {
              extraOptions = {
                cssExport: getCssExport("Global", importSource, value),
              }
            } else if (packageName === "@emotion/styled" && exportName === "default") {
              extraOptions = {
                styledBaseImport: undefined,
              }
            }
            const [exportTransformer, defaultOptions] =
              // $FlowFixMe
              Array.isArray(packageTransformers[exportName])
                ? packageTransformers[exportName]
                : [packageTransformers[exportName]]
            transformers[localExportName] = [
              exportTransformer,
              {
                ...defaultOptions,
                ...extraOptions,
                ...options2,
              },
            ]
          }
          macros2[importSource] = createTransformerMacro(transformers, {
            importSource,
          })
        }

        state.pluginMacros = {
          "@emotion/styled": webStyledMacro,
          "@emotion/react": core_macro_default,
          "@emotion/primitives": primitivesStyledMacro,
          "@emotion/native": nativeStyledMacro,
          "@emotion/css": vanillaEmotionMacro,
          ...macros2,
        }

        for (const node of path.node.body) {
          if (t.isImportDeclaration(node)) {
            const jsxReactImport = jsxReactImports.find(
              thing =>
                node.source.value === thing.importSource &&
                node.specifiers.some(
                  x => t.isImportSpecifier(x) && x.imported.name === thing.export,
                ),
            )
            if (jsxReactImport) {
              state.jsxReactImport = jsxReactImport
              break
            }
          }
        }
        state.transformCssProp = state.opts.cssPropOptimization === false ? false : true
        state.emotionSourceMap = state.opts.sourceMap === false ? false : true
      },

      JSXAttribute(path, state) {
        if (path.node.name.name !== "css" || !state.transformCssProp) {
          return
        }
        if (t.isJSXExpressionContainer(path.node.value)) {
          if (t.isArrayExpression(path.node.value.expression)) {
            transformCsslessArrayExpression({
              state,
              babel,
              path,
            })
          } else if (t.isObjectExpression(path.node.value.expression)) {
            transformCsslessObjectExpression({
              state,
              babel,
              path,
              cssImport: state.jsxReactImport,
            })
          }
        }
      },
      CallExpression: {
        exit(path, state) {
          try {
            if (
              t.isMemberExpression(path.node.callee) &&
              t.isIdentifier(path.node.callee.property) &&
              path.node.callee.property.name === "withComponent"
            ) {
              switch (path.node.arguments.length) {
                case 1:
                case 2: {
                  path.node.arguments[1] = getStyledOptions(t, path, state)
                }
              }
            }
          } catch (error) {
            throw path.buildCodeFrameError(error)
          }
        },
      },
    },
  }
}
