<template>
  <el-form v-bind="ElFormProps" ref="formRef" class="base-form" @submit.prevent>
    <template v-for="formGroup in formGroups" :key="formGroup.groupKey">
      <component :is="props.transition ? ElCollapseTransition : LessRenderComponent">
        <custom-block v-show="!formGroup.hidden" :title="formGroup.groupTitle">
          <template v-for="row in formGroup.rows" :key="row.rowKey">
            <component :is="props.transition ? ElCollapseTransition : LessRenderComponent">
              <el-row v-show="!row.rowProp.hidden" v-bind="row.rowProp">
                <template v-for="col in row.cols" :key="col.colKey">
                  <el-col v-show="!col.hidden" v-bind="col.colProp">
                    <el-form-item
                      v-bind="
                        omit(col, ['colProp', 'slot', 'rowKey', 'colIndex', 'slotType', 'slotName', 'itemDescription'])
                      "
                    >
                      <template
                        v-for="[usedItemSlotName, itemSlotName] in getSlotKeys(col, 'item', ['label', 'error'])"
                        #[itemSlotName]
                      >
                        <slot
                          :name="usedItemSlotName"
                          v-bind="{ item: col, model: props.model, index: col.colIndex }"
                        ></slot>
                      </template>
                      <template #default>
                        <slot
                          :name="getSlotKeys(col, 'item', ['default'])[0]?.[0]"
                          v-bind="{ item: col, model: props.model, index: col.colIndex }"
                        >
                          <component
                            :is="FormItemComponentMap[col.slotType]"
                            v-if="col.slotType"
                            v-bind="Object.assign({}, FormItemComponentDefault[col.slotType], col.slot)"
                            v-model="getFieldValue(props.model, col.prop).value"
                          >
                            <template
                              v-for="[usedSlotName, slotName] in getSlotKeys(
                                col,
                                col.slotType,
                                FormItemComponentSlots[col.slotType]
                              )"
                              #[slotName]
                            >
                              <slot
                                :name="usedSlotName"
                                v-bind="{ item: col, model: props.model, index: col.colIndex }"
                              ></slot>
                            </template>
                          </component>
                        </slot>
                      </template>
                    </el-form-item>
                  </el-col>
                  <el-row v-if="col.itemDescription?.description || getSlotKeys(col, 'item', ['description'])[0]?.[0]">
                    <slot
                      :name="getSlotKeys(col, 'item', ['description'])[0]?.[0]"
                      v-bind="{ item: col, model: props.model, index: col.colIndex }"
                    >
                      <el-form-item label-width="0">
                        <span
                          v-if="col.itemDescription?.description"
                          :key="getSlotKeys(col, 'item', ['description'])[0]?.[0]"
                          class="inline-flex items-center form-item-description"
                          :class="{ 'required-asterisk': col.itemDescription?.requiredAsterisk !== false }"
                        >
                          {{ col.itemDescription?.description }}
                        </span>
                      </el-form-item>
                    </slot>
                  </el-row>
                </template>
              </el-row>
            </component>
          </template>
        </custom-block>
      </component>
    </template>
    <slot></slot>
  </el-form>
</template>

<script setup lang="tsx" name="EpForm">
import { computed, useSlots, shallowRef, markRaw, defineComponent } from 'vue'
import {
  ElForm,
  ElFormItem,
  ElInput,
  ElInputNumber,
  ElAutocomplete,
  ElSwitch,
  ElRadio,
  ElDatePicker,
  ElRow,
  ElCol,
  ElCollapseTransition,
} from 'element-plus'
import CustomBlock from '@/components/common/CustomBlock.vue'
import BaseSelect from '@/components/element/BaseSelect.vue'
import BaseCheckBox from '@/components/element/BaseCheckBox.vue'
import BaseRadio from '@/components/element/BaseRadio.vue'
import { get, set, omit } from 'lodash-es'
import type { EpFormItem, BaseFormProp, FormSupportComponentMap, FormGroup, EpFormRows } from 'global-type'

const LessRenderComponent = defineComponent({
  name: 'LessRender',
  render() {
    const slots = useSlots()
    return slots?.default?.()
  },
})

interface BaseEpForm extends BaseFormProp {
  model: Record<string, unknown>
  rules?: Required<BaseFormProp>['rules']
  items?: EpFormItem[]
  rows?: EpFormRows
  groups?: FormGroup[]
  transition?: boolean
}

type RequiredGroup = Required<ExtractArrayValue<Required<BaseEpForm>['groups']>>

type FormatterFormItem = AssignKeys<Required<BaseEpForm>['items'][0], { colIndex: number; colKey: string | number }>

interface FormatterFormGroup extends Omit<RequiredGroup, 'rowKeys'> {
  rows: {
    rowKey: string | number
    rowProp: Omit<ExtractRecordValue<Required<BaseEpForm>['rows']>, 'rowKey'>
    cols: FormatterFormItem[]
  }[]
}

const FormItemComponentMap = markRaw<FormSupportComponentMap>({
  autocomplete: ElAutocomplete,
  input: ElInput,
  inputNumber: ElInputNumber,
  checkbox: BaseCheckBox,
  radio: BaseRadio,
  select: BaseSelect,
  date: ElDatePicker,
  dateTime: ElDatePicker,
  switch: ElSwitch,
})

const FormItemComponentSlots = markRaw<Record<keyof FormSupportComponentMap, string[]>>({
  autocomplete: ['prefix', 'suffix', 'prepend', 'append', 'default'],
  input: ['prefix', 'suffix', 'prepend', 'append'],
  inputNumber: [],
  checkbox: ['default'],
  radio: ['default'],
  select: ['prefix', 'empty', 'default'],
  switch: [],
  date: ['range-separator', 'default'],
  dateTime: ['range-separator', 'default'],
})

const FormItemComponentDefault = markRaw<Partial<Record<keyof FormSupportComponentMap, EpFormItem['slot']>>>({
  inputNumber: {
    controls: false,
  },
})

const props = defineProps<BaseEpForm>()

const ElFormProps = computed(() => {
  const { items, groups, rows, ...formAttr } = props
  return formAttr
})

const formGroups = computed<FormatterFormGroup[]>(() => {
  const rows = props.items?.reduce((rowMap, formItem, index) => {
    const itemRowKey = formItem.rowKey || 'row-' + (index + 1)
    rowMap[itemRowKey] ??= { rowKey: itemRowKey, rowProp: props.rows?.[itemRowKey] || {}, groupUsed: false, cols: [] }
    rowMap[itemRowKey].cols.push({ ...formItem, colKey: `col-${rowMap[itemRowKey].cols.length}`, colIndex: index })
    return rowMap
  }, {} as Record<string | number, FormatterFormGroup['rows'][0] & { groupUsed: boolean }>)

  const groups = props.groups?.map((groupConfig, index) => {
    let groupRows: FormatterFormGroup['rows'] = []
    groupConfig.rowKeys?.forEach((rowKey) => {
      if (rows?.[rowKey]) {
        rows[rowKey].groupUsed = true
        groupRows.push(rows[rowKey])
      }
    })
    const groupInfo: FormatterFormGroup = {
      hidden: groupConfig.hidden ?? false,
      groupKey: groupConfig.groupKey || 'group-' + (index + 1),
      groupTitle: groupConfig.groupTitle || '',
      rows: groupRows,
    }
    return groupInfo
  })

  return (groups || []).concat({
    rows: Object.values(rows || {}).filter((row) => !row.groupUsed),
    groupKey: 'default-group',
    groupTitle: '',
    hidden: false,
  })
})

const slots = useSlots()

function getSlotKeys(formItem: FormatterFormItem, itemName: string, slotNames: string[]) {
  const tag = (formItem.slotName || formItem.prop || formItem.colIndex) ?? undefined
  const slotKey = slotNames.map((slotName) => [
    ['form', itemName, tag, slotName === 'default' ? '' : slotName].filter((t) => t !== '').join('-'),
    slotName,
  ])
  return slotKey.filter(([slotName]) => typeof slots[slotName] === 'function')
}

function getFieldValue(prop: Record<string, any>, key?: string | string[]) {
  return {
    get value() {
      return key ? get(prop, key) : undefined
    },
    set value(val: any) {
      key ? set(prop, key, val) : void 0
    },
  }
}

const formRef = shallowRef<InstanceType<typeof ElForm>>()

defineExpose({ formRef })
</script>

<style scoped lang="scss">
.base-form {
  :deep(.el-select) {
    width: 100%;
  }

  &.el-form--inline :deep(.el-form-item) {
    align-items: center;
    margin-bottom: 0;
  }

  :deep(.el-form-item__content) {
    word-break: break-all;
  }

  .form-item-description {
    margin-left: 40px;
    color: $FormItemDescriptionColor;
    font-size: $SmallFont;
    &.required-asterisk:before {
      content: '*';
      color: $DangerColor;
      margin-right: 0.285em;
    }
  }
}
</style>
