defmodule CSV.Encoding.Encoder do
  use CSV.Defaults

  @moduledoc ~S"""
  The Encoder CSV module takes a table stream and transforms it into RFC 4180
  compliant stream of lines for writing to a CSV File or other IO.
  """

  @doc """
  Encode a table stream into a stream of RFC 4180 compliant CSV lines for
  writing to a file or other IO.

  ## Options

  These are the options:

    * `:separator`   – The separator token to use, defaults to `?,`.
      Must be a codepoint (syntax: ? + your separator token).
    * `:delimiter`   – The delimiter token to use, defaults to `\"\\r\\n\"`.
    * `:headers`     – When set to `true`, uses the keys of the first map as
      the first element in the stream. All subsequent elements are the values
      of the maps. When set to a list, will use the given list as the first
      element in the stream and order all subsequent elements using that list.
      When set to `false` (default), will use the raw inputs as elements.
      When set to anything but `false`, all elements in the input stream are
      assumed to be maps.

  ## Examples

  Convert a stream of rows with cells into a stream of lines:

      iex> [~w(a b), ~w(c d)]
      iex> |> CSV.Encoding.Encoder.encode
      iex> |> Enum.take(2)
      [\"a,b\\r\\n\", \"c,d\\r\\n\"]

  Convert a stream of maps into a stream of lines:

      iex> [%{"a" => 1, "b" => 2}, %{"a" => 3, "b" => 4}]
      iex> |> CSV.Encoding.Encoder.encode(headers: true)
      iex> |> Enum.to_list()
      [\"a,b\\r\\n\", \"1,2\\r\\n\", \"3,4\\r\\n\"]

  Convert a stream of rows with cells with escape sequences into a stream of
  lines:

      iex> [[\"a\\nb\", \"\\tc\"], [\"de\", \"\\tf\\\"\"]]
      iex> |> CSV.Encoding.Encoder.encode(separator: ?\\t, delimiter: \"\\n\")
      iex> |> Enum.take(2)
      [\"\\\"a\\\\nb\\\"\\t\\\"\\\\tc\\\"\\n\", \"de\\t\\\"\\\\tf\\\"\\\"\\\"\\n\"]
  """

  def encode(stream, options \\ []) do
    headers = options |> Keyword.get(:headers, false)

    encode_stream(stream, headers, options)
  end

  defp encode_stream(stream, false, options) do
    stream
    |> Stream.transform(0, fn row, acc ->
      {[encode_row(row, options)], acc + 1}
    end)
  end

  defp encode_stream(stream, headers, options) do
    stream
    |> Stream.transform(0, fn
      row, 0 ->
        {[
           encode_row(get_headers(row, headers), options),
           encode_row(get_values(row, headers), options)
         ], 1}

      row, acc ->
        {[encode_row(get_values(row, headers), options)], acc + 1}
    end)
  end

  defp get_headers(row, true), do: Map.keys(row)
  defp get_headers(_row, headers), do: headers

  defp get_values(row, true), do: Map.values(row)
  defp get_values(row, headers), do: headers |> Enum.map(&Map.get(row, &1))

  defp encode_row(row, options) do
    separator = options |> Keyword.get(:separator, @separator)
    delimiter = options |> Keyword.get(:delimiter, @delimiter)

    encoded =
      row
      |> Enum.map(&encode_cell(&1, separator, delimiter))
      |> Enum.join(<<separator::utf8>>)

    encoded <> delimiter
  end

  defp encode_cell(cell, separator, delimiter) do
    CSV.Encode.encode(cell, separator: separator, delimiter: delimiter)
  end
end
