import { Ref, UnwrapRef, ComputedRef } from 'vue-demi'

type AnyFunction = (...args: any[]) => any
type DeepIndex<
  T,
  Keys extends readonly PropertyKey[],
  R = unknown
> = Keys extends [infer First, ...infer Rest]
  ? First extends keyof T
    ? Rest extends readonly PropertyKey[]
      ? DeepIndex<T[First], Rest>
      : R
    : R
  : T
type MaybeRef<T> = T extends Ref<infer V> ? T | V : Ref<T> | T
type ExcludePrimitives<T> = T extends AnyFunction
  ? never
  : T extends object
  ? T
  : never
type DeepPartial<T extends object> = T extends readonly any[]
  ? {
      [K in keyof T]: DeepPartialImpl<T[K]>
    }
  : {
      [K in keyof T]?: DeepPartialImpl<T[K]> | undefined
    }
type DeepPartialImpl<T> = T extends AnyFunction
  ? T
  : T extends object
  ? DeepPartial<T>
  : T

type ValidationBehaviorString = keyof ValidationBehaviorFunctions
interface ValidationBehaviorFunctions {}
type ValidationBehaviorInfo<T = any> = {
  /**
   * `True` if the paired rule of this behavior has an error.
   */
  hasError: boolean
  /**
   * The touched state of the field.
   */
  touched: boolean
  /**
   * The dirty state of the field.
   */
  dirty: boolean
  /**
   * `True` if the validation was triggered with the `force` flag.
   */
  force: boolean
  /**
   * `True` if the validation was triggered with the `submit` flag.
   */
  submit: boolean
  /**
   * The `$value` property of the field.
   */
  value: T
}
type ValidationBehaviorFunction = (info: ValidationBehaviorInfo) => boolean
type ValidationBehavior = ValidationBehaviorString | ValidationBehaviorFunction

type SimpleRule<TParam = any> = (value: TParam) => any
type KeyedRule<TParams extends readonly any[] = any[]> = (
  ...values: [...TParams]
) => any
type RuleWithKey<T extends readonly any[] = any[]> = {
  key: string
  rule?: KeyedRule<T>
}
type FieldSimpleRule<TParam = any> =
  | SimpleRule<TParam>
  | [
      validationBehavior: ValidationBehavior,
      rule: SimpleRule<TParam>,
      debounce?: number
    ]
  | [rule: SimpleRule<TParam>, debounce: number]
type FieldKeyedRule<TParams extends readonly any[]> =
  | RuleWithKey<TParams>
  | [
      validationBehavior: ValidationBehavior,
      rule: RuleWithKey<TParams>,
      debounce?: number
    ]
  | [rule: RuleWithKey<TParams>, debounce: number]
type FieldRule<TSimpleParams, TKeyedParams extends readonly any[] = any[]> =
  | FieldSimpleRule<UnwrapRef<TSimpleParams>>
  | FieldKeyedRule<TKeyedParams>

type ValidateFieldsPredicateParameter = {
  key: string
  value: any
  path: string[]
}
type ValidateFieldsPredicate = (
  value: ValidateFieldsPredicateParameter
) => boolean

type Field<T, E extends object = Record<string, never>> = {
  /**
   * The field's default value.
   */
  $value: MaybeRef<T>
  /**
   * Rules to use for validation.
   */
  $rules?: FieldRule<T>[]
} & (E extends Record<string, never> ? unknown : E)
type ValidateOptions = {
  /**
   * Set the field touched when called.
   *
   * @default true
   */
  setTouched?: boolean
  /**
   * Validate with the `force` flag set.
   *
   * @default true
   */
  force?: boolean
}
type TransformedField<T, E extends object = Record<string, never>> = {
  /**
   * The unique id of this field.
   */
  $uid: number
  /**
   * The current field's value.
   */
  $value: T
  /**
   * A list of validation error messages.
   */
  $errors: string[]
  /**
   * The error status of this field.
   */
  $hasError: boolean
  /**
   * `True` while this field has any pending rules.
   */
  $validating: boolean
  /**
   * `True` if the field is touched. In most cases,
   * this value should be set together with the `blur` event.
   * Either through `$validate` or manually.
   */
  $touched: boolean
  /**
   * `True` if the `$value` of this field has changed at least once.
   */
  $dirty: boolean
  /**
   * Validate this field.
   *
   * @param options - Validate options to use
   * @default
   * ```
   * { setTouched: true, force: true }
   * ```
   */
  $validate(options?: ValidateOptions): Promise<void>
} & (E extends Record<string, never> ? unknown : UnwrapRef<E>)
/**
 * Unwrap the `$value` property of all fields in `FormData`.
 */
type ResultFormData<FormData> = FormData extends any
  ? {
      [K in keyof FormData]: ResultFormDataImpl<FormData[K]>
    }
  : never
type ResultFormDataImpl<T> = T extends {
  $value: infer V
}
  ? UnwrapRef<Exclude<MaybeRef<V>, Ref>>
  : T extends object
  ? ResultFormData<T>
  : T
/**
 * Receive the name of every field in `FormData` as a union of strings.
 */
type FieldNames<FormData extends object> = FieldNamesImpl<FormData, never>
type FieldNamesImpl<FormData, K> = ExcludePrimitives<FormData> extends never
  ? never
  : FormData extends {
      $value: any
    }
  ? K
  : FormData extends readonly (infer A)[]
  ? FieldNamesImpl<A, K>
  : {
      [K in keyof FormData]-?: ExcludePrimitives<FormData[K]> extends {
        $value: any
      }
        ? K
        : FieldNamesImpl<ExcludePrimitives<FormData[K]>, K>
    }[keyof FormData]
/**
 * Transforms every field in `FormData` into transformed fields.
 */
type TransformFormData<FormData> = FormData extends any
  ? {
      [K in keyof FormData]: TransformFormDataImpl<FormData[K]>
    }
  : never
type TransformFormDataImpl<T> = T extends {
  $value: infer V
}
  ? TransformedField<
      UnwrapRef<Exclude<MaybeRef<V>, Ref>>,
      Omit<T, '$value' | '$rules'>
    >
  : T extends object
  ? TransformFormData<T>
  : T

/**
 * Vue composition function for form validation.
 * For type inference in `useValidation` make sure to define the structure of your
 * form data upfront and pass it as the generic parameter `FormData`.
 *
 * @param formData - The structure of your form data
 *
 * @example
 * ```
 * type FormData = {
 *   name: Field<string>,
 *   password: Field<string>
 * }
 *
 * const { form } = useValidation<FormData>({
 *   name: {
 *     $value: '',
 *     $rules: []
 *   },
 *   password: {
 *     $value: '',
 *     $rules: []
 *   }
 * })
 * ```
 */
declare function useValidation<FormData extends object>(
  formData: FormData
): UseValidation<FormData>
type UseValidation<FormData extends object> = {
  /**
   * A transformed reactive form data object.
   */
  form: TransformFormData<FormData>
  /**
   * `True` during validation after calling `validateFields` when there were rules returning a `Promise`.
   */
  submitting: Ref<boolean>
  /**
   * `True` while the form has any pending rules.
   */
  validating: ComputedRef<boolean>
  /**
   * `True` if the form has any error.
   */
  hasError: ComputedRef<boolean>
  /**
   * All current validation error messages.
   */
  errors: ComputedRef<string[]>
  /**
   * Validate all fields and return a `Promise` containing the resulting form data.
   *
   * @param options - Options to use for validation
   * @throws `ValidationError`
   */
  validateFields(options?: {
    /**
     * A list of field names to validate.
     *
     * @default
     * ```
     * undefined // meaning validate all
     * ```
     */
    names?: FieldNames<FormData>[]
    /**
     * Filter which values to keep in the resulting form data.
     * Used like `Array.prototype.filter` but for objects.
     *
     * @default
     * ```
     * () => true // meaning keep all
     * ```
     */
    predicate?: ValidateFieldsPredicate
  }): Promise<ResultFormData<FormData>>
  /**
   * Reset all fields to their default value or pass an object to set specific values.
   * It will not create new fields not present in the form data initially.
   *
   * @param formData - Form data to set specific values. It has the same structure as the object passed to `useValidation`
   */
  resetFields(formData?: DeepPartial<ResultFormData<FormData>>): void
  /**
   * Adds a new value to the form data.
   *
   * @param path - A path for the new value
   * @param value - The value to add
   */
  add<Keys extends readonly (string | number)[]>(
    path: readonly [...Keys],
    value: DeepIndex<FormData, Keys> extends (infer TArray)[]
      ? TArray
      : DeepIndex<FormData, Keys>
  ): void
  /**
   * Removes a value from the form data.
   *
   * @param path - A path to the value to remove
   */
  remove(path: (string | number)[]): void
}

type Configuration = {
  defaultValidationBehavior: ValidationBehaviorString
  validationBehavior: {
    [K in ValidationBehaviorString]: ValidationBehaviorFunction
  }
}
type Validation = {
  install(): void
}
/**
 * Configure the validation behavior of `useValidation`.
 *
 * @param configuration - The form validation configuration
 */
declare function createValidation(configuration: Configuration): Validation

declare class ValidationError extends Error {
  constructor()
}

export {
  AnyFunction,
  DeepPartial,
  ExcludePrimitives,
  Field,
  FieldKeyedRule,
  FieldNames,
  FieldSimpleRule,
  KeyedRule,
  ResultFormData,
  SimpleRule,
  TransformFormData,
  TransformedField,
  UseValidation,
  ValidateFieldsPredicate,
  ValidateFieldsPredicateParameter,
  ValidateOptions,
  Validation,
  ValidationBehavior,
  ValidationBehaviorFunction,
  ValidationBehaviorFunctions,
  ValidationBehaviorInfo,
  ValidationBehaviorString,
  ValidationError,
  createValidation,
  useValidation
}
