<script setup lang="ts">
import lodash from "lodash";
import { computed, getCurrentInstance, nextTick, onMounted, reactive, watch } from "vue";
import { breakpointsTailwind, useBreakpoints } from "@vueuse/core";
import { ElForm, ElFormItem, ElLoadingDirective } from "element-plus";
import { Expression, Request, Typeof, Utils } from "@ease-forge/shared";
import { calcBreakpointValue, toVNode } from "@/utils/Utils.ts";
import { type DataFormData, type DataFormItem, type DataFormProps, type DataFormState, type FormData, type FormField, type WatchFormFieldValue } from "./DataFormTypes.ts";
import { dataFormInputComponents } from "./DataFormConstant.ts";
import { dataPathToNamePath } from "./DataFormUtils.tsx";

const vLoading = ElLoadingDirective;

defineOptions({
    name: 'DataForm',
});

// 组件事件定义
const emit = defineEmits<{
    /** loading变化事件 */
    loadingChange: [loading: boolean];
    /** 表单字段变化 */
    // fieldsChange: [changedFields: Array<FieldData>, allFields: Array<FieldData>];
    /** 表单值变化 */
    dataChange: [newData: any];
}>();

// 定义组件插槽
const slots = defineSlots<{
    /** 提交区域插槽 */
    submit?: (props?: any) => Array<VueNode>;
}>();

// 当前组件对象
const instance = getCurrentInstance();

// 读取组件 props 属性
const props = withDefaults(defineProps<DataFormProps>(), {
    name: () => lodash.uniqueId("data_form_"),
    defColumnCount: 2,
    layout: "onlyLabelFixed",
    labelWidth: "120px",
    formItemHeightMode: "mini",
    components: () => dataFormInputComponents,
});

// state 属性
const state = reactive<DataFormState>({
    loading: false,
    data: props.data,
    columnCount: calcColumnCount(props.columnCount),
    dataFormItems: toDataFormItems(props.formFields, props.data),
});

// 内部数据
const data: DataFormData = {
    firstDataChange: true,
    oldData: {},
    formRef: null,
    inputRefs: {},
    ctxData: {},
};

// 响应式断点
const breakpoints = useBreakpoints(props.breakpoints ?? breakpointsTailwind);
// 表单行内容
const formRows = computed(() => getFormRows(state.dataFormItems, state.columnCount));

// 监听响应式断点
if (Typeof.isObj(props.columnCount)) {
    watch(breakpoints.current(), current => state.columnCount = calcColumnCount(props.columnCount, current), { immediate: true });
}
// 加载数据
if (props.autoLoadData && props.dataApi) reload().finally();
// 监听data
watch(() => state.data, newData => {
    const firstDataChange = data.firstDataChange;
    dataChange(state.dataFormItems, newData);
    if (!firstDataChange) emit("dataChange", newData);
}, { immediate: true, deep: true });
// 监听loading
watch(() => state.loading, loading => emit("loadingChange", loading));
// 设置ctxData(表单上下文数据)
onMounted(() => data.ctxData.instance = instance!);

/** bothFixed：label和input都固定宽度布局 */
function isBothFixed() {
    return props.layout === "bothFixed";
}

/**
 * 将 Array<FormField> 装换成 Array<DataFormItem>
 * @param formFields    表单字段配置
 * @param formData      表单数据
 */
function toDataFormItems(formFields?: Array<FormField>, formData?: FormData) {
    if (!formFields) return [];
    return formFields.map(formField => toDataFormItem(formField, formData));
}

/**
 * 将 FormField 装换成 DataFormItem
 * @param formField 表单字段配置
 * @param formData  表单数据
 */
function toDataFormItem(formField: FormField, formData?: FormData) {
    const { dataPath, widthCount, label, input, inputProps, format, transform, extInputs, hidden, watchValues, rawProps } = formField;
    const item: DataFormItem = {
        widthCount: widthCount ?? 1,
        inputRef: dataPath?.replaceAll(/[.\[\]]/g, '_') + '_' + lodash.uniqueId(),
        formItemProps: { ...rawProps },
        dataPath,
        format,
        transform,
        hidden,
    };
    if (dataPath) {
        item.formItemProps.name = dataPathToNamePath(dataPath);
    } else {
        item.formItemProps.autoLink = false;
    }
    if (dataPath && Typeof.isFunction(label)) {
        const dataValue = Expression.getKeyPathValue(dataPath, formData);
        const vnode = label(dataValue, dataPath, formData);
        item.formItemProps.label = toVNode(vnode);
    } else if (label) {
        item.formItemProps.label = label;
    }
    if (input) {
        item.input = resolveInputComponent(input);
        if (item.input && inputProps) {
            item.inputProps = inputProps;
        }
    }
    if (Typeof.isArray(watchValues)) {
        item.watchValues = watchValues;
    } else if (watchValues) {
        item.watchValues = [watchValues];
    }
    // formItemProps
    const { style, class: className, labelWidth, labelPosition, showMessage, inlineMessage, rules, required } = formField;
    if (Typeof.hasValue(style)) item.formItemProps.style = style;
    if (Typeof.hasValue(className)) item.formItemProps.class = className;
    if (Typeof.hasValue(labelWidth)) item.formItemProps.labelWidth = labelWidth;
    if (Typeof.hasValue(labelPosition)) item.formItemProps.labelPosition = labelPosition;
    if (Typeof.hasValue(showMessage)) item.formItemProps.showMessage = showMessage;
    if (Typeof.hasValue(inlineMessage)) item.formItemProps.inlineMessage = inlineMessage;
    if (Typeof.hasValue(rules)) item.formItemProps.rules = rules;
    if (Typeof.hasValue(required)) item.formItemProps.required = required;
    // TODO bothFixed 配置下，字段的 widthCount > 1 时，字段宽度需要自适应
    if (widthCount && widthCount > 1 && isBothFixed()) {
        // if (!item.formItemProps.wrapperCol) item.formItemProps.wrapperCol = {};
        // if (!item.formItemProps.wrapperCol.style) item.formItemProps.wrapperCol.style = {};
        // const width = `calc((${props.labelWidth} + ${props.inputWidth}) * ${widthCount - 1} + ${props.inputWidth})`;
        // if (!item.formItemProps.wrapperCol.style.width) item.formItemProps.wrapperCol.style.width = width;
        // if (!item.formItemProps.wrapperCol.style.flex) item.formItemProps.wrapperCol.style.flex = `0 0 ${width}`;
    }
    // extInputs
    if (Typeof.isArray(extInputs)) {
        item.extInputs = extInputs.map(extInput => toDataFormItem(extInput, formData));
    }
    return item;
}

/** 获取 input vue组件 */
function resolveInputComponent(input: any) {
    if (!Typeof.isStr(input)) return input;
    const components = props.components ?? {};
    const cmp = components[input];
    if (!cmp) console.warn("input组件未注册：", input);
    return cmp;
}

/**
 * 根据 dataFormItems 和 columnCount 配置计算出表单行渲染内容
 * @param dataFormItems 表单项数组
 * @param columnCount   一行显示的字段数量
 */
function getFormRows(dataFormItems: Array<DataFormItem>, columnCount: number) {
    if (columnCount < 1) columnCount = 1;
    const formRows: Array<Array<DataFormItem>> = [];
    let formRow: Array<DataFormItem> | undefined;
    let columnIndex = 0;
    for (let formItem of dataFormItems) {
        if (formItem.hidden) {
            continue;
        }
        // 第一行
        if (!formRow) {
            formRow = [];
            formRows.push(formRow);
        }
        // 判断是否需要换行
        if (columnIndex >= columnCount) {
            columnIndex = 0;
            formRow = [];
            formRows.push(formRow);
        }
        // 当前行剩下的列数
        const leftOverColumnCount = columnCount - columnIndex;
        // 当前数据需要占用的列数
        let currentColumnCount = Math.min(formItem.widthCount, columnCount);
        // 剩下的列数不足以放下当前表单项，需要换行
        if (currentColumnCount > leftOverColumnCount) {
            columnIndex = 0;
            formRow = [];
            formRows.push(formRow);
        }
        // 增加列索引
        columnIndex += currentColumnCount;
        // formItem 加入当前行
        formRow.push(formItem);
    }
    // 增加 submit 区域
    if (slots.submit && formRows.length > 0) {
        formRow = formRows[formRows.length - 1];
        const nextRow = formRow.length >= columnCount;
        const submitFormItem: DataFormItem = {
            widthCount: nextRow ? columnCount : columnCount - formRow.length,
            inputRef: "__submit",
            input: slots.submit,
            inputProps: {},
            formItemProps: {
                autoLink: false,
                ...props.submitFormItemProps,
            },
        };
        if (nextRow) {
            formRows.push([submitFormItem]);
        } else {
            // 优化 submit 区域与上一个字段直接的间距
            if (!submitFormItem.formItemProps.style) submitFormItem.formItemProps.style = {};
            if (!submitFormItem.formItemProps.style.paddingLeft && !submitFormItem.formItemProps.style["padding-left"]) {
                submitFormItem.formItemProps.style.paddingLeft = "12px";
            }
            formRow.push(submitFormItem);
        }
        // 优化表单只有单行时的 submit 区域宽度(更加自然合理)
        if (formRows.length === 1) {
            if (submitFormItem.formItemProps.class) {
                submitFormItem.formItemProps.class = `${submitFormItem.formItemProps.class} data-form-item-auto`;
            } else {
                submitFormItem.formItemProps.class = "data-form-item-auto";
            }
        }
    }
    return formRows;
}

/** 表单数据变化，执行watchValues */
function dataChange(dataFormItems: Array<DataFormItem>, newFormData?: FormData) {
    // Record<dataPath, Array<{watch, watchOwnerDataFormItem}>>
    const watchRecord: Record<string, Array<{ watch: WatchFormFieldValue; item: DataFormItem; }>> = {};
    for (let dataFormItem of dataFormItems) {
        const { watchValues } = dataFormItem;
        if (!watchValues) continue;
        for (let watchValue of watchValues) {
            const { dataPath, onChange } = watchValue;
            if (!dataPath || !onChange) continue;
            if (!watchRecord[dataPath]) watchRecord[dataPath] = [];
            watchRecord[dataPath].push({ watch: watchValue, item: dataFormItem });
        }
    }
    // 收集 watchValues onChange
    const watchFns: Array<Function> = [];
    for (let dataPath in watchRecord) {
        const watchItems = watchRecord[dataPath];
        const oldValue = data.oldData[dataPath];
        const newValue = Expression.getKeyPathValue(dataPath, newFormData);
        const hasChange = !Utils.equalsValues(oldValue, newValue);
        for (let watchItem of watchItems) {
            const { watch: { onChange, immediate }, item: dataFormItem } = watchItem;
            if (!onChange) continue;
            const input = data.inputRefs[dataFormItem.inputRef];
            const form = data.formRef;
            if (immediate && data.firstDataChange) {
                // 配置了 immediate, 第一次触发 onChange 会忽略 hasChange
                watchFns.push(() => onChange(oldValue, newValue, newFormData, dataFormItem, input, form, data.ctxData));
            } else if (hasChange) {
                watchFns.push(() => onChange(oldValue, newValue, newFormData, dataFormItem, input, form, data.ctxData));
            }
        }
        data.oldData[dataPath] = newValue;
    }
    data.firstDataChange = false;
    // 执行 watchValues | 理论上不需要 nextTick, 由于 antd 组件的 bug 才使用 nextTick
    nextTick(() => watchFns.forEach(onChange => onChange()));
}

/**
 * 根据响应式断点配置，计算一行显示的字段数量
 * @param columnCount   响应式断点配置
 * @param current       当前响应式断点值
 */
function calcColumnCount(columnCount?: DataFormProps["columnCount"], current?: Array<string>): number {
    return Math.min(calcBreakpointValue(columnCount, current, props.defColumnCount ?? 2), 24);
}

/**
 * 重新加载数据
 * @param dataApi 请求配置
 */
async function reload(dataApi?: HttpRequestConfig<FormData>) {
    const requestConfig: HttpRequestConfig<FormData> = lodash.defaultsDeep({}, dataApi, props.dataApi);
    if (Typeof.noValue(requestConfig.url)) {
        console.warn("未配置服务端数据API url");
        return;
    }
    state.loading = true;
    try {
        state.data = await Request.request.request(requestConfig);
    } finally {
        state.loading = false;
    }
}

/** 计算输入组件的值 */
function calcInputValue(dataFormItem: DataFormItem) {
    const { dataPath, format } = dataFormItem;
    if (!dataPath) return;
    const dataValue = Expression.getKeyPathValue(dataPath, state.data);
    if (!format) return dataValue;
    if (Typeof.isArray(format)) {
        const dictItem = window.globalConfig.matchDictItem(format, dataValue);
        return dictItem?.text ?? dataValue;
    } else if (Typeof.isFunction(format)) {
        return format(dataValue, dataPath, state.data);
    }
    return dataValue;
}

/** 设置输入值 */
function setInputValue(newVal: any, dataFormItem: DataFormItem) {
    const { dataPath, transform } = dataFormItem;
    if (!dataPath) return;
    let value = newVal;
    if (Typeof.isArray(transform)) {
        const dictItem = window.globalConfig.matchDictItem(transform, newVal);
        value = dictItem?.text ?? newVal;
    } else if (Typeof.isFunction(transform)) {
        value = transform(newVal, dataPath, state.data);
    }
    Expression.setKeyPathValue(dataPath, state.data, value);
}

interface DataFormExpose {
    state: DataFormState;
    data: DataFormData;
}

const expose: DataFormExpose = {
    state,
    data,
};
// 定义组件公开内容
defineExpose(expose);

export type {
    DataFormProps,
    DataFormState,
}
</script>

<template>
    <ElForm
        :ref="($ref: any) => data.formRef = $ref"
        :class="[
            'data-form',
            {
                'data-form-label-fixed': !isBothFixed(),
                'data-form-label-input-fixed': isBothFixed(),
            },
        ]"
        v-loading="state.loading"
        :labelPosition="props.labelPosition"
        :labelSuffix="props.labelSuffix"
        :labelWidth="props.labelWidth"
        :size="props.size"
        :showMessage="props.showMessage"
        :inlineMessage="props.inlineMessage"
        :requireAsteriskPosition="props.requireAsteriskPosition"
        :hideRequiredAsterisk="props.hideRequiredAsterisk"
        :rules="props.rules"
        :validateOnRuleChange="props.validateOnRuleChange"
        :disabled="props.disabled"
        :scrollToError="props.scrollToError"
        :scrollIntoViewOptions="props.scrollIntoViewOptions"
        v-bind="props.rawProps"
        :model="state.data"
    >
        <div v-for="(formRow, rowIdx) in formRows" class="data-form-row">
            <template v-for="(dataFormItem, celIdx) in formRow">
                <div
                    v-if="dataFormItem.hidden"
                    :key="`div_${rowIdx}_${celIdx}`"
                    :class="[
                        'data-form-item',
                        'data-form-item-hidden',
                        `data-form-item-flex-${Math.min(dataFormItem.widthCount, state.columnCount)}`,
                        {
                            'multiple-width-count': Math.min(dataFormItem.widthCount, state.columnCount) > 1,
                        },
                    ]"
                />
                <ElFormItem
                    v-else
                    :key="`form_item_${rowIdx}_${celIdx}`"
                    v-bind="dataFormItem.formItemProps"
                    :class="[
                        'data-form-item',
                        `data-form-item-flex-${Math.min(dataFormItem.widthCount, state.columnCount)}`,
                        {
                            'multiple-width-count': Math.min(dataFormItem.widthCount, state.columnCount) > 1,
                            'data-form-item-has-ext-input': dataFormItem.extInputs && dataFormItem.extInputs.length > 0,
                        },
                    ]"
                >
                    <component
                        v-if="dataFormItem.input"
                        :key="dataFormItem.inputRef"
                        class="data-form-item-main-input"
                        :is="dataFormItem.input"
                        :ref="($ref: any) => data.inputRefs[dataFormItem.inputRef] = $ref"
                        v-bind="dataFormItem.inputProps"
                        :modelValue="calcInputValue(dataFormItem)"
                        @update:modelValue="(newVal: any) => setInputValue(newVal, dataFormItem)"
                    />
                    <template v-for="(extInput, innerIdx) in dataFormItem.extInputs">
                        <ElFormItem
                            :key="`${rowIdx}_${celIdx}_${innerIdx}`"
                            v-if="extInput.input"
                            v-bind="extInput.formItemProps"
                            class="data-form-item-ext-input"
                        >
                            <component
                                v-if="extInput.input"
                                :key="extInput.inputRef"
                                :is="extInput.input"
                                :ref="($ref: any) => data.inputRefs[extInput.inputRef] = $ref"
                                v-bind="extInput.inputProps"
                                :modelValue="calcInputValue(extInput)"
                                @update:modelValue="(newVal: any) => setInputValue(newVal, extInput)"
                            />
                        </ElFormItem>
                    </template>
                </ElFormItem>
            </template>
            <div
                v-if="state.columnCount - lodash.sum(formRow.map(item => item.widthCount)) > 0"
                :key="`div_${rowIdx}`"
                :class="[
                    'data-form-item',
                    'data-form-item-empty',
                    `data-form-item-flex-${state.columnCount - lodash.sum(formRow.map(item => item.widthCount))}`,
                ]"
            />
        </div>
    </ElForm>
</template>

<style scoped>
.data-form {
    width: 100%;
    height: 100%;
}

.data-form :deep(.el-date-editor) {
    --el-input-width: 100%;
    --el-date-editor-width: 100%;
}

.data-form :deep(.el-cascader) {
    width: 100%;
}

.data-form-row {
    display: flex;
    flex-direction: row;
    flex-wrap: nowrap;
}

.data-form-row > .data-form-item[class*=data-form-item-flex-]:not(.multiple-width-count) {
    flex-shrink: 0;
}

.data-form-item.data-form-item-flex-1 {
    flex: 1 1 0;
}

.data-form-item.data-form-item-flex-2 {
    flex: 2 1 0;
}

.data-form-item.data-form-item-flex-3 {
    flex: 3 1 0;
}

.data-form-item.data-form-item-flex-4 {
    flex: 4 1 0;
}

.data-form-item.data-form-item-flex-5 {
    flex: 5 1 0;
}

.data-form-item.data-form-item-flex-6 {
    flex: 6 1 0;
}

.data-form-item.data-form-item-flex-7 {
    flex: 7 1 0;
}

.data-form-item.data-form-item-flex-8 {
    flex: 8 1 0;
}

.data-form-item.data-form-item-flex-9 {
    flex: 9 1 0;
}

.data-form-item.data-form-item-flex-10 {
    flex: 10 1 0;
}

.data-form-item.data-form-item-flex-11 {
    flex: 11 1 0;
}

.data-form-item.data-form-item-flex-12 {
    flex: 12 1 0;
}

.data-form-item.data-form-item-flex-13 {
    flex: 13 1 0;
}

.data-form-item.data-form-item-flex-14 {
    flex: 14 1 0;
}

.data-form-item.data-form-item-flex-15 {
    flex: 15 1 0;
}

.data-form-item.data-form-item-flex-16 {
    flex: 16 1 0;
}

.data-form-item.data-form-item-flex-17 {
    flex: 17 1 0;
}

.data-form-item.data-form-item-flex-18 {
    flex: 18 1 0;
}

.data-form-item.data-form-item-flex-19 {
    flex: 19 1 0;
}

.data-form-item.data-form-item-flex-20 {
    flex: 20 1 0;
}

.data-form-item.data-form-item-flex-21 {
    flex: 21 1 0;
}

.data-form-item.data-form-item-flex-22 {
    flex: 22 1 0;
}

.data-form-item.data-form-item-flex-23 {
    flex: 23 1 0;
}

.data-form-item.data-form-item-flex-24 {
    flex: 24 1 0;
}
</style>
