defmodule Protox.MergeMessage do
  @moduledoc """
  This module provides a helper function to merge messages.

  alias Protox.Field

  @doc """
  Singular fields of `msg` will be overwritten, if specified in `from`, except for
  embedded messages which will be merged. Repeated fields will be concatenated.

  Note that "specified" has a different meaning in protobuf 2 and 3:
  - 2: if the singular field from `from` is nil, the value from `msg` is kept
  - 3: if the singular field from `from` is set to the default value, the value from `msg` is
  kept. This behaviour matches the C++ reference implementation behaviour.

  - `msg` and `from` must be of the same type; or
  - either `msg` or `from` is `nil`: the non-nil message is returned; or
  - both are `nil`: `nil` is returned

  # Example
      iex> r1 = %Protobuf2{a: 0, s: :ONE}
      iex> r2 = %Protobuf2{a: nil, s: :TWO}
      iex> Protox.MergeMessage.merge(r1, r2)
      %Protobuf2{a: 0, s: :TWO}
      iex> Protox.MergeMessage.merge(r2, r1)
      %Protobuf2{a: 0, s: :ONE}
  @spec merge(struct() | nil, struct() | nil) :: struct() | nil
  def merge(nil, from), do: from
  def merge(msg, nil), do: msg

  def merge(msg, from) do
    unknown_fields_name = msg.__struct__.unknown_fields_name()

    Map.merge(msg, from, fn
      :__struct__, v1, _v2 ->

      ^unknown_fields_name, v1, _v2 ->

      name, v1, v2 ->
        merge_field(msg, name, v1, v2)

  # -- Private

  defp merge_field(msg, name, v1, v2) do
    case msg.__struct__.field_def(name) do
      {:ok, %Field{kind: :packed}} ->
        v1 ++ v2

      {:ok, %Field{kind: :unpacked}} ->
        v1 ++ v2

      {:ok, %Field{kind: {:scalar, _}, type: {:message, _}}} ->
        merge(v1, v2)

      {:ok, %Field{kind: {:scalar, _}}} ->
        {:ok, default} = msg.__struct__.default(name)
        merge_scalar(msg.__struct__.syntax(), v1, v2, default)

      {:ok, %Field{kind: :map, type: {_, {:message, _}}}} ->
        Map.merge(v1, v2, fn _key, w1, w2 -> merge(w1, w2) end)

      {:ok, %Field{kind: :map}} ->
        Map.merge(v1, v2)

      {:error, :no_such_field} ->
        merge_oneof(msg, v1, v2)

  defp merge_scalar(:proto2, v1, nil, _default), do: v1
  defp merge_scalar(:proto3, v1, v2, v2), do: v1
  defp merge_scalar(_syntax, _v1, v2, _default), do: v2

  defp merge_oneof(
         {v1_child_field, v1_child_value},
         {v2_child_field, v2_child_value} = v2
       when v1_child_field == v2_child_field do
    {:ok, v1_child_field_def} = msg.__struct__.field_def(v1_child_field)
    {:ok, v2_child_field_def} = msg.__struct__.field_def(v2_child_field)

    if is_oneof_message(v1_child_field_def) and is_oneof_message(v2_child_field_def) do
      {v1_child_field, merge(v1_child_value, v2_child_value)}

  defp merge_oneof(_msg, v1, nil), do: v1
  defp merge_oneof(_msg, _v1, v2), do: v2

  defp is_oneof_message(%Field{kind: {:oneof, _}, type: {:message, _}}), do: true
  defp is_oneof_message(_), do: false