lib/kuddle/encoder.ex

defmodule Kuddle.Encoder do
  @moduledoc """
  Encodes a Kuddle document into a KDL blob
  """
  alias Kuddle.Value
  alias Kuddle.Node

  import Kuddle.Utils

  @doc """
  Encodes a kuddle document as a KDL string
  """
  @spec encode(Kuddle.Decoder.document()) ::
          {:ok, String.t()}
          | {:error, term()}
  def encode([]) do
    {:ok, "\n"}
  end

  def encode(doc) do
    case do_encode(doc, []) do
      {:ok, rows} ->
        {:ok, IO.iodata_to_binary(rows)}
    end
  end

  defp do_encode([], rows) do
    {:ok, Enum.reverse(rows)}
  end

  defp do_encode([%Node{name: name, attributes: attrs, children: nil} | rest], rows) do
    node_name = encode_node_name(name)

    result = [node_name]

    result =
      case encode_node_attributes(attrs, []) do
        [] ->
          result

        node_attrs ->
          [result, " ", Enum.intersperse(node_attrs, " ")]
      end

    do_encode(rest, [[result, "\n"] | rows])
  end

  defp do_encode([%Node{name: name, attributes: attrs, children: children} | rest], rows) do
    node_name = encode_node_name(name)

    result = [node_name]

    result =
      case encode_node_attributes(attrs, []) do
        [] ->
          result

        node_attrs ->
          [result, " ", Enum.intersperse(node_attrs, " ")]
      end

    result = [result, " {\n"]
    result =
      case children do
        [] ->
          result

        children ->
          case do_encode(children, []) do
            {:ok, rows} ->
              [
                result,
                indent(rows, "    "),
                "\n",
              ]
          end
      end

    result = [result, "}\n"]

    do_encode(rest, [result | rows])
  end

  defp encode_node_attributes([%Value{} = value | rest], acc) do
    encode_node_attributes(rest, [encode_value(value) | acc])
  end

  defp encode_node_attributes([{%Value{} = key, %Value{} = value} | rest], acc) do
    result = [encode_value(key), "=", encode_value(value)]
    encode_node_attributes(rest, [result | acc])
  end

  defp encode_node_attributes([], acc) do
    Enum.reverse(acc)
  end

  defp encode_value(%Value{value: nil}) do
    "null"
  end

  defp encode_value(%Value{type: :boolean, value: value}) when is_boolean(value) do
    Atom.to_string(value)
  end

  defp encode_value(%Value{type: :string, value: value}) when is_binary(value) do
    encode_string(value)
  end

  defp encode_value(%Value{type: :integer, value: value, format: format}) when is_integer(value) do
    case format do
      :bin ->
        ["0b", Integer.to_string(value, 2)]

      :oct ->
        ["0o", Integer.to_string(value, 8)]

      :dec ->
        Integer.to_string(value, 10)

      :hex ->
        ["0x", String.downcase(Integer.to_string(value, 16))]
    end
  end

  defp encode_value(%Value{type: :float, value: value}) when is_float(value) do
    String.upcase(Float.to_string(value))
  end

  defp encode_value(%Value{type: :float, value: %Decimal{} = value}) do
    String.upcase(Decimal.to_string(value, :scientific))
  end

  defp encode_value(%Value{type: :id, value: value}) when is_binary(value) do
    value
  end

  defp encode_string(str) do
    "\"" <> do_encode_string(str, []) <> "\""
  end

  defp do_encode_string(<<>>, acc) do
    IO.iodata_to_binary(Enum.reverse(acc))
  end

  defp do_encode_string(<<"/", rest::binary>>, acc) do
    do_encode_string(rest, ["\\/" | acc])
  end

  defp do_encode_string(<<"\\", rest::binary>>, acc) do
    do_encode_string(rest, ["\\\\" | acc])
  end

  defp do_encode_string(<<"\"", rest::binary>>, acc) do
    do_encode_string(rest, ["\\\"" | acc])
  end

  defp do_encode_string(<<"\b", rest::binary>>, acc) do
    do_encode_string(rest, ["\\b" | acc])
  end

  defp do_encode_string(<<"\f", rest::binary>>, acc) do
    do_encode_string(rest, ["\\f" | acc])
  end

  defp do_encode_string(<<"\r", rest::binary>>, acc) do
    do_encode_string(rest, ["\\r" | acc])
  end

  defp do_encode_string(<<"\n", rest::binary>>, acc) do
    do_encode_string(rest, ["\\n" | acc])
  end

  defp do_encode_string(<<"\t", rest::binary>>, acc) do
    do_encode_string(rest, ["\\t" | acc])
  end

  defp do_encode_string(<<c::utf8, rest::binary>>, acc) do
    do_encode_string(rest, [<<c::utf8>> | acc])
  end

  defp encode_node_name(name) do
    if valid_identifier?(name) and not need_quote?(name) do
      name
    else
      encode_string(name)
    end
  end

  defp indent(rows, spacer) do
    rows
    |> IO.iodata_to_binary()
    |> String.trim_trailing()
    |> String.split("\n")
    |> Enum.map(fn row ->
      [spacer, row]
    end)
    |> Enum.intersperse("\n")
  end
end