lib/io/ansi/table/line.ex

defmodule IO.ANSI.Table.Line do
  @moduledoc """
  Formats the line of a table.
  """

  use PersistConfig

  alias IO.ANSI.Plus, as: ANSI
  alias IO.ANSI.Table.{Column, LineType, Spec, Style}

  @type elem :: String.t()
  @typep item :: String.t()
  @typep delimiter :: any

  @ansi_enabled get_env(:ansi_enabled, true)

  @doc """
  Deploys `elements` by interlacing them with `filler` and `borders`
  (left, inner and right).

  ## Examples

      iex> alias IO.ANSI.Table.Line
      iex> elements = ["Number", "Created at", "Title"]
      iex> borders = ["+-", "-+-", "-+"]
      iex> Line.items(elements, borders)
      [
        "", "+-"        , "", # filler, left border, filler
        "", "Number"    , "", # filler, element, filler
        "", "-+-"       , "", # filler, inner border, filler
        "", "Created at", "", # filler, element, filler
        "", "-+-"       , "", # filler, inner border, filler
        "", "Title"     , "", # filler, element, filler
        "", "-+"        , ""  # filler, right border, filler
      ]
  """
  @spec items([elem], [Style.border()], String.t) :: [item]
  def items(elems, [left, inner, right] = _borders, filler \\ "") do
    deploy(elems, delimiters(left, inner, right, filler))
  end

  @doc """
  Deploys the style attributes of a given line `type` and table `spec`.

  ## Examples

      iex> alias IO.ANSI.Table.Line
      iex> # We use a map instead of a %Spec{} for conciseness.
      iex> spec = %{style: :medium, sort_attrs: [nil, :asc, nil]}
      iex> type = :header
      iex> Line.item_attrs(type, spec)
      [
        :normal, :gold                , :normal, # left border
        :normal, :canary              , :normal, # non key column
        :normal, :gold                , :normal, # inner border
        :normal, [:canary, :underline], :normal, # key column
        :normal, :gold                , :normal, # inner border
        :normal, :canary              , :normal, # non key column
        :normal, :gold                , :normal  # right border
      ]
  """
  @spec item_attrs(LineType.t(), Spec.t()) :: [Style.attr()]
  def item_attrs(type, spec) do
    # Wrap attributes in braces to prevent flattening...
    border_attr = {Style.border_attr(spec.style, type)}
    filler_attr = {Style.filler_attr(spec.style, type)}
    key_attr = {Style.key_attr(spec.style, type)}
    non_key_attr = {Style.non_key_attr(spec.style, type)}

    spec.sort_attrs
    |> Enum.map(&if &1 in [:asc, :desc], do: key_attr, else: non_key_attr)
    |> deploy(delimiters(border_attr, filler_attr))
    |> Enum.map(fn {attr} -> attr end) # unwrap attributes
  end

  @doc """
  Deploys the widths of `elements` for a given line `type` and table `spec`.

  ## Examples

      iex> alias IO.ANSI.Table.Line
      iex> # We use a map instead of a %Spec{} for conciseness.
      iex> spec = %{
      ...>   style: :medium,
      ...>   align_attrs: [:right, :center, nil],
      ...>   column_widths: [7, 13, 9]
      ...> }
      iex> dashes = ["═══════", "═════════════", "═════════"]
      iex> elems = ["Number", "Created at", "Title"]
      iex> {Line.item_widths(dashes, :top, spec),
      ...>  Line.item_widths(elems, :header, spec)}
      {[0, 2, 0,  0, 7, 0,  0, 3, 0,  0, 13, 0,  0, 3, 0,  0, 9, 0,  0, 2, 0],
       [0, 1, 1,  1, 6, 0,  1, 1, 1,  1, 10, 2,  1, 1, 1,  0, 5, 4,  1, 1, 0]}
  """
  @spec item_widths([elem], LineType.t(), Spec.t()) :: [Column.width()]
  def item_widths(elems, type, spec) do
    Enum.zip([spec.column_widths, elems, spec.align_attrs])
    |> Enum.map(fn {width, elem, attr} -> Column.spread(width, elem, attr) end)
    |> deploy(Style.border_spreads(spec.style, type))
  end

  @doc ~S"""
  Returns an Erlang io format reflecting `item widths` and `item attributes`.
  It consists of a string with embedded
  [ANSI codes](https://en.wikipedia.org/wiki/ANSI_escape_code)
  (escape sequences), if emitting ANSI codes is enabled.

  Here are a few ANSI codes:

    - light yellow - \\e[93m
    - light cyan   - \\e[96m
    - reset        - \\e[0m

  ## Examples

      iex> alias IO.ANSI.Table.Line
      iex> item_widths = [2, 0, 6]
      iex> item_attrs = [:light_yellow, :normal, :light_cyan]
      iex> Line.format(item_widths, item_attrs, true)
      "\e[93m~-2ts\e[0m~-0ts\e[96m~-6ts\e[0m~n"
  """
  @spec format([Column.width()], [Style.attr()], boolean) :: String.t()
  def format(item_widths, item_attrs, ansi_enabled? \\ @ansi_enabled) do
    ansidata_list =
      Enum.zip(item_widths, item_attrs)
      |> Enum.map(fn
        {width, :normal} -> "~-#{width}ts" # t for Unicode translation
        {width, attr} -> ANSI.format([attr, "~-#{width}ts"], ansi_enabled?)
      end)

    "#{ansidata_list}~n" # => string embedded with ANSI escape sequences
  end

  ## Private functions

  # @doc """
  # Deploys `elements` by interlacing them with `delimiters`
  # (left, inner and right).

  # The inner `delimiter` is inserted between all `elements` and
  # the result is then surrounded by the left and right `delimiters`.

  # Returns a flattened list in case any `element` or `delimiter` is a list.

  # ## Examples

  #     iex> alias IO.ANSI.Table.Line
  #     iex> elems = ["Number", "Created at", "Title"]
  #     iex> delimiters = ["<", "=", ">"]
  #     iex> Line.deploy(elems, delimiters)
  #     ["<", "Number", "=", "Created at", "=", "Title", ">"]

  #     iex> alias IO.ANSI.Table.Line
  #     iex> elems = [[1, 6, 0], [1, 10, 2], [0, 5, 4]]
  #     iex> delimiters = [[0, 1, 1], [1, 1, 1], [1, 1, 0]]
  #     iex> Line.deploy(elems, delimiters)
  #     [0, 1, 1,  1, 6, 0,  1, 1, 1,  1, 10, 2,  1, 1, 1,  0, 5, 4,  1, 1, 0]

  #     iex> alias IO.ANSI.Table.Line
  #     iex> elems = ["Number", "Created at", "Title"]
  #     iex> delimiters = [
  #     ...>   [    ["", "+-" , ""], ""],
  #     ...>   ["", ["", "-+-", ""], ""],
  #     ...>   ["", ["", "-+" , ""]    ]
  #     ...> ]
  #     iex> Line.deploy(elems, delimiters)
  #     [
  #       "", "+-"        , "",
  #       "", "Number"    , "",
  #       "", "-+-"       , "",
  #       "", "Created at", "",
  #       "", "-+-"       , "",
  #       "", "Title"     , "",
  #       "", "-+"        , ""
  #     ]

  #     iex> alias IO.ANSI.Table.Line
  #     iex> elems = [{:canary}, {[:canary, :underline]}, {:canary}]
  #     iex> delimiters = [
  #     ...>   [           [{:normal}, {:gold}, {:normal}], {:normal}],
  #     ...>   [{:normal}, [{:normal}, {:gold}, {:normal}], {:normal}],
  #     ...>   [{:normal}, [{:normal}, {:gold}, {:normal}]           ]
  #     ...> ]
  #     iex> Line.deploy(elems, delimiters)
  #     [
  #       {:normal}, {:gold}                , {:normal},
  #       {:normal}, {:canary}              , {:normal},
  #       {:normal}, {:gold}                , {:normal},
  #       {:normal}, {[:canary, :underline]}, {:normal},
  #       {:normal}, {:gold}                , {:normal},
  #       {:normal}, {:canary}              , {:normal},
  #       {:normal}, {:gold}                , {:normal}
  #     ]
  # """
  @spec deploy([any], [delimiter]) :: [any]
  defp deploy(elems, [left, inner, right] = _delimiters) do
    [left] ++ Enum.intersperse(elems, inner) ++ [right] |> List.flatten()
  end

  # @doc """
  # Returns a list of `delimiters` for deployment.

  # ## Examples

  #     iex> alias IO.ANSI.Table.Line
  #     iex> Line.delimiters(:gold, :normal)
  #     [
  #       [         [:normal, :gold, :normal], :normal],
  #       [:normal, [:normal, :gold, :normal], :normal],
  #       [:normal, [:normal, :gold, :normal]         ]
  #     ]
  # """
  @spec delimiters(any, any) :: [any]
  defp delimiters(uniq, filler), do: delimiters(uniq, uniq, uniq, filler)

  # @doc """
  # Returns a list of `delimiters` (left, inner and right) for deployment.

  # ## Examples

  #     iex> alias IO.ANSI.Table.Line
  #     iex> Line.delimiters("+-", "-+-", "-+", "")
  #     [
  #       [    ["", "+-" , ""], ""],
  #       ["", ["", "-+-", ""], ""],
  #       ["", ["", "-+" , ""]    ]
  #     ]
  # """
  @spec delimiters(any, any, any, any) :: [any]
  defp delimiters(left, inner, right, filler) do
    [
      [        [filler, left,  filler], filler], # left delimiter
      [filler, [filler, inner, filler], filler], # inner delimiter
      [filler, [filler, right, filler]        ]  # right delimiter
    ]
  end
end