import {
  observable,
  observe,
  action,
  computed,
  isObservableArray,
  toJS,
  untracked,
  makeObservable,
  autorun,
  runInAction,
} from "mobx";
import { isEmpty, isEqual, isNil, isPlainObject, debounce, omit } from "lodash";
import Base from "./Base";

import {
  $try,
  hasFiles,
  isBool,
  isEvent,
  pathToStruct,
  isArrayFromStruct,
} from "./utils";

import { parseInput, parseCheckOutput, defaultValue } from "./parser";

import { OptionsModel, OptionsEnum } from "./models/OptionsModel";
import { FieldInterface, FieldConstructor } from "./models/FieldInterface";
import { FieldPropsEnum } from "./models/FieldProps";

const applyFieldPropFunc = (instance: any, prop: any): any => {
    if (typeof prop !== "function") return prop;
  return prop({
    field: instance,
    form: instance.state.form,
  });
};

const retrieveFieldPropFunc = (prop: any): Function | any | undefined =>
  typeof prop === "function" ? prop : undefined;

const propGetter = (instance: any, prop: FieldPropsEnum): any =>
  typeof instance[`_${prop}`] === "function"
    ? instance[`_${prop}`].apply(instance, [
        {
          form: instance.state.form,
          field: instance,
        },
      ])
    : instance[`$${prop}`];

const setupFieldProps = (instance: any, props: any, data: any) =>
  Object.assign(instance, {
    // retrieve functions
    _label: retrieveFieldPropFunc(props.$label || data?.label),
    _placeholder: retrieveFieldPropFunc(
      props.$placeholder || data?.placeholder,
    ),
    _disabled: retrieveFieldPropFunc(props.$disabled || data?.disabled),
    _rules: retrieveFieldPropFunc(props.$rules || data?.rules),
    _related: retrieveFieldPropFunc(props.$related || data?.related),
    _deleted: retrieveFieldPropFunc(props.$deleted || data?.deleted),
    _validators: retrieveFieldPropFunc(props.$validators || data?.validators),
    _validatedWith: retrieveFieldPropFunc(
      props.$validatedWith || data?.validatedWith,
    ),
    _bindings: retrieveFieldPropFunc(props.$bindings || data?.bindings),
    _extra: retrieveFieldPropFunc(props.$extra || data?.extra),
    _options: retrieveFieldPropFunc(props.$options || data?.options),
    _autoFocus: retrieveFieldPropFunc(props.$autoFocus || data?.autoFocus),
    _inputMode: retrieveFieldPropFunc(props.$inputMode || data?.inputMode),
    // apply functions or value
    $label: applyFieldPropFunc(instance, props.$label || data?.label || ""),
    $placeholder: applyFieldPropFunc(
      instance,
      props.$placeholder || data?.placeholder || "",
    ),
    $disabled: applyFieldPropFunc(
      instance,
      props.$disabled || data?.disabled || false,
    ),
    $rules: applyFieldPropFunc(instance, props.$rules || data?.rules || null),
    $related: applyFieldPropFunc(
      instance,
      props.$related || data?.related || [],
    ),
    $deleted: applyFieldPropFunc(
      instance,
      props.$deleted || data?.deleted || false,
    ),
    $validatedWith: applyFieldPropFunc(
      instance,
      props.$validatedWith || data?.validatedWith || FieldPropsEnum.value,
    ),
    $bindings: applyFieldPropFunc(
      instance,
      props.$bindings || data?.bindings || FieldPropsEnum.default,
    ),
    $extra: applyFieldPropFunc(instance, props.$extra || data?.extra || null),
    $options: applyFieldPropFunc(
      instance,
      props.$options || data?.options || {},
    ),
    $autoFocus: applyFieldPropFunc(
      instance,
      props.$autoFocus || data?.autoFocus || false,
    ),
    $inputMode: applyFieldPropFunc(
      instance,
      props.$inputMode || data?.inputMode || undefined,
    ),
    $validators: applyFieldPropFunc(
      instance,
      props.$validators || data?.validators || null,
    ),
    // other props
    $hooks: props.$hooks || data?.hooks || {},
    $handlers: props.$handlers || data?.handlers || {},
    $observers: props.$observers || data?.observers || null,
    $interceptors: props.$interceptors || data?.interceptors || null,
    $ref: props.$ref || data?.ref || undefined,
    $nullable: props.$nullable || data?.nullable || false,
    $autoComplete: props.$autoComplete || data?.autoComplete || undefined,
  });

const setupDefaultProp = (
  instance: Field,
  data: any,
  props: any,
  update: boolean,
  {
    isEmptyArray,
    fallbackValueOption,
  }: { isEmptyArray: boolean; fallbackValueOption: any },
) =>
  parseInput((val: any) => val, {
    isEmptyArray,
    type: instance.type,
    unified: update
      ? defaultValue({
          fallbackValueOption,
          type: instance.type,
          value: instance.value,
        })
      : data?.default,
    separated: props.$default,
    fallback: instance.$initial,
  });

export default class Field<T = any> extends Base<Record<string, any>> implements FieldInterface<T> {
  hasInitialNestedFields = false;
  incremental = false;

  id: any;
  key: any;
  name: any;

  $observers: any;
  $interceptors: any;

  $converter = ($: any) => $;
  $input = ($: any) => $;
  $output = ($: any) => $;

  _value!: Function;
  _label!: Function;
  _placeholder!: Function;
  _disabled!: Function;
  _rules!: Function;
  _related!: Function;
  _deleted!: Function;
  _validatedWith!: Function;
  _validators!: Function;
  _bindings!: Function;
  _extra!: Function;
  _options!: Function;
  _autoFocus!: Function;
  _inputMode!: Function;

  $options: OptionsModel | undefined = undefined;
  $value: any = undefined;
  $type: string | undefined = undefined;
  $label: string | undefined = undefined;
  $placeholder: string | undefined = undefined;
  $default: any = undefined;
  $initial: any = undefined;
  $bindings: any = undefined;
  $extra: any = undefined;
  $related: string[] | undefined = undefined;
  $validatedWith: string | undefined = undefined;
  $validators: any[] | undefined = undefined;
  $rules: string[] | undefined = undefined;

  $disabled: boolean = false;
  $focused: boolean = false;
  $blurred: boolean = false;
  $deleted: boolean = false;
  $autoFocus: boolean = false;
  $inputMode: string | undefined = undefined;
  $ref: any = undefined;
  $nullable: boolean = false;
  $autoComplete: string | undefined = undefined;

  showError: boolean = false;
  errorSync: string | null = null;
  errorAsync: string | null = null;

  validationErrorStack: string[] = [];
  validationFunctionsData: any[] = [];
  validationAsyncData: { valid: boolean; message: string | null } = { valid: true, message: null };
  debouncedValidation: any;

  disposeValidationOnBlur: any;
  disposeValidationOnChange: any;

  files: any = undefined;

  constructor({
    key,
    path,
    struct,
    data = {},
    props = {},
    update = false,
    state,
  }: FieldConstructor) {
    super();

    makeObservable(this, {
      $options: observable,
      $value: observable,
      $type: observable,
      $label: observable,
      $placeholder: observable,
      $default: observable,
      $initial: observable,
      $bindings: observable,
      $extra: observable,
      $related: observable,
      $validatedWith: observable,
      $validators: observable,
      $rules: observable,
      $disabled: observable,
      $focused: observable,
      $blurred: observable,
      $deleted: observable,
      showError: observable,
      errorSync: observable,
      errorAsync: observable,
      validationErrorStack: observable,
      validationFunctionsData: observable,
      validationAsyncData: observable,
      files: observable,
      autoFocus: computed,
      inputMode: computed,
      ref: computed,
      checkValidationErrors: computed,
      checked: computed,
      value: computed,
      initial: computed,
      default: computed,
      actionRunning: computed,
      type: computed,
      label: computed,
      placeholder: computed,
      extra: computed,
      options: computed,
      bindings: computed,
      related: computed,
      disabled: computed,
      rules: computed,
      validators: computed,
      validatedValue: computed,
      error: computed,
      hasError: computed,
      isValid: computed,
      isDefault: computed,
      isDirty: computed,
      isPristine: computed,
      isEmpty: computed,
      blurred: computed,
      touched: computed,
      deleted: computed,
      setupField: action,
      initNestedFields: action,
      invalidate: action,
      setValidationAsyncData: action,
      resetValidation: action,
      clear: action,
      reset: action,
      focus: action,
      blur: action,
      showErrors: action,
      update: action,
    });

    this.state = state;

    this.setupField(key, path, struct, data, props, update);
    // this.checkValidationPlugins();
    this.initNestedFields(data, update);

    this.incremental = this.hasIncrementalKeys;

    this.debouncedValidation = debounce(
      this.validate,
      this.state.options.get(OptionsEnum.validationDebounceWait, this),
      this.state.options.get(OptionsEnum.validationDebounceOptions, this),
    );

    this.observeValidationOnBlur();
    this.observeValidationOnChange();

    this.initMOBXEvent(FieldPropsEnum.observers);
    this.initMOBXEvent(FieldPropsEnum.interceptors);

    // setup hooks & handlers from initialization methods
    runInAction(() =>
      Object.assign(this.$hooks, (this as any).hooks?.apply(this, [this])),
    );
    runInAction(() =>
      Object.assign(
        this.$handlers,
        (this as any).handlers?.apply(this, [this]),
      ),
    );

    this.execHook(FieldPropsEnum.onInit);

    // handle Field onChange Hook
    autorun(() => this.changed && this.execHook(FieldPropsEnum.onChange));
  }

  /* ------------------------------------------------------------------ */
  /* COMPUTED */

  get checkValidationErrors(): boolean {
    return (
      !this.validationAsyncData.valid ||
      !isEmpty(this.validationErrorStack) ||
      typeof this.errorAsync === 'string' ||
      typeof this.errorSync === 'string'
    );
  }

  set value(newVal: T) {
    let val: any = newVal;
    if (
      typeof val === 'string' &&
      this.state.options.get(OptionsEnum.autoTrimValue, this)
    ) {
      val = val.trim();
    }
    if (this.$value === val) return;
    if (this.handleSetNumberValue(val)) return;
    this.$value = this.$converter(val);
    this.$changed++;
    if (!this.actionRunning) {
      this.state.form.$changed++;
    }
  }

  handleSetNumberValue(newVal: any): boolean | undefined {
    if (!this.state.options.get(OptionsEnum.autoParseNumbers, this))
      return false;

    if (typeof this.$initial === 'number' || this.type == "number") {
      if (
        new RegExp("^-?\\d+(,\\d+)*(\\.\\d+([eE]\\d+)?)?$", "g").exec(newVal)
      ) {
        this.$value = this.$converter(Number(newVal));
        this.$changed++;
        if (!this.actionRunning) {
          this.state.form.$changed++;
        }
        return true;
      }
    }
  }

  get actionRunning(): boolean {
    return this.submitting || this.clearing || this.resetting;
  }

  get checked(): T | undefined {
    return this.type === "checkbox" ? this.value : undefined;
  }

  get value(): T {
    return typeof this._value === "function" && !this.hasNestedFields
      ? propGetter(this, FieldPropsEnum.value)
      : this.getComputedProp(FieldPropsEnum.value);
  }

  get initial(): T {
    return this.$initial
      ? toJS(this.$initial)
      : this.getComputedProp(FieldPropsEnum.initial);
  }

  get default(): T {
    return this.$default
      ? toJS(this.$default)
      : this.getComputedProp(FieldPropsEnum.default);
  }

  set initial(val: T) {
    this.$initial = val;
  }

  set default(val: T) {
    this.$default = val;
  }

  get nullable(): boolean {
    return propGetter(this, FieldPropsEnum.nullable);
  }

  get autoComplete(): string | undefined {
    return propGetter(this, FieldPropsEnum.autoComplete);
  }

  get ref(): any {
    return propGetter(this, FieldPropsEnum.ref);
  }

  get extra(): any {
    return propGetter(this, FieldPropsEnum.extra);
  }

  get autoFocus(): any {
    return propGetter(this, FieldPropsEnum.autoFocus);
  }

  get inputMode(): any {
    return propGetter(this, FieldPropsEnum.inputMode);
  }

  get type(): any {
    return propGetter(this, FieldPropsEnum.type);
  }

  get label(): any {
    return propGetter(this, FieldPropsEnum.label);
  }

  get placeholder(): any {
    return propGetter(this, FieldPropsEnum.placeholder);
  }

  get options(): any {
    return propGetter(this, FieldPropsEnum.options);
  }

  get bindings(): any {
    return propGetter(this, FieldPropsEnum.bindings);
  }

  get related(): any {
    return propGetter(this, FieldPropsEnum.related);
  }

  get disabled(): any {
    return propGetter(this, FieldPropsEnum.disabled);
  }

  get rules(): any {
    return propGetter(this, FieldPropsEnum.rules);
  }

  get validators(): any {
    return propGetter(this, FieldPropsEnum.validators);
  }

  get validatedWith(): any {
    return propGetter(this, FieldPropsEnum.validatedWith);
  }

  get validatedValue(): any {
    return parseCheckOutput(this, this.validatedWith);
  }

  get error(): string | null {
    if (this.showError === false) return null;
    return this.errorAsync || this.errorSync || this.firstError() || null;
  }

  get hasError(): boolean {
    return (
      this.checkValidationErrors || this.check(FieldPropsEnum.hasError, true)
    );
  }

  get isValid(): boolean {
    return (
      !this.checkValidationErrors && this.check(FieldPropsEnum.isValid, true)
    );
  }

  get isDefault(): boolean {
    return !isNil(this.default) && isEqual(this.default, this.value);
  }

  get isDirty(): boolean {
    const value = this.changed ? this.value : this.initial;
    return !isEqual(this.initial, value);
  }

  get isPristine(): boolean {
    const value = this.changed ? this.value : this.initial;
    return isEqual(this.initial, value);
  }

  get isEmpty(): boolean {
    if (this.hasNestedFields) return this.check(FieldPropsEnum.isEmpty, true);
    if (typeof this.value === 'boolean') return !!this.$value;
    if (typeof this.value === 'number') return false;
    if (this.value instanceof Date) return false;
    if (this.value === null) return false;
    return isEmpty(this.value);
  }

  get focused(): boolean {
    return this.hasNestedFields
      ? this.check(FieldPropsEnum.focused, true)
      : this.$focused;
  }

  get blurred(): boolean {
    return this.hasNestedFields
      ? this.check(FieldPropsEnum.blurred, true)
      : this.$blurred;
  }

  get touched(): boolean {
    return this.hasNestedFields
      ? this.check(FieldPropsEnum.touched, true)
      : this.$touched;
  }

  get deleted(): boolean {
    return this.hasNestedFields
      ? this.check(FieldPropsEnum.deleted, true)
      : this.$deleted;
  }

  /* ------------------------------------------------------------------ */
  /* EVENTS HANDLERS */

  sync = action((e: any, v: any = null) => {
    const $get = ($: any) =>
      isBool($, this.value) ? $.target.checked : $.target.value;

    // assume "v" or "e" are the values
    if (isNil(e) || isNil(e.target)) {
      if (!isNil(v) && !isNil(v.target)) {
        v = $get(v); // eslint-disable-line
      }

      this.value = $try(e, v);
      return;
    }

    if (!isNil(e.target)) {
      this.value = $get(e);
      return;
    }

    this.value = e;
  });

  onSync = (...args: any) =>
    this.type === "file"
      ? this.onDrop(...args)
      : this.execHandler(
          FieldPropsEnum.onChange,
          args,
          this.sync,
          FieldPropsEnum.onSync,
        );

  onChange = this.onSync;

  onToggle = (...args: any) =>
    this.execHandler(FieldPropsEnum.onToggle, args, this.sync);

  onBlur = (...args: any) =>
    this.execHandler(
      FieldPropsEnum.onBlur,
      args,
      action(() => {
        this.$focused = false;
        this.$blurred = true;
      }),
    );

  onFocus = (...args: any) =>
    this.execHandler(
      FieldPropsEnum.onFocus,
      args,
      action(() => {
        this.$focused = true;
        this.$touched = true;
      }),
    );

  onDrop = (...args: any) =>
    this.execHandler(
      FieldPropsEnum.onDrop,
      args,
      action(() => {
        const e = args[0];
        let files: unknown[] | null = null;

        if (isEvent(e) && hasFiles(e)) {
          files = Array.from(e.target.files);
        }

        this.files = [...(this.files || []), ...(files || args)];
      }),
    );

  onKeyDown = (...args: any) =>
    this.execHandler(FieldPropsEnum.onKeyDown, args);

  onKeyUp = (...args: any) => this.execHandler(FieldPropsEnum.onKeyUp, args);

  setupField(
    $key: string,
    $path: string | undefined | null,
    _$struct: string | undefined | null,
    $data: any,
    $props: any,
    update: boolean,
  ): void {
    this.key = $key;
    this.path = $path;
    this.id = this.state.options.get(OptionsEnum.uniqueId)?.apply(this, [this]);
    const fallbackValueOption: any = this.state.options.get(
      OptionsEnum.fallbackValue,
      this,
    );
    const applyInputConverterOnInit: boolean = this.state.options.get(
      OptionsEnum.applyInputConverterOnInit,
      this,
    );
    const struct: string[] = this.state.struct();
    const structPath: string = pathToStruct(this.path ?? "");
    const isEmptyArray: boolean = isArrayFromStruct(struct, structPath);

    const { $type, $input, $output, $converter, $converters, $computed } =
      $props;

    if (isPlainObject($data)) {
      const { type, input, output, converter, converters, computed } = $data;

      this.name = $data.name ?? String($key);
      this.$type = $type || type || "text";
      this.$converter = $try(
        $converter,
        $converters,
        converter,
        converters,
        this.$converter,
      );
      this.$input = $try($input, input, this.$input);
      this.$output = $try($output, output, this.$output);

      const value = parseInput(
        applyInputConverterOnInit ? this.$input : (val: any) => val,
        {
          fallbackValueOption,
          isEmptyArray,
          type: this.type,
          unified: computed || $data.value,
          separated: $computed || $props.$value,
          fallback: $props.$initial,
        },
      );

      this._value = retrieveFieldPropFunc(value);
      this.$value =
        typeof this._value === "function"
          ? applyFieldPropFunc(this, value)
          : value;

      this.$initial = parseInput((val: any) => val, {
        fallbackValueOption,
        isEmptyArray,
        type: this.type,
        unified: $data.initial,
        separated: $props.$initial,
        fallback: this.$value,
      });

      this.$default = setupDefaultProp(this, $data, $props, update, {
        fallbackValueOption,
        isEmptyArray,
      });

      setupFieldProps(this, $props, $data);
      return;
    }

    /* The field IS the value here */
    this.name = String($key);
    this.$type = $type || "text";
    this.$converter = $try($converter, $converters, this.$converter);
    this.$input = $try($input, this.$input);
    this.$output = $try($output, this.$output);

    const value = parseInput(
      applyInputConverterOnInit ? this.$input : (val: any) => val,
      {
        fallbackValueOption,
        isEmptyArray,
        type: this.type,
        unified: $computed || $data,
        separated: $computed || $props.$value,
      },
    );

    this._value = retrieveFieldPropFunc(value);
    this.$value =
      typeof this._value === "function"
        ? applyFieldPropFunc(this, value)
        : value;

    this.$initial = parseInput((val: any) => val, {
      fallbackValueOption,
      isEmptyArray,
      type: this.type,
      unified: $data,
      separated: $props.$initial,
      fallback: this.$value,
    });

    this.$default = setupDefaultProp(this, $data, $props, update, {
      fallbackValueOption,
      isEmptyArray,
    });

    setupFieldProps(this, $props, $data);
  }

  getComputedProp(key: string): any {
    if (this.incremental || this.hasNestedFields) {
      return key === FieldPropsEnum.value
        ? this.get(key, false)
        : untracked(() => this.get(key, false));
    }

    // @ts-ignore
    const val = this[`$${key}`];

    if (Array.isArray(val) || isObservableArray(val)) {
      return [].slice.call(val);
    }

    return toJS(val);
  }

  // checkValidationPlugins(): void {
  //   const { drivers } = this.state.form.validator;
  //   const form = this.state.form.name ? `${this.state.form.name}/` : "";

  //   if (isNil(drivers.dvr) && !isNil(this.rules)) {
  //     throw new Error(
  //       `The DVR validation rules are defined but no DVR plugin provided. Field: "${
  //         form + this.path
  //       }".`
  //     );
  //   }

  //   if (isNil(drivers.vjf) && !isNil(this.validators)) {
  //     throw new Error(
  //       `The VJF validators functions are defined but no VJF plugin provided. Field: "${
  //         form + this.path
  //       }".`
  //     );
  //   }
  // }

  initNestedFields(field: any, update: boolean): void {
    const fields = isNil(field) ? null : field.fields;

    if (Array.isArray(fields) && !isEmpty(fields)) {
      this.hasInitialNestedFields = true;
    }

    this.initFields({ fields }, update);

    if (!update && Array.isArray(fields) && isEmpty(fields)) {
      if (Array.isArray(this.value) && !isEmpty(this.value)) {
        this.hasInitialNestedFields = true;
        this.initFields({ fields, values: this.value }, update);
      }
    }
  }

  invalidate(
    message: string,
    deep: boolean = true,
    async: boolean = false,
  ): void {
    if (async === true) {
      this.errorAsync = message;
      this.showErrors(true, deep);
      return;
    }

    if (Array.isArray(message)) {
      this.validationErrorStack = message;
      this.showErrors(true, deep);
      return;
    }

    this.validationErrorStack.unshift(message);
    this.showErrors(true, deep);
  }

  setValidationAsyncData(
    valid: boolean = false,
    message: string | null = null,
  ): void {
    this.validationAsyncData = { valid, message };
  }

  resetValidation(deep: boolean = false): void {
    this.showError = false;
    this.errorSync = null;
    this.errorAsync = null;
    this.validationAsyncData = { valid: true, message: null };
    this.validationFunctionsData = [];
    this.validationErrorStack = [];
    Promise.resolve().then(
      action(() => {
        this.$resetting = false;
        this.$clearing = false;
      }),
    );

    deep && this.each((field: FieldInterface) => field.resetValidation(deep));
  }

  clear(deep: boolean = true, execHook: boolean = true): void {
    execHook && this.execHook(FieldPropsEnum.onClear);
    this.$clearing = true;
    this.$touched = false;
    this.$blurred = false;
    this.$changed = 0;
    this.files = undefined;

    this.$value = defaultValue({
      fallbackValueOption: this.state.options.get(OptionsEnum.fallbackValue),
      value: this.$value,
      nullable: this.$nullable,
      type: this.type,
    });

    deep && this.each((field: FieldInterface) => field.clear(deep));

    this.state.options.get(OptionsEnum.validateOnClear, this)
      ? this.validate({
          showErrors: this.state.options.get(
            OptionsEnum.showErrorsOnClear,
            this,
          ),
        })
      : this.resetValidation(deep);
  }

  reset(deep: boolean = true, execHook: boolean = true): void {
    execHook && this.execHook(FieldPropsEnum.onReset);
    this.$resetting = true;
    this.$touched = false;
    this.$blurred = false;
    this.$changed = 0;
    this.files = undefined;

    const useDefaultValue = this.$default !== this.$initial;
    if (useDefaultValue) this.value = this.$default;
    if (!useDefaultValue) this.value = this.$initial;

    deep && this.each((field: FieldInterface) => field.reset(deep));

    this.state.options.get(OptionsEnum.validateOnReset, this)
      ? this.validate({
          showErrors: this.state.options.get(
            OptionsEnum.showErrorsOnReset,
            this,
          ),
        })
      : this.resetValidation(deep);
  }

  focus(): void {
    if (this.ref && !this.focused) this.ref.focus();
    this.$focused = true;
    this.$touched = true;
  }

  blur(): void {
    if (this.ref && this.focused) this.ref.blur();
    this.$focused = false;
    this.$blurred = true;
  }

  trim(): void {
    if (typeof this.value !== 'string') return;
    this.$value = this.value.trim();
  }

  showErrors(show: boolean = true, deep: boolean = true): void {
    this.showError = show;
    this.errorSync = this.validationErrorStack.length ? (this.validationErrorStack[0] as string) : null;
    this.errorAsync = !this.validationAsyncData.valid
      ? this.validationAsyncData.message
      : null;
    deep && this.each((field: FieldInterface) => field.showErrors(show, deep));
  }

  observeValidationOnBlur(): void {
    const opt = this.state.options;
    if (opt.get(OptionsEnum.validateOnBlur, this)) {
      this.disposeValidationOnBlur = observe(
        this,
        "$focused",
        (change) =>
          change.newValue === false &&
          this.debouncedValidation({
            showErrors: opt.get(OptionsEnum.showErrorsOnBlur, this),
          }),
      );
    }
  }

  observeValidationOnChange(): void {
    const opt = this.state.options;
    if (opt.get(OptionsEnum.validateOnChange, this)) {
      this.disposeValidationOnChange = observe(
        this,
        "$value",
        () =>
          !this.actionRunning &&
          this.debouncedValidation({
            showErrors: opt.get(OptionsEnum.showErrorsOnChange, this),
          }),
      );
    } else if (
      opt.get(OptionsEnum.validateOnChangeAfterInitialBlur, this) ||
      opt.get(OptionsEnum.validateOnChangeAfterSubmit, this)
    ) {
      this.disposeValidationOnChange = observe(
        this,
        "$value",
        () =>
          !this.actionRunning &&
          ((opt.get(OptionsEnum.validateOnChangeAfterInitialBlur, this) &&
            this.blurred) ||
            (opt.get(OptionsEnum.validateOnChangeAfterSubmit, this) &&
              this.state.form.submitted)) &&
          this.debouncedValidation({
            showErrors: opt.get(OptionsEnum.showErrorsOnChange, this),
          }),
      );
    }
  }

  initMOBXEvent(type: string): void {
    const arr: any[] = (this as any)[`$${type}`];
    if (!Array.isArray(arr)) return;

    let fn: any;
    if (type === FieldPropsEnum.observers) fn = this.observe;
    if (type === FieldPropsEnum.interceptors) fn = this.intercept;
    arr.map((obj: any) => fn(omit(obj, FieldPropsEnum.path)));
  }

  bind(props = {}) {
    return {
      ...this.state.bindings.load(this, this.bindings, props),
      ref: ($ref: any) => (this.$ref = $ref),
    };
  }

  override update(fields: any): void {
    if (!isPlainObject(fields)) {
      throw new Error("The update() method accepts only plain objects.");
    }
    const fallback = this.state.options.get(OptionsEnum.fallback, this);
    const applyInputConverterOnUpdate = this.state.options.get(
      OptionsEnum.applyInputConverterOnUpdate,
      this,
    );
    const x = this.state
      .struct()
      .findIndex((s) =>
        s.startsWith((this.path ?? "").replace(/\.\d+\./, "[].") + "[]"),
      );
    if (!fallback && this.fields.size === 0 && x < 0) {
      this.value = parseInput(
        applyInputConverterOnUpdate ? this.$input : (val: any) => val,
        {
          fallbackValueOption: this.state.options.get(
            OptionsEnum.fallbackValue,
            this,
          ),
          separated: fields,
        },
      );
      return;
    }
    super.update(fields);
  }
}
