import {
  ElementNode,
  ObjectExpression,
  createObjectExpression,
  NodeTypes,
  createObjectProperty,
  createSimpleExpression,
  createFunctionExpression,
  DirectiveNode,
  ElementTypes,
  ExpressionNode,
  Property,
  ChildNode,
  SourceLocation
} from '../ast'
import { TransformContext, NodeTransform } from '../transform'
import { createCompilerError, ErrorCodes } from '../errors'
import { isString } from '@vue/shared'

const isVSlot = (p: ElementNode['props'][0]): p is DirectiveNode =>
  p.type === NodeTypes.DIRECTIVE && p.name === 'slot'

// A NodeTransform that tracks scope identifiers for scoped slots so that they
// don't get prefixed by transformExpression. This transform is only applied
// in non-browser builds with { prefixIdentifiers: true }
export const trackSlotScopes: NodeTransform = (node, context) => {
  if (
    node.type === NodeTypes.ELEMENT &&
    (node.tagType === ElementTypes.COMPONENT ||
      node.tagType === ElementTypes.TEMPLATE)
  ) {
    const vSlot = node.props.find(isVSlot)
    if (vSlot && vSlot.exp) {
      const { identifiers } = vSlot.exp
      if (identifiers) {
        identifiers.forEach(context.addIdentifier)
        return () => {
          identifiers.forEach(context.removeIdentifier)
        }
      }
    }
  }
}

// Instead of being a DirectiveTransform, v-slot processing is called during
// transformElement to build the slots object for a component.
export function buildSlots(
  { props, children, loc }: ElementNode,
  context: TransformContext
): ObjectExpression {
  const slots: Property[] = []

  // 1. Check for default slot with slotProps on component itself.
  //    <Comp v-slot="{ prop }"/>
  const explicitDefaultSlot = props.find(isVSlot)
  if (explicitDefaultSlot) {
    const { arg, exp, loc } = explicitDefaultSlot
    if (arg) {
      context.onError(
        createCompilerError(ErrorCodes.X_NAMED_SLOT_ON_COMPONENT, loc)
      )
    }
    slots.push(buildSlot(`default`, exp, children, loc))
  }

  // 2. Iterate through children and check for template slots
  //    <template v-slot:foo="{ prop }">
  let hasTemplateSlots = false
  let extraneousChild: ChildNode | undefined = undefined
  const seenSlotNames = new Set<string>()
  for (let i = 0; i < children.length; i++) {
    const child = children[i]
    let slotDir
    if (
      child.type === NodeTypes.ELEMENT &&
      child.tagType === ElementTypes.TEMPLATE &&
      (slotDir = child.props.find(isVSlot))
    ) {
      hasTemplateSlots = true
      const { children, loc: nodeLoc } = child
      const { arg: slotName, exp: slotProps, loc: dirLoc } = slotDir
      if (explicitDefaultSlot) {
        // already has on-component default slot - this is incorrect usage.
        context.onError(
          createCompilerError(ErrorCodes.X_MIXED_SLOT_USAGE, dirLoc)
        )
        break
      } else {
        // check duplicate slot names
        if (
          !slotName ||
          (slotName.type === NodeTypes.SIMPLE_EXPRESSION && slotName.isStatic)
        ) {
          const name = slotName ? slotName.content : `default`
          if (seenSlotNames.has(name)) {
            context.onError(
              createCompilerError(ErrorCodes.X_DUPLICATE_SLOT_NAMES, dirLoc)
            )
            continue
          }
          seenSlotNames.add(name)
        }
        slots.push(
          buildSlot(slotName || `default`, slotProps, children, nodeLoc)
        )
      }
    } else if (!extraneousChild) {
      extraneousChild = child
    }
  }

  if (hasTemplateSlots && extraneousChild) {
    context.onError(
      createCompilerError(
        ErrorCodes.X_EXTRANEOUS_NON_SLOT_CHILDREN,
        extraneousChild.loc
      )
    )
  }

  if (!explicitDefaultSlot && !hasTemplateSlots) {
    // implicit default slot.
    slots.push(buildSlot(`default`, undefined, children, loc))
  }

  return createObjectExpression(slots, loc)
}

function buildSlot(
  name: string | ExpressionNode,
  slotProps: ExpressionNode | undefined,
  children: ChildNode[],
  loc: SourceLocation
): Property {
  return createObjectProperty(
    isString(name) ? createSimpleExpression(name, true, loc) : name,
    createFunctionExpression(
      slotProps,
      children,
      children.length ? children[0].loc : loc
    ),
    loc
  )
}
