/**
 * @license MIT
 * author: Cube Dev Team
 * @cube-dev/ui-kit v0.98.7
 * Released under the MIT license.
 */

import { useRef, useState } from 'react';
import { dotize } from '../../../tasty/index';
import { applyRules } from './validation';
function setValue(obj, path, value) {
    let a = path.split('.');
    let o = obj;
    while (a.length - 1) {
        let n = a.shift();
        if (!(n in o))
            o[n] = {};
        o = o[n];
    }
    o[a[0]] = value;
}
function isEqual(v1, v2) {
    return JSON.stringify(v1) === JSON.stringify(v2);
}
export class CubeFormInstance {
    forceReRender = () => { };
    defaultValues = {};
    fields = {};
    ref = {};
    isSubmitting = false;
    submitError = null;
    onValuesChange = () => { };
    onSubmit = () => { };
    constructor(forceReRender = () => { }) {
        this.forceReRender = forceReRender;
        this.getFieldValue = this.getFieldValue.bind(this);
        this.getFieldsValue = this.getFieldsValue.bind(this);
        this.setFieldValue = this.setFieldValue.bind(this);
        this.getFieldError = this.getFieldError.bind(this);
        this.getFieldInstance = this.getFieldInstance.bind(this);
        this.setInitialFieldsValue = this.setInitialFieldsValue.bind(this);
        this.resetFields = this.resetFields.bind(this);
        this.validateField = this.validateField.bind(this);
        this.validateFields = this.validateFields.bind(this);
        this.isFieldValid = this.isFieldValid.bind(this);
        this.createField = this.createField.bind(this);
        this.isFieldInvalid = this.isFieldInvalid.bind(this);
        this.isFieldTouched = this.isFieldTouched.bind(this);
        this.setFields = this.setFields.bind(this);
    }
    async submit() {
        return this.onSubmit?.(this.getFormData());
    }
    setFieldsValue = (newData, touched, skipRender) => {
        let flag = false;
        newData = { ...newData, ...dotize.convert(newData) };
        Object.keys(newData).forEach((name) => {
            let field = this.fields[name];
            if (!field) {
                return;
            }
            if (!field || isEqual(field.value, newData[name])) {
                if (field) {
                    field.errors = [];
                    field.status = undefined;
                }
                return;
            }
            flag = true;
            field.value = newData[name];
            field.errors = [];
            field.status = undefined;
            if (touched === true) {
                field.touched = touched;
            }
            else if (touched === false) {
                field.touched = false;
            }
        });
        if (flag && !skipRender) {
            if (touched) {
                this.onValuesChange && this.onValuesChange(this.getFormData());
                this.submitError = null;
            }
            this.forceReRender();
        }
    };
    getFieldValue(name) {
        return this.fields[name]?.value;
    }
    getFieldsValue() {
        return Object.values(this.fields).reduce((map, field) => {
            if (field && map) {
                map[field.name] = field.value;
            }
            return map;
        }, {});
    }
    /**
     * Similar to getFieldsValue() but respects '.' notation and creates nested objects.
     */
    getFormData() {
        const fieldsValue = this.getFieldsValue();
        return Object.keys(fieldsValue)
            .sort()
            .reduce((map, field) => {
            setValue(map, field, fieldsValue[field]);
            if (field.includes('.')) {
                delete map[field];
            }
            return map;
        }, {});
    }
    setFieldValue(name, value, isTouched = false, skipRender = false) {
        const field = this.fields[name];
        if (!field || isEqual(value, field.value)) {
            return;
        }
        field.value = value;
        if (typeof value === 'object' && !Array.isArray(value)) {
            Object.keys(this.fields)
                .filter((key) => key.startsWith(`${name}.`))
                .forEach((key) => {
                const objKey = key.replace(`${name}.`, '');
                this.setFieldValue(key, value[objKey] ?? null, isTouched, false);
            });
        }
        field.errors = [];
        field.status = undefined; // reset validation status
        if (isTouched) {
            field.touched = isTouched;
        }
        if (isTouched) {
            this.onValuesChange && this.onValuesChange(this.getFormData());
            this.submitError = null;
        }
        if (!skipRender) {
            this.forceReRender();
        }
    }
    getFieldInstance(name) {
        return this.fields[name];
    }
    setInitialFieldsValue(values) {
        this.defaultValues = { ...values, ...dotize.convert(values) };
    }
    updateInitialFieldsValue(values) {
        this.defaultValues = {
            ...this.defaultValues,
            ...values,
            ...dotize.convert(values),
        };
    }
    resetFields(names, skipRender) {
        names = names ?? Object.keys(this.fields);
        names.forEach((fieldName) => {
            const field = this.fields[fieldName];
            if (!field) {
                return;
            }
            const defaultValue = this.defaultValues[fieldName] ?? undefined;
            field.value = defaultValue;
            field.touched = false;
            field.errors = [];
            field.status = undefined;
            field.validationId = (field.validationId ?? 0) + 1;
        });
        if (!skipRender) {
            this.forceReRender();
        }
    }
    async validateField(name) {
        const field = this.getFieldInstance(name);
        if (
        // if there are no rules for such field
        !field ||
            !field.rules ||
            // or field is already validated and valid
            field.status === 'valid') {
            return Promise.resolve();
            // or field is already validated and invalid
        }
        else if (field.status === 'invalid') {
            return Promise.reject(field.errors[0]);
        }
        field.validating = true;
        field.status = undefined;
        field.validationId = (field.validationId ?? 0) + 1;
        const validationId = field.validationId;
        // store validation to make sure there is no race condition.
        return applyRules(field, this, validationId)
            .then(() => {
            if (field.validationId !== validationId)
                return;
            field.validating = false;
            if (!field.errors || field.errors.length) {
                field.errors = [];
            }
            field.status = 'valid';
            this.forceReRender();
        })
            .catch((err) => {
            if (field.validationId === validationId) {
                field.errors = [err];
                field.validating = false;
                field.status = 'invalid';
                this.forceReRender();
            }
            return Promise.reject([err]);
        });
    }
    validateFields(names) {
        const fieldsList = names || Object.keys(this.fields);
        const errorList = [];
        return Promise.allSettled(fieldsList.map((name) => {
            return this.validateField(name).catch((errors) => {
                errorList.push({ name, errors });
                return Promise.reject();
            });
        })).then(() => {
            if (errorList.length) {
                return Promise.reject(errorList);
            }
            return this.getFormData();
        });
    }
    isFieldValid(name) {
        const field = this.getFieldInstance(name);
        if (!field)
            return true;
        return !field.errors.length;
    }
    isFieldInvalid(name) {
        const field = this.getFieldInstance(name);
        if (!field)
            return false;
        return !!field.errors.length;
    }
    isFieldTouched(name) {
        const field = this.getFieldInstance(name);
        if (!field)
            return false;
        return !!field.touched;
    }
    get isTouched() {
        return Object.values(this.fields).some((field) => field?.touched);
    }
    get isDirty() {
        return Object.values(this.fields).some((field) => {
            return field && field.name
                ? JSON.stringify(field?.value) !==
                    JSON.stringify(this.defaultValues[field?.name])
                : false;
        });
    }
    /**
     * True if all fields are verified and valid
     * IMPORTANT: This is not the same as `!isInvalid`, because it also checks if all fields are verified.
     */
    get isValid() {
        return Object.values(this.fields).every((field) => {
            return field?.status === 'valid';
        });
    }
    /**
     * True if at least one field is verified and invalid.
     * IMPORTANT: This is not the same as `!isValid`, because it only checks that at least
     * one field is verified and invalid.
     */
    get isInvalid() {
        return Object.values(this.fields).some((field) => {
            return field?.status === 'invalid';
        });
    }
    getFieldError(name) {
        const field = this.getFieldInstance(name);
        if (!field)
            return [];
        return field.errors || [];
    }
    /**
     * @deprecated This field is not supposed to be used directly.
     */
    createField(name, skipRender) {
        // passing an empty name is incorrect, but we have to return a valid object to avoid inconsistency
        if (!name) {
            return this._createField(name);
        }
        if (!this.fields[name]) {
            this.fields[name] = this._createField(name);
        }
        if (!skipRender) {
            this.forceReRender();
        }
        return this.fields[name];
    }
    /**
     * @deprecated This field is not supposed to be used directly.
     */
    removeField(name, skipRender) {
        if (this.fields[name]) {
            delete this.fields[name];
        }
        if (!skipRender) {
            this.forceReRender();
        }
    }
    /**
     * @deprecated This method is not recommended. Use other ways to alter fields.
     * Use `setFieldValue` to change the value of a field.
     * Use `verifyField` to validate a field.
     * Use `setFieldError` to set an error for a field.
     * Use `clearFieldsValidation` to clear all errors for a field.
     */
    setFields(newFields) {
        newFields.forEach(({ name, value, errors }) => {
            this.fields[name] = this._createField(name, {
                value,
                errors,
            });
        });
        this.forceReRender();
    }
    resetFieldsValidation(names, skipRender) {
        (names || Object.keys(this.fields)).forEach((name) => {
            const field = this.getFieldInstance(name);
            if (!field)
                return;
            field.errors = [];
            field.status = undefined;
            field.validationId = (field.validationId ?? 0) + 1;
        });
        if (!skipRender) {
            this.forceReRender();
        }
    }
    setFieldError(name, error, skipRender) {
        const field = this.getFieldInstance(name);
        if (!field || !error)
            return;
        field.errors = [error];
        field.status = 'invalid';
        field.validationId = (field.validationId ?? 0) + 1;
        if (!skipRender) {
            this.forceReRender();
        }
    }
    setSubmitting(isSubmitting) {
        if (this.isSubmitting === isSubmitting)
            return;
        this.isSubmitting = isSubmitting;
        this.forceReRender();
    }
    _createField(name, data) {
        let obj = {
            name,
            validating: false,
            touched: false,
            errors: [],
            validationId: 0,
            value: this.defaultValues[name],
            ...data,
            // it should be impossible to define or override status value
            status: data?.errors?.length ? 'invalid' : undefined,
        };
        return obj;
    }
}
export function useForm(form, ref, options = {}) {
    const { onSubmit, onValuesChange } = options;
    const formRef = useRef(null);
    const [, forceUpdate] = useState({});
    if (!formRef.current) {
        if (form) {
            formRef.current = form;
        }
        else {
            // Create a new FormInstance if not provided
            const forceReRender = () => {
                forceUpdate({});
            };
            form = formRef.current = new CubeFormInstance(forceReRender);
        }
        form.ref = ref;
    }
    if (onSubmit) {
        formRef.current.onSubmit = onSubmit;
    }
    if (onValuesChange) {
        formRef.current.onValuesChange = onValuesChange;
    }
    return [formRef.current];
}

        