defmodule Util.Ext.IO do

  @moduledoc false
  # An IO module with support for debugging

  @doc """
  `dins` = 'Debug INSpect'

  Has the same interface as `IO.inspect` with additional options.

  - color - one of `:red`, `:green`, `:blue`, `:yellow`, `:cyan`, `:magenta`, `:white`

  - label - just like the label option in Elixir 1.4 - useful within pipes

  - length: 0 - print the label but not the item

  - display: `fn(item) -> "OUTPUT" end` custom display function

  - test: :show - show output in `:test` mode

  Note: by default, output is generated in `:dev` and `:prod` but not `:test`.
  """
  def dins(item, opts \\ []) do
    dins :stdio, item, opts
  end

  @doc """
  Inspect `item` according to the given options using the IO `device`.
  """
  def dins(device, item, opts) when is_list(opts) do
    label    = if lbl = opts[:label], do: [Util.Ext.String.to_chardata(lbl), ": "], else: []
    alt_opts = struct(Inspect.Opts, opts)
    new_item = display(item, opts)
    chardata = Inspect.Algebra.format(Inspect.Algebra.to_doc(new_item, alt_opts), alt_opts.width)
    [label, chardata, reset] = modify([label, chardata], opts)
    unless label == "" && chardata == "", do: IO.puts(device, [label, chardata, reset])
    item 
  end

  @doc """
  `tins` = 'Test INSpect'
  """
  def tins(item, opts \\ []) do
    dins :stdio, item, opts ++ [test: :show]
  end

  @doc """
  alias for dins
  """
  def inspect(item, opts \\ []) do
    dins :stdio, item, opts
  end

  @doc """
  Generate output in `:dev` and `:prod` modes, but not in `:test`.

  If you want output in test mode, fall back to `IO.puts`.
  """
  def puts(string) do
    case Mix.env do
      :test -> :OK
      _     -> IO.puts string
    end
  end

  @doc """
  Returns the element type.
  """
  def type(item) when is_atom(item)      , do: :atom
  def type(item) when is_binary(item)    , do: :binary
  def type(item) when is_bitstring(item) , do: :bitstring
  def type(item) when is_boolean(item)   , do: :boolean
  def type(item) when is_float(item)     , do: :float
  def type(item) when is_function(item)  , do: :function
  def type(item) when is_integer(item)   , do: :integer
  def type(item) when is_list(item)      , do: :list
  def type(item) when is_map(item)       , do: :map
  def type(item) when is_pid(item)       , do: :pid
  def type(item) when is_port(item)      , do: :port
  def type(item) when is_reference(item) , do: :reference
  def type(item) when is_tuple(item)     , do: :tuple
  def type(_ele) when true               , do: :unknown

  # ----------------------------------------------------------------

  # support for custom display function
  defp display(item, opts) do
    case opts[:display] do
      nil                       -> item
      fun when is_function(fun) -> fun.(item)
      _                         -> item
    end
  end

  # pipeline for the IO.inspect extensions
  defp modify([label, chardata], options) do
    {[label, chardata], options}
    |> lengthize
    |> colorize
    |> envize
    |> package_for_output
  end

  # package for printing by IO.puts - add ANSI reset
  defp package_for_output({[label, chardata], _options}) do
    [label, chardata, IO.ANSI.reset]
  end

  # don't print the item if the :length option is zero
  defp lengthize({[label, chardata], options}) do
    case options[:length] == 0 do
      true   -> {[label, ""], options}
      _      -> {[label, chardata], options}
    end
  end

  # don't print anything in test mode by default - can be overridden using `test: :show`
  defp envize({[label, chardata], options}) do
    default = [dev: :show, prod: :show, test: :hide, eval: :show]
    modlist = Enum.reduce options, default, fn({k, v}, acc) ->
      if Enum.member?([:test, :prod, :dev], k), do: Keyword.merge(acc, [{k, Util.Ext.String.to_atom(v)}]), else: acc end
    case Keyword.get(modlist, Mix.env) do
      :hide  -> {[""   , ""      ], options}
      _      -> {[label, chardata], options}
    end
  end

  # if necessary, add ANSI color codes
  defp colorize({[label, chardata], options}) do 
    case options[:color] do
      nil   -> {[label, chardata], options}
      color -> {colorcode([label, chardata], String.downcase(color)), options}
    end
  end

  # return strings with ANSI color code
  defp colorcode([label, chardata], color) do
    unless valid_color?(color), do: raise("Unrecognized color #{color}")
    acolor = Util.Ext.String.to_atom(color)
    ansi   = ansicode(acolor)
    [Enum.join([ansi, label]), Enum.join([ansi, chardata])]
  end

  # lookup ANSI color code
  defp ansicode(color) do
    case color do
      :red     -> IO.ANSI.red
      :green   -> IO.ANSI.green
      :blue    -> IO.ANSI.blue
      :yellow  -> IO.ANSI.yellow
      :cyan    -> IO.ANSI.cyan
      :magenta -> IO.ANSI.magenta
      :white   -> IO.ANSI.white
    end
  end

  # list of valid color names
  defp valid_colors do
    words = ~w(red green blue yellow cyan magenta white)
    atoms = Enum.map(words, fn(x) -> Util.Ext.String.to_atom(x) end)
    words ++ atoms
  end

  defp valid_color?(color), do: Enum.member?(valid_colors(), color)
end
