import type { ComponentType, ReactNode } from 'react';
import type {
  FieldProps,
  FieldPathId,
  FormContextType,
  GenericObjectType,
  RJSFSchema,
  Registry,
  StrictRJSFSchema,
  UiSchema,
} from '@rjsf/utils';
import {
  ANY_OF_KEY,
  getDiscriminatorFieldFromSchema,
  getTemplate,
  getTestIds,
  getUiOptions,
  hashObject,
  ID_KEY,
  lookupFromFormContext,
  ONE_OF_KEY,
  PROPERTIES_KEY,
  READONLY_KEY,
  toFieldPathId,
  UI_OPTIONS_KEY,
  UI_GLOBAL_OPTIONS_KEY,
  ITEMS_KEY,
  useDeepCompareMemo,
} from '@rjsf/utils';
import each from 'lodash/each';
import flatten from 'lodash/flatten';
import get from 'lodash/get';
import has from 'lodash/has';
import includes from 'lodash/includes';
import intersection from 'lodash/intersection';
import isEmpty from 'lodash/isEmpty';
import isEqual from 'lodash/isEqual';
import isFunction from 'lodash/isFunction';
import isObject from 'lodash/isObject';
import isPlainObject from 'lodash/isPlainObject';
import isString from 'lodash/isString';
import isUndefined from 'lodash/isUndefined';
import last from 'lodash/last';
import set from 'lodash/set';

/** The enumeration of the three different Layout GridTemplate type values
 */
export enum GridType {
  ROW = 'ui:row',
  COLUMN = 'ui:col',
  COLUMNS = 'ui:columns',
  CONDITION = 'ui:condition',
}

/** The enumeration of the different operators within a condition
 */
export enum Operators {
  ALL = 'all',
  SOME = 'some',
  NONE = 'none',
}

/** Type used to represent an object that contains anything */
type ConfigObject = Record<string, any>;

export interface GridProps extends GenericObjectType {
  /** The optional operator to use when comparing a field's value with the expected value for `GridType.CONDITION`
   */
  operator?: Operators;
  /** The optional name of the field from which to get the value for `GridType.CONDITION`
   */
  field?: string;
  /** The optional expected value against which to compare the field's value using the `operator`
   */
  value?: unknown;
}

export type GridSchemaType = Partial<Record<GridType, object>>;

/** The types which comprise the possibilities for the `layoutGridSchema` prop
 */
export type LayoutGridSchemaType = GridSchemaType | ConfigObject | string;

export interface LayoutGridFieldProps<
  T = any,
  S extends StrictRJSFSchema = RJSFSchema,
  F extends FormContextType = any,
> extends FieldProps<T, S, F> {
  /** Optional string or object used to describe the current level of the `LayoutGridField`
   */
  layoutGridSchema?: LayoutGridSchemaType;
}

/** The regular expression that is used to detect whether a string contains a lookup key
 */
export const LOOKUP_REGEX = /^\$lookup=(.+)/;

/** The constant representing the main layout grid schema option name in the `uiSchema`
 */
export const LAYOUT_GRID_UI_OPTION = 'layoutGrid';

/** The constant representing the main layout grid schema option name in the `uiSchema`
 */
export const LAYOUT_GRID_OPTION = `ui:${LAYOUT_GRID_UI_OPTION}`;

/** Type used to return options list and whether it has a discriminator */
interface OneOfOptionsInfoType<S extends StrictRJSFSchema = RJSFSchema> {
  options: S[];
  hasDiscriminator: boolean;
}

/** Type used to represent a React-based rendering component */
type RenderComponent = ComponentType<any>;

/** Type used to determine what are the UIComponent and props from the grid schema */
interface UIComponentPropsType {
  /** The name of the component */
  name: string;
  /** The render component if specified */
  UIComponent: RenderComponent | null;
  /** Any uiProps associated with the render component */
  uiProps: ConfigObject;
  /** The special case where the component is immediately rendered */
  rendered: ReactNode;
}

/** Returns either the `value` if it is non-nullish or the fallback
 *
 * @param [value] - The potential value to return if it is non-nullish
 * @param [fallback] - The fallback value to return if `value` is nullish
 * @returns - `value` if it is non-nullish otherwise `fallback`
 */
function getNonNullishValue<T = unknown>(value?: T, fallback?: T): T | undefined {
  return value ?? fallback;
}

/** Detects if a `str` is made up entirely of numeric characters
 *
 * @param str - The string to check to see if it is a numeric index
 * @return - True if the string consists entirely of numeric characters
 */
function isNumericIndex(str: string) {
  return /^\d+?$/.test(str); // Matches positive integers
}

const LAYOUT_GRID_FIELD_TEST_IDS = getTestIds();

/** Computes the uiSchema for the field with `name` from the `uiProps` and `uiSchema` provided. The field UI Schema
 * will always contain a copy of the global options from the `uiSchema` (so they can be passed down) as well as
 * copying them into the local ui options. When the `forceReadonly` flag is true, then the field UI Schema is
 * updated to make "readonly" be true. When the `schemaReadonly` flag is true AND the field UI Schema does NOT have
 * the flag already provided, then we also make "readonly" true. We always make sure to return the final value of the
 * field UI Schema's "readonly" flag as `uiReadonly` along with the `fieldUiSchema` in the return value.
 *
 * @param field - The name of the field to pull the existing UI Schema for
 * @param uiProps - Any props that should be put into the field's uiSchema
 * @param [uiSchema] - The optional UI Schema from which to get the UI schema for the field
 * @param [schemaReadonly] - Optional flag indicating whether the schema indicates the field is readonly
 * @param [forceReadonly] - Optional flag indicating whether the Form itself is in readonly mode
 */
export function computeFieldUiSchema<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends FormContextType = any>(
  field: string,
  uiProps: ConfigObject,
  uiSchema?: UiSchema<T, S, F>,
  schemaReadonly?: boolean,
  forceReadonly?: boolean,
) {
  const globalUiOptions = get(uiSchema, [UI_GLOBAL_OPTIONS_KEY], {});
  const localUiSchema = get(uiSchema, field);
  const localUiOptions = { ...get(localUiSchema, [UI_OPTIONS_KEY], {}), ...uiProps, ...globalUiOptions };
  const fieldUiSchema = { ...localUiSchema };
  if (!isEmpty(localUiOptions)) {
    set(fieldUiSchema, [UI_OPTIONS_KEY], localUiOptions);
  }
  if (!isEmpty(globalUiOptions)) {
    // pass the global uiOptions down to the field uiSchema so that they can be applied to all nested fields
    set(fieldUiSchema, [UI_GLOBAL_OPTIONS_KEY], globalUiOptions);
  }
  let { readonly: uiReadonly } = getUiOptions<T, S, F>(fieldUiSchema);
  if (forceReadonly === true || (isUndefined(uiReadonly) && schemaReadonly === true)) {
    // If we are forcing all widgets to be readonly, OR the schema indicates it is readonly AND the uiSchema does not
    // have an overriding value, then update the uiSchema to set readonly to true. Doing this will
    uiReadonly = true;
    if (has(localUiOptions, READONLY_KEY)) {
      // If the local options has the key value provided in it, then set that one to true
      set(fieldUiSchema, [UI_OPTIONS_KEY, READONLY_KEY], true);
    } else {
      // otherwise set the `ui:` version
      set(fieldUiSchema, `ui:${READONLY_KEY}`, true);
    }
  }
  return { fieldUiSchema, uiReadonly };
}

/** Given an `operator`, `datum` and `value` determines whether this condition is considered matching. Matching
 * depends on the `operator`. The `datum` and `value` are converted into arrays if they aren't already and then the
 * contents of the two arrays are compared using the `operator`. When `operator` is All, then the two arrays must be
 * equal to match. When `operator` is SOME then the intersection of the two arrays must have at least one value in
 * common to match. When `operator` is NONE then the intersection of the two arrays must not have any values in common
 * to match.
 *
 * @param [operator] - The optional operator for the condition
 * @param [datum] - The optional datum for the condition, this can be an item or a list of items of type unknown
 * @param [value='$0m3tH1nG Un3xP3cT3d'] The optional value for the condition, defaulting to a highly unlikely value
 *        to avoid comparing two undefined elements when `value` was forgotten in the condition definition.
 *        This can be an item or a list of items of type unknown
 * @returns - True if the condition matches, false otherwise
 */
export function conditionMatches(
  operator?: Operators,
  datum?: unknown,
  value: unknown = '$0m3tH1nG Un3xP3cT3d',
): boolean {
  const data = flatten([datum]).sort();
  const values = flatten([value]).sort();
  switch (operator) {
    case Operators.ALL:
      return isEqual(data, values);
    case Operators.SOME:
      return intersection(data, values).length > 0;
    case Operators.NONE:
      return intersection(data, values).length === 0;
    default:
      return false;
  }
}

/** From within the `layoutGridSchema` finds the `children` and any extra `gridProps` from the object keyed by
 * `schemaKey`. If the `children` contains extra `gridProps` and those props contain a `className` string, try to
 * lookup whether that `className` has a replacement value in the `registry` using the `FORM_CONTEXT_LOOKUP_BASE`.
 * When the `className` value contains multiple classNames separated by a space, the lookup will look for a
 * replacement value for each `className` and combine them into one.
 *
 * @param layoutGridSchema - The GridSchemaType instance from which to obtain the `schemaKey` children and extra props
 * @param schemaKey - A `GridType` value, used to get the children and extra props from within the `layoutGridSchema`
 * @param registry - The `@rjsf` Registry from which to look up `classNames` if they are present in the extra props
 * @returns - An object containing the list of `LayoutGridSchemaType` `children` and any extra `gridProps`
 * @throws - A `TypeError` when the `children` is not an array
 */
export function findChildrenAndProps<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends FormContextType = any>(
  layoutGridSchema: GridSchemaType,
  schemaKey: GridType,
  registry: Registry<T, S, F>,
) {
  let gridProps: GridProps = {};
  let children = layoutGridSchema[schemaKey];
  if (isPlainObject(children)) {
    const { children: elements, className: toMapClassNames, ...otherProps } = children as ConfigObject;
    children = elements;
    if (toMapClassNames) {
      const classes = toMapClassNames.split(' ');
      const className = classes.map((ele: string) => lookupFromFormContext<T, S, F>(registry, ele, ele)).join(' ');
      gridProps = { ...otherProps, className };
    } else {
      gridProps = otherProps;
    }
  }
  if (!Array.isArray(children)) {
    throw new TypeError(`Expected array for "${schemaKey}" in ${JSON.stringify(layoutGridSchema)}`);
  }
  return { children: children as LayoutGridSchemaType[], gridProps };
}

/** Computes the `rawSchema` and `fieldPathId` for a `schema` and a `potentialIndex`. If the `schema` is of type array,
 * has an `ITEMS_KEY` element and `potentialIndex` represents a numeric value, the element at `ITEMS_KEY` is checked
 * to see if it is an array. If it is AND the `potentialIndex`th element is available, it is used as the `rawSchema`,
 * otherwise the last value of the element is used. If it is not, then the element is used as the `rawSchema`. In
 * either case, an `fieldPathId` is computed for the array index. If the `schema` does not represent an array or the
 * `potentialIndex` is not a numeric value, then `rawSchema` is returned as undefined and given `fieldPathId` is returned
 * as is.
 *
 * @param schema - The schema to generate the fieldPathId for
 * @param fieldPathId - The FieldPathId for the schema
 * @param potentialIndex - A string containing a potential index
 * @returns - An object containing the `rawSchema` and `fieldPathId` of an array item, otherwise an undefined `rawSchema`
 */
export function computeArraySchemasIfPresent<S extends StrictRJSFSchema = RJSFSchema>(
  schema: S | undefined,
  fieldPathId: FieldPathId,
  potentialIndex: string,
): {
  rawSchema?: S;
  fieldPathId: FieldPathId;
} {
  let rawSchema: S | undefined;
  let resultPathId = fieldPathId;
  if (isNumericIndex(potentialIndex) && schema && schema?.type === 'array' && has(schema, ITEMS_KEY)) {
    const index = Number(potentialIndex);
    const items = schema[ITEMS_KEY];
    if (Array.isArray(items)) {
      if (index > items.length) {
        rawSchema = last(items) as S;
      } else {
        rawSchema = items[index] as S;
      }
    } else {
      rawSchema = items as S;
    }
    resultPathId = {
      [ID_KEY]: fieldPathId[ID_KEY],
      path: [...fieldPathId.path.slice(0, fieldPathId.path.length - 1), index],
    };
  }
  return { rawSchema, fieldPathId: resultPathId };
}

/** Given a `dottedPath` to a field in the `initialSchema`, iterate through each individual path in the schema until
 * the leaf path is found and returned (along with whether that leaf path `isRequired`) OR no schema exists for an
 * element in the path. If the leaf schema element happens to be a oneOf/anyOf then also return the oneOf/anyOf as
 * `options`.
 *
 * @param registry - The registry
 * @param dottedPath - The dotted-path to the field for which to get the schema
 * @param initialSchema - The initial schema to start the search from
 * @param formData - The formData, useful for resolving a oneOf/anyOf selection in the path hierarchy
 * @param initialFieldIdPath - The initial fieldPathId to start the search from
 * @returns - An object containing the destination schema, isRequired and isReadonly flags for the field and options
 *            info if a oneOf/anyOf
 */
export function getSchemaDetailsForField<
  T = any,
  S extends StrictRJSFSchema = RJSFSchema,
  F extends FormContextType = any,
>(
  registry: Registry<T, S, F>,
  dottedPath: string,
  initialSchema: S,
  formData: FieldProps<T, S, F>['formData'],
  initialFieldIdPath: FieldPathId,
): {
  schema?: S;
  isRequired: boolean;
  isReadonly?: boolean;
  optionsInfo?: OneOfOptionsInfoType<S>;
  fieldPathId: FieldPathId;
} {
  const { schemaUtils, globalFormOptions } = registry;
  let rawSchema: S = initialSchema;
  let fieldPathId = initialFieldIdPath;
  const parts: string[] = dottedPath.split('.');
  const leafPath: string | undefined = parts.pop(); // pop off the last element in the list as the leaf
  let schema: S | undefined = schemaUtils.retrieveSchema(rawSchema, formData); // always returns an object
  let innerData = formData;
  let isReadonly: boolean | undefined = schema.readOnly;

  // For all the remaining path parts
  parts.forEach((part) => {
    // dive into the properties of the current schema (when it exists) and get the schema for the next part
    fieldPathId = toFieldPathId(part, globalFormOptions, fieldPathId);
    if (has(schema, PROPERTIES_KEY)) {
      rawSchema = get(schema, [PROPERTIES_KEY, part], {}) as S;
    } else if (schema && (has(schema, ONE_OF_KEY) || has(schema, ANY_OF_KEY))) {
      const xxx = has(schema, ONE_OF_KEY) ? ONE_OF_KEY : ANY_OF_KEY;
      // When the schema represents a oneOf/anyOf, find the selected schema for it and grab the inner part
      const selectedSchema = schemaUtils.findSelectedOptionInXxxOf(schema, part, xxx, innerData);
      rawSchema = get(selectedSchema, [PROPERTIES_KEY, part], {}) as S;
    } else {
      const result = computeArraySchemasIfPresent<S>(schema, fieldPathId, part);
      rawSchema = result.rawSchema ?? ({} as S);
      fieldPathId = result.fieldPathId;
    }
    // Now drill into the innerData for the part, returning an empty object by default if it doesn't exist
    innerData = get(innerData, part, {}) as T;
    // Resolve any `$ref`s for the current rawSchema
    schema = schemaUtils.retrieveSchema(rawSchema, innerData);
    isReadonly = getNonNullishValue(schema.readOnly, isReadonly);
  });

  let optionsInfo: OneOfOptionsInfoType<S> | undefined;
  let isRequired = false;
  // retrieveSchema will return an empty schema in the worst case scenario, convert it to undefined
  if (isEmpty(schema)) {
    schema = undefined;
  }
  if (schema && leafPath) {
    // When we have both a schema and a leafPath...
    if (schema && (has(schema, ONE_OF_KEY) || has(schema, ANY_OF_KEY))) {
      const xxx = has(schema, ONE_OF_KEY) ? ONE_OF_KEY : ANY_OF_KEY;
      // Grab the selected schema for the oneOf/anyOf value for the leafPath using the innerData
      schema = schemaUtils.findSelectedOptionInXxxOf(schema, leafPath, xxx, innerData);
    }
    fieldPathId = toFieldPathId(leafPath, globalFormOptions, fieldPathId);
    isRequired = schema !== undefined && Array.isArray(schema.required) && includes(schema.required, leafPath);
    const result = computeArraySchemasIfPresent<S>(schema, fieldPathId, leafPath);
    if (result.rawSchema) {
      schema = result.rawSchema;
      fieldPathId = result.fieldPathId;
    } else {
      // Now grab the schema from the leafPath of the current schema properties
      schema = get(schema, [PROPERTIES_KEY, leafPath]) as S | undefined;
      // Resolve any `$ref`s for the current schema
      schema = schema ? schemaUtils.retrieveSchema(schema) : schema;
    }
    isReadonly = getNonNullishValue(schema?.readOnly, isReadonly);
    if (schema && (has(schema, ONE_OF_KEY) || has(schema, ANY_OF_KEY))) {
      const xxx = has(schema, ONE_OF_KEY) ? ONE_OF_KEY : ANY_OF_KEY;
      // Set the options if we have a schema with a oneOf/anyOf
      const discriminator = getDiscriminatorFieldFromSchema(schema);
      optionsInfo = { options: schema[xxx] as S[], hasDiscriminator: !!discriminator };
    }
  }

  return { schema, isRequired, isReadonly, optionsInfo, fieldPathId };
}

/** Gets the custom render component from the `render`, by either determining that it is either already a function or
 * it is a non-function value that can be used to look up the function in the registry. If no function can be found,
 * null is returned.
 *
 * @param render - The potential render function or lookup name to one
 * @param registry - The `@rjsf` Registry from which to look up `classNames` if they are present in the extra props
 * @returns - Either a render function if available, or null if not
 */
export function getCustomRenderComponent<
  T = any,
  S extends StrictRJSFSchema = RJSFSchema,
  F extends FormContextType = any,
>(render: string | RenderComponent, registry: Registry<T, S, F>): RenderComponent | null {
  let customRenderer = render;
  if (isString(customRenderer)) {
    customRenderer = lookupFromFormContext<T, S, F>(registry, customRenderer);
  }
  if (isFunction(customRenderer)) {
    return customRenderer;
  }
  return null;
}

/** Extract the `name`, and optional `render` and all other props from the `gridSchema`. We look up the `render` to
 * see if can be resolved to a UIComponent. If `name` does not exist and there is an optional `render` UIComponent, we
 * set the `rendered` component with only specified props for that component in the object.
 *
 * @param registry - The `@rjsf` Registry from which to look up `classNames` if they are present in the extra props
 * @param gridSchema - The string or object that represents the configuration for the grid field
 * @returns - The UIComponentPropsType computed from the gridSchema
 */
export function computeUIComponentPropsFromGridSchema<
  T = any,
  S extends StrictRJSFSchema = RJSFSchema,
  F extends FormContextType = any,
>(registry: Registry<T, S, F>, gridSchema?: string | ConfigObject): UIComponentPropsType {
  let name: string;
  let UIComponent: RenderComponent | null = null;
  let uiProps: ConfigObject = {};
  let rendered: ReactNode | undefined;
  if (isString(gridSchema) || isUndefined(gridSchema)) {
    name = gridSchema ?? '';
  } else {
    const { name: innerName = '', render, ...innerProps } = gridSchema;
    name = innerName;
    uiProps = innerProps;
    if (!isEmpty(uiProps)) {
      // Transform any `$lookup=` in the uiProps props with the appropriate value
      each(uiProps, (prop: ConfigObject, key: string) => {
        if (isString(prop)) {
          const match: string[] | null = LOOKUP_REGEX.exec(prop);
          if (Array.isArray(match) && match.length > 1) {
            const lookupName = match[1];
            uiProps[key] = lookupFromFormContext(registry, lookupName, lookupName);
          }
        }
      });
    }
    UIComponent = getCustomRenderComponent<T, S, F>(render, registry);
    if (!innerName && UIComponent) {
      rendered = <UIComponent {...innerProps} data-testid={LAYOUT_GRID_FIELD_TEST_IDS.uiComponent} />;
    }
  }
  return { name, UIComponent, uiProps, rendered };
}

/**
 * The props for the LayoutGridFieldChildren component.
 */
type LayoutGridFieldChildrenProps<
  T = any,
  S extends StrictRJSFSchema = RJSFSchema,
  F extends FormContextType = any,
> = LayoutGridFieldProps<T, S, F> & {
  /** The list of strings or objects that represents the configurations for the children fields */
  childrenLayoutGridSchemaId: LayoutGridSchemaType[];
};

/** Iterates through all the `childrenLayoutGridSchemaId`, rendering a nested `LayoutGridField` for each item in the
 * list, passing all the props for the current `LayoutGridField` along, updating the `schema` by calling
 * `retrieveSchema()` on it to resolve any `$ref`s. In addition to the updated `schema`, each item in
 * `childrenLayoutGridSchemaId` is passed as `layoutGridSchema`.
 *
 * @returns - The nested `LayoutGridField`s
 */
function LayoutGridFieldChildren<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends FormContextType = any>(
  props: LayoutGridFieldChildrenProps<T, S, F>,
) {
  const { childrenLayoutGridSchemaId, ...layoutGridFieldProps } = props;
  const { registry, schema: rawSchema, formData } = layoutGridFieldProps;
  const { schemaUtils } = registry;
  const schema = schemaUtils.retrieveSchema(rawSchema, formData);
  return childrenLayoutGridSchemaId.map((layoutGridSchema) => (
    <LayoutGridField<T, S, F>
      {...layoutGridFieldProps}
      key={`layoutGrid-${hashObject(layoutGridSchema)}`}
      schema={schema}
      layoutGridSchema={layoutGridSchema}
    />
  ));
}

/**
 * Describes the props for LayoutGridCondition, LayoutGridCol, LayoutGridColumns, and LayoutGridRow. This is typically
 * the original props passed through from the nearest ancestor LayoutGridField, plus the layoutGridSchema for the
 * current node.
 */
type LayoutFieldProps<
  T = any,
  S extends StrictRJSFSchema = RJSFSchema,
  F extends FormContextType = any,
> = LayoutGridFieldProps<T, S, F> & {
  /**  The string or object that represents the configuration for the grid field */
  layoutGridSchema: GridSchemaType;
};

/** Renders the `children` of the `GridType.CONDITION` if it passes. The `layoutGridSchema` for the
 * `GridType.CONDITION` is separated into the `children` and other `gridProps`. The `gridProps` are used to extract
 * the `operator`, `field` and `value` of the condition. If the condition matches, then all of the `children` are
 * rendered, otherwise null is returned.
 *
 * @returns - The rendered the children for the `GridType.CONDITION` or null
 */
function LayoutGridCondition<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends FormContextType = any>(
  props: LayoutFieldProps<T, S, F>,
) {
  const { layoutGridSchema, ...layoutGridFieldProps } = props;
  const { formData, registry } = layoutGridFieldProps;
  const { children, gridProps } = findChildrenAndProps<T, S, F>(layoutGridSchema, GridType.CONDITION, registry);
  const { operator, field = '', value } = gridProps;
  const fieldData = get(formData, field, null);
  if (conditionMatches(operator, fieldData, value)) {
    return <LayoutGridFieldChildren {...layoutGridFieldProps} childrenLayoutGridSchemaId={children} />;
  }
  return null;
}

/** Renders a `GridTemplate` as an item. The `layoutGridSchema` for the `GridType.COLUMN` is separated into the
 * `children` and other `gridProps`. The `gridProps` will be spread onto the outer `GridTemplate`. Inside the
 * `GridTemplate` all the `children` are rendered.
 *
 * @returns - The rendered `GridTemplate` containing the children for the `GridType.COLUMN`
 */
function LayoutGridCol<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends FormContextType = any>(
  props: LayoutFieldProps<T, S, F>,
) {
  const { layoutGridSchema, ...layoutGridFieldProps } = props;
  const { registry, uiSchema } = layoutGridFieldProps;
  const { children, gridProps } = findChildrenAndProps<T, S, F>(layoutGridSchema, GridType.COLUMN, registry);
  const uiOptions = getUiOptions<T, S, F>(uiSchema);
  const GridTemplate = getTemplate<'GridTemplate', T, S, F>('GridTemplate', registry, uiOptions);

  return (
    <GridTemplate column data-testid={LAYOUT_GRID_FIELD_TEST_IDS.col} {...gridProps}>
      <LayoutGridFieldChildren {...layoutGridFieldProps} childrenLayoutGridSchemaId={children} />
    </GridTemplate>
  );
}

/** Renders a `GridTemplate` as an item. The `layoutGridSchema` for the `GridType.COLUMNS` is separated into the
 *  `children` and other `gridProps`. The `children` is iterated on and `gridProps` will be spread onto the outer
 *  `GridTemplate`. Each child will have their own rendered `GridTemplate`.
 *
 * @returns - The rendered `GridTemplate` containing the children for the `GridType.COLUMNS`
 */
function LayoutGridColumns<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends FormContextType = any>(
  props: LayoutFieldProps<T, S, F>,
) {
  const { layoutGridSchema, ...layoutGridFieldProps } = props;

  const { registry, uiSchema } = layoutGridFieldProps;
  const { children, gridProps } = findChildrenAndProps<T, S, F>(layoutGridSchema, GridType.COLUMNS, registry);
  const uiOptions = getUiOptions<T, S, F>(uiSchema);
  const GridTemplate = getTemplate<'GridTemplate', T, S, F>('GridTemplate', registry, uiOptions);

  return children.map((child) => (
    <GridTemplate
      column
      key={`column-${hashObject(child)}`}
      data-testid={LAYOUT_GRID_FIELD_TEST_IDS.col}
      {...gridProps}
    >
      <LayoutGridFieldChildren {...layoutGridFieldProps} childrenLayoutGridSchemaId={[child]} />
    </GridTemplate>
  ));
}

/** Renders a `GridTemplate` as a container. The `layoutGridSchema` for the `GridType.ROW` is separated into the
 * `children` and other `gridProps`. The `gridProps` will be spread onto the outer `GridTemplate`. Inside of the
 * `GridTemplate` all of the `children` are rendered.
 *
 * @returns - The rendered `GridTemplate` containing the children for the `GridType.ROW`
 */
function LayoutGridRow<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends FormContextType = any>(
  props: LayoutFieldProps<T, S, F>,
) {
  const { layoutGridSchema, ...layoutGridFieldProps } = props;

  const { registry, uiSchema } = layoutGridFieldProps;
  const { children, gridProps } = findChildrenAndProps<T, S, F>(layoutGridSchema, GridType.ROW, registry);
  const uiOptions = getUiOptions<T, S, F>(uiSchema);
  const GridTemplate = getTemplate<'GridTemplate', T, S, F>('GridTemplate', registry, uiOptions);

  return (
    <GridTemplate {...gridProps} data-testid={LAYOUT_GRID_FIELD_TEST_IDS.row}>
      <LayoutGridFieldChildren {...layoutGridFieldProps} childrenLayoutGridSchemaId={children} />
    </GridTemplate>
  );
}

/**
 * The props for the LayoutGridFieldComponent.
 */
type LayoutGridFieldComponentProps<
  T = any,
  S extends StrictRJSFSchema = RJSFSchema,
  F extends FormContextType = any,
> = LayoutGridFieldProps<T, S, F> & {
  /** The string or object that represents the configuration for the grid field */
  gridSchema?: ConfigObject | string;
};

/** Renders the field described by `gridSchema`. If `gridSchema` is not an object, then is will be assumed
 * to be the dotted-path to the field in the schema. Otherwise, we extract the `name`, and optional `render` and all
 * other props. If `name` does not exist and there is an optional `render`, we return the `render` component with only
 * specified props for that component. If `name` exists, we take the name, the initial & root schemas and the formData
 * and get the destination schema, is required state and optional oneOf/anyOf options for it. If the destination
 * schema was located along with oneOf/anyOf options then a `LayoutMultiSchemaField` will be rendered with the
 * `uiSchema`, `errorSchema`, `fieldPathId` and `formData` drilled down to the dotted-path field, spreading any other
 * props from `gridSchema` into the `ui:options`. If the destination schema located without any oneOf/anyOf options,
 * then a `SchemaField` will be rendered with the same props as mentioned in the previous sentence. If no destination
 * schema was located, but a custom render component was found, then it will be rendered with many of the non-event
 * handling props. If none of the previous render paths are valid, then a null is returned.
 *
 * @returns - One of `LayoutMultiSchemaField`, `SchemaField`, a custom render component or null, depending
 */
function LayoutGridFieldComponent<T = any, S extends StrictRJSFSchema = RJSFSchema, F extends FormContextType = any>(
  props: LayoutGridFieldComponentProps<T, S, F>,
) {
  const {
    gridSchema,
    schema: initialSchema,
    uiSchema,
    errorSchema,
    fieldPathId,
    onBlur,
    onFocus,
    formData,
    readonly,
    registry,
    layoutGridSchema, // Used to pull this out of otherProps since we don't want to pass it through
    ...otherProps
  } = props;
  const { onChange } = otherProps;
  const { fields } = registry;
  const { SchemaField, LayoutMultiSchemaField } = fields;

  const uiComponentProps = computeUIComponentPropsFromGridSchema(registry, gridSchema);
  const { name, UIComponent, uiProps } = uiComponentProps;
  const {
    schema,
    isRequired,
    isReadonly,
    optionsInfo,
    fieldPathId: fieldIdSchema,
  } = getSchemaDetailsForField<T, S, F>(registry, name, initialSchema, formData, fieldPathId);
  const memoFieldPathId = useDeepCompareMemo<FieldPathId>(fieldIdSchema);

  if (uiComponentProps.rendered) {
    return uiComponentProps.rendered;
  }

  if (schema) {
    const Field = optionsInfo?.hasDiscriminator ? LayoutMultiSchemaField : SchemaField;
    // Call this function to get the appropriate UISchema, which will always have its `readonly` state matching the
    // `uiReadonly` flag that it returns. This is done since the `SchemaField` will always defer to the `readonly`
    // state in the uiSchema over anything in the props or schema. Because we are implementing the "readonly" state of
    // the `Form` via the prop passed to `LayoutGridField` we need to make sure the uiSchema always has a true value
    // when it is needed
    const { fieldUiSchema, uiReadonly } = computeFieldUiSchema<T, S, F>(name, uiProps, uiSchema, isReadonly, readonly);

    return (
      <Field
        data-testid={
          optionsInfo?.hasDiscriminator
            ? LAYOUT_GRID_FIELD_TEST_IDS.layoutMultiSchemaField
            : LAYOUT_GRID_FIELD_TEST_IDS.field
        }
        {...otherProps}
        name={name}
        required={isRequired}
        readonly={uiReadonly}
        schema={schema}
        uiSchema={fieldUiSchema}
        errorSchema={get(errorSchema, name)}
        fieldPathId={memoFieldPathId}
        formData={get(formData, name)}
        onChange={onChange}
        onBlur={onBlur}
        onFocus={onFocus}
        options={optionsInfo?.options}
        registry={registry}
      />
    );
  }

  if (UIComponent) {
    return (
      <UIComponent
        data-testid={LAYOUT_GRID_FIELD_TEST_IDS.uiComponent}
        {...otherProps}
        name={name}
        required={isRequired}
        formData={formData}
        readOnly={!!isReadonly || readonly}
        errorSchema={errorSchema}
        uiSchema={uiSchema}
        schema={initialSchema}
        fieldPathId={fieldPathId}
        onBlur={onBlur}
        onFocus={onFocus}
        registry={registry}
        {...uiProps}
      />
    );
  }
  return null;
}

/** The `LayoutGridField` will render a schema, uiSchema and formData combination out into a GridTemplate in the shape
 * described in the uiSchema. To define the grid to use to render the elements within a field in the schema, provide in
 * the uiSchema for that field the object contained under a `ui:layoutGrid` element. E.g. (as a JSON object):
 *
 * ```
 * {
 *   "field1" : {
 *     "ui:field": "LayoutGridField",
 *     "ui:layoutGrid": {
 *       "ui:row": { ... }
 *     }
 *   }
 * }
 * ```
 *
 * The outermost level of a `LayoutGridField` is the `ui:row` that defines the nested rows, columns, and/or condition
 * elements (i.e. "grid elements") in the grid. This definition is either a simple "grid elements" OR an object with
 * native `GridTemplate` implementation specific props and a `children` array of "grid elements". E.g. (as JSON objects):
 *
 * Simple `ui:row` definition, without additional `GridTemplate` props:
 * ```
 *  "ui:row": [
 *    { "ui:row"|"ui:col"|"ui:columns"|"ui:condition": ... },
 *    ...
 *  ]
 * ```
 *
 * Complex `ui:row` definition, with additional `GridTemplate` (this example uses @mui/material/Grid2 native props):
 * ```
 *  "ui:row": {
 *    "spacing": 2,
 *    "size": { md": 4 },
 *    "alignContent": "flex-start",
 *    "className": "GridRow",
 *    "children": [
 *      { "ui:row"|"ui:col"|"ui:columns"|"ui:condition": ... },
 *      ...
 *    ]
 *  }
 * ```
 *
 * NOTE: Special note about the native `className` prop values. All className values will automatically be looked up in
 *       the `formContext.lookupMap` in case they have been defined using a CSS-in-JS approach. In other words, from the
 *       example above, if the `Form` was constructed with a `lookupMap` set to `{ GridRow: cssInJs.GridRowClass }`
 *       then when rendered, the native `GridTemplate` will get the `className` with the value from
 *       `cssInJs.GridRowClass`. This automatic lookup will happen for any of the "grid elements" when rendering with
 *       `GridTemplate` props. If multiple className values are present, for example:
 *       `{ className: 'GridRow GridColumn' }`, the classNames are split apart, looked up individually, and joined
 *       together to form one className with the values from `cssInJs.GridRowClass` and `cssInJs.GridColumnClass`.
 *
 * The `ui:col` grid element is used to specify the list of columns within a grid row. A `ui:col` element can take on
 * several forms: 1) a simple list of dotted-path field names within the root field; 2) a list of objects containing the
 * dotted-path field `name` any other props that are gathered into `ui:options` for the field; 3) a list with a one-off
 * `render` functional component with or without a non-field `name` identifier and any other to-be-spread props; and
 * 4) an object with native `GridTemplate` implementation specific props and a `children` array with 1) or 2) or even a
 * nested `ui:row` or a `ui:condition` containing a `ui:row` (although this should be used carefully). E.g.
 * (as JSON objects):
 *
 * Simple `ui:col` definition, without additional `GridTemplate` props and form 1 only children:
 * ```
 * "ui:col": ["innerField", "inner.grandChild", ...]
 * ```
 *
 * Complicated `ui:col` definition, without additional `GridTemplate` props and form 2 only children:
 * ```
 * "ui:col": [
 *    { "name": "innerField", "fullWidth": true },
 *    { "name": "inner.grandChild", "convertOther": true },
 *    ...
 *  ]
 * ```
 *
 * More complicated `ui:col` definition, without additional `GridTemplate` props and form 2 children, one being a
 * one-off `render` functional component without a non-field `name` identifier
 * ```
 * "ui:col": [
 *   "innerField",
 *     {
 *       "render": "WizardNavButton",
 *       "isNext": true,
 *       "size": "large"
 *     }
 *  ]
 *  ```
 *
 * Most complicated `ui:col` definition, additional `GridTemplate` props and form 1, 2 and 3 children  (this example
 * uses @mui/material/Grid2 native props):
 * ```
 * "ui:col": {
 *    "size": { "md": 4 },
 *    "className": "GridColumn",
 *    "children": [
 *      "innerField",
 *      { "name": "inner.grandChild", "convertOther": true },
 *      { "name": "customRender", "render": "CustomRender", toSpread: "prop-value" }
 *      { "ui:row|ui:condition": ... }
 *      ...
 *    ]
 *    }
 * ```
 *
 * NOTE: If a `name` prop does not exist or its value does not match any field in a schema, then it is assumed to be a
 *       custom `render` component. If the `render` prop does not exist, a null render will occur. If `render` is a
 *       string, its value will be looked up in the `formContext.lookupMap` first before defaulting to a null render.
 *
 * The `ui:columns` grid element is syntactic sugar to specify a set of `ui:col` columns that all share the same set of
 * native `GridTemplate` props. In other words rather than writing the following configuration that renders a
 * `<GridTemplate>` element with 3 `<GridTemplate column className="GridColumn col-md-4">` nodes and 2
 * `<GridTemplate column className="col-md-6">` nodes within it (one for each of the fields contained in the `children`
 * list):
 *
 * ```
 * "ui:row": {
 *   "children": [
 *     {
 *       "ui:col": {
 *         "className": "GridColumn col-md-4",
 *         "children": ["innerField"],
 *       }
 *     },
 *     {
 *       "ui:col": {
 *         "className": "GridColumn col-md-4",
 *         "children": ["inner.grandChild"],
 *       }
 *     },
 *     {
 *       "ui:col": {
 *         "className": "GridColumn col-md-4",
 *         "children": [{ "name": "inner.grandChild2" }],
 *       }
 *     },
 *     {
 *       "ui:col": {
 *         "className": "col-md-6",
 *         "children": ["innerField2"],
 *       }
 *     },
 *     {
 *       "ui:col": {
 *         "className": "col-md-6",
 *         "children": ["inner.grandChild3"],
 *       }
 *     },

 *   ]
 * }
 * ```
 *
 * One can write this instead:
 * ```
 * "ui:row": {
 *   "children": [
 *     {
 *       "ui:columns": {
 *         "className": "GridColumn col-md-4",
 *         "children": ["innerField", "inner.grandChild", { "name": "inner.grandChild2", "convertOther": true }],
 *       }
 *     },
 *     {
 *       "ui:columns": {
 *         "className": "col-md-6",
 *         "children": ["innerField2", "inner.grandChild3"],
 *       }
 *     }
 *   ]
 * }
 * ```
 *
 * NOTE: This syntax differs from
 *       `"ui:col": { "className": "col-md-6", "children": ["innerField2", "inner.grandChild3"] }` in that
 *       the `ui:col` will render the two children fields inside a single `<GridTemplate "className": "col-md-6",>`
 *       element.
 *
 * The final grid element, `ui:condition`, allows for conditionally displaying "grid elements" within a row based on the
 * current value of a field as it relates to a (list of) hard-coded value(s). There are four elements that make up a
 * `ui:condition`: 1) the dotted-path `field` name within the root field that makes up the left-side of the condition;
 * 2) the hard-coded `value` (single or list) that makes up the right-side of the condition; 3) the `operator` that
 * controls how the left and right sides of the condition are compared; and 4) the `children` array that defines the
 * "grid elements" to display if the condition passes.
 *
 * A `ui:condition` uses one of three `operators` when deciding if a condition passes: 1) The `all` operator will pass
 * when the right-side and left-side contains all the same value(s); 2) the `some` operator will pass when the
 * right-side and left-side contain as least one value in common; 3) the `none` operator will pass when the right-side
 * and left-side do not contain any values in common. E.g. (as JSON objects):
 *
 * Here is how to render an if-then-else for `field2` which is an enum that has 3 known values and supports allowing
 * any other value:
 * ```
 * "ui:row": [
 *   {
 *     "ui:condition": {
 *       "field": "field2",
 *       "operator": "all",
 *       "value": "value1",
 *       "children": [
 *         { "ui:row": [...] },
 *       ],
 *     }
 *   },
 *   {
 *     "ui:condition": {
 *       "field": "field2",
 *       "operator": "some",
 *       "value": ["value2", "value3"],
 *       "children": [
 *         { "ui:row": [...] },
 *       ],
 *     }
 *   },
 *   {
 *     "ui:condition": {
 *       "field": "field2",
 *       "operator": "none",
 *       "value": ["value1", "value2", "value3"],
 *       "children": [
 *         { "ui:row": [...] },
 *       ],
 *     }
 *   }
 * ]
 * ```
 */
export default function LayoutGridField<
  T = any,
  S extends StrictRJSFSchema = RJSFSchema,
  F extends FormContextType = any,
>(props: LayoutGridFieldProps<T, S, F>) {
  /** Render the `LayoutGridField`. If there isn't a `layoutGridSchema` prop defined, then try pulling it out of the
   * `uiSchema` via `ui:LayoutGridField`. If `layoutGridSchema` is an object, then check to see if any of the properties
   * match one of the `GridType`s. If so, call the appropriate render function for the type. Otherwise, just call the
   * generic `renderField()` function with the `layoutGridSchema`.
   */
  const { uiSchema } = props;
  let { layoutGridSchema } = props;
  const uiOptions = getUiOptions<T, S, F>(uiSchema);
  if (!layoutGridSchema && LAYOUT_GRID_UI_OPTION in uiOptions && isObject(uiOptions[LAYOUT_GRID_UI_OPTION])) {
    layoutGridSchema = uiOptions[LAYOUT_GRID_UI_OPTION];
  }

  if (isObject(layoutGridSchema)) {
    if (GridType.ROW in layoutGridSchema) {
      return <LayoutGridRow {...props} layoutGridSchema={layoutGridSchema} />;
    }
    if (GridType.COLUMN in layoutGridSchema) {
      return <LayoutGridCol {...props} layoutGridSchema={layoutGridSchema} />;
    }
    if (GridType.COLUMNS in layoutGridSchema) {
      return <LayoutGridColumns {...props} layoutGridSchema={layoutGridSchema} />;
    }
    if (GridType.CONDITION in layoutGridSchema) {
      return <LayoutGridCondition {...props} layoutGridSchema={layoutGridSchema} />;
    }
  }
  return <LayoutGridFieldComponent {...props} gridSchema={layoutGridSchema} />;
}

LayoutGridField.TEST_IDS = LAYOUT_GRID_FIELD_TEST_IDS;
