import CloseOutlined from '@ant-design/icons-vue/CloseOutlined'

import type { CSSProperties, ExtractPropTypes, PropType } from 'vue'
import {
  computed,
  defineComponent,
  inject,
  nextTick,
  onMounted,
  onUnmounted,
  provide,
  shallowRef,
  watch,
} from 'vue'

import type { KeyboardEventHandler, MouseEventHandler } from '../_util/EventInterface'
import classnames from '../_util/classNames'
import isNumeric from '../_util/isNumeric'
import omit from '../_util/omit'
import { getPropsSlot, initDefaultProps } from '../_util/props-util'
import { getTransitionName, getTransitionProps } from '../_util/transition'
import type { CustomSlotsType } from '../_util/type'
import { objectType, withInstall } from '../_util/type'
import PropTypes from '../_util/vue-types'
import useConfigInject from '../config-provider/hooks/useConfigInject'
import { NoCompactStyle } from '../space/Compact'
import VcDrawer from '../vc-drawer'
import devWarning from '../vc-util/devWarning'
import useStyle from './style'

type ILevelMove = number | [number, number]

const PlacementTypes = ['top', 'right', 'bottom', 'left'] as const
export type placementType = (typeof PlacementTypes)[number]

const SizeTypes = ['default', 'large'] as const
export type sizeType = (typeof SizeTypes)[number]

export interface PushState {
  distance: string | number
}

const defaultPushState: PushState = { distance: 180 }
type getContainerFunc = () => HTMLElement
export function drawerProps() {
  return {
    autofocus: { type: Boolean, default: undefined },
    closable: { type: Boolean, default: undefined },
    closeIcon: PropTypes.any,
    destroyOnClose: { type: Boolean, default: undefined },
    forceRender: { type: Boolean, default: undefined },
    getContainer: {
      type: [String, Function, Boolean, Object] as PropType<
        string | HTMLElement | getContainerFunc | false
      >,
      default: undefined as string | HTMLElement | getContainerFunc | false,
    },
    maskClosable: { type: Boolean, default: undefined },
    mask: { type: Boolean, default: undefined },
    maskStyle: objectType<CSSProperties>(),
    rootClassName: String,
    rootStyle: objectType<CSSProperties>(),
    size: {
      type: String as PropType<sizeType>,
    },
    drawerStyle: objectType<CSSProperties>(),
    headerStyle: objectType<CSSProperties>(),
    bodyStyle: objectType<CSSProperties>(),
    contentWrapperStyle: {
      type: Object as PropType<CSSProperties>,
      default: undefined as CSSProperties,
    },
    title: PropTypes.any,
    /** @deprecated Please use `open` instead */
    visible: { type: Boolean, default: undefined },
    open: { type: Boolean, default: undefined },
    width: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
    height: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
    zIndex: Number,
    prefixCls: String,
    push: PropTypes.oneOfType([PropTypes.looseBool, { type: Object as PropType<PushState> }]),
    placement: PropTypes.oneOf(PlacementTypes),
    keyboard: { type: Boolean, default: undefined },
    extra: PropTypes.any,
    footer: PropTypes.any,
    footerStyle: objectType<CSSProperties>(),
    level: PropTypes.any,
    levelMove: {
      type: [Number, Array, Function] as PropType<
        ILevelMove | ((e: { target: HTMLElement; open: boolean }) => ILevelMove)
      >,
    },
    handle: PropTypes.any,
    /** @deprecated Use `@afterVisibleChange` instead */
    afterVisibleChange: Function as PropType<(visible: boolean) => void>,
    /** @deprecated Please use `@afterOpenChange` instead */
    onAfterVisibleChange: Function as PropType<(visible: boolean) => void>,
    onAfterOpenChange: Function as PropType<(open: boolean) => void>,
    /** @deprecated Please use `onUpdate:open` instead */
    'onUpdate:visible': Function as PropType<(visible: boolean) => void>,
    'onUpdate:open': Function as PropType<(open: boolean) => void>,
    onClose: Function as PropType<MouseEventHandler | KeyboardEventHandler>,
  }
}

export type DrawerProps = Partial<ExtractPropTypes<ReturnType<typeof drawerProps>>>

const Drawer = defineComponent({
  compatConfig: { MODE: 3 },
  name: 'ADrawer',
  inheritAttrs: false,
  props: initDefaultProps(drawerProps(), {
    closable: true,
    placement: 'right',
    maskClosable: true,
    mask: true,
    level: null,
    keyboard: true,
    push: defaultPushState,
  }),
  slots: Object as CustomSlotsType<{
    closeIcon: any
    title: any
    extra: any
    footer: any
    handle: any
    default: any
  }>,
  // emits: ['update:visible', 'close', 'afterVisibleChange'],
  setup(props, { emit, slots, attrs }) {
    const sPush = shallowRef(false)
    const destroyClose = shallowRef(false)
    const vcDrawer = shallowRef(null)
    const load = shallowRef(false)
    const visible = shallowRef(false)
    const mergedOpen = computed(() => props.open ?? props.visible)
    watch(
      mergedOpen,
      () => {
        if (mergedOpen.value) {
          load.value = true
        } else {
          visible.value = false
        }
      },
      { immediate: true },
    )
    watch(
      [mergedOpen, load],
      () => {
        if (mergedOpen.value && load.value) {
          visible.value = true
        }
      },
      { immediate: true },
    )
    const parentDrawerOpts = inject('parentDrawerOpts', null)
    const { prefixCls, getPopupContainer, direction } = useConfigInject('drawer', props)
    const [wrapSSR, hashId] = useStyle(prefixCls)
    const getContainer = computed(() =>
      // 有可能为 false，所以不能直接判断
      props.getContainer === undefined && getPopupContainer?.value
        ? () => getPopupContainer.value(document.body)
        : props.getContainer,
    )

    devWarning(
      !props.afterVisibleChange,
      'Drawer',
      '`afterVisibleChange` prop is deprecated, please use `@afterVisibleChange` event instead',
    )
    // ========================== Warning ===========================
    if (process.env.NODE_ENV !== 'production') {
      ;[
        ['visible', 'open'],
        ['onUpdate:visible', 'onUpdate:open'],
        ['onAfterVisibleChange', 'onAfterOpenChange'],
      ].forEach(([deprecatedName, newName]) => {
        devWarning(
          !props[deprecatedName],
          'Drawer',
          `\`${deprecatedName}\` is deprecated, please use \`${newName}\` instead.`,
        )
      })
    }

    const setPush = () => {
      sPush.value = true
    }

    const setPull = () => {
      sPush.value = false
      nextTick(() => {
        domFocus()
      })
    }
    provide('parentDrawerOpts', {
      setPush,
      setPull,
    })

    onMounted(() => {
      if (mergedOpen.value && parentDrawerOpts) {
        parentDrawerOpts.setPush()
      }
    })

    onUnmounted(() => {
      if (parentDrawerOpts) {
        parentDrawerOpts.setPull()
      }
    })

    watch(
      visible,
      () => {
        if (parentDrawerOpts) {
          if (visible.value) {
            parentDrawerOpts.setPush()
          } else {
            parentDrawerOpts.setPull()
          }
        }
      },
      { flush: 'post' },
    )

    const domFocus = () => {
      vcDrawer.value?.domFocus?.()
    }

    const close = (e: Event) => {
      emit('update:visible', false)
      emit('update:open', false)
      emit('close', e)
    }

    const afterVisibleChange = (open: boolean) => {
      if (!open) {
        if (destroyClose.value === false) {
          // set true only once
          destroyClose.value = true
        }
        if (props.destroyOnClose) {
          load.value = false
        }
      }
      props.afterVisibleChange?.(open)
      emit('afterVisibleChange', open)
      emit('afterOpenChange', open)
    }

    const pushTransform = computed(() => {
      const { push, placement } = props
      let distance: number | string
      if (typeof push === 'boolean') {
        distance = push ? defaultPushState.distance : 0
      } else {
        distance = push!.distance
      }

      distance = Number.parseFloat(String(distance || 0))

      if (placement === 'left' || placement === 'right') {
        return `translateX(${placement === 'left' ? distance : -distance}px)`
      }
      if (placement === 'top' || placement === 'bottom') {
        return `translateY(${placement === 'top' ? distance : -distance}px)`
      }
      return null
    })
    // ============================ Size ============================
    const mergedWidth = computed(() => props.width ?? (props.size === 'large' ? 736 : 378))
    const mergedHeight = computed(() => props.height ?? (props.size === 'large' ? 736 : 378))
    const offsetStyle = computed(() => {
      // https://github.com/ant-design/ant-design/issues/24287
      const { mask, placement } = props
      if (!visible.value && !mask) {
        return {}
      }
      const val: CSSProperties = {}
      if (placement === 'left' || placement === 'right') {
        val.width = isNumeric(mergedWidth.value) ? `${mergedWidth.value}px` : mergedWidth.value
      } else {
        val.height = isNumeric(mergedHeight.value) ? `${mergedHeight.value}px` : mergedHeight.value
      }
      return val
    })

    const wrapperStyle = computed(() => {
      const { zIndex, contentWrapperStyle } = props
      const val = offsetStyle.value
      return [
        { zIndex, transform: sPush.value ? pushTransform.value : undefined },
        { ...contentWrapperStyle },
        val,
      ]
    })

    const renderHeader = (prefixCls: string) => {
      const { closable, headerStyle } = props
      const extra = getPropsSlot(slots, props, 'extra')
      const title = getPropsSlot(slots, props, 'title')
      if (!title && !closable) {
        return null
      }

      return (
        <div
          class={classnames(`${prefixCls}-header`, {
            [`${prefixCls}-header-close-only`]: closable && !title && !extra,
          })}
          style={headerStyle}
        >
          <div class={`${prefixCls}-header-title`}>
            {renderCloseIcon(prefixCls)}
            {title && <div class={`${prefixCls}-title`}>{title}</div>}
          </div>
          {extra && <div class={`${prefixCls}-extra`}>{extra}</div>}
        </div>
      )
    }

    const renderCloseIcon = (prefixCls: string) => {
      const { closable } = props
      const $closeIcon = slots.closeIcon ? slots.closeIcon?.() : props.closeIcon
      return (
        closable && (
          <button key="closer" onClick={close} aria-label="Close" class={`${prefixCls}-close`}>
            {$closeIcon === undefined ? <CloseOutlined></CloseOutlined> : $closeIcon}
          </button>
        )
      )
    }

    const renderBody = (prefixCls: string) => {
      if (destroyClose.value && !props.forceRender && !load.value) {
        return null
      }

      const { bodyStyle, drawerStyle } = props

      return (
        <div class={`${prefixCls}-wrapper-body`} style={drawerStyle}>
          {renderHeader(prefixCls)}
          <div key="body" class={`${prefixCls}-body`} style={bodyStyle}>
            {slots.default?.()}
          </div>
          {renderFooter(prefixCls)}
        </div>
      )
    }

    const renderFooter = (prefixCls: string) => {
      const footer = getPropsSlot(slots, props, 'footer')
      if (!footer) {
        return null
      }

      const footerClassName = `${prefixCls}-footer`
      return (
        <div class={footerClassName} style={props.footerStyle}>
          {footer}
        </div>
      )
    }
    const drawerClassName = computed(() =>
      classnames(
        {
          'no-mask': !props.mask,
          [`${prefixCls.value}-rtl`]: direction.value === 'rtl',
        },
        props.rootClassName,
        hashId.value,
      ),
    )
    // =========================== Motion ===========================
    const maskMotion = computed(() => {
      return getTransitionProps(getTransitionName(prefixCls.value, 'mask-motion'))
    })
    const panelMotion = (motionPlacement: string) => {
      return getTransitionProps(
        getTransitionName(prefixCls.value, `panel-motion-${motionPlacement}`),
      )
    }

    return () => {
      const { width, height, placement, mask, forceRender, ...rest } = props

      const vcDrawerProps: any = {
        ...attrs,
        ...omit(rest, [
          'size',
          'closeIcon',
          'closable',
          'destroyOnClose',
          'drawerStyle',
          'headerStyle',
          'bodyStyle',
          'title',
          'push',
          'onAfterVisibleChange',
          'onClose',
          'onUpdate:visible',
          'onUpdate:open',
          'visible',
        ]),
        forceRender,
        onClose: close,
        afterVisibleChange,
        handler: false,
        prefixCls: prefixCls.value,
        open: visible.value,
        showMask: mask,
        placement,
        ref: vcDrawer,
      }
      return wrapSSR(
        <NoCompactStyle>
          <VcDrawer
            {...vcDrawerProps}
            maskMotion={maskMotion.value}
            motion={panelMotion}
            width={mergedWidth.value}
            height={mergedHeight.value}
            getContainer={getContainer.value}
            rootClassName={drawerClassName.value}
            rootStyle={props.rootStyle}
            contentWrapperStyle={wrapperStyle.value}
            v-slots={{
              handler: props.handle ? () => props.handle : slots.handle,
              default: () => renderBody(prefixCls.value),
            }}
          ></VcDrawer>
        </NoCompactStyle>,
      )
    }
  },
})

export default withInstall(Drawer)
