defmodule Liquex.Represent do
  @moduledoc """
  Helper methods for maps

  alias Liquex.Representable

  @doc """
  Convert any object and deeply maps atom keys to strings

  The value `deep` determines if it should eagerly represent the object. If
  set to false, it will return a function for any nested objects.

  Lazy values are automatically evaluated in the Liquid rendering engine.
  @spec represent(any, boolean) :: any
  def represent(value, lazy \\ false)

  def represent(struct, lazy) when is_struct(struct) do
    |> Map.from_struct()
    |> represent(lazy)

  def represent(value, lazy) when is_map(value) do
    |>, lazy))
    |> Enum.into(%{})

  # Walk the list and represent the keys of of any map members
  def represent(value, lazy) when is_list(value), do:, &lazy_represent(&1, lazy))

  def represent(value, _), do: value

  @spec expand(term) :: term
  @doc """
  Expands a previously represented object.

  Useful when the represented object contains lazy fields. This can be useful
  for generating JSON values from a represented object. For example, if you
  had a `dump` filter that dumped a value to the page as JSON, you would use
  this function so that lazy functions get represented correctly instead of
  as functions.
  def expand(value) when is_struct(value), do: value
  def expand(value) when is_function(value), do: expand(value.())

  def expand(value) when is_map(value) do
    |> {k, v} -> {k, expand(v)} end)
    |> Enum.into(%{})

  def expand(value) when is_list(value), do:, &expand/1)
  def expand(value), do: value

  defp to_string_key({k, v}, lazy) when is_atom(k),
    do: to_string_key({Atom.to_string(k), v}, lazy)

  defp to_string_key({k, v}, lazy), do: {k, lazy_represent(v, lazy)}

  defp lazy_represent(value, true) do
    case Representable.is_lazy(value) do
      true -> fn -> Liquex.Representable.represent(value, true) end
      _ -> Liquex.Representable.represent(value, true)

  defp lazy_represent(v, false), do: Liquex.Representable.represent(v, false)