lib/pdf/dictionary.ex

defmodule Pdf.Dictionary do
  @moduledoc false
  import Pdf.Utils
  import Pdf.Size

  @dict_start "<<\n"
  @dict_start_length byte_size(@dict_start)
  @dict_end ">>"
  @dict_end_length byte_size(@dict_end)
  @initial_length @dict_start_length + @dict_end_length
  @value_separator " "
  @value_separator_length byte_size(@value_separator)
  @entry_separator "\n"
  @entry_separator_length byte_size(@entry_separator)

  defstruct size: @initial_length, entries: %{}

  def new, do: %__MODULE__{size: @initial_length}

  def new(map) do
    map
    |> Enum.filter(fn {_, value} -> value != nil end)
    |> Enum.reduce(new(), fn {key, value}, dictionary ->
      put(dictionary, key, value)
    end)
  end

  def put(dictionary, key, value) when is_binary(value), do: put(dictionary, key, s(value))

  def put(dictionary, key, value) do
    key = n(key)

    case Map.fetch(dictionary.entries, key) do
      :error ->
        entries = Map.put(dictionary.entries, key, value)
        size = increment_internal_size(dictionary, key, value)
        %{dictionary | entries: entries, size: size}

      {:ok, ^value} ->
        dictionary

      {:ok, old_value} ->
        entries = Map.put(dictionary.entries, key, value)
        size = decrement_internal_size(dictionary, key, old_value)
        size = increment_internal_size(%{dictionary | size: size}, key, value)

        %{dictionary | entries: entries, size: size}
    end
  end

  defp increment_internal_size(%__MODULE__{size: size}, key, %{size: _size}),
    do: size + size_of(key) + @value_separator_length + @entry_separator_length

  defp increment_internal_size(dictionary, key, value),
    do: increment_internal_size(dictionary, key, %{size: 0}) + size_of(value)

  defp decrement_internal_size(%__MODULE__{size: size}, key, %{size: _size}),
    do: size - size_of(key) - @value_separator_length - @entry_separator_length

  defp decrement_internal_size(dictionary, key, value),
    do: decrement_internal_size(dictionary, key, %{size: 0}) - size_of(value)

  def size(dict) do
    dict
    |> to_iolist()
    |> :binary.list_to_bin()
    |> byte_size()
  end

  def to_iolist(dictionary) do
    [@dict_start, entries_to_iolist(dictionary.entries), @dict_end]
  end

  defp entries_to_iolist(%{} = entries), do: entries_to_iolist(Enum.to_list(entries))
  defp entries_to_iolist([]), do: []
  defp entries_to_iolist([entry | tail]), do: [entry_to_iolist(entry) | entries_to_iolist(tail)]

  defp entry_to_iolist({key, value}),
    do: Pdf.Export.to_iolist([key, @value_separator, value, @entry_separator])

  defimpl Pdf.Size do
    def size_of(%Pdf.Dictionary{} = dictionary), do: Pdf.Dictionary.size(dictionary)
  end

  defimpl Pdf.Export do
    def to_iolist(dictionary), do: Pdf.Dictionary.to_iolist(dictionary)
  end

  def to_map(%__MODULE__{entries: entries}) do
    entries
    |> Enum.map(fn {{:name, name}, value} ->
      {name, to_value(value)}
    end)
    |> Map.new()
  end

  defp to_value({:name, name}), do: name
  defp to_value({:string, string}), do: string
  defp to_value(other), do: other
end