import React from "react";
import { getDefaultRegistry } from "@rjsf/core";
import { isEmpty } from "lodash-es";
import { hasValue, isFieldInvalid, isFieldRequired, getErrorText, } from "../validation-display";
import { resetDependentFields, schemaAllowsNull } from "./field-helpers";
import { FieldTemplate } from "../templates";
import { getHelpComponent } from "../help";
import { TimePicker } from "../widgets/time-picker";
import { RowLayoutContext } from "../row/row-context";
const { fields: defaultFields } = getDefaultRegistry();
const BaseSchemaField = defaultFields.SchemaField;
/**
 * If given a value and schema, check if the value should be translated
 * from null to undefined to avoid issues with validation
 *
 * This function needs to be used for several reasons:
 * 1. CustomEvent does not allow `detail` to equal `undefined`, but we can call onChange with `undefined` in React
 * 2. `null` is treated as a valid value in a jsonschema and with marshmallow and it has its own type (null)
 * 3. Without changing `null` to `undefined` there would be no way to remove a field from
 * from the submitted form data once it had any data. (when POSTing, undefined is not posted since its not valid json)
 * 4. The only applies to custom web components since widgets handle transforming null/'' to undefined depending on the widget
 * and its purpose.
 *
 * Example:
 * If I have an object `{ name?: string, email?: string }` that I am using a custom web component for `name`,
 * then initially, the formData will be `{}` which is fine because neither name or email are required. Then if i input a
 * value for name the formData will be `{ name: 'some_value' }` which is also fine. But then if I want to remove name
 * from the form data I would delete all the text from the name input field. Web components would emit this empty value
 * as '' or null. If the web component tries to emit `undefined`, null would be emitted instead because CustomEvent has
 * a default `detail` value of null
 *
 * @param value - the value associated with the schema
 * @param schema - the schema for the value
 * @returns whether or not null should be changed to undefined
 */
const shouldChangeToUndefined = (value, schema) => {
    return value === null && !schemaAllowsNull(schema);
};
const hasCustomComponent = (schema) => {
    var _a, _b;
    const name = (_b = (_a = schema.lime) === null || _a === void 0 ? void 0 : _a.component) === null || _b === void 0 ? void 0 : _b.name;
    if (!name) {
        return false;
    }
    try {
        verifyCustomComponentIsDefined(name);
    }
    catch (_c) {
        console.warn(`Custom component ${name} not defined`);
        return false;
    }
    return true;
};
const verifyCustomComponentIsDefined = (elementName) => {
    if (!customElements.get(elementName)) {
        throw new Error(`Custom form element '${elementName}' is not defined!`);
    }
};
const getCustomComponent = (schema) => {
    var _a, _b, _c, _d;
    const name = (_b = (_a = schema.lime) === null || _a === void 0 ? void 0 : _a.component) === null || _b === void 0 ? void 0 : _b.name;
    const props = ((_d = (_c = schema.lime) === null || _c === void 0 ? void 0 : _c.component) === null || _d === void 0 ? void 0 : _d.props) || {};
    return { name: name, props: props };
};
/**
 * Create properties from the factory that is set on `limel-form`
 *
 * @param formContext - the form context
 * @param schema - the schema for the current field
 * @returns the properties created by the factory
 */
export function getFactoryProps(formContext, schema) {
    const factory = formContext.propsFactory;
    if (typeof factory !== 'function') {
        return {};
    }
    const props = factory(schema);
    if (!props) {
        return {};
    }
    return props;
}
export class SchemaField extends React.Component {
    constructor(props) {
        super(props);
        this.state = {
            modified: false,
        };
        this.handleChange = this.handleChange.bind(this);
        this.handleCustomComponentChange =
            this.handleCustomComponentChange.bind(this);
    }
    isInvalid() {
        const { modified } = this.state;
        const { errorSchema, required, schema } = this.props;
        return isFieldInvalid({
            hasErrors: !isEmpty(errorSchema),
            modified: modified,
            hasValue: hasValue(this.props.formData),
            required: isFieldRequired({ required, minItems: schema.minItems }),
        });
    }
    getHelperText() {
        var _a, _b, _c;
        const { errorSchema, schema } = this.props;
        const hasErrors = !isEmpty(errorSchema) && '__errors' in errorSchema;
        const errors = this.isInvalid() && hasErrors ? errorSchema.__errors : [];
        const fallbackText = (_c = (_b = (_a = schema.lime) === null || _a === void 0 ? void 0 : _a.component) === null || _b === void 0 ? void 0 : _b.props) === null || _c === void 0 ? void 0 : _c.helperText;
        return getErrorText(errors, schema.description, fallbackText);
    }
    handleCustomComponentChange(event) {
        const { schema } = this.props;
        event.stopPropagation();
        let value = event.nativeEvent.detail;
        if (shouldChangeToUndefined(value, schema)) {
            value = undefined;
        }
        this.handleChange(value, this.props.fieldPathId.path);
    }
    handleChange(data, path) {
        const { formData, schema } = this.props;
        const { rootSchema } = this.props.registry;
        this.setState({ modified: true });
        if (this.isDeepLeafChange(path)) {
            this.props.onChange(data, path);
            return;
        }
        const newData = resetDependentFields(formData, data, schema, rootSchema);
        this.props.onChange(newData, path);
    }
    /**
     * RJSF v6's built-in ArrayField shares one onChange handler across all
     * descendants, so changes to leaves deep inside an array item bubble
     * through this SchemaField with a path that is deeper than its own.
     * The enclosing ArrayField rebuilds the affected item and runs
     * `resetDependentFields` at the item level (see array-field.ts), so we
     * must pass the partial leaf data through untouched here.
     * @param path
     */
    isDeepLeafChange(path) {
        const { fieldPathId } = this.props;
        return !!fieldPathId && path.length > fieldPathId.path.length;
    }
    buildCustomComponentProps() {
        const { disabled, readonly, name, registry, schema: rawSchema, errorSchema, fieldPathId, } = this.props;
        const schema = rawSchema;
        const factoryProps = getFactoryProps(registry.formContext, schema);
        return Object.assign(Object.assign({}, factoryProps), { value: this.props.formData, required: isFieldRequired({
                required: this.props.required,
                minItems: schema.minItems,
            }), readonly: readonly || schema.readOnly, disabled: disabled, invalid: this.isInvalid(), label: this.context ? '' : schema.title, helperText: this.context && !this.isInvalid() ? '' : this.getHelperText(), ref: (element) => {
                var _a;
                element.formInfo = {
                    schema: schema,
                    rootSchema: registry.formContext.schema,
                    errorSchema: errorSchema,
                    rootValue: registry.formContext.rootValue,
                    name: name,
                    schemaPath: (_a = fieldPathId === null || fieldPathId === void 0 ? void 0 : fieldPathId.path) === null || _a === void 0 ? void 0 : _a.map(String),
                };
                return () => { };
            } });
    }
    renderCustomComponent(props) {
        const schema = props.schema;
        const { name, props: userDefinedComponentProps } = getCustomComponent(schema);
        verifyCustomComponentIsDefined(name);
        const component = React.createElement(name, Object.assign(Object.assign(Object.assign({}, userDefinedComponentProps), this.buildCustomComponentProps()), { onChange: this.handleCustomComponentChange }));
        return React.createElement(FieldTemplate, Object.assign(Object.assign({}, this.props), { classNames: 'rjsf-field rjsf-field-custom' }), component, getHelpComponent(schema));
    }
    render() {
        var _a;
        if ((_a = this.props.schema.lime) === null || _a === void 0 ? void 0 : _a.hidden) {
            return null;
        }
        if (hasCustomComponent(this.props.schema)) {
            return this.renderCustomComponent(this.props);
        }
        const fieldProps = Object.assign(Object.assign({}, this.props), { onChange: this.handleChange });
        if (this.props.schema.format === 'time') {
            fieldProps.uiSchema = Object.assign({ 'ui:widget': TimePicker }, fieldProps.uiSchema);
        }
        return React.createElement(BaseSchemaField, fieldProps);
    }
}
SchemaField.contextType = RowLayoutContext;
