import { useRef, useEffect, useState, useCallback } from "react";
import { DefaultState, DefaultError, FormState, ChildFormState, Validator, FieldsOfType, KeysOfType, ErrorType } from "./form";

/**
 * Creates a new root form.
 * This hook doesn't cause a rerender.
 * @param defaultValues The default values for this form. When this value changes, nothing happens, use useEffect() with form.setValues to set form values on state change.
 * @param validator The validator to use, optional.
 * @param validateOnChange Validate on change? Optional, default is false.
 * @param validateOnMount Validate on mount? Optional, default is false.
 * @param defaultState The default state for this form. Form state contains custom global states, example: isSubmitting, isLoading ... Optional, default is `{ isSubmitting: false }`.
 */
export function useForm<T extends object, State = DefaultState, Error extends string = DefaultError>(
    defaultValues: T,
    validator?: Validator<T, Error>,
    validateOnChange = false,
    validateOnMount = false,
    defaultState?: State
) {
    let c = useRef<FormState<T, State, Error> | null>(null);

    if (!c.current) {
        c.current = new FormState(
            defaultValues,
            defaultValues,
            defaultState ?? ({ isSubmitting: false } as any),
            validator,
            validateOnMount,
            validateOnChange
        );
    }

    return c.current;
}

/**
 * Creates a nested form for another root or nested form. You must use this for object and array (see useArrayField) field.
 * This hook doesn't cause a rerender.
 * @param parentForm The parent form.
 * @param name The parent's field to create a child form for.
 */
export function useObjectField<
    T extends FieldsOfType<any, object | any[]>,
    K extends KeysOfType<T, object | any[]>,
    State = DefaultState,
    Error extends string = DefaultError
>(parentForm: FormState<T, State, Error>, name: K) {
    let c = useRef<ChildFormState<T, K, State, Error> | null>(null);
    if (!c.current) {
        c.current = new ChildFormState(parentForm, name);
    }

    useEffect(() => {
        // Update parent and child form
        parentForm.childMap[name] = c.current!;
        c.current!.name = name;

        // First, set new default values, without validating
        c.current!.setValues(parentForm.defaultValues[name], false, true, true, false);
        // Then, set new values and validate if needed
        c.current!.setValues(parentForm.values[name], c.current!.validateOnMount, false, true, true);

        return () => {
            // Only delete if is not already overwritten by new form
            if (parentForm.childMap[name] === c.current!) {
                delete parentForm.childMap[name];
            }
        };
    }, [parentForm, name]);

    return c.current;
}

export interface FormField<
    T extends object = any,
    K extends keyof T = never,
    State extends DefaultState = DefaultState,
    Error extends string = DefaultError
> {
    value: T[K];
    defaultValue: T[K];
    setValue: (value: T[K]) => void;
    dirty: boolean;
    error: ErrorType<T[K], Error> | undefined;
    state: State;
    form: FormState<T, State, Error>;
}

/**
 * Listen for changes on a form's field. Behaves like useState.
 * You shouldn't use this hook in large components, as it rerenders each time something changes. Use the wrapper <Listener /> instead.
 * @param form The form to listen on.
 * @param name The form's field to listen to.
 */
export function useListener<T extends object, K extends keyof T, State extends DefaultState = DefaultState, Error extends string = DefaultError>(
    form: FormState<T, State, Error>,
    name: K
): FormField<T, K, State, Error> {
    const [, setRender] = useState(0);

    useEffect(() => {
        let id = form.listen(name, () => {
            setRender((e) => e + 1);
        });
        return () => form.ignore(name, id);
    }, [form, name]);

    return {
        value: form.values[name],
        defaultValue: form.defaultValues[name],
        setValue: (value: T[K]) => form.setValue(name, value),
        dirty: form.dirtyMap[name] ?? false,
        error: form.errorMap[name],
        state: form.state,
        form
    };
}

/**
 * Listens for any change on this form. Behaves like useState.
 * You shouldn't use this hook in large components, as it rerenders each time something changes. Use the wrapper <AnyListener /> instead.
 *
 * @param form The form that was passed in.
 */
export function useAnyListener<T extends object, State = DefaultState, Error extends string = DefaultError>(form: FormState<T, State, Error>) {
    const [, setRender] = useState(0);

    useEffect(() => {
        let id = form.listenAny(() => {
            setRender((e) => e + 1);
        });
        return () => {
            form.ignoreAny(id);
        };
    }, [form]);

    return form;
}

/**
 * This is a wrapper around useObjectForm, with useful functions to manipulate arrays.
 * This hook does cause a rerender, but only if the array size changes.
 * @param parentForm The parent form.
 * @param name The parent's field to create a child form for.
 */
export function useArrayField<
    T extends FieldsOfType<any, any[]>,
    K extends KeysOfType<T, any[] | object>,
    State = DefaultState,
    Error extends string = DefaultError
>(parentForm: FormState<T, State, Error>, name: K) {
    const form = useObjectField(parentForm, name);
    const oldLength = useRef(-1);
    const [, setRender] = useState(0);

    // Only rerender when array size changed
    useEffect(() => {
        let id = parentForm.listen(name, () => {
            let val = parentForm.values[name] as any;
            if (Array.isArray(val) && val.length !== oldLength.current) {
                setRender((i) => i + 1);
                oldLength.current = val.length;
            }
        });
        return () => parentForm.ignore(name, id);
    }, []);

    const append = useCallback((value: NonNullable<T[K]>[any]) => {
        form.setValues([...(form.values as any), value] as any, true, false);
    }, []);

    const remove = useCallback((index: number) => {
        let newValues = [...(form.values as any)];
        newValues.splice(index, 1);
        form.setValues(newValues as any, true, false);
    }, []);

    const clear = useCallback(() => {
        form.setValues([] as any, true, false);
    }, []);

    const move = useCallback((from: number, to: number) => {
        if (to === from) return;
        let newArr = [...(form.values as any)];
        var target = newArr[from];
        var increment = to < from ? -1 : 1;
        for (var k = from; k !== to; k += increment) {
            newArr[k] = newArr[k + increment];
        }
        newArr[to] = target;
        form.setValues(newArr as any, true, false);
    }, []);

    const swap = useCallback((index: number, newIndex: number) => {
        if (index === newIndex) {
            return;
        }
        let values = [...(form.values as any)];
        [values[index], values[newIndex]] = [values[newIndex], values[index]];
        form.setValues(values as any, true, false);
    }, []);

    return {
        remove: remove,
        move: move,
        swap: swap,
        clear: clear,
        append: append,
        form: form,
        values: form.values,
        setValues: form.setValues.bind(form)
    };
}

/**
 * Listen for truthy changes (if a value becomes truthy or falsy) on a form's field.
 * @param form The form to listen on.
 * @param name The form's field to listen to.
 */
export function useTruthyListener<T extends object, K extends keyof T, State = DefaultState, Error extends string = DefaultError>(
    form: FormState<T, State, Error>,
    name: K
) {
    const oldTruthy = useRef(!!form.values[name]);
    const [, setRender] = useState(0);

    useEffect(() => {
        let id = form.listen(name, () => {
            let thruthly = !!form.values[name];
            if (thruthly !== oldTruthy.current) {
                setRender((i) => i + 1);
                oldTruthy.current = thruthly;
            }
        });
        return () => form.ignore(name, id);
    }, []);

    return !form.values[name];
}