import type { App, ExtractPropTypes } from 'vue'
import { computed, defineComponent, ref } from 'vue'

import classNames from '../_util/classNames'
import omit from '../_util/omit'
import { flattenChildren } from '../_util/props-util'
import initDefaultProps from '../_util/props-util/initDefaultProps'
import type { InputStatus } from '../_util/statusUtils'
import { getMergedStatus, getStatusClassNames } from '../_util/statusUtils'
import type { SelectCommonPlacement } from '../_util/transition'
import { getTransitionDirection } from '../_util/transition'
import { booleanType, functionType, objectType, someType, stringType } from '../_util/type'
import type { CustomSlotsType } from '../_util/type'
import PropTypes from '../_util/vue-types'
import type { SizeType } from '../config-provider'
import { useInjectDisabled } from '../config-provider/DisabledContext'
import useConfigInject from '../config-provider/hooks/useConfigInject'
import { FormItemInputContext, useInjectFormItemContext } from '../form/FormItemContext'
// CSSINJS
import useSelectStyle from '../select/style'
import getIcons from '../select/utils/iconUtil'
import { useCompactItemContext } from '../space/Compact'
import type { TreeProps } from '../tree'
import type { SwitcherIconProps } from '../tree/utils/iconUtil'
import renderSwitcherIcon from '../tree/utils/iconUtil'
import type { BaseSelectRef } from '../vc-select'
import VcTreeSelect, {
  SHOW_ALL,
  SHOW_CHILD,
  SHOW_PARENT,
  TreeNode,
  treeSelectProps as vcTreeSelectProps,
} from '../vc-tree-select'
import type { BaseOptionType, DefaultOptionType } from '../vc-tree-select/TreeSelect'
import type { FieldNames, Key } from '../vc-tree-select/interface'
import devWarning from '../vc-util/devWarning'
import { warning } from '../vc-util/warning'
import useStyle from './style'

const getTransitionName = (rootPrefixCls: string, motion: string, transitionName?: string) => {
  if (transitionName !== undefined) {
    return transitionName
  }
  return `${rootPrefixCls}-${motion}`
}

type RawValue = string | number

export interface LabeledValue {
  key?: string
  value: RawValue
  label?: any
}

export type SelectValue = RawValue | RawValue[] | LabeledValue | LabeledValue[]

export type RefTreeSelectProps = BaseSelectRef

export function treeSelectProps<
  ValueType = any,
  OptionType extends BaseOptionType | DefaultOptionType = DefaultOptionType,
>() {
  return {
    ...omit(vcTreeSelectProps<ValueType, OptionType>(), [
      'showTreeIcon',
      'treeMotion',
      'inputIcon',
      'getInputElement',
      'treeLine',
      'customSlots',
    ]),
    suffixIcon: PropTypes.any,
    size: stringType<SizeType>(),
    bordered: booleanType(),
    treeLine: someType<TreeProps['showLine']>([Boolean, Object]),
    replaceFields: objectType<FieldNames>(),
    placement: stringType<SelectCommonPlacement>(),
    status: stringType<InputStatus>(),
    popupClassName: String,
    /** @deprecated Please use `popupClassName` instead */
    dropdownClassName: String,
    'onUpdate:value': functionType<(value: any) => void>(),
    'onUpdate:treeExpandedKeys': functionType<(keys: Key[]) => void>(),
    'onUpdate:searchValue': functionType<(value: string) => void>(),
  }
}
export type TreeSelectProps = Partial<ExtractPropTypes<ReturnType<typeof treeSelectProps>>>

const TreeSelect = defineComponent({
  compatConfig: { MODE: 3 },
  name: 'ATreeSelect',
  inheritAttrs: false,
  props: initDefaultProps(treeSelectProps(), {
    choiceTransitionName: '',
    listHeight: 256,
    treeIcon: false,
    listItemHeight: 26,
    bordered: true,
  }),
  slots: Object as CustomSlotsType<{
    title?: any
    titleRender?: any
    placeholder?: any
    maxTagPlaceholder?: any
    treeIcon?: any
    switcherIcon?: any
    notFoundContent?: any
    default?: any
    leafIcon?: any
    tagRender?: any
    suffixIcon?: any
  }>,
  setup(props, { attrs, slots, expose, emit }) {
    warning(
      !(props.treeData === undefined && slots.default),
      '`children` of TreeSelect is deprecated. Please use `treeData` instead.',
    )
    devWarning(
      props.multiple !== false || !props.treeCheckable,
      'TreeSelect',
      '`multiple` will always be `true` when `treeCheckable` is true',
    )
    devWarning(
      props.replaceFields === undefined,
      'TreeSelect',
      '`replaceFields` is deprecated, please use fieldNames instead',
    )
    devWarning(
      !props.dropdownClassName,
      'TreeSelect',
      '`dropdownClassName` is deprecated. Please use `popupClassName` instead.',
    )

    const formItemContext = useInjectFormItemContext()
    const formItemInputContext = FormItemInputContext.useInject()
    const mergedStatus = computed(() => getMergedStatus(formItemInputContext.status, props.status))
    const {
      prefixCls,
      renderEmpty,
      direction,
      virtual,
      dropdownMatchSelectWidth,
      size: contextSize,
      getPopupContainer,
      getPrefixCls,
      disabled,
    } = useConfigInject('select', props)
    const { compactSize, compactItemClassnames } = useCompactItemContext(prefixCls, direction)
    const mergedSize = computed(() => compactSize.value || contextSize.value)
    const contextDisabled = useInjectDisabled()
    const mergedDisabled = computed(() => disabled.value ?? contextDisabled.value)
    const rootPrefixCls = computed(() => getPrefixCls())
    // ===================== Placement =====================
    const placement = computed(() => {
      if (props.placement !== undefined) {
        return props.placement
      }
      return direction.value === 'rtl'
        ? ('bottomRight' as SelectCommonPlacement)
        : ('bottomLeft' as SelectCommonPlacement)
    })
    const transitionName = computed(() =>
      getTransitionName(
        rootPrefixCls.value,
        getTransitionDirection(placement.value),
        props.transitionName,
      ),
    )
    const choiceTransitionName = computed(() =>
      getTransitionName(rootPrefixCls.value, '', props.choiceTransitionName),
    )
    const treePrefixCls = computed(() => getPrefixCls('select-tree', props.prefixCls))
    const treeSelectPrefixCls = computed(() => getPrefixCls('tree-select', props.prefixCls))

    // style
    const [wrapSelectSSR, hashId] = useSelectStyle(prefixCls)
    const [wrapTreeSelectSSR] = useStyle(treeSelectPrefixCls, treePrefixCls)

    const mergedDropdownClassName = computed(() =>
      classNames(
        props.popupClassName || props.dropdownClassName,
        `${treeSelectPrefixCls.value}-dropdown`,
        {
          [`${treeSelectPrefixCls.value}-dropdown-rtl`]: direction.value === 'rtl',
        },
        hashId.value,
      ),
    )

    const isMultiple = computed(() => !!(props.treeCheckable || props.multiple))
    const mergedShowArrow = computed(() =>
      props.showArrow !== undefined ? props.showArrow : props.loading || !isMultiple.value,
    )

    const treeSelectRef = ref()
    expose({
      focus() {
        treeSelectRef.value.focus?.()
      },

      blur() {
        treeSelectRef.value.blur?.()
      },
    })

    const handleChange: TreeSelectProps['onChange'] = (...args: any[]) => {
      emit('update:value', args[0])
      emit('change', ...args)
      formItemContext.onFieldChange()
    }
    const handleTreeExpand: TreeSelectProps['onTreeExpand'] = (keys: Key[]) => {
      emit('update:treeExpandedKeys', keys)
      emit('treeExpand', keys)
    }
    const handleSearch: TreeSelectProps['onSearch'] = (value: string) => {
      emit('update:searchValue', value)
      emit('search', value)
    }
    const handleBlur = (e: FocusEvent) => {
      emit('blur', e)
      formItemContext.onFieldBlur()
    }
    return () => {
      const {
        notFoundContent = slots.notFoundContent?.(),
        prefixCls: customizePrefixCls,
        bordered,
        listHeight,
        listItemHeight,
        multiple,
        treeIcon,
        treeLine,
        showArrow,
        switcherIcon = slots.switcherIcon?.(),
        fieldNames = props.replaceFields,
        id = formItemContext.id.value,
        placeholder = slots.placeholder?.(),
      } = props
      const { isFormItemInput, hasFeedback, feedbackIcon } = formItemInputContext
      // ===================== Icons =====================
      const { suffixIcon, removeIcon, clearIcon } = getIcons(
        {
          ...props,
          multiple: isMultiple.value,
          showArrow: mergedShowArrow.value,
          hasFeedback,
          feedbackIcon,
          prefixCls: prefixCls.value,
        },
        slots,
      )

      // ===================== Empty =====================
      let mergedNotFound
      if (notFoundContent !== undefined) {
        mergedNotFound = notFoundContent
      } else {
        mergedNotFound = renderEmpty('Select')
      }
      // ==================== Render =====================
      const selectProps = omit(props as typeof props & { itemIcon: any; switcherIcon: any }, [
        'suffixIcon',
        'itemIcon',
        'removeIcon',
        'clearIcon',
        'switcherIcon',
        'bordered',
        'status',
        'onUpdate:value',
        'onUpdate:treeExpandedKeys',
        'onUpdate:searchValue',
      ])

      const mergedClassName = classNames(
        !customizePrefixCls && treeSelectPrefixCls.value,
        {
          [`${prefixCls.value}-lg`]: mergedSize.value === 'large',
          [`${prefixCls.value}-sm`]: mergedSize.value === 'small',
          [`${prefixCls.value}-rtl`]: direction.value === 'rtl',
          [`${prefixCls.value}-borderless`]: !bordered,
          [`${prefixCls.value}-in-form-item`]: isFormItemInput,
        },
        getStatusClassNames(prefixCls.value, mergedStatus.value, hasFeedback),
        compactItemClassnames.value,
        attrs.class,
        hashId.value,
      )
      const otherProps: any = {}
      if (props.treeData === undefined && slots.default) {
        otherProps.children = flattenChildren(slots.default())
      }
      return wrapSelectSSR(
        wrapTreeSelectSSR(
          <VcTreeSelect
            {...attrs}
            {...selectProps}
            disabled={mergedDisabled.value}
            virtual={virtual.value}
            dropdownMatchSelectWidth={dropdownMatchSelectWidth.value}
            id={id}
            fieldNames={fieldNames}
            ref={treeSelectRef}
            prefixCls={prefixCls.value}
            class={mergedClassName}
            listHeight={listHeight}
            listItemHeight={listItemHeight}
            treeLine={!!treeLine}
            inputIcon={suffixIcon}
            multiple={multiple}
            removeIcon={removeIcon}
            clearIcon={clearIcon}
            switcherIcon={(nodeProps: SwitcherIconProps) =>
              renderSwitcherIcon(
                treePrefixCls.value,
                switcherIcon,
                nodeProps,
                slots.leafIcon,
                treeLine,
              )
            }
            showTreeIcon={treeIcon as any}
            notFoundContent={mergedNotFound}
            getPopupContainer={getPopupContainer?.value}
            treeMotion={null}
            dropdownClassName={mergedDropdownClassName.value}
            choiceTransitionName={choiceTransitionName.value}
            onChange={handleChange}
            onBlur={handleBlur}
            onSearch={handleSearch}
            onTreeExpand={handleTreeExpand}
            v-slots={{
              ...slots,
              treeCheckable: () => <span class={`${prefixCls.value}-tree-checkbox-inner`} />,
            }}
            {...otherProps}
            transitionName={transitionName.value}
            customSlots={{
              ...slots,
              treeCheckable: () => <span class={`${prefixCls.value}-tree-checkbox-inner`} />,
            }}
            maxTagPlaceholder={props.maxTagPlaceholder || slots.maxTagPlaceholder}
            placement={placement.value}
            showArrow={hasFeedback || showArrow}
            placeholder={placeholder}
          />,
        ),
      )
    }
  },
})

/* istanbul ignore next */
export const TreeSelectNode = TreeNode
export default Object.assign(TreeSelect, {
  TreeNode,
  SHOW_ALL: SHOW_ALL as typeof SHOW_ALL,
  SHOW_PARENT: SHOW_PARENT as typeof SHOW_PARENT,
  SHOW_CHILD: SHOW_CHILD as typeof SHOW_CHILD,
  install: (app: App) => {
    app.component(TreeSelect.name, TreeSelect)
    app.component(TreeSelectNode.displayName, TreeSelectNode)
    return app
  },
})
