lib/io/ansi/table/line.ex

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

  use PersistConfig

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

  @typedoc "Line element"
  @type elem :: String.t()
  @typedoc "Line item"
  @type item :: String.t()

  @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, borders, filler \\ "") do
    deploy(elems, delimiters(borders, 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 I/O format (see `:io.format/2`) reflecting `item widths` and
  `item attributes`.

  It consists of a string with control sequences and 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> column_spreads = [[1, 6, 0], [1, 10, 2], [0, 5, 4]]

  #     iex> border_spreads = [[0, 1, 1], [1, 1, 1], [1, 1, 0]]

  #     iex> Line.deploy(column_spreads, border_spreads)

  #     [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> border_delimiters = [

  #     ...>   [    ["", "+-" , ""], ""],

  #     ...>   ["", ["", "-+-", ""], ""],

  #     ...>   ["", ["", "-+" , ""]    ]

  #     ...> ]

  #     iex> Line.deploy(elems, border_delimiters)

  #     [

  #       "", "+-"        , "",

  #       "", "Number"    , "",

  #       "", "-+-"       , "",

  #       "", "Created at", "",

  #       "", "-+-"       , "",

  #       "", "Title"     , "",

  #       "", "-+"        , ""

  #     ]


  #     iex> alias IO.ANSI.Table.Line

  #     iex> border_attrs = [{:canary}, {[:canary, :underline]}, {:canary}]

  #     iex> attr_delimiters = [

  #     ...>   [           [{:normal}, {:gold}, {:normal}], {:normal}],

  #     ...>   [{:normal}, [{:normal}, {:gold}, {:normal}], {:normal}],

  #     ...>   [{:normal}, [{:normal}, {:gold}, {:normal}]           ]

  #     ...> ]

  #     iex> Line.deploy(border_attrs, attr_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], [any]) :: [any]
  defp deploy(elems, [left, inner, right] = _delimiters) do
    [left] ++ Enum.intersperse(elems, inner) ++ [right] |> List.flatten()
  end

  # @doc """

  # Returns a list of "delimiters" (left, inner and right).


  # ## Examples


  #     iex> alias IO.ANSI.Table.Line

  #     iex> Line.delimiters(["+-", "-+-", "-+"], "")

  #     [

  #       [    ["", "+-" , ""], ""],

  #       ["", ["", "-+-", ""], ""],

  #       ["", ["", "-+" , ""]    ]

  #     ]


  #     iex> alias IO.ANSI.Table.Line

  #     iex> Line.delimiters(["╔═", "═╦═", "═╗"], "~")

  #     [

  #       [     ["~", "╔═" , "~"], "~"],

  #       ["~", ["~", "═╦═", "~"], "~"],

  #       ["~", ["~", "═╗" , "~"]     ]

  #     ]


  #     iex> alias IO.ANSI.Table.Line

  #     iex> Line.delimiters(:gold, :normal)

  #     [

  #       [         [:normal, :gold, :normal], :normal],

  #       [:normal, [:normal, :gold, :normal], :normal],

  #       [:normal, [:normal, :gold, :normal]         ]

  #     ]


  #     iex> alias IO.ANSI.Table.Line

  #     iex> Line.delimiters({[:gold, :underline]}, {:normal})

  #     [

  #       [           [{:normal}, {[:gold, :underline]}, {:normal}], {:normal}],

  #       [{:normal}, [{:normal}, {[:gold, :underline]}, {:normal}], {:normal}],

  #       [{:normal}, [{:normal}, {[:gold, :underline]}, {:normal}]           ]

  #     ]


  #     iex> alias IO.ANSI.Table.Line

  #     iex> Line.delimiters('what', %{})

  #     [

  #       [     [%{}, 'what', %{}], %{}],

  #       [%{}, [%{}, 'what', %{}], %{}],

  #       [%{}, [%{}, 'what', %{}]     ]

  #     ]

  # """

  @spec delimiters(any, any) :: [any]
  defp delimiters([left, inner, right] = _borders, filler) do
    [
      [        [filler, left,  filler], filler], # left delimiter

      [filler, [filler, inner, filler], filler], # inner delimiter

      [filler, [filler, right, filler]        ]  # right delimiter

    ]
  end

  defp delimiters(border_attr, filler_attr) do
    delimiters([border_attr, border_attr, border_attr], filler_attr)
  end
end