import JSONSchemaField from "@rjsf/core/lib/components/fields/SchemaField";
import React from "react";
import { isEmpty, capitalize } from "lodash-es";
import { resetDependentFields } from "./field-helpers";
import { FieldTemplate } from "../templates";
import { getHelpComponent } from "../help";
import { TimePicker } from "../widgets/time-picker";
/**
 * 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 && !schema.type.includes('null');
};
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) => {
    const supportsCustomElements = 'customElements' in window;
    if (!supportsCustomElements) {
        throw new Error('Custom form elements are not supported by this browser!');
    }
    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);
    }
    hasValue() {
        const value = this.getValue();
        if (!value) {
            return false;
        }
        if (Array.isArray(value)) {
            return value.length > 0;
        }
        if (value instanceof Date) {
            return true;
        }
        if (typeof value === 'object') {
            return !isEmpty(value);
        }
        return true;
    }
    getLabel() {
        const { schema } = this.props;
        return schema.title;
    }
    isInvalid() {
        const { modified } = this.state;
        const { errorSchema } = this.props;
        return ((modified || this.hasValue() || !this.isRequired()) &&
            !isEmpty(errorSchema));
    }
    isRequired() {
        const { required, schema } = this.props;
        return required || schema.minItems > 0;
    }
    getHelperText() {
        var _a, _b, _c;
        const { errorSchema, schema } = this.props;
        if (!this.isInvalid()) {
            const helperText = (_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 helperText || schema.description;
        }
        if (!isEmpty(errorSchema) && '__errors' in errorSchema) {
            return capitalize(errorSchema.__errors[0]);
        }
        return schema.description;
    }
    getValue() {
        const { formData } = this.props;
        return formData;
    }
    handleCustomComponentChange(event) {
        const { schema } = this.props;
        event.stopPropagation();
        let value = event.nativeEvent.detail;
        if (shouldChangeToUndefined(value, schema)) {
            value = undefined;
        }
        this.handleChange(value);
    }
    handleChange(data) {
        const { formData, schema } = this.props;
        const { rootSchema } = this.props.registry;
        this.setState({ modified: true });
        const newData = resetDependentFields(formData, data, schema, rootSchema);
        this.props.onChange(newData);
    }
    buildCustomComponentProps() {
        const { disabled, readonly, name, registry, schema, errorSchema, idSchema, } = this.props;
        const factoryProps = getFactoryProps(registry.formContext, schema);
        return Object.assign(Object.assign({}, factoryProps), { value: this.getValue(), required: this.isRequired(), readonly: readonly || schema.readOnly, disabled: disabled, invalid: this.isInvalid(), label: this.getLabel(), helperText: this.getHelperText(), ref: (element) => {
                element.formInfo = {
                    schema: schema,
                    rootSchema: registry.formContext.schema,
                    errorSchema: errorSchema,
                    rootValue: registry.formContext.rootValue,
                    name: name,
                    schemaPath: this.getSchemaPath(idSchema === null || idSchema === void 0 ? void 0 : idSchema.$id),
                };
                return () => { };
            } });
    }
    renderCustomComponent(props) {
        const { name, props: userDefinedComponentProps } = getCustomComponent(props.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: 'form-group field field-custom' }), component, getHelpComponent(props.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(JSONSchemaField, fieldProps);
    }
    /**
     * Gets the path to the current property within the schema
     *
     * @param schemaId - the id of the schema
     * @returns an array with the schema path for the current property
     * @example
     * const schemaId = 'root_sections_0_controls_0_name';
     * const schemaPath = getSchemaPath(schemaId);
     * // ➡ ['sections', '0', 'controls', '0', 'name']
     */
    getSchemaPath(schemaId) {
        if (schemaId === undefined) {
            return undefined;
        }
        return schemaId.replace('root_', '').split('_');
    }
}
