defmodule Ecto.Changeset do
  @moduledoc ~S"""
  Changesets allow filtering, casting, validation and
  definition of constraints when manipulating structs.

  There is an example of working with changesets in the introductory
  documentation in the `Ecto` module. The functions `cast/4` and
  `change/2` are the usual entry points for creating changesets.
  The first one is used to cast and validate external parameters,
  such as parameters sent through a form, API, command line, etc.
  The second one is used to change data directly from your application.

  The remaining functions in this module, such as validations,
  constraints, association handling, are about manipulating
  changesets.

  ## External vs internal data

  Changesets allow working with two kinds of data:

    * internal to the application - for example programmatically generated,
      or coming from other subsystems. This use case is primarily covered
      by the `change/2` and `put_change/3` functions.

    * external to the application - for example data provided by the user in
      a form that needs to be type-converted and properly validated. This
      use case is primarily covered by the `cast/4` function.

  When working with external data, the data is typically provided
  as maps with string keys (also known as parameters). On the other hand,
  when working with internal data, you typically have maps of atom keys
  or structs. This duality allows you to track the nature of your data:
  if you have structs or maps with atom keys, it means the data has been
  parsed/validated.

  If you have external data or you have maps that may have either
  string or atom keys, consider using `cast/4` to create a changeset.
  The changeset will parse and validate these parameters and provide APIs
  to safely manipulate and change the data accordingly.

  ## Validations and constraints

  Ecto changesets provide both validations and constraints which
  are ultimately turned into errors in case something goes wrong.

  The difference between them is that most validations can be
  executed without a need to interact with the database and, therefore,
  are always executed before attempting to insert or update the entry
  in the database. Validations run immediately when a validation function
  is called on the data that is contained in the changeset at that time.

  Some validations may happen against the database but
  they are inherently unsafe. Those validations start with a `unsafe_`
  prefix, such as `unsafe_validate_unique/4`.

  On the other hand, constraints rely on the database and are always safe.
  As a consequence, validations are always checked before constraints.
  Constraints won't even be checked in case validations failed.

  Let's see an example:

      defmodule User do
        use Ecto.Schema
        import Ecto.Changeset

        schema "users" do
          field :name
          field :email
          field :age, :integer
        end

        def changeset(user, params \\ %{}) do
          user
          |> cast(params, [:name, :email, :age])
          |> validate_required([:name, :email])
          |> validate_format(:email, ~r/@/)
          |> validate_inclusion(:age, 18..100)
          |> unique_constraint(:email)
        end
      end

  In the `changeset/2` function above, we define three validations.
  They check that `name` and `email` fields are present in the
  changeset, the e-mail is of the specified format, and the age is
  between 18 and 100 - as well as a unique constraint in the email
  field.

  Let's suppose the e-mail is given but the age is invalid. The
  changeset would have the following errors:

      changeset = User.changeset(%User{}, %{age: 0, email: "mary@example.com"})
      {:error, changeset} = Repo.insert(changeset)
      changeset.errors #=> [age: {"is invalid", []}, name: {"can't be blank", []}]

  In this case, we haven't checked the unique constraint in the
  e-mail field because the data did not validate. Let's fix the
  age and the name, and assume that the e-mail already exists in the
  database:

      changeset = User.changeset(%User{}, %{age: 42, name: "Mary", email: "mary@example.com"})
      {:error, changeset} = Repo.insert(changeset)
      changeset.errors #=> [email: {"has already been taken", []}]

  Validations and constraints define an explicit boundary when the check
  happens. By moving constraints to the database, we also provide a safe,
  correct and data-race free means of checking the user input.

  ### Deferred constraints

  Some databases support deferred constraints, i.e., constraints which are
  checked at the end of the transaction rather than at the end of each statement.

  Changesets do not support this type of constraints. When working with deferred
  constraints, a violation while invoking `c:Ecto.Repo.insert/2` or `c:Ecto.Repo.update/2` won't
  return `{:error, changeset}`, but rather raise an error at the end of the
  transaction.

  ## Empty values

  Many times, the data given on cast needs to be further pruned, specially
  regarding empty values. For example, if you are gathering data to be
  cast from the command line or through an HTML form or any other text-based
  format, it is likely those means cannot express nil values. For
  those reasons, changesets include the concept of empty values.

  When applying changes using `cast/4`, an empty value will be automatically
  converted to the field's default value. If the field is an array type, any
  empty value inside the array will be removed.

  Empty values are stored as a list in the changeset's `:empty_values` field.
  The list contains elements of type `t:empty_value/0`. Those are either values,
  which will be considered empty if they
  match, or a function that must return a boolean if the value is empty or
  not. By default, Ecto uses `Ecto.Changeset.empty_values/0` which will mark
  a field as empty if it is a string made only of whitespace characters.
  You can also pass the `:empty_values` option to `cast/4` in case you want
  to change how a particular `cast/4` work.

  ## Associations, embeds, and on replace

  Using changesets you can work with associations as well as with embedded
  structs. There are two primary APIs:

    * `cast_assoc/3` and `cast_embed/3` - those functions are used when
      working with external data. In particular, they allow you to change
      associations and embeds alongside the parent struct, all at once.

    * `put_assoc/4` and `put_embed/4` - it allows you to replace the
      association or embed as a whole. This can be used to move associated
      data from one entry to another, to completely remove or replace
      existing entries.

  These functions are opinionated on how it works with associations.
  If you need different behaviour or explicit control over the associated
  data, you can skip this functionality and use `Ecto.Multi` to encode how
  several database operations will happen on several schemas and changesets
  at once.

  You can learn more about working with associations in our documentation,
  including cheatsheets and practical examples. Check out:

    * The docs for `cast_assoc/3` and `put_assoc/3`
    * The [associations cheatsheet](associations.html)
    * The [Constraints and Upserts guide](constraints-and-upserts.html)
    * The [Polymorphic associations with many to many guide](polymorphic-associations-with-many-to-many.html)

  ### The `:on_replace` option

  When using any of those APIs, you may run into situations where Ecto sees
  data is being replaced. For example, imagine a Post has many Comments where
  the comments have IDs 1, 2 and 3. If you call `cast_assoc/3` passing only
  the IDs 1 and 2, Ecto will consider 3 is being "replaced" and it will raise
  by default. Such behaviour can be changed when defining the relation by
  setting `:on_replace` option when defining your association/embed according
  to the values below:

    * `:raise` (default) - do not allow removing association or embedded
      data via parent changesets
    * `:mark_as_invalid` - if attempting to remove the association or
      embedded data via parent changeset - an error will be added to the parent
      changeset, and it will be marked as invalid
    * `:nilify` - sets owner reference column to `nil` (available only for
      associations). Use this on a `belongs_to` column to allow the association
      to be cleared out so that it can be set to a new value. Will set `action`
      on associated changesets to `:replace`
    * `:update` - updates the association, available only for `has_one`, `belongs_to`
      and `embeds_one`. This option will update all the fields given to the changeset
      including the id for the association
    * `:delete` - removes the association or related data from the database.
      This option has to be used carefully (see below). Will set `action` on associated
      changesets to `:replace`
    * `:delete_if_exists` - like `:delete` except that it ignores any stale entry
      error. For instance, if you set `on_replace: :delete` but the replaced
      resource was already deleted by a separate request, it will raise a
      `Ecto.StaleEntryError`. `:delete_if_exists` makes it so it will only delete
      if the entry still exists

  The `:delete` and `:delete_if_exists` options must be used carefully as they allow
  users to delete any associated data by simply setting it to `nil` or an empty list.
  If you need deletion, it is often preferred to add a separate boolean virtual field
  in the schema and manually mark the changeset for deletion if the `:delete` field is
  set in the params, as in the example below. Note that we don't call `cast/4` in this
  case because we don't want to prevent deletion if a change is invalid (changes are
  irrelevant if the entity needs to be deleted).

      defmodule Comment do
        use Ecto.Schema
        import Ecto.Changeset

        schema "comments" do
          field :body, :string
          field :delete, :boolean, virtual: true
        end

        def changeset(comment, %{"delete" => "true"}) do
          %{Ecto.Changeset.change(comment, delete: true) | action: :delete}
        end

        def changeset(comment, params) do
          cast(comment, params, [:body])
        end
      end

  ## Schemaless changesets

  In the changeset examples so far, we have always used changesets to validate
  and cast data contained in a struct defined by an Ecto schema, such as the `%User{}`
  struct defined by the `User` module.

  However, changesets can also be used with "regular" structs too by passing a tuple
  with the data and its types:

      user = %User{}
      types = %{name: :string, email: :string, age: :integer}
      params = %{name: "Callum", email: "callum@example.com", age: 27}
      changeset =
        {user, types}
        |> Ecto.Changeset.cast(params, Map.keys(types))
        |> Ecto.Changeset.validate_required(...)
        |> Ecto.Changeset.validate_length(...)

  where the user struct refers to the definition in the following module:

      defmodule User do
        defstruct [:name, :email, :age]
      end

  Changesets can also be used with data in a plain map, by following the same API:

      data  = %{}
      types = %{name: :string, email: :string, age: :integer}
      params = %{name: "Callum", email: "callum@example.com", age: 27}
      changeset =
        {data, types}
        |> Ecto.Changeset.cast(params, Map.keys(types))
        |> Ecto.Changeset.validate_required(...)
        |> Ecto.Changeset.validate_length(...)

  Besides the basic types which are mentioned above, such as `:boolean` and `:string`,
  parameterized types can also be used in schemaless changesets. They implement
  the `Ecto.ParameterizedType` behaviour and we can create the necessary type info by
  calling the `init/2` function.

  For example, to use `Ecto.Enum` in a schemaless changeset:

      types = %{
        name: :string,
        role: Ecto.ParameterizedType.init(Ecto.Enum, values: [:reader, :editor, :admin])
      }

      data  = %{}
      params = %{name: "Callum", role: "reader"}

      changeset =
        {data, types}
        |> Ecto.Changeset.cast(params, Map.keys(types))
        |> Ecto.Changeset.validate_required(...)
        |> Ecto.Changeset.validate_length(...)

  Schemaless changesets make Ecto extremely useful to cast, validate and prune data even
  if it is not meant to be persisted to the database.

  ### Changeset actions

  Changesets have an action field which is usually set by `Ecto.Repo`
  whenever one of the operations such as `insert` or `update` is called:

      changeset = User.changeset(%User{}, %{age: 42, email: "mary@example.com"})
      {:error, changeset} = Repo.insert(changeset)
      changeset.action
      #=> :insert

  This means that when working with changesets that are not meant to be
  persisted to the database, such as schemaless changesets, you may need
  to explicitly set the action to one specific value. Frameworks such as
  Phoenix [use the action value to define how HTML forms should
  act](https://hexdocs.pm/phoenix_live_view/Phoenix.Component.html#form/1-a-note-on-errors).

  Instead of setting the action manually, you may use `apply_action/2` that
  emulates operations such as `c:Ecto.Repo.insert`. `apply_action/2` will return
  `{:ok, changes}` if the changeset is valid or `{:error, changeset}`, with
  the given `action` set in the changeset in case of errors.

  ## The Ecto.Changeset struct

  The public fields are:

    * `valid?`       - Stores if the changeset is valid
    * `data`         - The changeset source data, for example, a struct
    * `params`       - The parameters as given on changeset creation
    * `changes`      - The `changes` from parameters that were approved in casting
    * `errors`       - All errors from validations
    * `required`     - All required fields as a list of atoms
    * `action`       - The action to be performed with the changeset
    * `types`        - Cache of the data's field types
    * `empty_values` - A list of values to be considered empty
    * `repo`         - The repository applying the changeset (only set after a Repo function is called)
    * `repo_opts`    - A keyword list of options given to the underlying repository operation

  The following fields are private and must not be accessed directly.

    * `validations`
    * `constraints`
    * `filters`
    * `prepare`

  ### Redacting fields in inspect

  To hide a field's value from the inspect protocol of `Ecto.Changeset`, mark
  the field as `redact: true` in the schema, and it will display with the
  value `**redacted**`.
  """

  require Logger
  require Ecto.Query
  alias __MODULE__
  alias Ecto.Changeset.Relation
  alias Ecto.Schema.Metadata

  @empty_values [&Ecto.Type.empty_trimmed_string?/1]

  # If a new field is added here, def merge must be adapted
  defstruct valid?: false,
            data: nil,
            params: nil,
            changes: %{},
            errors: [],
            validations: [],
            required: [],
            prepare: [],
            constraints: [],
            filters: %{},
            action: nil,
            types: nil,
            empty_values: @empty_values,
            repo: nil,
            repo_opts: []

  @type t(data_type) :: %Changeset{
          valid?: boolean(),
          repo: atom | nil,
          repo_opts: Keyword.t(),
          data: data_type,
          params: %{optional(String.t()) => term} | nil,
          changes: %{optional(atom) => term},
          required: [atom],
          prepare: [(t -> t)],
          errors: [{atom, error}],
          constraints: [constraint],
          validations: [{atom, term}],
          filters: %{optional(atom) => term},
          action: action,
          types: nil | %{atom => Ecto.Type.t() | {:assoc, term()} | {:embed, term()}}
        }

  @type t :: t(Ecto.Schema.t() | map | nil)
  @type error :: {String.t(), Keyword.t()}
  @type action :: nil | :insert | :update | :delete | :replace | :ignore | atom
  @type constraint :: %{
          type: :check | :exclusion | :foreign_key | :unique,
          constraint: String.t() | Regex.t(),
          match: :exact | :suffix | :prefix,
          field: atom,
          error_message: String.t(),
          error_type: atom
        }
  @type data :: map()
  @type types :: map()
  @type traverse_result :: %{atom => [term] | traverse_result}

  @typedoc """
  A possible value that you can pass to the `:empty_values` option.

  See `empty_values/0` and the [*Empty values* section](#module-empty-values) in
  the module documentation for more information.
  """
  @typedoc since: "3.11.0"
  @type empty_value :: (term() -> boolean()) | binary() | list() | map() | tuple()

  @number_validators %{
    less_than: {&</2, "must be less than %{number}"},
    greater_than: {&>/2, "must be greater than %{number}"},
    less_than_or_equal_to: {&<=/2, "must be less than or equal to %{number}"},
    greater_than_or_equal_to: {&>=/2, "must be greater than or equal to %{number}"},
    equal_to: {&==/2, "must be equal to %{number}"},
    not_equal_to: {&!=/2, "must be not equal to %{number}"}
  }

  @relations [:embed, :assoc]
  @match_types [:exact, :suffix, :prefix]

  @doc """
  Wraps the given data in a changeset or adds changes to a changeset.

  `changes` is a map or keyword where the key is an atom representing a
  field, association or embed and the value is a term. Note the `value` is
  directly stored in the changeset with no validation whatsoever. For this
  reason, this function is meant for working with data internal to the
  application.

  When changing embeds and associations, see `put_assoc/4` for a complete
  reference on the accepted values.

  This function is useful for:

    * wrapping a struct inside a changeset
    * directly changing a struct without performing castings nor validations
    * directly bulk-adding changes to a changeset

  Changed attributes will only be added if the change does not have the
  same value as the field in the data.

  When a changeset is passed as the first argument, the changes passed as the
  second argument are merged over the changes already in the changeset if they
  differ from the values in the struct.

  When a `{data, types}` is passed as the first argument, a changeset is
  created with the given data and types and marked as valid.

  See `cast/4` if you'd prefer to cast and validate external parameters.

  ## Examples

      iex> changeset = change(%Post{})
      %Ecto.Changeset{...}
      iex> changeset.valid?
      true
      iex> changeset.changes
      %{}

      iex> changeset = change(%Post{author: "bar"}, title: "title")
      iex> changeset.changes
      %{title: "title"}

      iex> changeset = change(%Post{title: "title"}, title: "title")
      iex> changeset.changes
      %{}

      iex> changeset = change(changeset, %{title: "new title", body: "body"})
      iex> changeset.changes.title
      "new title"
      iex> changeset.changes.body
      "body"

  """
  @spec change(Ecto.Schema.t() | t | {data, types}, %{atom => term} | Keyword.t()) :: t
  def change(data, changes \\ %{})

  def change({data, types}, changes) when is_map(data) do
    change(%Changeset{data: data, types: Enum.into(types, %{}), valid?: true}, changes)
  end

  def change(%Changeset{types: nil}, _changes) do
    raise ArgumentError, "changeset does not have types information"
  end

  def change(%Changeset{changes: changes, types: types} = changeset, new_changes)
      when is_map(new_changes) or is_list(new_changes) do
    {changes, errors, valid?} =
      get_changed(changeset.data, types, changes, new_changes, changeset.errors, changeset.valid?)

    %{changeset | changes: changes, errors: errors, valid?: valid?}
  end

  def change(%{__struct__: struct} = data, changes) when is_map(changes) or is_list(changes) do
    types = struct.__changeset__()
    {changes, errors, valid?} = get_changed(data, types, %{}, changes, [], true)
    %Changeset{valid?: valid?, data: data, changes: changes, errors: errors, types: types}
  end

  defp get_changed(data, types, old_changes, new_changes, errors, valid?) do
    Enum.reduce(new_changes, {old_changes, errors, valid?}, fn
      {key, value}, {changes, errors, valid?} ->
        put_change(data, changes, errors, valid?, key, value, Map.get(types, key))

      _, _ ->
        raise ArgumentError,
              "invalid changes being applied to changeset. " <>
                "Expected a keyword list or a map, got: #{inspect(new_changes)}"
    end)
  end

  @doc """
  Returns true if a field was changed in a changeset.

  This function can check associations and embeds, but doesn't support the `:to`
  and `:from` options for such fields.

  ## Options

    * `:to` - Check if the field was changed to a specific value
    * `:from` - Check if the field was changed from a specific value

  ## Examples

      iex> post = %Post{title: "Foo", body: "Old"}
      iex> changeset = change(post, %{title: "New title", body: "Old"})

      iex> changed?(changeset, :body)
      false

      iex> changed?(changeset, :title)
      true

      iex> changed?(changeset, :title, to: "NEW TITLE")
      false
  """
  @spec changed?(t, atom, Keyword.t()) :: boolean
  def changed?(%Changeset{} = changeset, field, opts \\ []) when is_atom(field) do
    case Map.fetch(changeset.types, field) do
      {:ok, type} ->
        case fetch_change(changeset, field) do
          {:ok, new_value} ->
            case type do
              {tag, relation} when tag in @relations ->
                if opts != [] do
                  raise ArgumentError, "invalid options for #{tag} field"
                end

                relation_changed?(relation.cardinality, new_value)

              _ ->
                Enum.all?(opts, fn
                  {:from, from} ->
                    Ecto.Type.equal?(type, Map.get(changeset.data, field), from)

                  {:to, to} ->
                    Ecto.Type.equal?(type, new_value, to)

                  other ->
                    raise ArgumentError, "unknown option #{inspect(other)}"
                end)
            end

          :error ->
            false
        end

      :error ->
        raise ArgumentError, "field #{inspect(field)} doesn't exist"
    end
  end

  defp relation_changed?(:one, changeset) do
    changeset.action != :update or changeset.changes != %{}
  end

  defp relation_changed?(:many, changesets) do
    Enum.any?(changesets, &relation_changed?(:one, &1))
  end

  @doc """
  Returns the default empty values used by `Ecto.Changeset`.

  By default, Ecto marks a field as empty if it is a string made
  only of whitespace characters. If you want to provide your
  additional empty values on top of the default, such as an empty
  list, you can write:

      @empty_values [[]] ++ Ecto.Changeset.empty_values()

  Then, you can pass `empty_values: @empty_values` on `cast/3`.

  See also the [*Empty values* section](#module-empty-values) for more
  information.
  """
  @doc since: "3.10.0"
  @spec empty_values() :: [empty_value()]
  def empty_values do
    @empty_values
  end

  @doc """
  Applies the given `params` as changes on the `data` according to
  the set of `permitted` keys. Returns a changeset.

  `data` may be either a changeset, a schema struct or a `{data, types}`
  tuple. The second argument is a map of `params` that are cast according
  to the type information from `data`. `params` is a map with string keys
  or a map with atom keys, containing potentially invalid data. Mixed keys
  are not allowed.

  During casting, all `permitted` parameters whose values match the specified
  type information will have their key name converted to an atom and stored
  together with the value as a change in the `:changes` field of the changeset.
  If the cast value matches the current value for the field, it will not be
  included in `:changes` unless the `force_changes: true` option is
  provided. All parameters that are not explicitly permitted are ignored.

  If casting of all fields is successful, the changeset is returned as valid.

  Note that `cast/4` validates the types in the `params`, but not in the given
  `data`.

  ## Options

    * `:empty_values` - a list of values to be considered as empty when casting.
      Empty values are always replaced by the default value of the respective field.
      If the field is an array type, any empty value inside of the array will be removed.
      To set this option while keeping the current default, use `empty_values/0` and add
      your additional empty values

    * `:force_changes` - a boolean indicating whether to include values that don't alter
      the current data in `:changes`. See `force_change/3` for more information, Defaults
      to `false`

    * `:message` - a function of arity 2 that is used to create the error message when
      casting fails. It is called for every field that cannot be casted and receives the
      field name as the first argument and the error metadata as the second argument. It
      must return a string or `nil`. If a string is returned it will be used as the error
      message. If `nil` is returned the default error message will be used. The field type
      is given under the `:type` key in the metadata

  ## Examples

      iex> changeset = cast(post, params, [:title])
      iex> if changeset.valid? do
      ...>   Repo.update!(changeset)
      ...> end

  Passing a changeset as the first argument:

      iex> changeset = cast(post, %{title: "Hello"}, [:title])
      iex> new_changeset = cast(changeset, %{title: "Foo", body: "World"}, [:body])
      iex> new_changeset.params
      %{"title" => "Hello", "body" => "World"}

  Or creating a changeset from a simple map with types:

      iex> data = %{title: "hello"}
      iex> types = %{title: :string}
      iex> changeset = cast({data, types}, %{title: "world"}, [:title])
      iex> apply_changes(changeset)
      %{title: "world"}

  You can use empty values (and even cast multiple times) to change
  what is considered an empty value:

      # Using default
      iex> params = %{title: "", topics: []}
      iex> changeset = cast(post, params, [:title, :topics])
      iex> changeset.params
      %{topics: []}

      # Changing default
      iex> params = %{title: "", topics: []}
      iex> changeset = cast(post, params, [:topics], empty_values: [[], nil])
      iex> changeset.params
      %{title: ""}

      # Augmenting default
      iex> params = %{title: "", topics: []}
      iex> changeset =
      ...>   cast(post, params, [:topics], empty_values: [[], nil] ++ Ecto.Changeset.empty_values())
      iex> changeset.params
      %{}

  You can define a custom error message function.

      # Using field name
      iex> params = %{title: 1, body: 2}
      iex> custom_errors = [title: "must be a string"]
      iex> msg_func = fn field, _meta -> custom_errors[field] end
      iex> changeset = cast(post, params, [:title, :body], message: msg_func)
      iex> changeset.errors
      [
        title: {"must be a string", [type: :string, validation: :cast]},
        body: {"is_invalid", [type: :string, validation: :cast]}
      ]

      # Using field type
      iex> params = %{title: 1, body: 2}
      iex> custom_errors = [string: "must be a string"]
      iex> msg_func = fn _field, meta ->
      ...    type = meta[:type]
      ...    custom_errors[type]
      ...  end
      iex> changeset = cast(post, params, [:title, :body], message: msg_func)
      iex> changeset.errors
      [
        title: {"must be a string", [type: :string, validation: :cast]},
        body: {"must be a string", [type: :string, validation: :cast]}
      ]

  ## Composing casts

  `cast/4` also accepts a changeset as its first argument. In such cases, all
  the effects caused by the call to `cast/4` (additional errors and changes)
  are simply added to the ones already present in the argument changeset.
  Parameters are merged (**not deep-merged**) and the ones passed to `cast/4`
  take precedence over the ones already in the changeset.
  """
  @spec cast(
          Ecto.Schema.t() | t | {data, types},
          %{binary => term} | %{atom => term} | :invalid,
          [atom],
          Keyword.t()
        ) :: t
  def cast(data, params, permitted, opts \\ [])

  def cast(_data, %{__struct__: _} = params, _permitted, _opts) do
    raise Ecto.CastError,
      type: :map,
      value: params,
      message: "expected params to be a :map, got: `#{inspect(params)}`"
  end

  def cast({data, types}, params, permitted, opts) when is_map(data) do
    cast(data, types, %{}, params, permitted, opts)
  end

  def cast(%Changeset{types: nil}, _params, _permitted, _opts) do
    raise ArgumentError, "changeset does not have types information"
  end

  def cast(%Changeset{} = changeset, params, permitted, opts) do
    %{changes: changes, data: data, types: types, empty_values: empty_values} = changeset

    opts =
      cond do
        opts[:empty_values] ->
          opts

        empty_values != empty_values() ->
          # TODO: Remove changeset.empty_values field in Ecto v3.14
          IO.warn(
            "Changing the empty_values field of Ecto.Changeset is deprecated, " <>
              "please pass the :empty_values option on cast instead"
          )

          [empty_values: empty_values] ++ opts

        true ->
          [empty_values: empty_values] ++ opts
      end

    new_changeset = cast(data, types, changes, params, permitted, opts)
    cast_merge(changeset, new_changeset)
  end

  def cast(%{__struct__: module} = data, params, permitted, opts) do
    cast(data, module.__changeset__(), %{}, params, permitted, opts)
  end

  defp cast(%{} = data, %{} = types, %{} = changes, :invalid, permitted, _opts)
       when is_list(permitted) do
    _ = Enum.each(permitted, &cast_key/1)
    %Changeset{params: nil, data: data, valid?: false, errors: [], changes: changes, types: types}
  end

  defp cast(%{} = data, %{} = types, %{} = changes, %{} = params, permitted, opts)
       when is_list(permitted) do
    empty_values = Keyword.get(opts, :empty_values, @empty_values)
    force? = Keyword.get(opts, :force_changes, false)
    params = convert_params(params)
    msg_func = Keyword.get(opts, :message, fn _, _ -> nil end)

    unless is_function(msg_func, 2) do
      raise ArgumentError,
            "expected `:message` to be a function of arity 2, received: #{inspect(msg_func)}"
    end

    defaults =
      case data do
        %{__struct__: struct} -> struct.__struct__()
        %{} -> %{}
      end

    {changes, errors, valid?} =
      Enum.reduce(
        permitted,
        {changes, [], true},
        &process_param(&1, params, types, data, empty_values, defaults, force?, msg_func, &2)
      )

    %Changeset{
      params: params,
      data: data,
      valid?: valid?,
      errors: Enum.reverse(errors),
      changes: changes,
      types: types
    }
  end

  defp cast(%{}, %{}, %{}, params, permitted, _opts) when is_list(permitted) do
    raise Ecto.CastError,
      type: :map,
      value: params,
      message: "expected params to be a :map, got: `#{inspect(params)}`"
  end

  defp process_param(
         key,
         params,
         types,
         data,
         empty_values,
         defaults,
         force?,
         msg_func,
         {changes, errors, valid?}
       ) do
    {key, param_key} = cast_key(key)
    type = cast_type!(types, key)

    current =
      case changes do
        %{^key => value} -> value
        _ -> Map.get(data, key)
      end

    case cast_field(key, param_key, type, params, current, empty_values, defaults, force?, valid?) do
      {:ok, value, valid?} ->
        {Map.put(changes, key, value), errors, valid?}

      :missing ->
        {changes, errors, valid?}

      {:invalid, custom_errors} ->
        {default_message, metadata} =
          custom_errors
          |> Keyword.put_new(:validation, :cast)
          |> Keyword.put(:type, type)
          |> Keyword.pop(:message, "is invalid")

        message =
          case msg_func.(key, metadata) do
            nil -> default_message
            user_message -> user_message
          end

        {changes, [{key, {message, metadata}} | errors], false}
    end
  end

  defp cast_type!(types, key) do
    case types do
      %{^key => {tag, _}} when tag in @relations ->
        raise "casting #{tag}s with cast/4 for #{inspect(key)} field is not supported, use cast_#{tag}/3 instead"

      %{^key => type} ->
        type

      _ ->
        known_fields = types |> Map.keys() |> Enum.map_join(", ", &inspect/1)

        raise ArgumentError,
              "unknown field `#{inspect(key)}` given to cast. Either the field does not exist or it is a " <>
                ":through association (which are read-only). The known fields are: #{known_fields}"
    end
  end

  defp cast_key(key) when is_atom(key),
    do: {key, Atom.to_string(key)}

  defp cast_key(key) do
    raise ArgumentError, "cast/3 expects a list of atom keys, got key: `#{inspect(key)}`"
  end

  defp cast_field(key, param_key, type, params, current, empty_values, defaults, force?, valid?) do
    case params do
      %{^param_key => value} ->
        value = filter_empty_values(type, value, empty_values, defaults, key)

        case Ecto.Type.cast(type, value) do
          {:ok, value} ->
            if not force? and Ecto.Type.equal?(type, current, value) do
              :missing
            else
              {:ok, value, valid?}
            end

          :error ->
            {:invalid, []}

          {:error, custom_errors} when is_list(custom_errors) ->
            {:invalid, custom_errors}
        end

      _ ->
        :missing
    end
  end

  defp filter_empty_values(type, value, empty_values, defaults, key) do
    case filter_empty_values(type, value, empty_values) do
      :empty -> Map.get(defaults, key)
      {:ok, value} -> value
    end
  end

  defp filter_empty_values({:array, type}, value, empty_values) when is_list(value) do
    value =
      for elem <- value,
          {:ok, elem} <- [filter_empty_values(type, elem, empty_values)],
          do: elem

    if value in empty_values do
      :empty
    else
      {:ok, value}
    end
  end

  defp filter_empty_values(_type, value, empty_values) do
    filter_empty_value(empty_values, value)
  end

  defp filter_empty_value([head | tail], value) when is_function(head) do
    case head.(value) do
      true -> :empty
      false -> filter_empty_value(tail, value)
    end
  end

  defp filter_empty_value([value | _tail], value),
    do: :empty

  defp filter_empty_value([_head | tail], value),
    do: filter_empty_value(tail, value)

  defp filter_empty_value([], value),
    do: {:ok, value}

  # We only look at the first element because traversing the whole map
  # can be expensive and it was showing up during profiling. This means
  # we won't always raise, but the check only exists for user convenience
  # anyway, and it is not a guarantee.
  defp convert_params(params) do
    case :maps.next(:maps.iterator(params)) do
      {key, _, _} when is_atom(key) ->
        for {key, value} <- params, into: %{} do
          if is_atom(key) do
            {Atom.to_string(key), value}
          else
            raise Ecto.CastError,
              type: :map,
              value: params,
              message:
                "expected params to be a map with atoms or string keys, " <>
                  "got a map with mixed keys: #{inspect(params)}"
          end
        end

      _ ->
        params
    end
  end

  ## Casting related

  @doc """
  Casts the given association with the changeset parameters.

  This function should be used when working with the entire association at
  once (and not a single element of a many-style association) and receiving
  data external to the application.

  `cast_assoc/3` matches the records extracted from the database
  and compares it with the parameters received from an external source.
  Therefore, it is expected that the data in the changeset has explicitly
  preloaded the association being cast and that all of the IDs exist and
  are unique.

  For example, imagine a user has many addresses relationship where
  post data is sent as follows

      %{"name" => "john doe", "addresses" => [
        %{"street" => "somewhere", "country" => "brazil", "id" => 1},
        %{"street" => "elsewhere", "country" => "poland"},
      ]}

  and then

      User
      |> Repo.get!(id)
      |> Repo.preload(:addresses) # Only required when updating data
      |> Ecto.Changeset.cast(params, [])
      |> Ecto.Changeset.cast_assoc(:addresses, with: &MyApp.Address.changeset/2)

  The parameters for the given association will be retrieved
  from `changeset.params`. Those parameters are expected to be
  a map with attributes, similar to the ones passed to `cast/4`.
  Once parameters are retrieved, `cast_assoc/3` will match those
  parameters with the associations already in the changeset data.

  Once `cast_assoc/3` is called, Ecto will compare each parameter
  with the user's already preloaded addresses and act as follows:

    * If the parameter does not contain an ID, the parameter data
      will be passed to `MyApp.Address.changeset/2` with a new struct
      and become an insert operation. We only consider the ID as not
      given if there is no "id" key or if its value is strictly `nil`

    * If the parameter contains an ID and there is no associated child
      with such ID, the parameter data will be passed to
      `MyApp.Address.changeset/2` with a new struct and become an insert
      operation

    * If the parameter contains an ID and there is an associated child
      with such ID, the parameter data will be passed to
      `MyApp.Address.changeset/2` with the existing struct and become an
      update operation

    * If there is an associated child with an ID and its ID is not given
      as parameter, the `:on_replace` callback for that association will
      be invoked (see the "On replace" section on the module documentation)

  If two or more addresses have the same IDs, Ecto will consider that an
  error and add an error to the changeset saying that there are duplicate
  entries.

  Every time the `MyApp.Address.changeset/2` function is invoked, it must
  return a changeset. This changeset will always be included under `changes`
  of the parent changeset, even if there are no changes. This is done for
  reflection purposes, allowing developers to introspect validations and
  other metadata from the association. Once the parent changeset is given
  to an `Ecto.Repo` function, all entries will be inserted/updated/deleted
  within the same transaction.

  As you see above, this function is opinionated on how it works. If you
  need different behaviour or if you need explicit control over the associated
  data, you can either use `put_assoc/4` or use `Ecto.Multi` to encode how
  several database operations will happen on several schemas and changesets
  at once.

  ## Custom actions

  Developers are allowed to explicitly set the `:action` field of a
  changeset to instruct Ecto how to act in certain situations. Let's suppose
  that, if one of the associations has only empty fields, you want to ignore
  the entry altogether instead of showing an error. The changeset function could
  be written like this:

      def changeset(struct, params) do
        struct
        |> cast(params, [:title, :body])
        |> validate_required([:title, :body])
        |> case do
          %{valid?: false, changes: changes} = changeset when changes == %{} ->
            # If the changeset is invalid and has no changes, it is
            # because all required fields are missing, so we ignore it.
            %{changeset | action: :ignore}
          changeset ->
            changeset
        end
      end

  You can also set it to delete if you want data to be deleted based on the
  received parameters (such as a checkbox or any other indicator).

  ## Partial changes for many-style associations

  By preloading an association using a custom query you can confine the behavior
  of `cast_assoc/3`. This opens up the possibility to work on a subset of the data,
  instead of all associations in the database.

  Taking the initial example of users having addresses imagine those addresses
  are set up to belong to a country. If you want to allow users to bulk edit all
  addresses that belong to a single country, you can do so by changing the preload
  query:

      query = from MyApp.Address, where: [country: ^edit_country]

      User
      |> Repo.get!(id)
      |> Repo.preload(addresses: query)
      |> Ecto.Changeset.cast(params, [])
      |> Ecto.Changeset.cast_assoc(:addresses)

  This will allow you to cast and update only the association for the given country.
  The important point for partial changes is that any addresses, which were not
  preloaded won't be changed.

  ## Sorting and deleting from -many collections

  In earlier examples, we passed a -many style association as a list:

      %{"name" => "john doe", "addresses" => [
        %{"street" => "somewhere", "country" => "brazil", "id" => 1},
        %{"street" => "elsewhere", "country" => "poland"},
      ]}

  However, it is also common to pass the addresses as a map, where each
  key is an integer representing its position:

      %{"name" => "john doe", "addresses" => %{
        0 => %{"street" => "somewhere", "country" => "brazil", "id" => 1},
        1 => %{"street" => "elsewhere", "country" => "poland"}
      }}

  Using indexes becomes specially useful with two supporting options:
  `:sort_param` and `:drop_param`. These options tell the indexes should
  be reordered or deleted from the data. For example, if you did:

      cast_embed(changeset, :addresses,
        sort_param: :addresses_sort,
        drop_param: :addresses_drop)

  You can now submit this:

      %{"name" => "john doe", "addresses" => %{...}, "addresses_drop" => [0]}

  And now the entry with index 0 will be dropped from the params before casting.
  Note this requires setting the relevant `:on_replace` option on your
  associations/embeds definition.

  Similar, for sorting, you could do:

      %{"name" => "john doe", "addresses" => %{...}, "addresses_sort" => [1, 0]}

  And that will internally sort the elements so 1 comes before 0. Note that
  any index not present in `"addresses_sort"` will come _before_ any of the
  sorted indexes. If an index is not found, an empty entry is added in its
  place.

  For embeds, this guarantees the embeds will be rewritten in the given order.
  However, for associations, this is not enough. You will have to add a
  `field :position, :integer` to the schema and add a with function of arity 3
  to add the position to your children changeset. For example, you could implement:

      defp child_changeset(child, _changes, position) do
        child
        |> change(position: position)
      end

  And by passing it to `:with`, it will be called with the final position of the
  item:

      changeset
      |> cast_assoc(:children, sort_param: ..., with: &child_changeset/3)

  These parameters can be powerful in certain UIs as it allows you to decouple
  the sorting and replacement of the data from its representation.

  ## More resources

  You can learn more about working with associations in our documentation,
  including cheatsheets and practical examples. Check out:

    * The docs for `put_assoc/3`
    * The [associations cheatsheet](associations.html)
    * The [Constraints and Upserts guide](constraints-and-upserts.html)
    * The [Polymorphic associations with many to many guide](polymorphic-associations-with-many-to-many.html)

  ## Options

    * `:required` - if the association is a required field. For associations of cardinality
      one, a non-nil value satisfies this validation. For associations with many entries,
      a non-empty list is satisfactory.

    * `:required_message` - the message on failure, defaults to "can't be blank"

    * `:invalid_message` - the message on failure, defaults to "is invalid"

    * `:force_update_on_change` - force the parent record to be updated in the
      repository if there is a change, defaults to `true`

    * `:with` - the function to build the changeset from params. Defaults to the
      `changeset/2` function of the associated module. It can be an anonymous
      function that expects two arguments: the associated struct to be cast and its
      parameters. It must return a changeset. For associations with cardinality `:many`,
      functions with arity 3 are accepted, and the third argument will be the position
      of the associated element in the list, or `nil`, if the association is being replaced.

    * `:drop_param` - the parameter name which keeps a list of indexes to drop
      from the relation parameters

    * `:sort_param` - the parameter name which keeps a list of indexes to sort
      from the relation parameters. Unknown indexes are considered to be new
      entries. Non-listed indexes will come before any sorted ones. See
      `cast_assoc/3` for more information

  """
  def cast_assoc(changeset, name, opts \\ []) when is_atom(name) do
    cast_relation(:assoc, changeset, name, opts)
  end

  @doc """
  Casts the given embed with the changeset parameters.

  The parameters for the given embed will be retrieved
  from `changeset.params`. Those parameters are expected to be
  a map with attributes, similar to the ones passed to `cast/4`.
  Once parameters are retrieved, `cast_embed/3` will match those
  parameters with the embeds already in the changeset record.
  See `cast_assoc/3` for an example of working with casts and
  associations which would also apply for embeds.

  The changeset must have been previously `cast` using
  `cast/4` before this function is invoked.

  ## Options

    * `:required` - if the embed is a required field. For embeds of cardinality
      one, a non-nil value satisfies this validation. For embeds with many entries,
      a non-empty list is satisfactory.

    * `:required_message` - the message on failure, defaults to "can't be blank"

    * `:invalid_message` - the message on failure, defaults to "is invalid"

    * `:force_update_on_change` - force the parent record to be updated in the
      repository if there is a change, defaults to `true`

    * `:with` - the function to build the changeset from params. Defaults to the
      `changeset/2` function of the associated module. It must be an anonymous
      function that expects two arguments: the embedded struct to be cast and its
      parameters. It must return a changeset. For embeds with cardinality `:many`,
      functions with arity 3 are accepted, and the third argument will be the position
      of the associated element in the list, or `nil`, if the embed is being replaced.

    * `:drop_param` - the parameter name which keeps a list of indexes to drop
      from the relation parameters

    * `:sort_param` - the parameter name which keeps a list of indexes to sort
      from the relation parameters. Unknown indexes are considered to be new
      entries. Non-listed indexes will come before any sorted ones. See
      `cast_assoc/3` for more information

  """
  def cast_embed(changeset, name, opts \\ []) when is_atom(name) do
    cast_relation(:embed, changeset, name, opts)
  end

  defp cast_relation(type, %Changeset{data: data, types: types}, _name, _opts)
       when data == nil or types == nil do
    raise ArgumentError,
          "cast_#{type}/3 expects the changeset to be cast. " <>
            "Please call cast/4 before calling cast_#{type}/3"
  end

  defp cast_relation(type, %Changeset{} = changeset, key, opts) do
    {key, param_key} = cast_key(key)
    %{data: data, types: types, params: params, changes: changes} = changeset
    %{related: related} = relation = relation!(:cast, type, key, Map.get(types, key))
    params = params || %{}

    {changeset, required?} =
      if opts[:required] do
        {update_in(changeset.required, &[key | &1]), true}
      else
        {changeset, false}
      end

    on_cast = Keyword.get_lazy(opts, :with, fn -> on_cast_default(type, related) end)
    sort = opts_key_from_params(:sort_param, opts, params)
    drop = opts_key_from_params(:drop_param, opts, params)

    changeset =
      if is_map_key(params, param_key) or is_list(sort) or is_list(drop) do
        value = Map.get(params, param_key)
        original = Map.get(data, key)
        current = Relation.load!(data, original)
        value = cast_params(relation, value, sort, drop)

        case Relation.cast(relation, data, value, current, on_cast) do
          {:ok, change, relation_valid?} when change != original ->
            valid? = changeset.valid? and relation_valid?
            changes = Map.put(changes, key, change)
            changeset = %{force_update(changeset, opts) | changes: changes, valid?: valid?}
            missing_relation(changeset, key, current, required?, relation, opts)

          {:error, {message, meta}} ->
            meta = [validation: type] ++ meta
            error = {key, {message(opts, :invalid_message, message), meta}}
            %{changeset | errors: [error | changeset.errors], valid?: false}

          # ignore or ok with change == original
          _ ->
            missing_relation(changeset, key, current, required?, relation, opts)
        end
      else
        missing_relation(changeset, key, Map.get(data, key), required?, relation, opts)
      end

    update_in(changeset.types[key], fn {type, relation} ->
      {type, %{relation | on_cast: on_cast}}
    end)
  end

  defp cast_params(%{cardinality: :many} = relation, nil, sort, drop)
       when is_list(sort) or is_list(drop) do
    cast_params(relation, %{}, sort, drop)
  end

  defp cast_params(%{cardinality: :many}, value, sort, drop) when is_map(value) do
    drop = if is_list(drop), do: drop, else: []

    {sorted, pending} =
      if is_list(sort) do
        Enum.map_reduce(sort -- drop, value, &Map.pop(&2, &1, %{}))
      else
        {[], value}
      end

    sorted ++
      (pending
       |> Map.drop(drop)
       |> Enum.map(&key_as_int/1)
       |> Enum.sort()
       |> Enum.map(&elem(&1, 1)))
  end

  defp cast_params(%{cardinality: :one}, value, sort, drop) do
    if sort do
      raise ArgumentError, ":sort_param not supported for belongs_to/has_one"
    end

    if drop do
      raise ArgumentError, ":drop_param not supported for belongs_to/has_one"
    end

    value
  end

  defp cast_params(_relation, value, _sort, _drop) do
    value
  end

  defp opts_key_from_params(opt, opts, params) do
    if key = opts[opt] do
      Map.get(params, Atom.to_string(key), nil)
    end
  end

  # We check for the byte size to avoid creating unnecessary large integers
  # which would never map to a database key (u64 is 20 digits only).
  defp key_as_int({key, val}) when is_binary(key) and byte_size(key) < 32 do
    case Integer.parse(key) do
      {key, ""} -> {key, val}
      _ -> {key, val}
    end
  end

  defp key_as_int(key_val), do: key_val

  defp on_cast_default(type, module) do
    fn struct, params ->
      try do
        module.changeset(struct, params)
      rescue
        e in UndefinedFunctionError ->
          case __STACKTRACE__ do
            [{^module, :changeset, args_or_arity, _}]
            when args_or_arity == 2
            when length(args_or_arity) == 2 ->
              raise ArgumentError, """
              the module #{inspect(module)} does not define a changeset/2 function,
              which is used by cast_#{type}/3. You need to either:

                1. implement the #{type}.changeset/2 function
                2. pass the :with option to cast_#{type}/3 with an anonymous
                   function of arity 2 (or possibly arity 3, if using has_many or
                   embeds_many)

              When using an inline embed, the :with option must be given
              """

            stacktrace ->
              reraise e, stacktrace
          end
      end
    end
  end

  defp missing_relation(changeset, name, current, required?, relation, opts) do
    %{changes: changes, errors: errors} = changeset
    current_changes = Map.get(changes, name, current)

    if required? and Relation.empty?(relation, current_changes) do
      errors = [
        {name, {message(opts, :required_message, "can't be blank"), [validation: :required]}}
        | errors
      ]

      %{changeset | errors: errors, valid?: false}
    else
      changeset
    end
  end

  defp relation!(_op, type, _name, {type, relation}),
    do: relation

  defp relation!(op, :assoc, name, nil) do
    raise ArgumentError,
          "cannot #{op} assoc `#{name}`, assoc `#{name}` not found. Make sure it is spelled correctly and that the association type is not read-only"
  end

  defp relation!(op, type, name, nil) do
    raise ArgumentError,
          "cannot #{op} #{type} `#{name}`, #{type} `#{name}` not found. Make sure that it exists and is spelled correctly"
  end

  defp relation!(op, type, name, {other, _}) when other in @relations do
    raise ArgumentError,
          "expected `#{name}` to be an #{type} in `#{op}_#{type}`, got: `#{other}`"
  end

  defp relation!(op, type, name, schema_type) do
    raise ArgumentError,
          "expected `#{name}` to be an #{type} in `#{op}_#{type}`, got: `#{inspect(schema_type)}`"
  end

  defp force_update(changeset, opts) do
    if Keyword.get(opts, :force_update_on_change, true) do
      put_in(changeset.repo_opts[:force], true)
    else
      changeset
    end
  end

  ## Working with changesets

  @doc """
  Merges two changesets.

  This function merges two changesets provided they have been applied to the
  same data (their `:data` field is equal); if the data differs, an
  `ArgumentError` exception is raised. If one of the changesets has a `:repo`
  field which is not `nil`, then the value of that field is used as the `:repo`
  field of the resulting changeset; if both changesets have a non-`nil` and
  different `:repo` field, an `ArgumentError` exception is raised.

  The other fields are merged with the following criteria:

    * `params` - params are merged (not deep-merged) giving precedence to the
      params of `changeset2` in case of a conflict. If both changesets have their
      `:params` fields set to `nil`, the resulting changeset will have its params
      set to `nil` too.
    * `changes` - changes are merged giving precedence to the `changeset2`
      changes.
    * `errors` and `validations` - they are simply concatenated.
    * `required` - required fields are merged; all the fields that appear
      in the required list of both changesets are moved to the required
      list of the resulting changeset.

  ## Examples

      iex> changeset1 = cast(%Post{}, %{title: "Title"}, [:title])
      iex> changeset2 = cast(%Post{}, %{title: "New title", body: "Body"}, [:title, :body])
      iex> changeset = merge(changeset1, changeset2)
      iex> changeset.changes
      %{body: "Body", title: "New title"}

      iex> changeset1 = cast(%Post{body: "Body"}, %{title: "Title"}, [:title])
      iex> changeset2 = cast(%Post{}, %{title: "New title"}, [:title])
      iex> merge(changeset1, changeset2)
      ** (ArgumentError) different :data when merging changesets

  """
  @spec merge(t, t) :: t
  def merge(changeset1, changeset2)

  def merge(%Changeset{data: data} = cs1, %Changeset{data: data} = cs2) do
    new_repo = merge_identical(cs1.repo, cs2.repo, "repos")
    new_repo_opts = Keyword.merge(cs1.repo_opts, cs2.repo_opts)
    new_action = merge_identical(cs1.action, cs2.action, "actions")
    new_filters = Map.merge(cs1.filters, cs2.filters)
    new_validations = cs1.validations ++ cs2.validations
    new_constraints = cs1.constraints ++ cs2.constraints

    cast_merge(
      %{
        cs1
        | repo: new_repo,
          repo_opts: new_repo_opts,
          filters: new_filters,
          action: new_action,
          validations: new_validations,
          constraints: new_constraints
      },
      cs2
    )
  end

  def merge(%Changeset{}, %Changeset{}) do
    raise ArgumentError, message: "different :data when merging changesets"
  end

  defp cast_merge(cs1, cs2) do
    new_params = (cs1.params || cs2.params) && Map.merge(cs1.params || %{}, cs2.params || %{})
    new_changes = Map.merge(cs1.changes, cs2.changes)
    new_errors = Enum.uniq(cs1.errors ++ cs2.errors)
    new_required = Enum.uniq(cs1.required ++ cs2.required)
    new_types = cs1.types || cs2.types
    new_valid? = cs1.valid? and cs2.valid?

    %{
      cs1
      | params: new_params,
        valid?: new_valid?,
        errors: new_errors,
        types: new_types,
        changes: new_changes,
        required: new_required
    }
  end

  defp merge_identical(object, nil, _thing), do: object
  defp merge_identical(nil, object, _thing), do: object
  defp merge_identical(object, object, _thing), do: object

  defp merge_identical(lhs, rhs, thing) do
    raise ArgumentError,
          "different #{thing} (`#{inspect(lhs)}` and " <>
            "`#{inspect(rhs)}`) when merging changesets"
  end

  @doc """
  Fetches the given field from changes or from the data.

  While `fetch_change/2` only looks at the current `changes`
  to retrieve a value, this function looks at the changes and
  then falls back on the data, finally returning `:error` if
  no value is available.

  For relations, these functions will return the changeset
  original data with changes applied. To retrieve raw changesets,
  please use `fetch_change/2`.

  ## Examples

      iex> post = %Post{title: "Foo", body: "Bar baz bong"}
      iex> changeset = change(post, %{title: "New title"})
      iex> fetch_field(changeset, :title)
      {:changes, "New title"}
      iex> fetch_field(changeset, :body)
      {:data, "Bar baz bong"}
      iex> fetch_field(changeset, :not_a_field)
      :error

  """
  @spec fetch_field(t, atom) :: {:changes, term} | {:data, term} | :error
  def fetch_field(%Changeset{changes: changes, data: data, types: types}, key)
      when is_atom(key) do
    case Map.fetch(changes, key) do
      {:ok, value} ->
        {:changes, change_as_field(types, key, value)}

      :error ->
        case Map.fetch(data, key) do
          {:ok, value} -> {:data, data_as_field(data, types, key, value)}
          :error -> :error
        end
    end
  end

  @doc """
  Same as `fetch_field/2` but returns the value or raises if the given key was not found.

  ## Examples

      iex> post = %Post{title: "Foo", body: "Bar baz bong"}
      iex> changeset = change(post, %{title: "New title"})
      iex> fetch_field!(changeset, :title)
      "New title"
      iex> fetch_field!(changeset, :other)
      ** (KeyError) key :other not found in: %Post{...}
  """
  @spec fetch_field!(t, atom) :: term
  def fetch_field!(changeset, key) do
    case fetch_field(changeset, key) do
      {_, value} ->
        value

      :error ->
        raise KeyError, key: key, term: changeset.data
    end
  end

  @doc """
  Gets a field from changes or from the data.

  While `get_change/3` only looks at the current `changes`
  to retrieve a value, this function looks at the changes and
  then falls back on the data, finally returning `default` if
  no value is available.

  For associations and embeds, this function always returns
  nil, a struct, or a list of structs. In case of changes,
  the changeset data will have all data applies. This guarantees
  a consistent result regardless if changes have been applied
  or not. Use `get_change/2` or `get_assoc/3`/`get_embed/3`
  if you want to retrieve the relations as changesets or
  if you want more fine-grained control.

      iex> post = %Post{title: "A title", body: "My body is a cage"}
      iex> changeset = change(post, %{title: "A new title"})
      iex> get_field(changeset, :title)
      "A new title"
      iex> get_field(changeset, :not_a_field, "Told you, not a field!")
      "Told you, not a field!"

  """
  @spec get_field(t, atom, term) :: term
  def get_field(%Changeset{changes: changes, data: data, types: types}, key, default \\ nil) do
    case changes do
      %{^key => value} ->
        change_as_field(types, key, value)

      %{} ->
        case data do
          %{^key => value} -> data_as_field(data, types, key, value)
          %{} -> default
        end
    end
  end

  defp change_as_field(types, key, value) do
    case types do
      %{^key => {tag, relation}} when tag in @relations ->
        Relation.apply_changes(relation, value)

      %{} ->
        value
    end
  end

  defp data_as_field(data, types, key, value) do
    case types do
      %{^key => {tag, _relation}} when tag in @relations ->
        Relation.load!(data, value)

      %{} ->
        value
    end
  end

  @doc """
  Gets the association entry or entries from changes or from the data.

  Returned data is normalized to changesets by default. Pass the `:struct`
  flag to retrieve the data as structs with changes applied, similar to `get_field/2`.

  ## Examples

      iex> %Author{posts: [%Post{id: 1, title: "hello"}]}
      ...> |> change()
      ...> |> get_assoc(:posts)
      [%Ecto.Changeset{data: %Post{id: 1, title: "hello"}, changes: %{}}]

      iex> %Author{posts: [%Post{id: 1, title: "hello"}]}
      ...> |> cast(%{posts: [%{id: 1, title: "world"}]}, [])
      ...> |> cast_assoc(:posts)
      ...> |> get_assoc(:posts, :changeset)
      [%Ecto.Changeset{data: %Post{id: 1, title: "hello"}, changes: %{title: "world"}}]

      iex> %Author{posts: [%Post{id: 1, title: "hello"}]}
      ...> |> cast(%{posts: [%{id: 1, title: "world"}]}, [])
      ...> |> cast_assoc(:posts)
      ...> |> get_assoc(:posts, :struct)
      [%Post{id: 1, title: "world"}]

  """
  def get_assoc(changeset, name, as \\ :changeset)

  def get_assoc(%Changeset{} = changeset, name, :struct) do
    get_field(changeset, name)
  end

  def get_assoc(%Changeset{} = changeset, name, :changeset) do
    get_relation(:assoc, changeset, name)
  end

  @doc """
  Gets the embedded entry or entries from changes or from the data.

  Returned data is normalized to changesets by default. Pass the `:struct`
  flag to retrieve the data as structs with changes applied, similar to `get_field/2`.

  ## Examples

      iex> %Post{comments: [%Comment{id: 1, body: "hello"}]}
      ...> |> change()
      ...> |> get_embed(:comments)
      [%Ecto.Changeset{data: %Comment{id: 1, body: "hello"}, changes: %{}}]

      iex> %Post{comments: [%Comment{id: 1, body: "hello"}]}
      ...> |> cast(%{comments: [%{id: 1, body: "world"}]}, [])
      ...> |> cast_embed(:comments)
      ...> |> get_embed(:comments, :changeset)
      [%Ecto.Changeset{data: %Comment{id: 1, body: "hello"}, changes: %{body: "world"}}]

      iex> %Post{comments: [%Comment{id: 1, body: "hello"}]}
      ...> |> cast(%{comments: [%{id: 1, body: "world"}]}, [])
      ...> |> cast_embed(:comments)
      ...> |> get_embed(:comments, :struct)
      [%Comment{id: 1, body: "world"}]

  """
  def get_embed(changeset, name, as \\ :changeset)

  def get_embed(%Changeset{} = changeset, name, :struct) do
    get_field(changeset, name)
  end

  def get_embed(%Changeset{} = changeset, name, :changeset) do
    get_relation(:embed, changeset, name)
  end

  defp get_relation(_tag, %{types: nil}, _name) do
    raise ArgumentError, "changeset does not have types information"
  end

  defp get_relation(tag, %{changes: changes, data: data, types: types}, name) do
    _ = relation!(:get, tag, name, Map.get(types, name))

    existing =
      case changes do
        %{^name => value} -> value
        %{} -> Relation.load!(data, Map.fetch!(data, name))
      end

    case existing do
      nil -> nil
      list when is_list(list) -> Enum.map(list, &change/1)
      item -> change(item)
    end
  end

  @doc """
  Fetches a change from the given changeset.

  This function only looks at the `:changes` field of the given `changeset` and
  returns `{:ok, value}` if the change is present or `:error` if it's not.

  ## Examples

      iex> changeset = change(%Post{body: "foo"}, %{title: "bar"})
      iex> fetch_change(changeset, :title)
      {:ok, "bar"}
      iex> fetch_change(changeset, :body)
      :error

  """
  @spec fetch_change(t, atom) :: {:ok, term} | :error
  def fetch_change(%Changeset{changes: changes} = _changeset, key) when is_atom(key) do
    Map.fetch(changes, key)
  end

  @doc """
  Same as `fetch_change/2` but returns the value or raises if the given key was not found.

  ## Examples

      iex> changeset = change(%Post{body: "foo"}, %{title: "bar"})
      iex> fetch_change!(changeset, :title)
      "bar"
      iex> fetch_change!(changeset, :body)
      ** (KeyError) key :body not found in: %{title: "bar"}
  """
  @spec fetch_change!(t, atom) :: term
  def fetch_change!(changeset, key) do
    case fetch_change(changeset, key) do
      {:ok, value} ->
        value

      :error ->
        raise KeyError, key: key, term: changeset.changes
    end
  end

  @doc """
  Gets a change or returns a default value.

  For associations and embeds, this function always returns
  nil, a changeset, or a list of changesets.

  ## Examples

      iex> changeset = change(%Post{body: "foo"}, %{title: "bar"})
      iex> get_change(changeset, :title)
      "bar"
      iex> get_change(changeset, :body)
      nil

  """
  @spec get_change(t, atom, term) :: term
  def get_change(%Changeset{changes: changes} = _changeset, key, default \\ nil)
      when is_atom(key) do
    Map.get(changes, key, default)
  end

  @doc """
  Updates a change.

  The given `function` is invoked with the change value only if there
  is a change for `key`. Once the function is invoked, it behaves as
  `put_changed/3`.

  Note that the value of the change can still be `nil` (unless the field
  was marked as required on `validate_required/3`).

  ## Examples

      iex> changeset = change(%Post{}, %{impressions: 1})
      iex> changeset = update_change(changeset, :impressions, &(&1 + 1))
      iex> changeset.changes.impressions
      2

  """
  @spec update_change(t, atom, (term -> term)) :: t
  def update_change(%Changeset{changes: changes} = changeset, key, function) when is_atom(key) do
    case Map.fetch(changes, key) do
      {:ok, value} ->
        put_change(changeset, key, function.(value))

      :error ->
        changeset
    end
  end

  @doc """
  Puts a change on the given `key` with `value`.

  `key` is an atom that represents any field, embed or
  association in the changeset. Note the `value` is directly
  stored in the changeset with no validation whatsoever.
  For this reason, this function is meant for working with
  data internal to the application.

  If the change is already present, it is overridden with
  the new value. If the change has the same value as in the
  changeset data, no changes are added (and any existing
  changes are removed).

  When changing embeds and associations, see `put_assoc/4`
  for a complete reference on the accepted values.

  ## Examples

      iex> changeset = change(%Post{}, %{title: "foo"})
      iex> changeset = put_change(changeset, :title, "bar")
      iex> changeset.changes
      %{title: "bar"}

      iex> changeset = change(%Post{title: "foo"})
      iex> changeset = put_change(changeset, :title, "foo")
      iex> changeset.changes
      %{}

  """
  @spec put_change(t, atom, term) :: t
  def put_change(%Changeset{types: nil}, _key, _value) do
    raise ArgumentError, "changeset does not have types information"
  end

  def put_change(%Changeset{data: data, types: types} = changeset, key, value) do
    type = Map.get(types, key)

    {changes, errors, valid?} =
      put_change(data, changeset.changes, changeset.errors, changeset.valid?, key, value, type)

    %{changeset | changes: changes, errors: errors, valid?: valid?}
  end

  defp put_change(data, changes, errors, valid?, key, value, {tag, relation})
       when tag in @relations do
    original = Map.get(data, key)
    current = Relation.load!(data, original)

    case Relation.change(relation, value, current) do
      {:ok, change, relation_valid?} when change != original ->
        {Map.put(changes, key, change), errors, valid? and relation_valid?}

      {:error, error} ->
        {changes, [{key, error} | errors], false}

      # ignore or ok with change == original
      _ ->
        {Map.delete(changes, key), errors, valid?}
    end
  end

  defp put_change(data, _changes, _errors, _valid?, key, _value, nil) when is_atom(key) do
    raise ArgumentError, "unknown field `#{inspect(key)}` in #{inspect(data)}"
  end

  defp put_change(_data, _changes, _errors, _valid?, key, _value, nil) when not is_atom(key) do
    raise ArgumentError,
          "field names given to change/put_change must be atoms, got: `#{inspect(key)}`"
  end

  defp put_change(data, changes, errors, valid?, key, value, type) do
    if not Ecto.Type.equal?(type, Map.get(data, key), value) do
      {Map.put(changes, key, value), errors, valid?}
    else
      {Map.delete(changes, key), errors, valid?}
    end
  end

  @doc """
  Puts the given association entry or entries as a change in the changeset.

  This function is used to work with associations as a whole. For example,
  if a Post has many Comments, it allows you to add, remove or change all
  comments at once. If your goal is to simply add a new comment to a post,
  then it is preferred to do so manually, as we will describe later in the
  "Example: Adding a comment to a post" section.

  This function requires the associated data to have been preloaded, except
  when the parent changeset has been newly built and not yet persisted.
  Missing data will invoke the `:on_replace` behaviour defined on the
  association.

  For associations with cardinality one, `nil` can be used to remove the existing
  entry. For associations with many entries, an empty list may be given instead.

  If the association has no changes, it will be skipped. If the association is
  invalid, the changeset will be marked as invalid. If the given value is not any
  of values below, it will raise.

  The associated data may be given in different formats:

    * a map or a keyword list representing changes to be applied to the
      associated data. A map or keyword list can be given to update the
      associated data as long as they have matching primary keys.
      For example, `put_assoc(changeset, :comments, [%{id: 1, title: "changed"}])`
      will locate the comment with `:id` of 1 and update its title.
      If no comment with such id exists, one is created on the fly.
      Since only a single comment was given, any other associated comment
      will be replaced. On all cases, it is expected the keys to be atoms.
      Opposite to `cast_assoc` and `embed_assoc`, the given map (or struct)
      is not validated in any way and will be inserted as is.
      This API is mostly used in scripts and tests, to make it straight-
      forward to create schemas with associations at once, such as:

          Ecto.Changeset.change(
            %Post{},
            title: "foo",
            comments: [
              %{body: "first"},
              %{body: "second"}
            ]
          )

    * changesets - when changesets are given, they are treated as the canonical
      data and the associated data currently stored in the association is either
      updated or replaced. For example, if you call
      `put_assoc(post_changeset, :comments, [list_of_comments_changesets])`,
      all comments with matching IDs will be updated according to the changesets.
      New comments or comments not associated to any post will be correctly
      associated. Currently associated comments that do not have a matching ID
      in the list of changesets will act according to the `:on_replace` association
      configuration (you can chose to raise, ignore the operation, update or delete
      them). If there are changes in any of the changesets, they will be
      persisted too.

    * structs - when structs are given, they are treated as the canonical data
      and the associated data currently stored in the association is replaced.
      For example, if you call
      `put_assoc(post_changeset, :comments, [list_of_comments_structs])`,
      all comments with matching IDs will be replaced by the new structs.
      New comments or comments not associated to any post will be correctly
      associated. Currently associated comments that do not have a matching ID
      in the list of changesets will act according to the `:on_replace`
      association configuration (you can chose to raise, ignore the operation,
      update or delete them). Different to passing changesets, structs are not
      change tracked in any fashion. In other words, if you change a comment
      struct and give it to `put_assoc/4`, the updates in the struct won't be
      persisted. You must use changesets instead. `put_assoc/4` with structs
      only takes care of guaranteeing that the comments and the parent data
      are associated. This is extremely useful when associating existing data,
      as we will see in the "Example: Adding tags to a post" section.

  Once the parent changeset is given to an `Ecto.Repo` function, all entries
  will be inserted/updated/deleted within the same transaction.

  If you need different behaviour or explicit control over how this function
  behaves, you can drop it altogether and use `Ecto.Multi` to encode how several
  database operations will happen on several schemas and changesets at once.

  ## Example: Adding a comment to a post

  Imagine a relationship where Post has many comments and you want to add a
  new comment to an existing post. While it is possible to use `put_assoc/4`
  for this, it would be unnecessarily complex. Let's see an example.

  First, let's fetch the post with all existing comments:

      post = Post |> Repo.get!(1) |> Repo.preload(:comments)

  The following approach is **wrong**:

      post
      |> Ecto.Changeset.change()
      |> Ecto.Changeset.put_assoc(:comments, [%Comment{body: "bad example!"}])
      |> Repo.update!()

  The reason why the example above is wrong is because `put_assoc/4` always
  works with the **full data**. So the example above will effectively **erase
  all previous comments** and only keep the comment you are currently adding.
  Instead, you could try:

      post
      |> Ecto.Changeset.change()
      |> Ecto.Changeset.put_assoc(:comments, [%Comment{body: "so-so example!"} | post.comments])
      |> Repo.update!()

  In this example, we prepend the new comment to the list of existing comments.
  Ecto will diff the list of comments currently in `post` with the list of comments
  given, and correctly insert the new comment to the database. Note, however,
  Ecto is doing a lot of work just to figure out something we knew since the
  beginning, which is that there is only one new comment.

  In cases like above, when you want to work only on a single entry, it is
  much easier to simply work on the associated directly. For example, we
  could instead set the `post` association in the comment:

      %Comment{body: "better example"}
      |> Ecto.Changeset.change()
      |> Ecto.Changeset.put_assoc(:post, post)
      |> Repo.insert!()

  Alternatively, we can make sure that when we create a comment, it is already
  associated to the post:

      Ecto.build_assoc(post, :comments)
      |> Ecto.Changeset.change(body: "great example!")
      |> Repo.insert!()

  Or we can simply set the post_id in the comment itself:

      %Comment{body: "better example", post_id: post.id}
      |> Repo.insert!()

  In other words, when you find yourself wanting to work only with a subset
  of the data, then using `put_assoc/4` is most likely unnecessary. Instead,
  you want to work on the other side of the association.

  Let's see an example where using `put_assoc/4` is a good fit.

  ## Example: Adding tags to a post

  Imagine you are receiving a set of tags you want to associate to a post.
  Let's imagine that those tags exist upfront and are all persisted to the
  database. Imagine we get the data in this format:

      params = %{"title" => "new post", "tags" => ["learner"]}

  Now, since the tags already exist, we will bring all of them from the
  database and put them directly in the post:

      tags = Repo.all(from t in Tag, where: t.name in ^params["tags"])

      post
      |> Repo.preload(:tags)
      |> Ecto.Changeset.cast(params, [:title]) # No need to allow :tags as we put them directly
      |> Ecto.Changeset.put_assoc(:tags, tags) # Explicitly set the tags

  Since in this case we always require the user to pass all tags
  directly, using `put_assoc/4` is a great fit. It will automatically
  remove any tag not given and properly associate all of the given
  tags with the post.

  Furthermore, since the tag information is given as structs read directly
  from the database, Ecto will treat the data as correct and only do the
  minimum necessary to guarantee that posts and tags are associated,
  without trying to update or diff any of the fields in the tag struct.

  Although it accepts an `opts` argument, there are no options currently
  supported by `put_assoc/4`.

  ## More resources

  You can learn more about working with associations in our documentation,
  including cheatsheets and practical examples. Check out:

    * The docs for `cast_assoc/3`
    * The [associations cheatsheet](associations.html)
    * The [Constraints and Upserts guide](constraints-and-upserts.html)
    * The [Polymorphic associations with many to many guide](polymorphic-associations-with-many-to-many.html)
  """
  def put_assoc(%Changeset{} = changeset, name, value, opts \\ []) do
    put_relation(:assoc, changeset, name, value, opts)
  end

  @doc """
  Puts the given embed entry or entries as a change in the changeset.

  This function is used to work with embeds as a whole. For embeds with
  cardinality one, `nil` can be used to remove the existing entry. For
  embeds with many entries, an empty list may be given instead.

  If the embed has no changes, it will be skipped. If the embed is
  invalid, the changeset will be marked as invalid.

  The list of supported values and their behaviour is described in
  `put_assoc/4`. If the given value is not any of values listed there,
  it will raise.

  Although this function accepts an `opts` argument, there are no options
  currently supported by `put_embed/4`.
  """
  def put_embed(%Changeset{} = changeset, name, value, opts \\ []) do
    put_relation(:embed, changeset, name, value, opts)
  end

  defp put_relation(_tag, %{types: nil}, _name, _value, _opts) do
    raise ArgumentError, "changeset does not have types information"
  end

  defp put_relation(tag, changeset, name, value, _opts) do
    %{data: data, types: types, changes: changes, errors: errors, valid?: valid?} = changeset
    relation = relation!(:put, tag, name, Map.get(types, name))

    {changes, errors, valid?} =
      put_change(data, changes, errors, valid?, name, value, {tag, relation})

    %{changeset | changes: changes, errors: errors, valid?: valid?}
  end

  @doc """
  Forces a change on the given `key` with `value`.

  If the change is already present, it is overridden with
  the new value. If the value is later modified via
  `put_change/3` and `update_change/3`, reverting back to
  its original value, the change will be reverted unless
  `force_change/3` is called once again.

  ## Examples

      iex> changeset = change(%Post{author: "bar"}, %{title: "foo"})
      iex> changeset = force_change(changeset, :title, "bar")
      iex> changeset.changes
      %{title: "bar"}

      iex> changeset = force_change(changeset, :author, "bar")
      iex> changeset.changes
      %{title: "bar", author: "bar"}

  """
  @spec force_change(t, atom, term) :: t
  def force_change(%Changeset{types: nil}, _key, _value) do
    raise ArgumentError, "changeset does not have types information"
  end

  def force_change(%Changeset{types: types} = changeset, key, value) do
    case Map.get(types, key) do
      {tag, _} when tag in @relations ->
        raise "changing #{tag}s with force_change/3 is not supported, " <>
                "please use put_#{tag}/4 instead"

      nil ->
        raise ArgumentError, "unknown field `#{inspect(key)}` in #{inspect(changeset.data)}"

      _ ->
        put_in(changeset.changes[key], value)
    end
  end

  @doc """
  Deletes a change with the given key.

  ## Examples

      iex> changeset = change(%Post{}, %{title: "foo"})
      iex> changeset = delete_change(changeset, :title)
      iex> get_change(changeset, :title)
      nil

  """
  @spec delete_change(t, atom) :: t
  def delete_change(%Changeset{} = changeset, key) when is_atom(key) do
    update_in(changeset.changes, &Map.delete(&1, key))
  end

  @doc """
  Applies the changeset changes to the changeset data.

  This operation will return the underlying data with changes
  regardless if the changeset is valid or not. See `apply_action/2`
  for a similar function that ensures the changeset is valid.

  ## Examples

      iex> changeset = change(%Post{author: "bar"}, %{title: "foo"})
      iex> apply_changes(changeset)
      %Post{author: "bar", title: "foo"}

  """
  @spec apply_changes(t) :: Ecto.Schema.t() | data
  def apply_changes(%Changeset{changes: changes, data: data}) when changes == %{} do
    data
  end

  def apply_changes(%Changeset{changes: changes, data: data, types: types}) do
    Enum.reduce(changes, data, fn {key, value}, acc ->
      case Map.fetch(types, key) do
        {:ok, {tag, relation}} when tag in @relations ->
          apply_relation_changes(acc, key, relation, value)

        {:ok, _} ->
          Map.put(acc, key, value)

        :error ->
          acc
      end
    end)
  end

  @doc """
  Applies the changeset action only if the changes are valid.

  If the changes are valid, all changes are applied to the changeset data.
  If the changes are invalid, no changes are applied, and an error tuple
  is returned with the changeset containing the action that was attempted
  to be applied.

  The action may be any atom.

  ## Examples

      iex> {:ok, data} = apply_action(changeset, :update)

      iex> {:ok, data} = apply_action(changeset, :my_action)

      iex> {:error, changeset} = apply_action(changeset, :update)
      %Ecto.Changeset{action: :update}
  """
  @spec apply_action(t, atom) :: {:ok, Ecto.Schema.t() | data} | {:error, t}
  def apply_action(%Changeset{} = changeset, action) when is_atom(action) do
    if changeset.valid? do
      {:ok, apply_changes(changeset)}
    else
      {:error, %Changeset{changeset | action: action}}
    end
  end

  def apply_action(%Changeset{}, action) do
    raise ArgumentError, "expected action to be an atom, got: #{inspect(action)}"
  end

  @doc """
  Applies the changeset action if the changes are valid or raises an error.

  ## Examples

      iex> changeset = change(%Post{author: "bar"}, %{title: "foo"})
      iex> apply_action!(changeset, :update)
      %Post{author: "bar", title: "foo"}

      iex> changeset = change(%Post{author: "bar"}, %{title: :bad})
      iex> apply_action!(changeset, :update)
      ** (Ecto.InvalidChangesetError) could not perform update because changeset is invalid.

  See `apply_action/2` for more information.
  """
  @spec apply_action!(t, atom) :: Ecto.Schema.t() | data
  def apply_action!(%Changeset{} = changeset, action) do
    case apply_action(changeset, action) do
      {:ok, data} ->
        data

      {:error, changeset} ->
        raise Ecto.InvalidChangesetError, action: action, changeset: changeset
    end
  end

  ## Validations

  @doc ~S"""
  Returns a keyword list of the validations for this changeset.

  The keys in the list are the names of fields, and the values are a
  validation associated with the field. A field may occur multiple
  times in the list.

  ## Example

      %Post{}
      |> change()
      |> validate_format(:title, ~r/^\w+:\s/, message: "must start with a topic")
      |> validate_length(:title, max: 100)
      |> validations()
      #=> [
        title: {:length, [ max: 100 ]},
        title: {:format, ~r/^\w+:\s/}
      ]

  The following validations may be included in the result. The list is
  not necessarily exhaustive. For example, custom validations written
  by the developer will also appear in our return value.

  This first group contains validations that hold a keyword list of validators.
  This list may also include a `:message` key.

    * `{:length, [option]}`

      * `min: n`
      * `max: n`
      * `is: n`
      * `count: :graphemes | :codepoints`

    * `{:number,  [option]}`

      * `equal_to: n`
      * `greater_than: n`
      * `greater_than_or_equal_to: n`
      * `less_than: n`
      * `less_than_or_equal_to: n`

  The other validators simply take a value:

    * `{:exclusion, Enum.t}`
    * `{:format, ~r/pattern/}`
    * `{:inclusion, Enum.t}`
    * `{:subset, Enum.t}`

  Note that calling `validate_required/3` does not store the validation under the
  `changeset.validations` key (and so won't be included in the result of this
  function). The required fields are stored under the `changeset.required` key.
  """
  @spec validations(t) :: [{atom, term}]
  def validations(%Changeset{validations: validations}) do
    validations
  end

  @doc """
  Adds an error to the changeset.

  An additional keyword list `keys` can be passed to provide additional
  contextual information for the error. This is useful when using
  `traverse_errors/2` and when translating errors with `Gettext`

  ## Examples

      iex> changeset = change(%Post{}, %{title: ""})
      iex> changeset = add_error(changeset, :title, "empty")
      iex> changeset.errors
      [title: {"empty", []}]
      iex> changeset.valid?
      false

      iex> changeset = change(%Post{}, %{title: ""})
      iex> changeset = add_error(changeset, :title, "empty", additional: "info")
      iex> changeset.errors
      [title: {"empty", [additional: "info"]}]
      iex> changeset.valid?
      false

      iex> changeset = change(%Post{}, %{tags: ["ecto", "elixir", "x"]})
      iex> changeset = add_error(changeset, :tags, "tag '%{val}' is too short", val: "x")
      iex> changeset.errors
      [tags: {"tag '%{val}' is too short", [val: "x"]}]
      iex> changeset.valid?
      false
  """
  @spec add_error(t, atom, String.t(), Keyword.t()) :: t
  def add_error(%Changeset{errors: errors} = changeset, key, message, keys \\ [])
      when is_binary(message) do
    %{changeset | errors: [{key, {message, keys}} | errors], valid?: false}
  end

  @doc """
  Validates the given `field` change.

  It invokes the `validator` function to perform the validation
  only if a change for the given `field` exists and the change
  value is not `nil`. The function must return a list of errors
  (with an empty list meaning no errors).

  In case there's at least one error, the list of errors will be appended to the
  `:errors` field of the changeset and the `:valid?` flag will be set to
  `false`.

  ## Examples

      iex> changeset = change(%Post{}, %{title: "foo"})
      iex> changeset = validate_change changeset, :title, fn :title, title  ->
      ...>   # Value must not be "foo"!
      ...>   if title == "foo" do
      ...>     [title: "cannot be foo"]
      ...>   else
      ...>     []
      ...>   end
      ...> end
      iex> changeset.errors
      [title: {"cannot be foo", []}]

      iex> changeset = change(%Post{}, %{title: "foo"})
      iex> changeset = validate_change changeset, :title, fn :title, title  ->
      ...>   if title == "foo" do
      ...>     [title: {"cannot be foo", additional: "info"}]
      ...>   else
      ...>     []
      ...>   end
      ...> end
      iex> changeset.errors
      [title: {"cannot be foo", [additional: "info"]}]

  """
  @spec validate_change(
          t,
          atom,
          (atom, term -> [{atom, String.t()} | {atom, {String.t(), Keyword.t()}}])
        ) :: t
  def validate_change(%Changeset{} = changeset, field, validator) when is_atom(field) do
    %{changes: changes, types: types, errors: errors} = changeset
    ensure_field_exists!(changeset, types, field)

    value = Map.get(changes, field)
    new = if is_nil(value), do: [], else: validator.(field, value)

    new =
      Enum.map(new, fn
        {key, val} when is_atom(key) and is_binary(val) ->
          {key, {val, []}}

        {key, {val, opts}} when is_atom(key) and is_binary(val) and is_list(opts) ->
          {key, {val, opts}}
      end)

    case new do
      [] -> changeset
      [_ | _] -> %{changeset | errors: new ++ errors, valid?: false}
    end
  end

  @doc """
  Stores the validation `metadata` and validates the given `field` change.

  Similar to `validate_change/3` but stores the validation metadata
  into the changeset validators. The validator metadata is often used
  as a reflection mechanism, to automatically generate code based on
  the available validations.

  ## Examples

      iex> changeset = change(%Post{}, %{title: "foo"})
      iex> changeset = validate_change changeset, :title, :useless_validator, fn
      ...>   _, _ -> []
      ...> end
      iex> changeset.validations
      [title: :useless_validator]

  """
  @spec validate_change(
          t,
          atom,
          term,
          (atom, term -> [{atom, String.t()} | {atom, {String.t(), Keyword.t()}}])
        ) :: t
  def validate_change(
        %Changeset{validations: validations} = changeset,
        field,
        metadata,
        validator
      ) do
    changeset = %{changeset | validations: [{field, metadata} | validations]}
    validate_change(changeset, field, validator)
  end

  @doc """
  Validates that one or more fields are present in the changeset.

  You can pass a single field name or a list of field names that
  are required.

  If the value of a field is `nil` or a string made only of whitespace,
  the changeset is marked as invalid, the field is removed from the
  changeset's changes, and an error is added. An error won't be added if
  the field already has an error.

  If a field is given to `validate_required/3` but it has not been passed
  as parameter during `cast/3` (i.e. it has not been changed), then
  `validate_required/3` will check for its current value in the data.
  If the data contains an non-empty value for the field, then no error is
  added. This allows developers to use `validate_required/3` to perform
  partial updates. For example, on `insert` all fields would be required,
  because their default values on the data are all `nil`, but on `update`,
  if you don't want to change a field that has been previously set,
  you are not required to pass it as a parameter, since `validate_required/3`
  won't add an error for missing changes as long as the value in the
  data given to the `changeset` is not empty.

  Do not use this function to validate associations that are required,
  instead pass the `:required` option to `cast_assoc/3` or `cast_embed/3`.

  Opposite to other validations, calling this function does not store
  the validation under the `changeset.validations` key. Instead, it
  stores all required fields under `changeset.required`.

  ## Options

    * `:message` - the message on failure, defaults to "can't be blank"

  ## Examples

      validate_required(changeset, :title)
      validate_required(changeset, [:title, :body])

  """
  @spec validate_required(t, list | atom, Keyword.t()) :: t
  def validate_required(%Changeset{} = changeset, fields, opts \\ []) when not is_nil(fields) do
    %{required: required, errors: errors, changes: changes} = changeset
    fields = List.wrap(fields)

    fields_with_errors =
      for field <- fields,
          field_missing?(changeset, field),
          is_nil(errors[field]),
          do: field

    case fields_with_errors do
      [] ->
        %{changeset | required: fields ++ required}

      _ ->
        message = message(opts, "can't be blank")
        new_errors = Enum.map(fields_with_errors, &{&1, {message, [validation: :required]}})
        changes = Map.drop(changes, fields_with_errors)

        %{
          changeset
          | changes: changes,
            required: fields ++ required,
            errors: new_errors ++ errors,
            valid?: false
        }
    end
  end

  @doc """
  Determines whether a field is missing in a changeset.

  The field passed into this function will have its presence evaluated
  according to the same rules as `validate_required/3`.

  This is useful when performing complex validations that are not possible with
  `validate_required/3`. For example, evaluating whether at least one field
  from a list is present or evaluating that exactly one field from a list is
  present.

  ## Examples

      iex> changeset = cast(%Post{}, %{color: "Red"}, [:color])
      iex> missing_fields = Enum.filter([:title, :body], &field_missing?(changeset, &1))
      iex> changeset =
      ...>   case missing_fields do
      ...>     [_, _] -> add_error(changeset, :title, "at least one of `:title` or `:body` must be present")
      ...>     _ -> changeset
      ...>   end
      ...> changeset.errors
      [title: {"at least one of `:title` or `:body` must be present", []}]

  """
  @spec field_missing?(t(), atom()) :: boolean()
  def field_missing?(%Changeset{} = changeset, field) when not is_nil(field) do
    ensure_field_not_many!(changeset.types, field) && missing?(changeset, field) &&
      ensure_field_exists!(changeset, changeset.types, field)
  end

  @doc """
  Validates that no existing record with a different primary key
  has the same values for these fields.

  This function exists to provide quick feedback to users of your
  application. It should not be relied on for any data guarantee as it
  has race conditions and is inherently unsafe. For example, if this
  check happens twice in the same time interval (because the user
  submitted a form twice), both checks may pass and you may end-up with
  duplicate entries in the database. Therefore, a `unique_constraint/3`
  should also be used to ensure your data won't get corrupted.

  However, because constraints are only checked if all validations
  succeed, this function can be used as an early check to provide
  early feedback to users, since most conflicting data will have been
  inserted prior to the current validation phase.

  When applying this validation to a schemas loaded from the database
  this check will exclude rows having the same primary key as set on
  the changeset, as those are supposed to be overwritten anyways.

  ## Options

    * `:message` - the message in case the constraint check fails,
      defaults to "has already been taken".

    * `:error_key` - the key to which changeset error will be added when
      check fails, defaults to the first field name of the given list of
      fields.

    * `:prefix` - the prefix to run the query on (such as the schema path
      in Postgres or the database in MySQL). See `Ecto.Repo` documentation
      for more information.

    * `:nulls_distinct` - a boolean controlling whether different null values
      are considered distinct (not equal). If `false`, `nil` values will have
      their uniqueness checked. Otherwise, the check will not be performed. This
      is only meaningful when paired with a unique index that treats nulls as equal,
      such as Postgres 15's `NULLS NOT DISTINCT` option. Defaults to `true`

    * `:repo_opts` - the options to pass to the `Ecto.Repo` call.

    * `:query` - the base query to use for the check. Defaults to the schema of
      the changeset. If the primary key is set, a clause will be added to exclude
      the changeset row itself from the check.

  ## Examples

      unsafe_validate_unique(changeset, :city_name, repo)
      unsafe_validate_unique(changeset, [:city_name, :state_name], repo)
      unsafe_validate_unique(changeset, [:city_name, :state_name], repo, message: "city must be unique within state")
      unsafe_validate_unique(changeset, [:city_name, :state_name], repo, prefix: "public")
      unsafe_validate_unique(changeset, [:city_name, :state_name], repo, query: from(c in City, where: is_nil(c.deleted_at)))

  """
  @spec unsafe_validate_unique(t, atom | [atom, ...], Ecto.Repo.t(), Keyword.t()) :: t
  def unsafe_validate_unique(%Changeset{} = changeset, fields, repo, opts \\ [])
      when is_list(opts) do
    {repo_opts, opts} = Keyword.pop(opts, :repo_opts, [])
    %{data: data, validations: validations} = changeset

    unless is_struct(data) and function_exported?(data.__struct__, :__schema__, 1) do
      raise ArgumentError,
            "unsafe_validate_unique/4 does not work with schemaless changesets, got #{inspect(data)}"
    end

    schema =
      case {changeset.data, opts[:query]} do
        # regular schema
        {%schema{__meta__: %Metadata{}}, _} ->
          schema

        # embedded schema with base query
        {%schema{}, base_query} when base_query != nil ->
          schema

        # embedded schema without base query
        {data, _} ->
          raise ArgumentError,
                "unsafe_validate_unique/4 does not work with embedded schemas unless " <>
                  "the `:query` option is specified, got: #{inspect(data)}"
      end

    fields = List.wrap(fields)

    changeset = %{
      changeset
      | validations: [{hd(fields), {:unsafe_unique, fields: fields}} | validations]
    }

    nulls_distinct = Keyword.get(opts, :nulls_distinct, true)
    where_clause = unsafe_unique_filter(fields, changeset, nulls_distinct)

    # No need to query if there is a prior error for the fields
    any_prior_errors_for_fields? = Enum.any?(changeset.errors, &(elem(&1, 0) in fields))

    # No need to query if we haven't changed any of the fields in question
    unrelated_changes? = Enum.all?(fields, &(not Map.has_key?(changeset.changes, &1)))

    # If one or more fields are `nil` and `nulls_distinct` is not false, we can't query for uniqueness
    distinct_nils? =
      if nulls_distinct == false do
        false
      else
        Enum.any?(where_clause, &(&1 |> elem(1) |> is_nil()))
      end

    if unrelated_changes? or distinct_nils? or any_prior_errors_for_fields? do
      changeset
    else
      query =
        Keyword.get(opts, :query, schema)
        |> maybe_exclude_itself(changeset)
        |> Ecto.Query.where(^where_clause)

      query =
        if prefix = opts[:prefix] do
          Ecto.Query.put_query_prefix(query, prefix)
        else
          query
        end

      if repo.exists?(query, repo_opts) do
        error_key = Keyword.get(opts, :error_key, hd(fields))

        add_error(changeset, error_key, message(opts, "has already been taken"),
          validation: :unsafe_unique,
          fields: fields
        )
      else
        changeset
      end
    end
  end

  defp unsafe_unique_filter(fields, changeset, false) do
    Enum.reduce(fields, Ecto.Query.dynamic(true), fn field, dynamic ->
      case get_field(changeset, field) do
        nil -> Ecto.Query.dynamic([q], ^dynamic and is_nil(field(q, ^field)))
        value -> Ecto.Query.dynamic([q], ^dynamic and field(q, ^field) == ^value)
      end
    end)
  end

  defp unsafe_unique_filter(fields, changeset, _nulls_distinct) do
    for field <- fields do
      {field, get_field(changeset, field)}
    end
  end

  defp maybe_exclude_itself(base_query, %{data: %schema{__meta__: %Metadata{}}} = changeset) do
    primary_keys_to_exclude =
      case Ecto.get_meta(changeset.data, :state) do
        :loaded ->
          :primary_key
          |> schema.__schema__()
          |> Enum.map(&{&1, get_field(changeset, &1)})

        _ ->
          []
      end

    case primary_keys_to_exclude do
      [{_pk_field, nil} | _remaining_pks] ->
        base_query

      [{pk_field, value} | remaining_pks] ->
        # generate a clean query (one that does not start with 'TRUE OR ...')
        first_expr = Ecto.Query.dynamic([q], field(q, ^pk_field) == ^value)

        Enum.reduce_while(remaining_pks, first_expr, fn
          {_pk_field, nil}, _expr ->
            {:halt, nil}

          {pk_field, value}, expr ->
            {:cont, Ecto.Query.dynamic([q], ^expr and field(q, ^pk_field) == ^value)}
        end)
        |> case do
          nil ->
            base_query

          matches_pk ->
            Ecto.Query.where(base_query, ^Ecto.Query.dynamic(not (^matches_pk)))
        end

      [] ->
        base_query
    end
  end

  defp maybe_exclude_itself(base_query, _changeset), do: base_query

  defp ensure_field_exists!(changeset = %Changeset{}, types, field) do
    unless Map.has_key?(types, field) do
      raise ArgumentError, "unknown field #{inspect(field)} in #{inspect(changeset.data)}"
    end

    true
  end

  defp ensure_field_not_many!(types, field) do
    case types do
      %{^field => {:assoc, %Ecto.Association.Has{cardinality: :many}}} ->
        IO.warn(
          "attempting to determine the presence of has_many association #{inspect(field)} " <>
            "with validate_required/3 or field_missing?/2 which has no effect. You can pass the " <>
            ":required option to Ecto.Changeset.cast_assoc/3 to achieve this."
        )

      %{^field => {:embed, %Ecto.Embedded{cardinality: :many}}} ->
        IO.warn(
          "attempting to determine the presence of embed_many field #{inspect(field)} " <>
            "with validate_required/3 or field_missing?/2 which has no effect. You can pass the " <>
            ":required option to Ecto.Changeset.cast_embed/3 to achieve this."
        )

      _ ->
        true
    end
  end

  defp missing?(changeset, field) when is_atom(field) do
    case get_field(changeset, field) do
      %{__struct__: Ecto.Association.NotLoaded} ->
        raise ArgumentError,
              "attempting to determine the presence of association `#{field}` " <>
                "that was not loaded. Please preload your associations " <>
                "before calling validate_required/3 or field_missing?/2. " <>
                "You may also consider passing the :required option to Ecto.Changeset.cast_assoc/3"

      value when is_binary(value) ->
        value == ""

      nil ->
        true

      _ ->
        false
    end
  end

  defp missing?(_changeset, field) do
    raise ArgumentError,
          "validate_required/3 and field_missing?/2 expect field names to be atoms, got: `#{inspect(field)}`"
  end

  @doc """
  Validates a change has the given format.

  The format has to be expressed as a regular expression.

  ## Options

    * `:message` - the message on failure, defaults to "has invalid format"

  ## Examples

      validate_format(changeset, :email, ~r/@/)

  """
  @spec validate_format(t, atom, Regex.t(), Keyword.t()) :: t
  def validate_format(changeset, field, format, opts \\ []) do
    validate_change(changeset, field, {:format, format}, fn _, value ->
      unless is_binary(value) do
        raise ArgumentError,
              "validate_format/4 expects changes to be strings, received: #{inspect(value)} for field `#{field}`"
      end

      if value =~ format,
        do: [],
        else: [{field, {message(opts, "has invalid format"), [validation: :format]}}]
    end)
  end

  @doc """
  Validates a change is included in the given enumerable.

  ## Options

    * `:message` - the message on failure, defaults to "is invalid"

  ## Examples

      validate_inclusion(changeset, :cardinal_direction, ["north", "east", "south", "west"])
      validate_inclusion(changeset, :age, 0..99)

  """
  @spec validate_inclusion(t, atom, Enum.t(), Keyword.t()) :: t
  def validate_inclusion(changeset, field, data, opts \\ []) do
    validate_change(changeset, field, {:inclusion, data}, fn _, value ->
      type = Map.fetch!(changeset.types, field)

      if Ecto.Type.include?(type, value, data),
        do: [],
        else: [{field, {message(opts, "is invalid"), [validation: :inclusion, enum: data]}}]
    end)
  end

  @doc ~S"""
  Validates a change, of type enum, is a subset of the given enumerable.

  This validates if a list of values belongs to the given enumerable.
  If you need to validate if a single value is inside the given enumerable,
  you should use `validate_inclusion/4` instead.

  Type of the field must be array.

  ## Options

    * `:message` - the message on failure, defaults to "has an invalid entry"

  ## Examples

      validate_subset(changeset, :pets, ["cat", "dog", "parrot"])
      validate_subset(changeset, :lottery_numbers, 0..99)

  """
  @spec validate_subset(t, atom, Enum.t(), Keyword.t()) :: t
  def validate_subset(changeset, field, data, opts \\ []) do
    validate_change(changeset, field, {:subset, data}, fn _, value ->
      element_type =
        case Map.fetch!(changeset.types, field) do
          {:array, element_type} ->
            element_type

          type ->
            # backwards compatibility: custom types use underlying type
            {:array, element_type} = Ecto.Type.type(type)
            element_type
        end

      case Enum.any?(value, fn element -> not Ecto.Type.include?(element_type, element, data) end) do
        true ->
          [{field, {message(opts, "has an invalid entry"), [validation: :subset, enum: data]}}]

        false ->
          []
      end
    end)
  end

  @doc """
  Validates a change is not included in the given enumerable.

  ## Options

    * `:message` - the message on failure, defaults to "is reserved"

  ## Examples

      validate_exclusion(changeset, :name, ~w(admin superadmin))

  """
  @spec validate_exclusion(t, atom, Enum.t(), Keyword.t()) :: t
  def validate_exclusion(changeset, field, data, opts \\ []) do
    validate_change(changeset, field, {:exclusion, data}, fn _, value ->
      type = Map.fetch!(changeset.types, field)

      if Ecto.Type.include?(type, value, data),
        do: [{field, {message(opts, "is reserved"), [validation: :exclusion, enum: data]}}],
        else: []
    end)
  end

  @doc """
  Validates a change is a string or list of the given length.

  Note that the length of a string is counted in graphemes by default. If using
  this validation to match a character limit of a database backend,
  it's likely that the limit ignores graphemes and limits the number
  of unicode characters. Then consider using the `:count` option to
  limit the number of codepoints (`:codepoints`), or limit the number of bytes (`:bytes`).

  ## Options

    * `:is` - the length must be exactly this value
    * `:min` - the length must be greater than or equal to this value
    * `:max` - the length must be less than or equal to this value
    * `:count` - what length to count for string, `:graphemes` (default), `:codepoints` or `:bytes`
    * `:message` - the message on failure, depending on the validation, is one of:
      * for strings:
        * "should be %{count} character(s)"
        * "should be at least %{count} character(s)"
        * "should be at most %{count} character(s)"
      * for binary:
        * "should be %{count} byte(s)"
        * "should be at least %{count} byte(s)"
        * "should be at most %{count} byte(s)"
      * for lists and maps:
        * "should have %{count} item(s)"
        * "should have at least %{count} item(s)"
        * "should have at most %{count} item(s)"

  ## Examples

      validate_length(changeset, :title, min: 3)
      validate_length(changeset, :title, max: 100)
      validate_length(changeset, :title, min: 3, max: 100)
      validate_length(changeset, :code, is: 9)
      validate_length(changeset, :topics, is: 2)
      validate_length(changeset, :icon, count: :bytes, max: 1024 * 16)

  """
  @spec validate_length(t, atom, Keyword.t()) :: t
  def validate_length(changeset, field, opts) when is_list(opts) do
    validate_change(changeset, field, {:length, opts}, fn
      _, value ->
        count_type = opts[:count] || :graphemes

        {type, length} =
          case {value, count_type} do
            {value, :codepoints} when is_binary(value) ->
              {:string, codepoints_length(value, 0)}

            {value, :graphemes} when is_binary(value) ->
              {:string, String.length(value)}

            {value, :bytes} when is_binary(value) ->
              {:binary, byte_size(value)}

            {value, _} when is_list(value) ->
              {:list, list_length(changeset, field, value)}

            {value, _} when is_map(value) ->
              {:map, map_size(value)}
          end

        error =
          ((is = opts[:is]) && wrong_length(type, length, is, opts)) ||
            ((min = opts[:min]) && too_short(type, length, min, opts)) ||
            ((max = opts[:max]) && too_long(type, length, max, opts))

        if error, do: [{field, error}], else: []
    end)
  end

  defp codepoints_length(<<_::utf8, rest::binary>>, acc), do: codepoints_length(rest, acc + 1)
  defp codepoints_length(<<_, rest::binary>>, acc), do: codepoints_length(rest, acc + 1)
  defp codepoints_length(<<>>, acc), do: acc

  defp list_length(%{types: types}, field, value) do
    case Map.fetch(types, field) do
      {:ok, {tag, _association}} when tag in [:embed, :assoc] ->
        length(Relation.filter_empty(value))

      _ ->
        length(value)
    end
  end

  defp wrong_length(_type, value, value, _opts), do: nil

  defp wrong_length(:string, _length, value, opts),
    do:
      {message(opts, "should be %{count} character(s)"),
       count: value, validation: :length, kind: :is, type: :string}

  defp wrong_length(:binary, _length, value, opts),
    do:
      {message(opts, "should be %{count} byte(s)"),
       count: value, validation: :length, kind: :is, type: :binary}

  defp wrong_length(:list, _length, value, opts),
    do:
      {message(opts, "should have %{count} item(s)"),
       count: value, validation: :length, kind: :is, type: :list}

  defp wrong_length(:map, _length, value, opts),
    do:
      {message(opts, "should have %{count} item(s)"),
       count: value, validation: :length, kind: :is, type: :map}

  defp too_short(_type, length, value, _opts) when length >= value, do: nil

  defp too_short(:string, _length, value, opts) do
    {message(opts, "should be at least %{count} character(s)"),
     count: value, validation: :length, kind: :min, type: :string}
  end

  defp too_short(:binary, _length, value, opts) do
    {message(opts, "should be at least %{count} byte(s)"),
     count: value, validation: :length, kind: :min, type: :binary}
  end

  defp too_short(:list, _length, value, opts) do
    {message(opts, "should have at least %{count} item(s)"),
     count: value, validation: :length, kind: :min, type: :list}
  end

  defp too_short(:map, _length, value, opts) do
    {message(opts, "should have at least %{count} item(s)"),
     count: value, validation: :length, kind: :min, type: :map}
  end

  defp too_long(_type, length, value, _opts) when length <= value, do: nil

  defp too_long(:string, _length, value, opts) do
    {message(opts, "should be at most %{count} character(s)"),
     count: value, validation: :length, kind: :max, type: :string}
  end

  defp too_long(:binary, _length, value, opts) do
    {message(opts, "should be at most %{count} byte(s)"),
     count: value, validation: :length, kind: :max, type: :binary}
  end

  defp too_long(:list, _length, value, opts) do
    {message(opts, "should have at most %{count} item(s)"),
     count: value, validation: :length, kind: :max, type: :list}
  end

  defp too_long(:map, _length, value, opts) do
    {message(opts, "should have at most %{count} item(s)"),
     count: value, validation: :length, kind: :max, type: :map}
  end

  @doc """
  Validates the properties of a number.

  ## Options

    * `:less_than`
    * `:greater_than`
    * `:less_than_or_equal_to`
    * `:greater_than_or_equal_to`
    * `:equal_to`
    * `:not_equal_to`
    * `:message` - the message on failure, defaults to one of:
      * "must be less than %{number}"
      * "must be greater than %{number}"
      * "must be less than or equal to %{number}"
      * "must be greater than or equal to %{number}"
      * "must be equal to %{number}"
      * "must be not equal to %{number}"

  ## Examples

      validate_number(changeset, :count, less_than: 3)
      validate_number(changeset, :pi, greater_than: 3, less_than: 4)
      validate_number(changeset, :the_answer_to_life_the_universe_and_everything, equal_to: 42)

  """
  @spec validate_number(t, atom, Keyword.t()) :: t
  def validate_number(changeset, field, opts) do
    validate_change(changeset, field, {:number, opts}, fn
      field, value ->
        {message, opts} = Keyword.pop(opts, :message)

        unless valid_number?(value) do
          raise ArgumentError,
                "expected field `#{field}` to be a decimal, integer, or float, got: #{inspect(value)}"
        end

        Enum.find_value(opts, [], fn {spec_key, target_value} ->
          case Map.fetch(@number_validators, spec_key) do
            {:ok, {spec_function, default_message}} ->
              unless valid_number?(target_value) do
                raise ArgumentError,
                      "expected option `#{spec_key}` to be a decimal, integer, or float, got: #{inspect(target_value)}"
              end

              compare_numbers(
                field,
                value,
                message || default_message,
                spec_key,
                spec_function,
                target_value
              )

            :error ->
              supported_options =
                @number_validators |> Map.keys() |> Enum.map_join("\n", &"  * #{inspect(&1)}")

              raise ArgumentError, """
              unknown option #{inspect(spec_key)} given to validate_number/3

              The supported options are:

              #{supported_options}
              """
          end
        end)
    end)
  end

  defp valid_number?(%Decimal{}), do: true
  defp valid_number?(other), do: is_number(other)

  defp compare_numbers(
         field,
         %Decimal{} = value,
         message,
         spec_key,
         _spec_function,
         %Decimal{} = target_value
       ) do
    result = Decimal.compare(value, target_value)

    case decimal_compare(result, spec_key) do
      true -> nil
      false -> [{field, {message, validation: :number, kind: spec_key, number: target_value}}]
    end
  end

  defp compare_numbers(field, value, message, spec_key, spec_function, %Decimal{} = target_value) do
    compare_numbers(field, decimal_new(value), message, spec_key, spec_function, target_value)
  end

  defp compare_numbers(field, %Decimal{} = value, message, spec_key, spec_function, target_value) do
    compare_numbers(field, value, message, spec_key, spec_function, decimal_new(target_value))
  end

  defp compare_numbers(field, value, message, spec_key, spec_function, target_value) do
    case apply(spec_function, [value, target_value]) do
      true -> nil
      false -> [{field, {message, validation: :number, kind: spec_key, number: target_value}}]
    end
  end

  defp decimal_new(term) when is_float(term), do: Decimal.from_float(term)
  defp decimal_new(term), do: Decimal.new(term)

  defp decimal_compare(:lt, spec), do: spec in [:less_than, :less_than_or_equal_to, :not_equal_to]

  defp decimal_compare(:gt, spec),
    do: spec in [:greater_than, :greater_than_or_equal_to, :not_equal_to]

  defp decimal_compare(:eq, spec),
    do: spec in [:equal_to, :less_than_or_equal_to, :greater_than_or_equal_to]

  @doc """
  Validates that the given parameter matches its confirmation.

  By calling `validate_confirmation(changeset, :email)`, this
  validation will check if both "email" and "email_confirmation"
  in the parameter map matches. Note this validation only looks
  at the parameters themselves, never the fields in the schema.
  As such as, the "email_confirmation" field does not need to be
  added as a virtual field in your schema.

  Note that if the confirmation field is missing, this does not
  add a validation error. This is done on purpose as you do not
  trigger confirmation validation in places where a confirmation
  is not required (for example, in APIs). You can force the
  confirmation parameter to be required in the options (see below).

  ## Options

    * `:message` - the message on failure, defaults to "does not match confirmation"
    * `:required` - boolean, sets whether existence of confirmation parameter
      is required for addition of error. Defaults to false

  ## Examples

      validate_confirmation(changeset, :email)
      validate_confirmation(changeset, :password, message: "does not match password")

      cast(data, params, [:password])
      |> validate_confirmation(:password, message: "does not match password")

  """
  @spec validate_confirmation(t, atom, Keyword.t()) :: t
  def validate_confirmation(changeset, field, opts \\ [])

  def validate_confirmation(%{params: params} = changeset, field, opts) when is_map(params) do
    param = Atom.to_string(field)
    error_param = "#{param}_confirmation"
    error_field = String.to_atom(error_param)
    value = Map.get(params, param)

    errors =
      case params do
        %{^error_param => ^value} ->
          []

        %{^error_param => _} ->
          [
            {error_field,
             {message(opts, "does not match confirmation"), [validation: :confirmation]}}
          ]

        %{} ->
          confirmation_missing(opts, error_field)
      end

    %{
      changeset
      | validations: [{field, {:confirmation, opts}} | changeset.validations],
        errors: errors ++ changeset.errors,
        valid?: changeset.valid? and errors == []
    }
  end

  def validate_confirmation(%{params: nil} = changeset, _, _) do
    changeset
  end

  defp confirmation_missing(opts, error_field) do
    required = Keyword.get(opts, :required, false)

    if required,
      do: [{error_field, {message(opts, "can't be blank"), [validation: :required]}}],
      else: []
  end

  defp message(opts, key \\ :message, default) do
    Keyword.get(opts, key, default)
  end

  @doc """
  Validates the given parameter is true.

  Note this validation only checks the parameter itself is true, never
  the field in the schema. That's because acceptance parameters do not need
  to be persisted, as by definition they would always be stored as `true`.

  ## Options

    * `:message` - the message on failure, defaults to "must be accepted"

  ## Examples

      validate_acceptance(changeset, :terms_of_service)
      validate_acceptance(changeset, :rules, message: "please accept rules")

  """
  @spec validate_acceptance(t, atom, Keyword.t()) :: t
  def validate_acceptance(changeset, field, opts \\ [])

  def validate_acceptance(%{params: params} = changeset, field, opts) do
    errors = validate_acceptance_errors(params, field, opts)

    %{
      changeset
      | validations: [{field, {:acceptance, opts}} | changeset.validations],
        errors: errors ++ changeset.errors,
        valid?: changeset.valid? and errors == []
    }
  end

  defp validate_acceptance_errors(nil, _field, _opts), do: []

  defp validate_acceptance_errors(params, field, opts) do
    param = Atom.to_string(field)
    value = Map.get(params, param)

    case Ecto.Type.cast(:boolean, value) do
      {:ok, true} -> []
      _ -> [{field, {message(opts, "must be accepted"), validation: :acceptance}}]
    end
  end

  ## Optimistic lock

  @doc ~S"""
  Applies optimistic locking to the changeset.

  [Optimistic
  locking](https://en.wikipedia.org/wiki/Optimistic_concurrency_control) (or
  *optimistic concurrency control*) is a technique that allows concurrent edits
  on a single record. While pessimistic locking works by locking a resource for
  an entire transaction, optimistic locking only checks if the resource changed
  before updating it.

  This is done by regularly fetching the record from the database, then checking
  whether another user has made changes to the record *only when updating the
  record*. This behaviour is ideal in situations where the chances of concurrent
  updates to the same record are low; if they're not, pessimistic locking or
  other concurrency patterns may be more suited.

  ## Usage

  Optimistic locking works by keeping a "version" counter for each record; this
  counter gets incremented each time a modification is made to a record. Hence,
  in order to use optimistic locking, a field must exist in your schema for
  versioning purpose. Such field is usually an integer but other types are
  supported.

  ## Examples

  Assuming we have a `Post` schema (stored in the `posts` table), the first step
  is to add a version column to the `posts` table:

      alter table(:posts) do
        add :lock_version, :integer, default: 1
      end

  The column name is arbitrary and doesn't need to be `:lock_version`. Now add
  a field to the schema too:

      defmodule Post do
        use Ecto.Schema

        schema "posts" do
          field :title, :string
          field :lock_version, :integer, default: 1
        end

        def changeset(:update, struct, params \\ %{}) do
          struct
          |> Ecto.Changeset.cast(params, [:title])
          |> Ecto.Changeset.optimistic_lock(:lock_version)
        end
      end

  Now let's take optimistic locking for a spin:

      iex> post = Repo.insert!(%Post{title: "foo"})
      %Post{id: 1, title: "foo", lock_version: 1}
      iex> valid_change = Post.changeset(:update, post, %{title: "bar"})
      iex> stale_change = Post.changeset(:update, post, %{title: "baz"})
      iex> Repo.update!(valid_change)
      %Post{id: 1, title: "bar", lock_version: 2}
      iex> Repo.update!(stale_change)
      ** (Ecto.StaleEntryError) attempted to update a stale entry:

      %Post{id: 1, title: "baz", lock_version: 1}

  When a conflict happens (a record which has been previously fetched is
  being updated, but that same record has been modified since it was
  fetched), an `Ecto.StaleEntryError` exception is raised.

  Optimistic locking also works with delete operations. Just call the
  `optimistic_lock/3` function with the data before delete:

      iex> changeset = Ecto.Changeset.optimistic_lock(post, :lock_version)
      iex> Repo.delete(changeset)

  `optimistic_lock/3` by default assumes the field
  being used as a lock is an integer. If you want to use another type,
  you need to pass the third argument customizing how the next value
  is generated:

      iex> Ecto.Changeset.optimistic_lock(post, :lock_uuid, fn _ -> Ecto.UUID.generate end)

  """
  @spec optimistic_lock(Ecto.Schema.t() | t, atom, (term -> term)) :: t
  def optimistic_lock(data_or_changeset, field, incrementer \\ &increment_with_rollover/1) do
    changeset = change(data_or_changeset, %{})
    current = get_field(changeset, field)

    # Apply these changes only inside the repo because we
    # don't want to permanently track the lock change.
    changeset =
      prepare_changes(changeset, fn changeset ->
        put_in(changeset.changes[field], incrementer.(current))
      end)

    if is_nil(current) do
      Logger.warning("""
      the current value of `#{field}` is `nil` and will not be used as a filter for optimistic locking. \
      To ensure `#{field}` is never `nil`, consider setting a default value.
      """)

      changeset
    else
      put_in(changeset.filters[field], current)
    end
  end

  # increment_with_rollover expect to be used with lock_version set as :integer in db schema
  # 2_147_483_647 is upper limit for signed integer for both PostgreSQL and MySQL
  defp increment_with_rollover(val) when val >= 2_147_483_647 do
    1
  end

  defp increment_with_rollover(val) when is_integer(val) do
    val + 1
  end

  @doc """
  Provides a function executed by the repository on insert/update/delete.

  If the changeset given to the repository is valid, the function given to
  `prepare_changes/2` will be called with the changeset and must return a
  changeset, allowing developers to do final adjustments to the changeset or
  to issue data consistency commands. The repository itself can be accessed
  inside the function under the `repo` field in the changeset. If the
  changeset given to the repository is invalid, the function will not be
  invoked.

  The given function is guaranteed to run inside the same transaction
  as the changeset operation for databases that do support transactions.

  ## Example

  A common use case is updating a counter cache, in this case updating a post's
  comment count when a comment is created:

      def create_comment(comment, params) do
        comment
        |> cast(params, [:body, :post_id])
        |> prepare_changes(fn changeset ->
             if post_id = get_change(changeset, :post_id) do
               query = from Post, where: [id: ^post_id]
               changeset.repo.update_all(query, inc: [comment_count: 1])
             end
             changeset
           end)
      end

  We retrieve the repo from the comment changeset itself and use
  update_all to update the counter cache in one query. Finally, the original
  changeset must be returned.
  """
  @spec prepare_changes(t, (t -> t)) :: t
  def prepare_changes(%Changeset{prepare: prepare} = changeset, function)
      when is_function(function, 1) do
    %{changeset | prepare: [function | prepare]}
  end

  ## Constraints

  @doc """
  Returns all constraints in a changeset.

  A constraint is a map with the following fields:

    * `:type` - the type of the constraint that will be checked in the database,
      such as `:check`, `:unique`, etc
    * `:constraint` - the database constraint name as a string or `Regex`. The constraint at
      the database level will be checked against this according to `:match` type
    * `:match` - the type of match Ecto will perform on a violated constraint
      against the `:constraint` value. It is `:exact`, `:suffix` or `:prefix`
    * `:field` - the field a violated constraint will apply the error to
    * `:error_message` - the error message in case of violated constraints
    * `:error_type` - the type of error that identifies the error message

  """
  @spec constraints(t) :: [constraint]
  def constraints(%Changeset{constraints: constraints}) do
    constraints
  end

  @doc """
  Checks for a check constraint in the given field.

  The check constraint works by relying on the database to check
  if the check constraint has been violated or not and, if so,
  Ecto converts it into a changeset error.

  In order to use the check constraint, the first step is
  to define the check constraint in a migration:

      create constraint("users", :age_must_be_positive, check: "age > 0")

  Now that a constraint exists, when modifying users, we could
  annotate the changeset with a check constraint so Ecto knows
  how to convert it into an error message:

      cast(user, params, [:age])
      |> check_constraint(:age, name: :age_must_be_positive)

  Now, when invoking `c:Ecto.Repo.insert/2` or `c:Ecto.Repo.update/2`,
  if the age is not positive, the underlying operation will fail
  but Ecto will convert the database exception into a changeset error
  and return an `{:error, changeset}` tuple. Note that the error will
  occur only after hitting the database, so it will not be visible
  until all other validations pass. If the constraint fails inside a
  transaction, the transaction will be marked as aborted.

  ## Options

    * `:message` - the message in case the constraint check fails.
      Defaults to "is invalid"
    * `:name` - the constraint name. By default, the constraint
      name is inferred from the table + field. If this option is given,
      the `field` argument only indicates the field the error will be
      added to. May be required explicitly for complex cases
    * `:match` - how the changeset constraint name is matched against the
      repo constraint, may be `:exact`, `:suffix` or `:prefix`. Defaults to
      `:exact`. `:suffix` matches any repo constraint which `ends_with?` `:name`
      to this changeset constraint. `:prefix` matches any repo constraint which
      `starts_with?` `:name` to this changeset constraint.

  """
  def check_constraint(changeset, field, opts \\ []) do
    name = opts[:name] || raise ArgumentError, "must supply the name of the constraint"
    message = message(opts, "is invalid")
    match_type = Keyword.get(opts, :match, :exact)

    add_constraint(changeset, :check, name, match_type, field, message, :check)
  end

  @doc """
  Checks for a unique constraint in the given field or list of fields.

  The unique constraint works by relying on the database to check
  if the unique constraint has been violated or not and, if so,
  Ecto converts it into a changeset error.

  In order to use the uniqueness constraint, the first step is
  to define the unique index in a migration:

      create unique_index(:users, [:email])

  Now that a constraint exists, when modifying users, we could
  annotate the changeset with a unique constraint so Ecto knows
  how to convert it into an error message:

      cast(user, params, [:email])
      |> unique_constraint(:email)

  Now, when invoking `c:Ecto.Repo.insert/2` or `c:Ecto.Repo.update/2`,
  if the email already exists, the underlying operation will fail but
  Ecto will convert the database exception into a changeset error and
  return an `{:error, changeset}` tuple. Note that the error will occur
  only after hitting the database, so it will not be visible until all
  other validations pass. If the constraint fails inside a transaction,
  the transaction will be marked as aborted.

  ## Options

    * `:message` - the message in case the constraint check fails,
      defaults to "has already been taken"

    * `:name` - the constraint name. By default, the constraint
      name is inferred from the table + field. If this option is given,
      the `field` argument only indicates the field the error will be
      added to. May be required explicitly for complex cases

    * `:match` - how the changeset constraint name is matched against the
      repo constraint, may be `:exact`, `:suffix` or `:prefix`. Defaults to
      `:exact`. `:suffix` matches any repo constraint which `ends_with?` `:name`
      to this changeset constraint. `:prefix` matches any repo constraint which
      `starts_with?` `:name` to this changeset constraint.

    * `:error_key` - the key to which changeset error will be added when
      check fails, defaults to the first field name of the given list of
      fields.

  ## Complex constraints

  Because the constraint logic is in the database, we can leverage
  all the database functionality when defining them. For example,
  let's suppose the e-mails are scoped by company id:

      # In migration
      create unique_index(:users, [:email, :company_id])

      # In the changeset function
      cast(user, params, [:email])
      |> unique_constraint([:email, :company_id])

  The first field name, `:email` in this case, will be used as the error
  key to the changeset errors keyword list. For example, the above
  `unique_constraint/3` would generate something like:

      Repo.insert!(%User{email: "john@elixir.org", company_id: 1})
      changeset = User.changeset(%User{}, %{email: "john@elixir.org", company_id: 1})
      {:error, changeset} = Repo.insert(changeset)
      changeset.errors #=> [email: {"has already been taken", []}]

  In complex cases, instead of relying on name inference, it may be best
  to set the constraint name explicitly:

      # In the migration
      create unique_index(:users, [:email, :company_id], name: :users_email_company_id_index)

      # In the changeset function
      cast(user, params, [:email])
      |> unique_constraint(:email, name: :users_email_company_id_index)

  ### Partitioning

  If your table is partitioned, then your unique index might look different
  per partition, e.g. Postgres adds p<number> to the middle of your key, like:

      users_p0_email_key
      users_p1_email_key
      ...
      users_p99_email_key

  In this case you can use the name and suffix options together to match on
  these dynamic indexes, like:

      cast(user, params, [:email])
      |> unique_constraint(:email, name: :email_key, match: :suffix)

  There are cases where the index has a number added both for table name and
  index name, generating an index name such as:

      user_p0_email_idx2
      user_p1_email_idx3
      ...
      user_p99_email_idx101

  In that case, a `Regex` can be used to match:

      cast(user, params, [:email])
      |> unique_constraint(:email, name: ~r/user_p\d+_email_idx\d+/)

  ## Case sensitivity

  Unfortunately, different databases provide different guarantees
  when it comes to case-sensitiveness. For example, in MySQL, comparisons
  are case-insensitive by default. In Postgres, users can define case
  insensitive column by using the `:citext` type/extension. In your migration:

      execute "CREATE EXTENSION IF NOT EXISTS citext"
      create table(:users) do
        ...
        add :email, :citext
        ...
      end

  If for some reason your database does not support case insensitive columns,
  you can explicitly downcase values before inserting/updating them:

      cast(data, params, [:email])
      |> update_change(:email, &String.downcase/1)
      |> unique_constraint(:email)

  """
  @spec unique_constraint(t, atom | [atom, ...], Keyword.t()) :: t
  def unique_constraint(changeset, field_or_fields, opts \\ [])

  def unique_constraint(changeset, field, opts) when is_atom(field) do
    unique_constraint(changeset, [field], opts)
  end

  def unique_constraint(changeset, [first_field | _] = fields, opts) do
    name = opts[:name] || unique_index_name(changeset, fields)
    message = message(opts, "has already been taken")
    match_type = Keyword.get(opts, :match, :exact)
    error_key = Keyword.get(opts, :error_key, first_field)

    add_constraint(changeset, :unique, name, match_type, error_key, message, :unique)
  end

  defp unique_index_name(changeset, fields) do
    field_names = Enum.map(fields, &get_field_source(changeset, &1))
    Enum.join([get_source(changeset)] ++ field_names ++ ["index"], "_")
  end

  @doc """
  Checks for foreign key constraint in the given field.

  The foreign key constraint works by relying on the database to
  check if the associated data exists or not. This is useful to
  guarantee that a child will only be created if the parent exists
  in the database too.

  In order to use the foreign key constraint the first step is
  to define the foreign key in a migration. This is often done
  with references. For example, imagine you are creating a
  comments table that belongs to posts. One would have:

      create table(:comments) do
        add :post_id, references(:posts)
      end

  By default, Ecto will generate a foreign key constraint with
  name "comments_post_id_fkey" (the name is configurable).

  Now that a constraint exists, when creating comments, we could
  annotate the changeset with foreign key constraint so Ecto knows
  how to convert it into an error message:

      cast(comment, params, [:post_id])
      |> foreign_key_constraint(:post_id)

  Now, when invoking `c:Ecto.Repo.insert/2` or `c:Ecto.Repo.update/2`,
  if the associated post does not exist, the underlying operation will
  fail but Ecto will convert the database exception into a changeset
  error and return an `{:error, changeset}` tuple. Note that the error
  will occur only after hitting the database, so it will not be visible
  until all other validations pass. If the constraint fails inside a
  transaction, the transaction will be marked as aborted.

  ## Options

    * `:message` - the message in case the constraint check fails,
      defaults to "does not exist"
    * `:name` - the constraint name. By default, the constraint
      name is inferred from the table + field. If this option is given,
      the `field` argument only indicates the field the error will be
      added to. May be required explicitly for complex cases
    * `:match` - how the changeset constraint name is matched against the
      repo constraint, may be `:exact`, `:suffix` or `:prefix`. Defaults to
      `:exact`. `:suffix` matches any repo constraint which `ends_with?` `:name`
      to this changeset constraint. `:prefix` matches any repo constraint which
      `starts_with?` `:name` to this changeset constraint.

  """
  @spec foreign_key_constraint(t, atom, Keyword.t()) :: t
  def foreign_key_constraint(changeset, field, opts \\ []) do
    name = opts[:name] || "#{get_source(changeset)}_#{get_field_source(changeset, field)}_fkey"
    match_type = Keyword.get(opts, :match, :exact)
    message = message(opts, "does not exist")

    add_constraint(changeset, :foreign_key, name, match_type, field, message, :foreign)
  end

  @doc """
  Checks the associated field exists.

  This is similar to `foreign_key_constraint/3` except that the
  field is inferred from the association definition. This is useful
  to guarantee that a child will only be created if the parent exists
  in the database too. Therefore, it only applies to `belongs_to`
  associations.

  As the name says, a constraint is required in the database for
  this function to work. Such constraint is often added as a
  reference to the child table:

      create table(:comments) do
        add :post_id, references(:posts)
      end

  Now, when inserting a comment, it is possible to forbid any
  comment to be added if the associated post does not exist:

      comment
      |> Ecto.Changeset.cast(params, [:post_id])
      |> Ecto.Changeset.assoc_constraint(:post)
      |> Repo.insert

  ## Options

    * `:message` - the message in case the constraint check fails,
      defaults to "does not exist"
    * `:name` - the constraint name. By default, the constraint
      name is inferred from the table + field. If this option is given,
      the `field` argument only indicates the field the error will be
      added to. May be required explicitly for complex cases
    * `:match` - how the changeset constraint name is matched against the
      repo constraint, may be `:exact`, `:suffix` or `:prefix`. Defaults to
      `:exact`. `:suffix` matches any repo constraint which `ends_with?` `:name`
      to this changeset constraint. `:prefix` matches any repo constraint which
      `starts_with?` `:name` to this changeset constraint.
  """
  @spec assoc_constraint(t, atom, Keyword.t()) :: t
  def assoc_constraint(changeset, assoc, opts \\ []) do
    name =
      opts[:name] ||
        case get_assoc_type(changeset, assoc) do
          %Ecto.Association.BelongsTo{owner_key: owner_key} ->
            "#{get_source(changeset)}_#{owner_key}_fkey"

          other ->
            raise ArgumentError,
                  "assoc_constraint can only be added to belongs to associations, got: #{inspect(other)}"
        end

    match_type = Keyword.get(opts, :match, :exact)
    message = message(opts, "does not exist")

    add_constraint(changeset, :foreign_key, name, match_type, assoc, message, :assoc)
  end

  @doc """
  Checks the associated field does not exist.

  This is similar to `foreign_key_constraint/3` except that the
  field is inferred from the association definition. This is useful
  to guarantee that parent can only be deleted (or have its primary
  key changed) if no child exists in the database. Therefore, it only
  applies to `has_*` associations.

  As the name says, a constraint is required in the database for
  this function to work. Such constraint is often added as a
  reference to the child table:

      create table(:comments) do
        add :post_id, references(:posts)
      end

  Now, when deleting the post, it is possible to forbid any post to
  be deleted if they still have comments attached to it:

      post
      |> Ecto.Changeset.change
      |> Ecto.Changeset.no_assoc_constraint(:comments)
      |> Repo.delete

  ## Options

    * `:message` - the message in case the constraint check fails,
      defaults to "is still associated with this entry" (for `has_one`)
      and "are still associated with this entry" (for `has_many`)
    * `:name` - the constraint name. By default, the constraint
      name is inferred from the table + field. If this option is given,
      the `field` argument only indicates the field the error will be
      added to. May be required explicitly for complex cases
    * `:match` - how the changeset constraint name is matched against the
      repo constraint, may be `:exact`, `:suffix` or `:prefix`. Defaults to
      `:exact`. `:suffix` matches any repo constraint which `ends_with?` `:name`
      to this changeset constraint. `:prefix` matches any repo constraint which
      `starts_with?` `:name` to this changeset constraint.

  """
  @spec no_assoc_constraint(t, atom, Keyword.t()) :: t
  def no_assoc_constraint(changeset, assoc, opts \\ []) do
    {name, message} =
      case get_assoc_type(changeset, assoc) do
        %Ecto.Association.Has{
          cardinality: cardinality,
          related_key: related_key,
          related: related
        } ->
          {opts[:name] || "#{related.__schema__(:source)}_#{related_key}_fkey",
           message(opts, no_assoc_message(cardinality))}

        other ->
          raise ArgumentError,
                "no_assoc_constraint can only be added to has one/many associations, got: #{inspect(other)}"
      end

    match_type = Keyword.get(opts, :match, :exact)

    add_constraint(changeset, :foreign_key, name, match_type, assoc, message, :no_assoc)
  end

  @doc """
  Checks for an exclusion constraint in the given field.

  The exclusion constraint works by relying on the database to check
  if the exclusion constraint has been violated or not and, if so,
  Ecto converts it into a changeset error.

  ## Options

    * `:message` - the message in case the constraint check fails,
      defaults to "violates an exclusion constraint"
    * `:name` - the constraint name. By default, the constraint
      name is inferred from the table + field. If this option is given,
      the `field` argument only indicates the field the error will be
      added to. May be required explicitly for complex cases
    * `:match` - how the changeset constraint name is matched against the
      repo constraint, may be `:exact`, `:suffix` or `:prefix`. Defaults to
      `:exact`. `:suffix` matches any repo constraint which `ends_with?` `:name`
      to this changeset constraint. `:prefix` matches any repo constraint which
      `starts_with?` `:name` to this changeset constraint.

  """
  def exclusion_constraint(changeset, field, opts \\ []) do
    name =
      opts[:name] || "#{get_source(changeset)}_#{get_field_source(changeset, field)}_exclusion"

    message = message(opts, "violates an exclusion constraint")
    match_type = Keyword.get(opts, :match, :exact)

    add_constraint(changeset, :exclusion, name, match_type, field, message, :exclusion)
  end

  defp no_assoc_message(:one), do: "is still associated with this entry"
  defp no_assoc_message(:many), do: "are still associated with this entry"

  defp add_constraint(
         %Changeset{constraints: constraints} = changeset,
         type,
         constraint,
         match,
         field,
         error_message,
         error_type
       )
       when is_atom(field) and is_binary(error_message) do
    constraint = %{
      constraint: normalize_constraint(constraint, match),
      error_message: error_message,
      error_type: error_type,
      field: field,
      match: match,
      type: type
    }

    %{changeset | constraints: [constraint | constraints]}
  end

  defp normalize_constraint(constraint, match) when is_atom(constraint) do
    normalize_constraint(Atom.to_string(constraint), match)
  end

  defp normalize_constraint(constraint, match) when is_binary(constraint) do
    unless match in @match_types do
      raise ArgumentError,
            "invalid match type: #{inspect(match)}. Allowed match types: #{inspect(@match_types)}"
    end

    constraint
  end

  defp normalize_constraint(%Regex{} = constraint, match) do
    if match != :exact do
      raise ArgumentError,
            "a Regex constraint only allows match type of :exact, got: #{inspect(match)}"
    end

    constraint
  end

  defp get_source(%{data: %{__meta__: %{source: source}}}) when is_binary(source),
    do: source

  defp get_source(%{data: data}) do
    raise ArgumentError,
          "cannot add constraint to changeset because it does not have a source, got: #{inspect(data)}"
  end

  defp get_source(item) do
    raise ArgumentError,
          "cannot add constraint because a changeset was not supplied, got: #{inspect(item)}"
  end

  defp get_assoc_type(%{types: types}, assoc) do
    case Map.fetch(types, assoc) do
      {:ok, {:assoc, association}} -> association
      _ -> raise_invalid_assoc(types, assoc)
    end
  end

  defp raise_invalid_assoc(types, assoc) do
    associations = for {_key, {:assoc, %{field: field}}} <- types, do: field
    one_of = if match?([_], associations), do: "", else: "one of "

    raise ArgumentError,
          "cannot add constraint to changeset because association `#{assoc}` does not exist. " <>
            "Did you mean #{one_of}`#{Enum.join(associations, "`, `")}`?"
  end

  defp get_field_source(%{data: %{__struct__: schema}}, field) when is_atom(schema),
    do: schema.__schema__(:field_source, field) || field

  defp get_field_source(%{}, field),
    do: field

  @doc ~S"""
  Traverses changeset errors and applies the given function to error messages.

  This function is particularly useful when associations and embeds
  are cast in the changeset as it will traverse all associations and
  embeds and place all errors in a series of nested maps.

  A changeset is supplied along with a function to apply to each
  error message as the changeset is traversed. The error message
  function receives an error tuple `{msg, opts}`, for example:

      {"should be at least %{count} characters", [count: 3, validation: :length, min: 3]}

  ## Examples

      iex> traverse_errors(changeset, fn {msg, opts} ->
      ...>   Regex.replace(~r"%{(\w+)}", msg, fn _, key ->
      ...>     opts |> Keyword.get(String.to_existing_atom(key), key) |> to_string()
      ...>   end)
      ...> end)
      %{title: ["should be at least 3 characters"]}

  Optionally function can accept three arguments: `changeset`, `field`
  and error tuple `{msg, opts}`. It is useful whenever you want to extract
  validations rules from `changeset.validations` to build detailed error
  description.
  """
  @spec traverse_errors(t, (error -> String.t()) | (Changeset.t(), atom, error -> String.t())) ::
          traverse_result
  def traverse_errors(
        %Changeset{errors: errors, changes: changes, types: types} = changeset,
        msg_func
      )
      when is_function(msg_func, 1) or is_function(msg_func, 3) do
    errors
    |> Enum.reverse()
    |> merge_keyword_keys(msg_func, changeset)
    |> merge_related_keys(changes, types, msg_func, &traverse_errors/2)
  end

  defp merge_keyword_keys(keyword_list, msg_func, _) when is_function(msg_func, 1) do
    Enum.reduce(keyword_list, %{}, fn {key, val}, acc ->
      val = msg_func.(val)
      Map.update(acc, key, [val], &[val | &1])
    end)
  end

  defp merge_keyword_keys(keyword_list, msg_func, changeset) when is_function(msg_func, 3) do
    Enum.reduce(keyword_list, %{}, fn {key, val}, acc ->
      val = msg_func.(changeset, key, val)
      Map.update(acc, key, [val], &[val | &1])
    end)
  end

  defp merge_related_keys(_, _, nil, _, _) do
    raise ArgumentError, "changeset does not have types information"
  end

  defp merge_related_keys(map, changes, types, msg_func, traverse_function) do
    Enum.reduce(types, map, fn
      {field, {tag, %{cardinality: :many}}}, acc when tag in @relations ->
        if changesets = Map.get(changes, field) do
          {child, all_empty?} =
            Enum.map_reduce(changesets, true, fn changeset, all_empty? ->
              child = traverse_function.(changeset, msg_func)
              {child, all_empty? and child == %{}}
            end)

          case all_empty? do
            true -> acc
            false -> Map.put(acc, field, child)
          end
        else
          acc
        end

      {field, {tag, %{cardinality: :one}}}, acc when tag in @relations ->
        if changeset = Map.get(changes, field) do
          case traverse_function.(changeset, msg_func) do
            child when child == %{} -> acc
            child -> Map.put(acc, field, child)
          end
        else
          acc
        end

      {_, _}, acc ->
        acc
    end)
  end

  defp apply_relation_changes(acc, key, relation, value) do
    relation_changed = Relation.apply_changes(relation, value)

    acc = Map.put(acc, key, relation_changed)

    with %Ecto.Association.BelongsTo{related_key: related_key} <- relation,
         %{^related_key => id} <- relation_changed do
      Map.put(acc, relation.owner_key, id)
    else
      _ -> acc
    end
  end

  @doc ~S"""
  Traverses changeset validations and applies the given function to validations.

  This behaves the same as `traverse_errors/2`, but operates on changeset
  validations instead of errors.

  ## Examples

      iex> traverse_validations(changeset, &(&1))
      %{title: [format: ~r/pattern/, length: [min: 1, max: 20]]}

      iex> traverse_validations(changeset, fn
      ...>   {:length, opts} -> {:length, "#{Keyword.get(opts, :min, 0)}-#{Keyword.get(opts, :max, 32)}"}
      ...>   {:format, %Regex{source: source}} -> {:format, "/#{source}/"}
      ...>   {other, opts} -> {other, inspect(opts)}
      ...> end)
      %{title: [format: "/pattern/", length: "1-20"]}
  """
  @spec traverse_validations(
          t,
          (error -> String.t()) | (Changeset.t(), atom, error -> String.t())
        ) :: traverse_result
  def traverse_validations(
        %Changeset{validations: validations, changes: changes, types: types} = changeset,
        msg_func
      )
      when is_function(msg_func, 1) or is_function(msg_func, 3) do
    validations
    |> Enum.reverse()
    |> merge_keyword_keys(msg_func, changeset)
    |> merge_related_keys(changes, types, msg_func, &traverse_validations/2)
  end
end

defimpl Inspect, for: Ecto.Changeset do
  import Inspect.Algebra

  def inspect(%Ecto.Changeset{data: data} = changeset, opts) do
    list =
      for attr <- [:action, :changes, :errors, :data, :valid?] do
        {attr, Map.get(changeset, attr)}
      end

    redacted_fields =
      case data do
        %type{} ->
          if function_exported?(type, :__schema__, 1) do
            type.__schema__(:redact_fields)
          else
            []
          end

        _ ->
          []
      end

    container_doc("#Ecto.Changeset<", list, ">", opts, fn
      {:action, action}, opts ->
        concat("action: ", to_doc(action, opts))

      {:changes, changes}, opts ->
        concat("changes: ", changes |> filter(redacted_fields) |> to_doc(opts))

      {:data, data}, _opts ->
        concat("data: ", to_struct(data, opts))

      {:errors, errors}, opts ->
        concat("errors: ", to_doc(errors, opts))

      {:valid?, valid?}, opts ->
        concat("valid?: ", to_doc(valid?, opts))
    end)
  end

  defp to_struct(%{__struct__: struct}, _opts), do: "#" <> Kernel.inspect(struct) <> "<>"
  defp to_struct(other, opts), do: to_doc(other, opts)

  defp filter(changes, redacted_fields) do
    Enum.reduce(redacted_fields, changes, fn redacted_field, changes ->
      if Map.has_key?(changes, redacted_field) do
        Map.put(changes, redacted_field, "**redacted**")
      else
        changes
      end
    end)
  end
end
