import {
  FileInfo,
  API,
  ArrowFunctionExpression,
  ASTPath,
  Identifier,
  VariableDeclarator,
  TSTypeReference,
  ObjectPattern,
  TSTypeLiteral,
  TSIntersectionType,
  TSTypeAnnotation,
  JSCodeshift,
  FunctionExpression,
  CallExpression,
} from 'jscodeshift'

export const parser = 'tsx'

const isIdentifier = (x: any): x is Identifier => (x as Identifier).type === 'Identifier'
const isTsTypeReference = (x: any): x is TSTypeReference => (x as TSTypeReference).type === 'TSTypeReference'
const isObjectPattern = (x: any): x is ObjectPattern => (x as ObjectPattern).type === 'ObjectPattern'
const isTsIntersectionType = (x: any): x is TSIntersectionType =>
  (x as TSIntersectionType).type === 'TSIntersectionType'
const isArrowFunctionExpression = (x: any): x is ArrowFunctionExpression =>
  (x as ArrowFunctionExpression).type === 'ArrowFunctionExpression'
// Using a function that accepts a component definition
const isCallExpression = (x: any): x is CallExpression => x?.type === 'CallExpression'
const isTSIntersectionType = (x: any): x is TSIntersectionType => x?.type === 'TSIntersectionType'

export default (fileInfo: FileInfo, { j }: API) => {
  function addPropsTypeToComponentBody(n: ASTPath<VariableDeclarator>) {
    // extract the Prop's type text
    let reactFcOrSfcNode
    if (isIdentifier(n.node.id)) {
      if (isTSIntersectionType(n.node.id.typeAnnotation!.typeAnnotation)) {
        reactFcOrSfcNode = n.node.id.typeAnnotation!.typeAnnotation.types[0] as TSTypeReference
      } else {
        reactFcOrSfcNode = n.node.id.typeAnnotation!.typeAnnotation as TSTypeReference
      }
    }

    // shape of React.FC (no props)
    if (!reactFcOrSfcNode?.typeParameters) {
      return
    }

    const outerNewTypeAnnotation = extractPropsDefinitionFromReactFC(j, reactFcOrSfcNode)
    // build the new nodes
    const componentFunctionNode = (isCallExpression(n.node.init) ? n.node.init.arguments[0] : n.node.init) as
      | ArrowFunctionExpression
      | FunctionExpression

    const paramsLength = componentFunctionNode?.params?.length
    // The remaining parameters except the first parameter
    let restParameters = []
    if (!paramsLength) {
      // if no params, it could be that the component is not actually using props, so nothing to do here
      return
    } else {
      restParameters = componentFunctionNode.params.slice(1, paramsLength)
    }

    const firstParam = componentFunctionNode.params[0]

    let componentFunctionFirstParameter: Identifier | ObjectPattern | undefined
    // form of (props) =>
    if (isIdentifier(firstParam)) {
      componentFunctionFirstParameter = j.identifier.from({
        ...firstParam,
        typeAnnotation: outerNewTypeAnnotation!,
      })
    }

    // form of ({ foo }) =>
    if (isObjectPattern(firstParam)) {
      const { properties, ...restParams } = firstParam
      componentFunctionFirstParameter = j.objectPattern.from({
        ...restParams,
        // remove locations because properties might have a spread like ({ id, ...rest }) => and it breaks otherwise
        properties: properties.map(({ loc, ...rest }) => {
          const key = rest.type.slice(0, 1).toLowerCase() + rest.type.slice(1)
          // This workaround is because the AST parsed has "RestElement, but codeshift (as well as the types) expects "RestProperty"
          // manually doing this works ok. restElement has the properties needed
          if (key === 'restElement') {
            const prop = rest as any
            return j.restProperty.from({ argument: prop.argument })
          }
          return j[key].from({ ...rest })
        }),
        typeAnnotation: outerNewTypeAnnotation!,
      })
    }

    let newInit: ArrowFunctionExpression | FunctionExpression | undefined
    if (isArrowFunctionExpression(componentFunctionNode)) {
      newInit = j.arrowFunctionExpression.from({
        ...componentFunctionNode,
        params: [componentFunctionFirstParameter!, ...restParameters],
      })
    } else {
      newInit = j.functionExpression.from({
        ...componentFunctionNode,
        params: [componentFunctionFirstParameter!, ...restParameters],
      })
    }
    let newVariableDeclarator: VariableDeclarator
    if (isCallExpression(n.node.init)) {
      newVariableDeclarator = j.variableDeclarator.from({
        ...n.node,
        init: {
          ...n.node.init,
          arguments: [newInit],
        },
      })
    } else {
      newVariableDeclarator = j.variableDeclarator.from({ ...n.node, init: newInit })
    }

    n.replace(newVariableDeclarator)
    return
  }

  function removeReactFCorSFCdeclaration(n: ASTPath<VariableDeclarator>) {
    const { id, ...restOfNode } = n.node
    const { typeAnnotation, ...restOfId } = id as Identifier
    const newId = j.identifier.from({ ...restOfId })
    const newVariableDeclarator = j.variableDeclarator.from({
      ...restOfNode,
      id: newId,
    })
    n.replace(newVariableDeclarator)
  }

  try {
    const root = j(fileInfo.source)
    let hasModifications = false
    const newSource = root
      .find(j.VariableDeclarator, (n: any) => {
        const identifier = n?.id
        let typeName
        if (isTSIntersectionType(identifier?.typeAnnotation?.typeAnnotation)) {
          typeName = identifier.typeAnnotation.typeAnnotation.types[0].typeName
        } else {
          typeName = identifier?.typeAnnotation?.typeAnnotation?.typeName
        }

        const genericParamsType = identifier?.typeAnnotation?.typeAnnotation?.typeParameters?.type
        // verify it is the shape of React.FC<Props> React.SFC<Props>, React.FC<{ type: string }>, FC<Props>, SFC<Props>, and so on

        const isEqualFcOrFunctionComponent = (name: string) => ['FC', 'FunctionComponent'].includes(name)
        const isFC =
          (typeName?.left?.name === 'React' && isEqualFcOrFunctionComponent(typeName?.right?.name)) ||
          isEqualFcOrFunctionComponent(typeName?.name)
        const isSFC = (typeName?.left?.name === 'React' && typeName?.right?.name === 'SFC') || typeName?.name === 'SFC'

        return (
          (isFC || isSFC) &&
          (['TSQualifiedName', 'TSTypeParameterInstantiation'].includes(genericParamsType) ||
            !identifier?.typeAnnotation?.typeAnnotation?.typeParameters)
        )
      })
      .forEach((n) => {
        hasModifications = true
        addPropsTypeToComponentBody(n)
        removeReactFCorSFCdeclaration(n)
      })
      .toSource()
    return hasModifications ? newSource : null
  } catch (e) {
    console.log(e)
  }
}

function extractPropsDefinitionFromReactFC(j: JSCodeshift, reactFcOrSfcNode: TSTypeReference): TSTypeAnnotation {
  const typeParameterFirstParam = reactFcOrSfcNode.typeParameters!.params[0]
  let newInnerTypeAnnotation: TSTypeReference | TSIntersectionType | TSTypeLiteral | undefined

  // form of React.FC<Props> or React.SFC<Props>
  if (isTsTypeReference(typeParameterFirstParam)) {
    const { loc, ...rest } = typeParameterFirstParam
    newInnerTypeAnnotation = j.tsTypeReference.from({ ...rest })
  } else if (isTsIntersectionType(typeParameterFirstParam)) {
    // form of React.FC<Props & Props2>
    const { loc, ...rest } = typeParameterFirstParam
    newInnerTypeAnnotation = j.tsIntersectionType.from({
      ...rest,
      types: rest.types.map((t) => buildDynamicalNodeByType(j, t)),
    })
  } else {
    // form of React.FC<{ foo: number }> or React.SFC<{ foo: number }>
    const inlineTypeDeclaration = typeParameterFirstParam as TSTypeLiteral
    // remove locations to avoid messing up with commans
    const newMembers = inlineTypeDeclaration.members.map((m) => buildDynamicalNodeByType(j, m))
    newInnerTypeAnnotation = j.tsTypeLiteral.from({ members: newMembers })
  }

  const outerNewTypeAnnotation = j.tsTypeAnnotation.from({ typeAnnotation: newInnerTypeAnnotation })
  return outerNewTypeAnnotation
}

// dynamically call the api method to build the proper node. For example TSPropertySignature becomes tsPropertySignature
function buildDynamicalNodeByType(j: JSCodeshift, { loc, ...rest }: any) {
  const key = rest.type.slice(0, 2).toLowerCase() + rest.type.slice(2)
  return j[key].from({ ...rest })
}