// @flow
import { createEmotionMacro } from './emotion-macro'
import { createStyledMacro } from './styled-macro'
import cssMacro, { transformCssCallExpression } from './css-macro'
import { addNamed } from '@babel/helper-module-imports'
import nodePath from 'path'
import { getSourceMap, getStyledOptions } from './utils'

let webStyledMacro = createStyledMacro({
  importPath: '@emotion/styled-base',
  originalImportPath: '@emotion/styled',
  isWeb: true
})
let nativeStyledMacro = createStyledMacro({
  importPath: '@emotion/native',
  originalImportPath: '@emotion/native',
  isWeb: false
})
let primitivesStyledMacro = createStyledMacro({
  importPath: '@emotion/primitives',
  originalImportPath: '@emotion/primitives',
  isWeb: false
})

export const macros = {
  createEmotionMacro,
  css: cssMacro,
  createStyledMacro
}

export type BabelPath = any

export type EmotionBabelPluginPass = any

let emotionCoreMacroThatsNotARealMacro = ({ references, state, babel }) => {
  Object.keys(references).forEach(refKey => {
    if (refKey === 'css') {
      references[refKey].forEach(path => {
        transformCssCallExpression({ babel, state, path: path.parentPath })
      })
    }
  })
}
emotionCoreMacroThatsNotARealMacro.keepImport = true

function getAbsolutePath(instancePath: string, rootPath: string) {
  if (instancePath.charAt(0) === '.') {
    let absoluteInstancePath = nodePath.resolve(rootPath, instancePath)
    return absoluteInstancePath
  }
  return false
}

function getInstancePathToCompare(instancePath: string, rootPath: string) {
  let absolutePath = getAbsolutePath(instancePath, rootPath)
  if (absolutePath === false) {
    return instancePath
  }
  return absolutePath
}

export default function(babel: *) {
  let t = babel.types
  return {
    name: 'emotion',
    inherits: require('babel-plugin-syntax-jsx'),
    visitor: {
      ImportDeclaration(path: *, state: *) {
        const hasFilepath =
          path.hub.file.opts.filename &&
          path.hub.file.opts.filename !== 'unknown'
        let dirname = hasFilepath
          ? nodePath.dirname(path.hub.file.opts.filename)
          : ''

        if (
          !state.pluginMacros[path.node.source.value] &&
          state.emotionInstancePaths.indexOf(
            getInstancePathToCompare(path.node.source.value, dirname)
          ) !== -1
        ) {
          state.pluginMacros[path.node.source.value] = createEmotionMacro(
            path.node.source.value
          )
        }
        let pluginMacros = state.pluginMacros
        // most of this is from https://github.com/kentcdodds/babel-plugin-macros/blob/master/src/index.js
        if (pluginMacros[path.node.source.value] === undefined) {
          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 }) => {
            let binding = path.scope.getBinding(localName)
            if (!binding) {
              shouldExit = true
              return byName
            }
            byName[importedName] = binding.referencePaths
            hasReferences =
              hasReferences || Boolean(byName[importedName].length)
            return byName
          },
          {}
        )
        if (!hasReferences || shouldExit) {
          return
        }
        /**
         * Other plugins that run before babel-plugin-macros might use path.replace, where a path is
         * put into its own replacement. Apparently babel does not update the scope after such
         * an operation. As a remedy, the whole scope is traversed again with an empty "Identifier"
         * visitor - this makes the problem go away.
         *
         * See: https://github.com/kentcdodds/import-all.macro/issues/7
         */
        state.file.scope.path.traverse({
          Identifier() {}
        })

        pluginMacros[path.node.source.value]({
          references: referencePathsByImportName,
          state,
          babel,
          isBabelMacrosCall: true,
          isEmotionCall: true
        })
        if (!pluginMacros[path.node.source.value].keepImport) {
          path.remove()
        }
      },
      Program(path: *, state: *) {
        state.emotionInstancePaths = (state.opts.instances || []).map(
          instancePath => getInstancePathToCompare(instancePath, process.cwd())
        )
        state.pluginMacros = {
          '@emotion/css': cssMacro,
          '@emotion/styled': webStyledMacro,
          '@emotion/core': emotionCoreMacroThatsNotARealMacro,
          '@emotion/primitives': primitivesStyledMacro,
          '@emotion/native': nativeStyledMacro,
          emotion: createEmotionMacro('emotion')
        }
        if (state.opts.cssPropOptimization === undefined) {
          for (const node of path.node.body) {
            if (
              t.isImportDeclaration(node) &&
              node.source.value === '@emotion/core' &&
              node.specifiers.some(
                x => t.isImportSpecifier(x) && x.imported.name === 'jsx'
              )
            ) {
              state.transformCssProp = true
              break
            }
          }
        } else {
          state.transformCssProp = state.opts.cssPropOptimization
        }

        if (state.opts.sourceMap === false) {
          state.emotionSourceMap = false
        } else {
          state.emotionSourceMap = true
        }
      },
      JSXAttribute(path: *, state: *) {
        if (path.node.name.name !== 'css' || !state.transformCssProp) {
          return
        }

        if (
          t.isJSXExpressionContainer(path.node.value) &&
          (t.isObjectExpression(path.node.value.expression) ||
            t.isArrayExpression(path.node.value.expression))
        ) {
          let expressionPath = path.get('value.expression')
          let sourceMap =
            state.emotionSourceMap && path.node.loc !== undefined
              ? 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)) {
            if (!state.cssIdentifier) {
              state.cssIdentifier = addNamed(path, 'css', '@emotion/core', {
                nameHint: 'css'
              })
            }

            expressionPath
              .get('callee')
              .replaceWith(t.cloneDeep(state.cssIdentifier))
          }
        }
      },
      CallExpression: {
        exit(path: BabelPath, state: EmotionBabelPluginPass) {
          try {
            if (
              path.node.callee &&
              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 (e) {
            throw path.buildCodeFrameError(e)
          }
        }
      }
    }
  }
}
