import get from 'lodash/get';
import isEmpty from 'lodash/isEmpty';
import { ANY_OF_KEY, DEFAULT_KEY, DEPENDENCIES_KEY, PROPERTIES_KEY, ONE_OF_KEY, REF_KEY, ALL_OF_KEY, } from '../constants';
import findSchemaDefinition from '../findSchemaDefinition';
import getClosestMatchingOption from './getClosestMatchingOption';
import getDiscriminatorFieldFromSchema from '../getDiscriminatorFieldFromSchema';
import getSchemaType from '../getSchemaType';
import isObject from '../isObject';
import isFixedItems from '../isFixedItems';
import mergeDefaultsWithFormData from '../mergeDefaultsWithFormData';
import mergeObjects from '../mergeObjects';
import mergeSchemas from '../mergeSchemas';
import isMultiSelect from './isMultiSelect';
import retrieveSchema, { resolveDependencies } from './retrieveSchema';
/** Enum that indicates how `schema.additionalItems` should be handled by the `getInnerSchemaForArrayItem()` function.
 */
export var AdditionalItemsHandling;
(function (AdditionalItemsHandling) {
    AdditionalItemsHandling[AdditionalItemsHandling["Ignore"] = 0] = "Ignore";
    AdditionalItemsHandling[AdditionalItemsHandling["Invert"] = 1] = "Invert";
    AdditionalItemsHandling[AdditionalItemsHandling["Fallback"] = 2] = "Fallback";
})(AdditionalItemsHandling || (AdditionalItemsHandling = {}));
/** Given a `schema` will return an inner schema that for an array item. This is computed differently based on the
 * `additionalItems` enum and the value of `idx`. There are four possible returns:
 * 1. If `idx` is >= 0, then if `schema.items` is an array the `idx`th element of the array is returned if it is a valid
 *    index and not a boolean, otherwise it falls through to 3.
 * 2. If `schema.items` is not an array AND truthy and not a boolean, then `schema.items` is returned since it actually
 *    is a schema, otherwise it falls through to 3.
 * 3. If `additionalItems` is not `AdditionalItemsHandling.Ignore` and `schema.additionalItems` is an object, then
 *    `schema.additionalItems` is returned since it actually is a schema, otherwise it falls through to 4.
 * 4. {} is returned representing an empty schema
 *
 * @param schema - The schema from which to get the particular item
 * @param [additionalItems=AdditionalItemsHandling.Ignore] - How do we want to handle additional items?
 * @param [idx=-1] - Index, if non-negative, will be used to return the idx-th element in a `schema.items` array
 * @returns - The best fit schema object from the `schema` given the `additionalItems` and `idx` modifiers
 */
export function getInnerSchemaForArrayItem(schema, additionalItems = AdditionalItemsHandling.Ignore, idx = -1) {
    if (idx >= 0) {
        if (Array.isArray(schema.items) && idx < schema.items.length) {
            const item = schema.items[idx];
            if (typeof item !== 'boolean') {
                return item;
            }
        }
    }
    else if (schema.items && !Array.isArray(schema.items) && typeof schema.items !== 'boolean') {
        return schema.items;
    }
    if (additionalItems !== AdditionalItemsHandling.Ignore && isObject(schema.additionalItems)) {
        return schema.additionalItems;
    }
    return {};
}
/** Either add `computedDefault` at `key` into `obj` or not add it based on its value, the value of
 * `includeUndefinedValues`, the value of `emptyObjectFields` and if its parent field is required. Generally undefined
 * `computedDefault` values are added only when `includeUndefinedValues` is either true/"excludeObjectChildren". If `
 * includeUndefinedValues` is false and `emptyObjectFields` is not "skipDefaults", then non-undefined and non-empty-object
 * values will be added based on certain conditions.
 *
 * @param obj - The object into which the computed default may be added
 * @param key - The key into the object at which the computed default may be added
 * @param computedDefault - The computed default value that maybe should be added to the obj
 * @param includeUndefinedValues - Optional flag, if true, cause undefined values to be added as defaults.
 *          If "excludeObjectChildren", cause undefined values for this object and pass `includeUndefinedValues` as
 *          false when computing defaults for any nested object properties. If "allowEmptyObject", prevents undefined
 *          values in this object while allow the object itself to be empty and passing `includeUndefinedValues` as
 *          false when computing defaults for any nested object properties.
 * @param isParentRequired - The optional boolean that indicates whether the parent field is required
 * @param requiredFields - The list of fields that are required
 * @param experimental_defaultFormStateBehavior - Optional configuration object, if provided, allows users to override
 *        default form state behavior
 */
function maybeAddDefaultToObject(obj, key, computedDefault, includeUndefinedValues, isParentRequired, requiredFields = [], experimental_defaultFormStateBehavior = {}) {
    const { emptyObjectFields = 'populateAllDefaults' } = experimental_defaultFormStateBehavior;
    if (includeUndefinedValues) {
        obj[key] = computedDefault;
    }
    else if (emptyObjectFields !== 'skipDefaults') {
        if (isObject(computedDefault)) {
            // If isParentRequired is undefined, then we are at the root level of the schema so defer to the requiredness of
            // the field key itself in the `requiredField` list
            const isSelfOrParentRequired = isParentRequired === undefined ? requiredFields.includes(key) : isParentRequired;
            // If emptyObjectFields 'skipEmptyDefaults' store computedDefault if it's a non-empty object(e.g. not {})
            if (emptyObjectFields === 'skipEmptyDefaults') {
                if (!isEmpty(computedDefault)) {
                    obj[key] = computedDefault;
                }
            }
            // Else store computedDefault if it's a non-empty object(e.g. not {}) and satisfies certain conditions
            // Condition 1: If computedDefault is not empty or if the key is a required field
            // Condition 2: If the parent object is required or emptyObjectFields is not 'populateRequiredDefaults'
            else if ((!isEmpty(computedDefault) || requiredFields.includes(key)) &&
                (isSelfOrParentRequired || emptyObjectFields !== 'populateRequiredDefaults')) {
                obj[key] = computedDefault;
            }
        }
        else if (
        // Store computedDefault if it's a defined primitive (e.g., true) and satisfies certain conditions
        // Condition 1: computedDefault is not undefined
        // Condition 2: If emptyObjectFields is 'populateAllDefaults' or 'skipEmptyDefaults) or if the key is a required field
        computedDefault !== undefined &&
            (emptyObjectFields === 'populateAllDefaults' ||
                emptyObjectFields === 'skipEmptyDefaults' ||
                requiredFields.includes(key))) {
            obj[key] = computedDefault;
        }
    }
}
/** Computes the defaults for the current `schema` given the `rawFormData` and `parentDefaults` if any. This drills into
 * each level of the schema, recursively, to fill out every level of defaults provided by the schema.
 *
 * @param validator - an implementation of the `ValidatorType` interface that will be used when necessary
 * @param rawSchema - The schema for which the default state is desired
 * @param {ComputeDefaultsProps} computeDefaultsProps - Optional props for this function
 * @returns - The resulting `formData` with all the defaults provided
 */
export function computeDefaults(validator, rawSchema, computeDefaultsProps = {}) {
    const { parentDefaults, rawFormData, rootSchema = {}, includeUndefinedValues = false, _recurseList = [], experimental_defaultFormStateBehavior = undefined, required, } = computeDefaultsProps;
    const formData = (isObject(rawFormData) ? rawFormData : {});
    const schema = isObject(rawSchema) ? rawSchema : {};
    // Compute the defaults recursively: give highest priority to deepest nodes.
    let defaults = parentDefaults;
    // If we get a new schema, then we need to recompute defaults again for the new schema found.
    let schemaToCompute = null;
    let updatedRecurseList = _recurseList;
    if (isObject(defaults) && isObject(schema.default)) {
        // For object defaults, only override parent defaults that are defined in
        // schema.default.
        defaults = mergeObjects(defaults, schema.default);
    }
    else if (DEFAULT_KEY in schema) {
        defaults = schema.default;
    }
    else if (REF_KEY in schema) {
        const refName = schema[REF_KEY];
        // Use referenced schema defaults for this node.
        if (!_recurseList.includes(refName)) {
            updatedRecurseList = _recurseList.concat(refName);
            schemaToCompute = findSchemaDefinition(refName, rootSchema);
        }
    }
    else if (DEPENDENCIES_KEY in schema) {
        // Get the default if set from properties to ensure the dependencies conditions are resolved based on it
        const defaultFormData = {
            ...formData,
            ...getDefaultBasedOnSchemaType(validator, schema, computeDefaultsProps, defaults),
        };
        const resolvedSchema = resolveDependencies(validator, schema, rootSchema, false, [], defaultFormData);
        schemaToCompute = resolvedSchema[0]; // pick the first element from resolve dependencies
    }
    else if (isFixedItems(schema)) {
        defaults = schema.items.map((itemSchema, idx) => computeDefaults(validator, itemSchema, {
            rootSchema,
            includeUndefinedValues,
            _recurseList,
            experimental_defaultFormStateBehavior,
            parentDefaults: Array.isArray(parentDefaults) ? parentDefaults[idx] : undefined,
            rawFormData: formData,
            required,
        }));
    }
    else if (ONE_OF_KEY in schema) {
        const { oneOf, ...remaining } = schema;
        if (oneOf.length === 0) {
            return undefined;
        }
        const discriminator = getDiscriminatorFieldFromSchema(schema);
        schemaToCompute = oneOf[getClosestMatchingOption(validator, rootSchema, isEmpty(formData) ? undefined : formData, oneOf, 0, discriminator)];
        schemaToCompute = mergeSchemas(remaining, schemaToCompute);
    }
    else if (ANY_OF_KEY in schema) {
        const { anyOf, ...remaining } = schema;
        if (anyOf.length === 0) {
            return undefined;
        }
        const discriminator = getDiscriminatorFieldFromSchema(schema);
        schemaToCompute = anyOf[getClosestMatchingOption(validator, rootSchema, isEmpty(formData) ? undefined : formData, anyOf, 0, discriminator)];
        schemaToCompute = mergeSchemas(remaining, schemaToCompute);
    }
    if (schemaToCompute) {
        return computeDefaults(validator, schemaToCompute, {
            rootSchema,
            includeUndefinedValues,
            _recurseList: updatedRecurseList,
            experimental_defaultFormStateBehavior,
            parentDefaults: defaults,
            rawFormData: formData,
            required,
        });
    }
    // No defaults defined for this node, fallback to generic typed ones.
    if (defaults === undefined) {
        defaults = schema.default;
    }
    const defaultBasedOnSchemaType = getDefaultBasedOnSchemaType(validator, schema, computeDefaultsProps, defaults);
    return defaultBasedOnSchemaType !== null && defaultBasedOnSchemaType !== void 0 ? defaultBasedOnSchemaType : defaults;
}
/** Computes the default value for objects.
 *
 * @param validator - an implementation of the `ValidatorType` interface that will be used when necessary
 * @param rawSchema - The schema for which the default state is desired
 * @param {ComputeDefaultsProps} computeDefaultsProps - Optional props for this function
 * @param defaults - Optional props for this function
 * @returns - The default value based on the schema type if they are defined for object or array schemas.
 */
export function getObjectDefaults(validator, rawSchema, { rawFormData, rootSchema = {}, includeUndefinedValues = false, _recurseList = [], experimental_defaultFormStateBehavior = undefined, required, } = {}, defaults) {
    {
        const formData = (isObject(rawFormData) ? rawFormData : {});
        const schema = rawSchema;
        // This is a custom addition that fixes this issue:
        // https://github.com/rjsf-team/react-jsonschema-form/issues/3832
        const retrievedSchema = (experimental_defaultFormStateBehavior === null || experimental_defaultFormStateBehavior === void 0 ? void 0 : experimental_defaultFormStateBehavior.allOf) === 'populateDefaults' && ALL_OF_KEY in schema
            ? retrieveSchema(validator, schema, rootSchema, formData)
            : schema;
        const objectDefaults = Object.keys(retrievedSchema.properties || {}).reduce((acc, key) => {
            var _a;
            // Compute the defaults for this node, with the parent defaults we might
            // have from a previous run: defaults[key].
            const computedDefault = computeDefaults(validator, get(retrievedSchema, [PROPERTIES_KEY, key]), {
                rootSchema,
                _recurseList,
                experimental_defaultFormStateBehavior,
                includeUndefinedValues: includeUndefinedValues === true,
                parentDefaults: get(defaults, [key]),
                rawFormData: get(formData, [key]),
                required: (_a = retrievedSchema.required) === null || _a === void 0 ? void 0 : _a.includes(key),
            });
            maybeAddDefaultToObject(acc, key, computedDefault, includeUndefinedValues, required, retrievedSchema.required, experimental_defaultFormStateBehavior);
            return acc;
        }, {});
        if (retrievedSchema.additionalProperties) {
            // as per spec additionalProperties may be either schema or boolean
            const additionalPropertiesSchema = isObject(retrievedSchema.additionalProperties)
                ? retrievedSchema.additionalProperties
                : {};
            const keys = new Set();
            if (isObject(defaults)) {
                Object.keys(defaults)
                    .filter((key) => !retrievedSchema.properties || !retrievedSchema.properties[key])
                    .forEach((key) => keys.add(key));
            }
            const formDataRequired = [];
            Object.keys(formData)
                .filter((key) => !retrievedSchema.properties || !retrievedSchema.properties[key])
                .forEach((key) => {
                keys.add(key);
                formDataRequired.push(key);
            });
            keys.forEach((key) => {
                var _a;
                const computedDefault = computeDefaults(validator, additionalPropertiesSchema, {
                    rootSchema,
                    _recurseList,
                    experimental_defaultFormStateBehavior,
                    includeUndefinedValues: includeUndefinedValues === true,
                    parentDefaults: get(defaults, [key]),
                    rawFormData: get(formData, [key]),
                    required: (_a = retrievedSchema.required) === null || _a === void 0 ? void 0 : _a.includes(key),
                });
                // Since these are additional properties we don't need to add the `experimental_defaultFormStateBehavior` prop
                maybeAddDefaultToObject(objectDefaults, key, computedDefault, includeUndefinedValues, required, formDataRequired);
            });
        }
        return objectDefaults;
    }
}
/** Computes the default value for arrays.
 *
 * @param validator - an implementation of the `ValidatorType` interface that will be used when necessary
 * @param rawSchema - The schema for which the default state is desired
 * @param {ComputeDefaultsProps} computeDefaultsProps - Optional props for this function
 * @param defaults - Optional props for this function
 * @returns - The default value based on the schema type if they are defined for object or array schemas.
 */
export function getArrayDefaults(validator, rawSchema, { rawFormData, rootSchema = {}, _recurseList = [], experimental_defaultFormStateBehavior = undefined, required, } = {}, defaults) {
    var _a, _b, _c, _d;
    const schema = rawSchema;
    const neverPopulate = ((_a = experimental_defaultFormStateBehavior === null || experimental_defaultFormStateBehavior === void 0 ? void 0 : experimental_defaultFormStateBehavior.arrayMinItems) === null || _a === void 0 ? void 0 : _a.populate) === 'never';
    const ignoreMinItemsFlagSet = ((_b = experimental_defaultFormStateBehavior === null || experimental_defaultFormStateBehavior === void 0 ? void 0 : experimental_defaultFormStateBehavior.arrayMinItems) === null || _b === void 0 ? void 0 : _b.populate) === 'requiredOnly';
    const isSkipEmptyDefaults = (experimental_defaultFormStateBehavior === null || experimental_defaultFormStateBehavior === void 0 ? void 0 : experimental_defaultFormStateBehavior.emptyObjectFields) === 'skipEmptyDefaults';
    const computeSkipPopulate = (_d = (_c = experimental_defaultFormStateBehavior === null || experimental_defaultFormStateBehavior === void 0 ? void 0 : experimental_defaultFormStateBehavior.arrayMinItems) === null || _c === void 0 ? void 0 : _c.computeSkipPopulate) !== null && _d !== void 0 ? _d : (() => false);
    const emptyDefault = isSkipEmptyDefaults ? undefined : [];
    // Inject defaults into existing array defaults
    if (Array.isArray(defaults)) {
        defaults = defaults.map((item, idx) => {
            const schemaItem = getInnerSchemaForArrayItem(schema, AdditionalItemsHandling.Fallback, idx);
            return computeDefaults(validator, schemaItem, {
                rootSchema,
                _recurseList,
                experimental_defaultFormStateBehavior,
                parentDefaults: item,
                required,
            });
        });
    }
    // Deeply inject defaults into already existing form data
    if (Array.isArray(rawFormData)) {
        const schemaItem = getInnerSchemaForArrayItem(schema);
        if (neverPopulate) {
            defaults = rawFormData;
        }
        else {
            defaults = rawFormData.map((item, idx) => {
                return computeDefaults(validator, schemaItem, {
                    rootSchema,
                    _recurseList,
                    experimental_defaultFormStateBehavior,
                    rawFormData: item,
                    parentDefaults: get(defaults, [idx]),
                    required,
                });
            });
        }
    }
    if (neverPopulate) {
        return defaults !== null && defaults !== void 0 ? defaults : emptyDefault;
    }
    if (ignoreMinItemsFlagSet && !required) {
        // If no form data exists or defaults are set leave the field empty/non-existent, otherwise
        // return form data/defaults
        return defaults ? defaults : undefined;
    }
    const defaultsLength = Array.isArray(defaults) ? defaults.length : 0;
    if (!schema.minItems ||
        isMultiSelect(validator, schema, rootSchema) ||
        computeSkipPopulate(validator, schema, rootSchema) ||
        schema.minItems <= defaultsLength) {
        return defaults ? defaults : emptyDefault;
    }
    const defaultEntries = (defaults || []);
    const fillerSchema = getInnerSchemaForArrayItem(schema, AdditionalItemsHandling.Invert);
    const fillerDefault = fillerSchema.default;
    // Calculate filler entries for remaining items (minItems - existing raw data/defaults)
    const fillerEntries = new Array(schema.minItems - defaultsLength).fill(computeDefaults(validator, fillerSchema, {
        parentDefaults: fillerDefault,
        rootSchema,
        _recurseList,
        experimental_defaultFormStateBehavior,
        required,
    }));
    // then fill up the rest with either the item default or empty, up to minItems
    return defaultEntries.concat(fillerEntries);
}
/** Computes the default value based on the schema type.
 *
 * @param validator - an implementation of the `ValidatorType` interface that will be used when necessary
 * @param rawSchema - The schema for which the default state is desired
 * @param {ComputeDefaultsProps} computeDefaultsProps - Optional props for this function
 * @param defaults - Optional props for this function
 * @returns - The default value based on the schema type if they are defined for object or array schemas.
 */
export function getDefaultBasedOnSchemaType(validator, rawSchema, computeDefaultsProps = {}, defaults) {
    switch (getSchemaType(rawSchema)) {
        // We need to recurse for object schema inner default values.
        case 'object': {
            return getObjectDefaults(validator, rawSchema, computeDefaultsProps, defaults);
        }
        case 'array': {
            return getArrayDefaults(validator, rawSchema, computeDefaultsProps, defaults);
        }
    }
}
/** Returns the superset of `formData` that includes the given set updated to include any missing fields that have
 * computed to have defaults provided in the `schema`.
 *
 * @param validator - An implementation of the `ValidatorType` interface that will be used when necessary
 * @param theSchema - The schema for which the default state is desired
 * @param [formData] - The current formData, if any, onto which to provide any missing defaults
 * @param [rootSchema] - The root schema, used to primarily to look up `$ref`s
 * @param [includeUndefinedValues=false] - Optional flag, if true, cause undefined values to be added as defaults.
 *          If "excludeObjectChildren", cause undefined values for this object and pass `includeUndefinedValues` as
 *          false when computing defaults for any nested object properties.
 * @param [experimental_defaultFormStateBehavior] Optional configuration object, if provided, allows users to override default form state behavior
 * @returns - The resulting `formData` with all the defaults provided
 */
export default function getDefaultFormState(validator, theSchema, formData, rootSchema, includeUndefinedValues = false, experimental_defaultFormStateBehavior) {
    if (!isObject(theSchema)) {
        throw new Error('Invalid schema: ' + theSchema);
    }
    const schema = retrieveSchema(validator, theSchema, rootSchema, formData);
    const defaults = computeDefaults(validator, schema, {
        rootSchema,
        includeUndefinedValues,
        experimental_defaultFormStateBehavior,
        rawFormData: formData,
    });
    if (formData === undefined || formData === null || (typeof formData === 'number' && isNaN(formData))) {
        // No form data? Use schema defaults.
        return defaults;
    }
    const { mergeExtraDefaults } = (experimental_defaultFormStateBehavior === null || experimental_defaultFormStateBehavior === void 0 ? void 0 : experimental_defaultFormStateBehavior.arrayMinItems) || {};
    if (isObject(formData)) {
        return mergeDefaultsWithFormData(defaults, formData, mergeExtraDefaults);
    }
    if (Array.isArray(formData)) {
        return mergeDefaultsWithFormData(defaults, formData, mergeExtraDefaults);
    }
    return formData;
}
//# sourceMappingURL=getDefaultFormState.js.map