import { makeAutoObservable } from 'mobx';
import { Field } from './Field';
import { FieldType } from './Field.interface';
import { getUniqueId } from '../../utils/string/getUniqueId';
import { List } from '..';
import { isEmpty } from '@firebase/util';

/**
 * FormMode is a type provided for convenience so the user knows if the form is being used
 * to edit a value, create a new one or just available for viewing so the user can make
 * a decision on what to do with the form.
 */
export type Mode = 'create' | 'edit' | 'view';

/**
 * A callback function that is called when the form is submitted
 * @param data any data that you may want to pass to the callback
 */
export type onSubmissionFormCompletedCallback = (data?: unknown) => void;

/**
 * Interface for the Form type used for when the use is defining the shape of the form
 */
export interface FormType {
  /**
   * Unique id for the form
   * @optional
   * @default random string
   */
  id?: string;
  /**
   * The fields of the form
   */
  fields: FieldType[];
  /**
   * A callback function needs to called when the form is submitted
   * Note: this callback is added for convenience
   * this callback does not alter any data in this form
   * @optional
   */
  onSubmit?: onSubmissionFormCompletedCallback;
  /**
   * The title of the form
   * @optional
   */
  title?: string;
  /**
   * The form mode.
   * @optional  "create" | "edit" | "view"
   * Determine if the form is being used to
   * create a new value or to edit an existing one.
   * this optional is provided for user convenience and it
   * does not affect the form behavior.
   */
  mode?: Mode;
}

export class FormV2 {
  /**
   * The initial values of the form.
   * It is used to reset the form to its initial state.
   */
  initialValues: Record<string, unknown>;
  fields: List<Field>;
  id: string;
  mode?: Mode;
  onSubmit?: (data?: unknown) => void;
  title?: string;

  constructor(form: FormType) {
    this.initialValues = form.fields.reduce((acc, f) => {
      if (f?.id) {
        // @ts-ignore
        acc[f.id] = f.value;
      } else {
        console.warn(`Field with value ${f?.value} does not have an id`);
      }
      return acc;
    }, {});
    this.onSubmit = form.onSubmit;
    this.id = form.id || getUniqueId();
    this.title = form.title;
    this.mode = form.mode;
    this.fields = getFormFields(form.fields);

    makeAutoObservable(this);
  }

  /**
   * Whether the form is valid or not.
   * A form is valid if all of its fields are valid.
   * @returns boolean whether the form is valid or not
   */
  get valid(): boolean {
    return this.fields.values.every((f) => f.valid);
  }

  /**
   * Get form data in a key value pair.
   */
  get data() {
    const fields = this.fields.values;
    const formData = {};
    fields.forEach((f) => {
      if (f?.id !== undefined) {
        // @ts-ignore
        formData[f.id] = f?.value;
      } else {
        console.warn(
          `Field with value ${f?.value} does not have an id. This field will be ignored`
        );
      }
    });
    return formData;
  }

  /**
   * Autofill the form with values.
   * @param values the values to autofill the form with
   */
  autofill = (values: Record<string, unknown>) => {
    this.fields.values.forEach((f) => {
      f.set(values[f.id] as string);
    });
  };

  /**
   * Set the value of the field
   * @param id the field id to set its value
   * @param value the value of the field
   */
  setValue = (id: string, value: string) => {
    const field = this.fields.get(id);
    if (field) {
      field.set(value);
    }
  };

  /**
   * Get the value of the field
   * @param id the id of the field you want to get its value
   * @returns the value of the field or undefined if the field does not exist or is undefined
   */
  getValue = (id: string) => {
    const field = this.fields.get(id);
    if (field) {
      return field.value;
    }
    return undefined;
  };

  /**
   * Get the field
   * @param id the id of the field you want to get
   * @returns the field instance or undefined if the field does not exist or is undefined
   */
  getField = (id: string) => {
    return this.fields.get(id);
  };

  /**
   * Reset the form to its initial state.
   */
  reset = () => {
    this.fields.values.forEach((f) => {
      const initialValue = this.initialValues[f.id];
      if (
        initialValue === undefined ||
        initialValue === null ||
        isEmpty(initialValue)
      ) {
        console.warn(`Field with id ${f.id} does not have an initial value`);
        return;
      }

      f.set(initialValue);
    });
  };

  /**
   * Dynamically add a field to the form.
   * @param field the field to add to the form
   */
  addField = (field: FieldType) => {
    this.add(new Field(field));
  };

  /**
   * Add multiple fields to the form.
   * @param fields the fields to add to the form
   */
  addFields = (fields: FieldType[]) => {
    fields.forEach((f) => {
      this.addField(f);
    });
  };
  /**
   * Create another field of the same type and add it to the form.
   * @param id the id of the field to duplicate
   */
  duplicateField = (id: string) => {
    const field = this.fields.get(id);
    if (field) {
      const fieldObj = field.obj as any;
      const newField = new Field({
        ...fieldObj,
        value: '',
        id: getUniqueId()
      });

      this.add(newField);
    }
  };

  /**
   * Get field object.
   * @param id id of the field to get its properties
   * @returns an object with the properties of the field
   */
  getFieldProperties = (id: string) => {
    const field = this.fields.get(id);
    if (field) {
      return field.obj;
    }
    return undefined;
  };

  /**
   * Dynamically add a field to the form.
   * @param field the field to add to the form
   * @private this method is private and should not be used
   */
  private add = (field: Field) => {
    this.fields.set(field);
  };

  /**
   * Remove a field from the form.
   * @param id the id of the field to remove
   */
  removeField = (id: string) => {
    this.fields.delete(id);
  };

  /**
   * Remove all fields from the form.
   */
  removeAllFields = () => {
    this.fields.reset();
  };
}

const getFormFields = (fields: FieldType[]) => {
  return new List<Field>(fields.map((f) => new Field(f)));
};
