Run the code formatter on Inspect.Algebra

defmodule Inspect.Opts do
  @moduledoc """
  Defines the Inspect.Opts used by the Inspect protocol.

  The following fields are available:

    * `:structs` - when `false`, structs are not formatted by the inspect
      protocol, they are instead printed as maps, defaults to `true`.

    * `:binaries` - when `:as_strings` all binaries will be printed as strings,
      non-printable bytes will be escaped.

      When `:as_binaries` all binaries will be printed in bit syntax.

      When the default `:infer`, the binary will be printed as a string if it
      is printable, otherwise in bit syntax.

    * `:charlists` - when `:as_charlists` all lists will be printed as char
      lists, non-printable elements will be escaped.

      When `:as_lists` all lists will be printed as lists.

      When the default `:infer`, the list will be printed as a charlist if it
      is printable, otherwise as list.

    * `:limit` - limits the number of items that are printed for tuples,
      bitstrings, maps, lists and any other collection of items. It does not
      apply to strings nor charlists and defaults to 50.

    * `:printable_limit` - limits the number of bytes that are printed for strings
      and char lists. Defaults to 4096.

    * `:pretty` - if set to `true` enables pretty printing, defaults to `false`.

    * `:width` - defaults to 80 characters, used when pretty is `true` or when
      printing to IO devices. Set to 0 to force each item to be printed on its
      own line.

    * `:base` - prints integers as `:binary`, `:octal`, `:decimal`, or `:hex`,
      defaults to `:decimal`. When inspecting binaries any `:base` other than
      `:decimal` implies `binaries: :as_binaries`.

    * `:safe` - when `false`, failures while inspecting structs will be raised
      as errors instead of being wrapped in the `Inspect.Error` exception. This
      is useful when debugging failures and crashes for custom inspect
      implementations

    * `:syntax_colors` - when set to a keyword list of colors the output will
      be colorized. The keys are types and the values are the colors to use for
      each type (for example, `[number: :red, atom: :blue]`). Types can include
      `:number`, `:atom`, `regex`, `:tuple`, `:map`, `:list`, and `:reset`.
      Colors can be any `t:IO.ANSI.ansidata/0` as accepted by `IO.ANSI.format/1`.

  """

  # TODO: Remove :char_lists key by 2.0
  defstruct structs: true,
            binaries: :infer,
            charlists: :infer,
            char_lists: :infer,
            limit: 50,
            printable_limit: 4096,
            width: 80,
            base: :decimal,
            pretty: false,
            safe: true,
            syntax_colors: []

  @type color_key :: atom

  # TODO: Remove :char_lists key and :as_char_lists value by 2.0
  @type t :: %__MODULE__{
          structs: boolean,
          binaries: :infer | :as_binaries | :as_strings,
          charlists: :infer | :as_lists | :as_charlists,
          char_lists: :infer | :as_lists | :as_char_lists,
          limit: pos_integer | :infinity,
          printable_limit: pos_integer | :infinity,
          width: pos_integer | :infinity,
          base: :decimal | :binary | :hex | :octal,
          pretty: boolean,
          safe: boolean,
          syntax_colors: [{color_key, IO.ANSI.ansidata()}]
        }
end

defmodule Inspect.Error do
  @moduledoc """
  Raised when a struct cannot be inspected.
  """
  defexception [:message]
end

defmodule Inspect.Algebra do
  @moduledoc ~S"""
  A set of functions for creating and manipulating algebra
  documents.

  This module implements the functionality described in
  ["Strictly Pretty" (2000) by Christian Lindig][0] with small
  additions, like support for binary nodes and a break mode that
  maximises use of horizontal space.

      iex> Inspect.Algebra.empty
      :doc_nil

      iex> "foo"
      "foo"

  With the functions in this module, we can concatenate different
  elements together and render them:

      iex> doc = Inspect.Algebra.concat(Inspect.Algebra.empty, "foo")
      iex> Inspect.Algebra.format(doc, 80)
      ["foo"]

  The functions `nest/2`, `space/2` and `line/2` help you put the
  document together into a rigid structure. However, the document
  algebra gets interesting when using functions like `glue/3` and
  `group/1`. A glue inserts a break between two documents. A group
  indicates a document that must fit the current line, otherwise
  breaks are rendered as new lines. Let's glue two docs together
  with a break, group it and then render it:

      iex> doc = Inspect.Algebra.glue("a", " ", "b")
      iex> doc = Inspect.Algebra.group(doc)
      iex> Inspect.Algebra.format(doc, 80)
      ["a", " ", "b"]

  Notice the break was represented as is, because we haven't reached
  a line limit. Once we do, it is replaced by a newline:

      iex> doc = Inspect.Algebra.glue(String.duplicate("a", 20), " ", "b")
      iex> doc = Inspect.Algebra.group(doc)
      iex> Inspect.Algebra.format(doc, 10)
      ["aaaaaaaaaaaaaaaaaaaa", "\n", "b"]

  This module uses the byte size to compute how much space there is
  left. If your document contains strings, then those need to be
  wrapped in `string/1`, which then relies on `String.length/1` to
  precompute the document size.

  Finally, this module also contains Elixir related functions, a bit
  tied to Elixir formatting, such as `to_doc/2`.

  ## Implementation details

  The original Haskell implementation of the algorithm by [Wadler][1]
  relies on lazy evaluation to unfold document groups on two alternatives:
  `:flat` (breaks as spaces) and `:break` (breaks as newlines).
  Implementing the same logic in a strict language such as Elixir leads
  to an exponential growth of possible documents, unless groups are explicitly
  encoded. Those groups are then reduced to a simple document, where the
  layout is already decided, per [Lindig][0].

  This implementation has two types of breaks: `:strict` and `:flex`. When
  a group does not fit, all strict breaks are treated as breaks. The flex
  breaks however are re-evaluated and may still be rendered as spaces.

  This implementation also adds `force_break/1` and `next_break_fits/2` which
  give more control over the document fitting.

  Custom pretty printers can be implemented using the documents returned
  by this module and by providing their own rendering functions.

    [0]: http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.34.2200
    [1]: http://homepages.inf.ed.ac.uk/wadler/papers/prettier/prettier.pdf

  """

  @container_separator ","
  @tail_separator " |"
  @newline "\n"
  @break :flex
  @next_break_fits :enabled

  # Functional interface to "doc" records

  @type t ::
          binary
          | :doc_nil
          | :doc_line
          | doc_string
          | doc_cons
          | doc_nest
          | doc_break
          | doc_group
          | doc_color
          | doc_force
          | doc_fits
          | doc_collapse

  @typep doc_string :: {:doc_string, t, non_neg_integer}
  defmacrop doc_string(string, length) do
    quote(do: {:doc_string, unquote(string), unquote(length)})
  end

  @typep doc_cons :: {:doc_cons, t, t}
  defmacrop doc_cons(left, right) do
    quote(do: {:doc_cons, unquote(left), unquote(right)})
  end

  @typep doc_nest :: {:doc_nest, t, :cursor | :reset | non_neg_integer, :always | :break}
  defmacrop doc_nest(doc, indent, always_or_break) do
    quote(do: {:doc_nest, unquote(doc), unquote(indent), unquote(always_or_break)})
  end

  @typep doc_break :: {:doc_break, binary, :flex | :strict}
  defmacrop doc_break(break, mode) do
    quote(do: {:doc_break, unquote(break), unquote(mode)})
  end

  @typep doc_group :: {:doc_group, t, :inherit | :self}
  defmacrop doc_group(group, mode) do
    quote(do: {:doc_group, unquote(group), unquote(mode)})
  end

  @typep doc_fits :: {:doc_fits, t, :enabled | :disabled}
  defmacrop doc_fits(group, mode) do
    quote(do: {:doc_fits, unquote(group), unquote(mode)})
  end

  @typep doc_force :: {:doc_force, t}
  defmacrop doc_force(group) do
    quote(do: {:doc_force, unquote(group)})
  end

  @typep doc_collapse :: {:doc_collapse, pos_integer()}
  defmacrop doc_collapse(count) do
    quote(do: {:doc_collapse, unquote(count)})
  end

  @typep doc_color :: {:doc_color, t, IO.ANSI.ansidata()}
  defmacrop doc_color(doc, color) do
    quote(do: {:doc_color, unquote(doc), unquote(color)})
  end

  defmacrop is_doc(doc) do
    if Macro.Env.in_guard?(__CALLER__) do
      do_is_doc(doc)
    else
      var = quote(do: doc)

      quote do
        unquote(var) = unquote(doc)
        unquote(do_is_doc(var))
      end
    end
  end

  @docs [
    :doc_string,
    :doc_cons,
    :doc_nest,
    :doc_break,
    :doc_group,
    :doc_color,
    :doc_force,
    :doc_fits,
    :doc_collapse
  ]

  defp do_is_doc(doc) do
    quote do
      is_binary(unquote(doc)) or unquote(doc) in [:doc_nil, :doc_line] or
        (is_tuple(unquote(doc)) and elem(unquote(doc), 0) in unquote(@docs))
    end
  end

  # Elixir + Inspect.Opts conveniences

  @doc """
  Converts an Elixir term to an algebra document
  according to the `Inspect` protocol.
  """
  @spec to_doc(any, Inspect.Opts.t()) :: t
  def to_doc(term, opts)

  def to_doc(%_{} = struct, %Inspect.Opts{} = opts) do
    if opts.structs do
      try do
        Inspect.inspect(struct, opts)
      rescue
        caught_exception ->
          stacktrace = System.stacktrace()

          # Because we try to raise a nice error message in case
          # we can't inspect a struct, there is a chance the error
          # message itself relies on the struct being printed, so
          # we need to trap the inspected messages to guarantee
          # we won't try to render any failed instruct when building
          # the error message.
          if Process.get(:inspect_trap) do
            Inspect.Map.inspect(struct, opts)
          else
            try do
              Process.put(:inspect_trap, true)

              res = Inspect.Map.inspect(struct, %{opts | syntax_colors: []})
              res = IO.iodata_to_binary(format(res, :infinity))

              message =
                "got #{inspect(caught_exception.__struct__)} with message " <>
                  "#{inspect(Exception.message(caught_exception))} while inspecting #{res}"

              exception = Inspect.Error.exception(message: message)

              if opts.safe do
                Inspect.inspect(exception, opts)
              else
                reraise(exception, stacktrace)
              end
            after
              Process.delete(:inspect_trap)
            end
          end
      end
    else
      Inspect.Map.inspect(struct, opts)
    end
  end

  def to_doc(arg, %Inspect.Opts{} = opts) do
    Inspect.inspect(arg, opts)
  end

  @doc ~S"""
  Wraps `collection` in `left` and `right` according to limit and contents.

  It uses the given `left` and `right` documents as surrounding and the
  separator document `separator` to separate items in `docs`. If all entries
  in the collection are simple documents (texts or strings), then this function
  attempts to put as much as possible on the same line. If they are not simple,
  only one entry is shown per line if they do not fit.

  The limit in the given `Inspect.Opts` is respected and when reached this
  function stops processing and outputs `"..."` instead.

  ## Options

    * `:separator` - the separator used between each doc
    * `:break` - when set to `:strict`, always break between each element. Defaults to
      `:flex` which breaks only when necessary and if all elements are text-based

  ## Examples

      iex> doc = Inspect.Algebra.container_doc("[", Enum.to_list(1..5), "]",
      ...>         %Inspect.Opts{limit: :infinity}, fn i, _opts -> to_string(i) end)
      iex> Inspect.Algebra.format(doc, 5) |> IO.iodata_to_binary
      "[1,\n 2,\n 3,\n 4,\n 5]"

      iex> doc = Inspect.Algebra.container_doc("[", Enum.to_list(1..5), "]",
      ...>         %Inspect.Opts{limit: 3}, fn i, _opts -> to_string(i) end)
      iex> Inspect.Algebra.format(doc, 20) |> IO.iodata_to_binary
      "[1, 2, 3, ...]"

      iex> doc = Inspect.Algebra.container_doc("[", Enum.to_list(1..5), "]",
      ...>         %Inspect.Opts{limit: 3}, fn i, _opts -> to_string(i) end, separator: "!")
      iex> Inspect.Algebra.format(doc, 20) |> IO.iodata_to_binary
      "[1! 2! 3! ...]"

  """
  @spec container_doc(t, [any], t, Inspect.Opts.t(), (term, Inspect.Opts.t() -> t), keyword()) ::
          t
  def container_doc(left, collection, right, inspect, fun, opts \\ [])
      when is_doc(left) and is_list(collection) and is_doc(right) and is_function(fun, 2) and
             is_list(opts) do
    case collection do
      [] ->
        concat(left, right)

      _ ->
        separator = Keyword.get(opts, :separator, @container_separator)
        flex? = Keyword.get(opts, :break, @break) == :flex
        {docs, simple?} = container_each(collection, inspect.limit, inspect, fun, [], flex?)
        docs = fold_doc(docs, &join(&1, &2, simple?, separator))

        case simple? do
          true -> concat(concat(left, nest(docs, :cursor)), right)
          false -> group(glue(nest(glue(left, "", docs), 2), "", right))
        end
    end
  end

  defp container_each([], _limit, _opts, _fun, acc, simple?) do
    {:lists.reverse(acc), simple?}
  end

  defp container_each(_, 0, _opts, _fun, acc, simple?) do
    {:lists.reverse(["..." | acc]), simple?}
  end

  defp container_each([term | terms], limit, opts, fun, acc, simple?) when is_list(terms) do
    limit = decrement(limit)
    doc = fun.(term, %{opts | limit: limit})
    container_each(terms, limit, opts, fun, [doc | acc], simple? and simple?(doc))
  end

  defp container_each([left | right], limit, opts, fun, acc, simple?) do
    limit = decrement(limit)
    left = fun.(left, %{opts | limit: limit})
    right = fun.(right, %{opts | limit: limit})
    simple? = simple? and simple?(left) and simple?(right)

    doc = join(left, right, simple?, @tail_separator)
    {:lists.reverse([doc | acc]), simple?}
  end

  defp decrement(:infinity), do: :infinity
  defp decrement(counter), do: counter - 1

  defp join(:doc_nil, :doc_nil, _, _), do: :doc_nil
  defp join(left, :doc_nil, _, _), do: left
  defp join(:doc_nil, right, _, _), do: right
  defp join(left, right, true, sep), do: flex_glue(concat(left, sep), right)
  defp join(left, right, false, sep), do: glue(concat(left, sep), right)

  defp simple?(doc_cons(left, right)), do: simple?(left) and simple?(right)
  defp simple?(doc_color(doc, _)), do: simple?(doc)
  defp simple?(doc_string(_, _)), do: true
  defp simple?(:doc_nil), do: true
  defp simple?(other), do: is_binary(other)

  @doc false
  # TODO: Deprecate on Elixir v1.8
  def surround(left, doc, right) when is_doc(left) and is_doc(doc) and is_doc(right) do
    concat(concat(left, nest(doc, 1)), right)
  end

  @doc false
  # TODO: Deprecate on Elixir v1.8
  def surround_many(
        left,
        docs,
        right,
        %Inspect.Opts{} = inspect,
        fun,
        separator \\ @container_separator
      )
      when is_doc(left) and is_list(docs) and is_doc(right) and is_function(fun, 2) do
    container_doc(left, docs, right, inspect, fun, separator: separator)
  end

  # Algebra API

  @doc """
  Returns a document entity used to represent nothingness.

  ## Examples

      iex> Inspect.Algebra.empty
      :doc_nil

  """
  @spec empty() :: :doc_nil
  def empty, do: :doc_nil

  @doc ~S"""
  Creates a document represented by string.

  While `Inspect.Algebra` accepts binaries as documents,
  those are counted by binary size. On the other hand,
  `string` documents are measured in terms of graphemes
  towards the document size.

  ## Examples

  The following document has 10 bytes and therefore it
  does not format to width 9 without breaks:

      iex> doc = Inspect.Algebra.glue("olá", " ", "mundo")
      iex> doc = Inspect.Algebra.group(doc)
      iex> Inspect.Algebra.format(doc, 9)
      ["olá", "\n", "mundo"]

  However, if we use `string`, then the string length is
  used, instead of byte size, correctly fitting:

      iex> string = Inspect.Algebra.string("olá")
      iex> doc = Inspect.Algebra.glue(string, " ", "mundo")
      iex> doc = Inspect.Algebra.group(doc)
      iex> Inspect.Algebra.format(doc, 9)
      ["olá", " ", "mundo"]

  """
  @spec string(String.t()) :: doc_string
  def string(string) when is_binary(string) do
    doc_string(string, String.length(string))
  end

  @doc ~S"""
  Concatenates two document entities returning a new document.

  ## Examples

      iex> doc = Inspect.Algebra.concat("hello", "world")
      iex> Inspect.Algebra.format(doc, 80)
      ["hello", "world"]

  """
  @spec concat(t, t) :: t
  def concat(doc1, doc2) when is_doc(doc1) and is_doc(doc2) do
    doc_cons(doc1, doc2)
  end

  @doc ~S"""
  Concatenates a list of documents returning a new document.

  ## Examples

      iex> doc = Inspect.Algebra.concat(["a", "b", "c"])
      iex> Inspect.Algebra.format(doc, 80)
      ["a", "b", "c"]

  """
  @spec concat([t]) :: t
  def concat(docs) when is_list(docs) do
    fold_doc(docs, &concat(&1, &2))
  end

  @doc ~S"""
  Colors a document if the `color_key` has a color in the options.
  """
  @spec color(t, Inspect.Opts.color_key(), Inspect.Opts.t()) :: doc_color
  def color(doc, color_key, %Inspect.Opts{syntax_colors: syntax_colors}) when is_doc(doc) do
    if precolor = Keyword.get(syntax_colors, color_key) do
      postcolor = Keyword.get(syntax_colors, :reset, :reset)
      concat(doc_color(doc, precolor), doc_color(empty(), postcolor))
    else
      doc
    end
  end

  @doc ~S"""
  Nests the given document at the given `level`.

  If `level` is an integer, that's the indentation appended
  to line breaks whenever they occur. If the level is `:cursor`,
  the current position of the "cursor" in the document becomes
  the nesting. If the level is `:reset`, it is set back to 0.

  `mode` can be `:always`, which means nesting always happen,
  or `:break`, which means nesting only happens inside a group
  that has been broken.

  ## Examples

      iex> doc = Inspect.Algebra.nest(Inspect.Algebra.glue("hello", "world"), 5)
      iex> doc = Inspect.Algebra.group(doc)
      iex> Inspect.Algebra.format(doc, 5)
      ["hello", "\n     ", "world"]

  """
  @spec nest(t, non_neg_integer) :: doc_nest
  def nest(doc, level, mode \\ :always)

  def nest(doc, :cursor, mode) when is_doc(doc) and mode in [:always, :break] do
    doc_nest(doc, :cursor, mode)
  end

  def nest(doc, :reset, mode) when is_doc(doc) and mode in [:always, :break] do
    doc_nest(doc, :reset, mode)
  end

  def nest(doc, 0, _mode) when is_doc(doc) do
    doc
  end

  def nest(doc, level, mode)
      when is_doc(doc) and is_integer(level) and level > 0 and mode in [:always, :break] do
    doc_nest(doc, level, mode)
  end

  @doc ~S"""
  Returns a document entity representing a break based on the given
  `string`.

  This break can be rendered as a linebreak or as the given `string`,
  depending on the `mode` of the chosen layout.

  ## Examples

  Let's create a document by concatenating two strings with a break between
  them:

      iex> doc = Inspect.Algebra.concat(["a", Inspect.Algebra.break("\t"), "b"])
      iex> Inspect.Algebra.format(doc, 80)
      ["a", "\t", "b"]

  Notice the break was represented with the given string, because we didn't
  reach a line limit. Once we do, it is replaced by a newline:

      iex> break = Inspect.Algebra.break("\t")
      iex> doc = Inspect.Algebra.concat([String.duplicate("a", 20), break, "b"])
      iex> doc = Inspect.Algebra.group(doc)
      iex> Inspect.Algebra.format(doc, 10)
      ["aaaaaaaaaaaaaaaaaaaa", "\n", "b"]

  """
  @spec break(binary) :: doc_break
  def break(string \\ " ") when is_binary(string) do
    doc_break(string, :strict)
  end

  @doc """
  Collapse any new lines and whitespace following this
  node and emitting up to `max` new lines.
  """
  @spec collapse_lines(pos_integer) :: doc_collapse
  def collapse_lines(max) when is_integer(max) and max > 0 do
    doc_collapse(max)
  end

  @doc """
  Considers the next break as fit.

  `mode` can be `:enabled` or `:disabled`. When `:enabled`,
  it will consider the document as fit as soon as it finds
  the next break, effectively cancelling the break. It will
  also ignore any `force_break/1`.

  When disabled, it behaves as usual and it will ignore
  any further `next_break_fits/2` instruction.
  """
  @spec next_break_fits(t) :: doc_fits
  def next_break_fits(doc, mode \\ @next_break_fits) when is_doc(doc) do
    doc_fits(doc, mode)
  end

  @doc """
  Forces the document to break.
  """
  @spec force_break(t) :: doc_force
  def force_break(doc) when is_doc(doc) do
    doc_force(doc)
  end

  @doc """
  Introduces a flex break.

  A flex break still causes a group to break, like
  a regular break, but it is re-evaluated when the
  documented is processed.

  This function is used by `container_doc/4` and friends
  to the maximum number of entries on the same line.
  """
  @spec flex_break(binary) :: doc_break
  def flex_break(string \\ " ") when is_binary(string) do
    doc_break(string, :flex)
  end

  @doc """
  Glues two documents (`doc1` and `doc2`) inserting a
  `flex_break/1` given by `break_string` between them.

  This function is used by `container_doc/6` and friends
  to the maximum number of entries on the same line.
  """
  @spec flex_glue(t, binary, t) :: t
  def flex_glue(doc1, break_string \\ " ", doc2) when is_binary(break_string) do
    concat(doc1, concat(flex_break(break_string), doc2))
  end

  @doc ~S"""
  Glues two documents (`doc1` and `doc2`) inserting the given
  break `break_string` between them.

  For more information on how the break is inserted, see `break/1`.

  ## Examples

      iex> doc = Inspect.Algebra.glue("hello", "world")
      iex> Inspect.Algebra.format(doc, 80)
      ["hello", " ", "world"]

      iex> doc = Inspect.Algebra.glue("hello", "\t", "world")
      iex> Inspect.Algebra.format(doc, 80)
      ["hello", "\t", "world"]

  """
  @spec glue(t, binary, t) :: t
  def glue(doc1, break_string \\ " ", doc2) when is_binary(break_string) do
    concat(doc1, concat(break(break_string), doc2))
  end

  @doc ~S"""
  Returns a group containing the specified document `doc`.

  Documents in a group are attempted to be rendered together
  to the best of the renderer ability.

  The group mode can also be set to `:inherit`, which means it
  automatically breaks if the parent group has broken too.

  ## Examples

      iex> doc = Inspect.Algebra.group(
      ...>   Inspect.Algebra.concat(
      ...>     Inspect.Algebra.group(
      ...>       Inspect.Algebra.concat(
      ...>         "Hello,",
      ...>         Inspect.Algebra.concat(
      ...>           Inspect.Algebra.break,
      ...>           "A"
      ...>         )
      ...>       )
      ...>     ),
      ...>     Inspect.Algebra.concat(
      ...>       Inspect.Algebra.break,
      ...>       "B"
      ...>     )
      ...> ))
      iex> Inspect.Algebra.format(doc, 80)
      ["Hello,", " ", "A", " ", "B"]
      iex> Inspect.Algebra.format(doc, 6)
      ["Hello,", "\n", "A", "\n", "B"]

  """
  @spec group(t) :: doc_group
  def group(doc, mode \\ :self) when is_doc(doc) do
    doc_group(doc, mode)
  end

  @doc ~S"""
  Inserts a mandatory single space between two documents.

  ## Examples

      iex> doc = Inspect.Algebra.space("Hughes", "Wadler")
      iex> Inspect.Algebra.format(doc, 5)
      ["Hughes", " ", "Wadler"]

  """
  @spec space(t, t) :: t
  def space(doc1, doc2), do: concat(doc1, concat(" ", doc2))

  @doc ~S"""
  A mandatory linebreak.

  A group with linebreaks will fit if all lines in the group fit.

  ## Examples

    iex> doc = Inspect.Algebra.concat(
    ...>   Inspect.Algebra.concat(
    ...>     "Hughes",
    ...>     Inspect.Algebra.line()
    ...>   ), "Wadler"
    ...> )
    iex> Inspect.Algebra.format(doc, 80)
    ["Hughes", "\n", "Wadler"]

  """
  @spec line() :: t
  def line(), do: :doc_line

  @doc ~S"""
  Inserts a mandatory linebreak between two documents.

  See `line/1`.

  ## Examples

      iex> doc = Inspect.Algebra.line("Hughes", "Wadler")
      iex> Inspect.Algebra.format(doc, 80)
      ["Hughes", "\n", "Wadler"]

  """
  @spec line(t, t) :: t
  def line(doc1, doc2), do: concat(doc1, concat(line(), doc2))

  @doc ~S"""
  Folds a list of documents into a document using the given folder function.

  The list of documents is folded "from the right"; in that, this function is
  similar to `List.foldr/3`, except that it doesn't expect an initial
  accumulator and uses the last element of `docs` as the initial accumulator.

  ## Examples

      iex> docs = ["A", "B", "C"]
      iex> docs = Inspect.Algebra.fold_doc(docs, fn(doc, acc) ->
      ...>   Inspect.Algebra.concat([doc, "!", acc])
      ...> end)
      iex> Inspect.Algebra.format(docs, 80)
      ["A", "!", "B", "!", "C"]

  """
  @spec fold_doc([t], (t, t -> t)) :: t
  def fold_doc(docs, folder_fun)

  def fold_doc([], _folder_fun), do: empty()
  def fold_doc([doc], _folder_fun), do: doc

  def fold_doc([doc | docs], folder_fun) when is_function(folder_fun, 2),
    do: folder_fun.(doc, fold_doc(docs, folder_fun))

  @doc ~S"""
  Formats a given document for a given width.

  Takes the maximum width and a document to print as its arguments
  and returns an IO data representation of the best layout for the
  document to fit in the given width.

  The document starts flat (without breaks) until a group is found.

  ## Examples

      iex> doc = Inspect.Algebra.glue("hello", " ", "world")
      iex> doc = Inspect.Algebra.group(doc)
      iex> doc |> Inspect.Algebra.format(30) |> IO.iodata_to_binary()
      "hello world"
      iex> doc |> Inspect.Algebra.format(10) |> IO.iodata_to_binary()
      "hello\nworld"

  """
  @spec format(t, non_neg_integer | :infinity) :: iodata
  def format(doc, width) when is_doc(doc) and (width == :infinity or width >= 0) do
    format(width, 0, [{0, :flat, doc}])
  end

  # Type representing the document mode to be rendered
  #
  #   * break - represents a fitted document with breaks as breaks
  #   * flat - represents a fitted document with breaks as flats
  #   * next_fits - represents a document being fitted that will cancel (fit) the next break
  #   * no_fitting - represents a document being fitted that will not accept next break fits
  #
  @typep mode :: :break | :flat | :next_fits | :no_fitting

  @spec fits?(integer, integer, [{integer, mode, t}]) :: boolean
  defp fits?(w, k, _) when k > w, do: false
  defp fits?(_, _, []), do: true

  # No fitting

  defp fits?(w, k, [{i, _, doc_fits(x, :disabled)} | t]),
    do: fits?(w, k, [{i, :no_fitting, x} | t])

  defp fits?(w, k, [{i, :no_fitting, doc_group(x, _)} | t]),
    do: fits?(w, k, [{i, :no_fitting, x} | t])

  defp fits?(w, k, [{i, :no_fitting, doc_fits(x, _)} | t]),
    do: fits?(w, k, [{i, :no_fitting, x} | t])

  ## Next fits

  defp fits?(w, k, [{i, _, doc_fits(x, :enabled)} | t]), do: fits?(w, k, [{i, :next_fits, x} | t])
  defp fits?(w, k, [{i, :next_fits, doc_force(x)} | t]), do: fits?(w, k, [{i, :next_fits, x} | t])

  defp fits?(w, k, [{i, :next_fits, doc_group(x, _)} | t]),
    do: fits?(w, k, [{i, :next_fits, x} | t])

  defp fits?(_, _, [{_, :next_fits, doc_break(_, _)} | _]), do: true
  defp fits?(_, _, [{_, :next_fits, :doc_line} | _]), do: true

  ## Fitting rules

  defp fits?(w, k, [{_, _, :doc_nil} | t]), do: fits?(w, k, t)
  defp fits?(w, _, [{i, _, :doc_line} | t]), do: fits?(w, i, t)
  defp fits?(w, _, [{i, _, doc_collapse(_)} | t]), do: fits?(w, i, t)
  defp fits?(w, k, [{i, m, doc_cons(x, y)} | t]), do: fits?(w, k, [{i, m, x} | [{i, m, y} | t]])
  defp fits?(w, k, [{i, m, doc_color(x, _)} | t]), do: fits?(w, k, [{i, m, x} | t])
  defp fits?(w, k, [{i, m, doc_nest(x, _, :break)} | t]), do: fits?(w, k, [{i, m, x} | t])

  defp fits?(w, k, [{i, m, doc_nest(x, j, _)} | t]),
    do: fits?(w, k, [{apply_nesting(i, k, j), m, x} | t])

  defp fits?(w, k, [{i, _, doc_group(x, _)} | t]), do: fits?(w, k, [{i, :flat, x} | t])
  defp fits?(w, k, [{_, _, doc_string(_, l)} | t]), do: fits?(w, k + l, t)
  defp fits?(w, k, [{_, _, s} | t]) when is_binary(s), do: fits?(w, k + byte_size(s), t)
  defp fits?(_, _, [{_, _, doc_force(_)} | _]), do: false
  defp fits?(_, _, [{_, :break, doc_break(_, _)} | _]), do: true
  defp fits?(w, k, [{_, _, doc_break(s, _)} | t]), do: fits?(w, k + byte_size(s), t)

  @spec format(integer | :infinity, integer, [{integer, mode, t}]) :: [binary]
  defp format(_, _, []), do: []
  defp format(w, k, [{_, _, :doc_nil} | t]), do: format(w, k, t)
  defp format(w, _, [{i, _, :doc_line} | t]), do: [indent(i) | format(w, i, t)]
  defp format(w, k, [{i, m, doc_cons(x, y)} | t]), do: format(w, k, [{i, m, x} | [{i, m, y} | t]])
  defp format(w, k, [{i, m, doc_color(x, c)} | t]), do: [ansi(c) | format(w, k, [{i, m, x} | t])]
  defp format(w, k, [{_, _, doc_string(s, l)} | t]), do: [s | format(w, k + l, t)]
  defp format(w, k, [{_, _, s} | t]) when is_binary(s), do: [s | format(w, k + byte_size(s), t)]
  defp format(w, k, [{i, m, doc_force(x)} | t]), do: format(w, k, [{i, m, x} | t])
  defp format(w, k, [{i, m, doc_fits(x, _)} | t]), do: format(w, k, [{i, m, x} | t])
  defp format(w, _, [{i, _, doc_collapse(max)} | t]), do: collapse(format(w, i, t), max, 0, i)

  # Flex breaks are not conditional to the mode
  defp format(w, k, [{i, _, doc_break(s, :flex)} | t]) do
    k = k + byte_size(s)

    if w == :infinity or fits?(w, k, t) do
      [s | format(w, k, t)]
    else
      [indent(i) | format(w, i, t)]
    end
  end

  # Strict breaks are conditional to the mode
  defp format(w, k, [{i, mode, doc_break(s, :strict)} | t]) do
    if mode == :break do
      [indent(i) | format(w, i, t)]
    else
      [s | format(w, k + byte_size(s), t)]
    end
  end

  # Nesting is conditional to the mode.
  defp format(w, k, [{i, mode, doc_nest(x, j, nest)} | t]) do
    if nest == :always or (nest == :break and mode == :break) do
      format(w, k, [{apply_nesting(i, k, j), mode, x} | t])
    else
      format(w, k, [{i, mode, x} | t])
    end
  end

  # Groups must do the fitting decision.
  defp format(w, k, [{i, :break, doc_group(x, :inherit)} | t]) do
    format(w, k, [{i, :break, x} | t])
  end

  defp format(w, k, [{i, _, doc_group(x, _)} | t]) do
    if w == :infinity or fits?(w, k, [{i, :flat, x}]) do
      format(w, k, [{i, :flat, x} | t])
    else
      format(w, k, [{i, :break, x} | t])
    end
  end

  defp collapse(["\n" <> _ | t], max, count, i) do
    collapse(t, max, count + 1, i)
  end

  defp collapse(["" | t], max, count, i) do
    collapse(t, max, count, i)
  end

  defp collapse(t, max, count, i) do
    [:binary.copy("\n", min(max, count)) <> :binary.copy(" ", i) | t]
  end

  defp apply_nesting(_, k, :cursor), do: k
  defp apply_nesting(_, _, :reset), do: 0
  defp apply_nesting(i, _, j), do: i + j

  defp ansi(color) do
    IO.ANSI.format_fragment(color, true)
  end

  defp indent(0), do: @newline
  defp indent(i), do: @newline <> :binary.copy(" ", i)
end

defmodule Inspect.Opts do
  @moduledoc """
  Defines the Inspect.Opts used by the Inspect protocol.

  The following fields are available:

    * `:structs` - when `false`, structs are not formatted by the inspect
      protocol, they are instead printed as maps, defaults to `true`.

    * `:binaries` - when `:as_strings` all binaries will be printed as strings,
      non-printable bytes will be escaped.

      When `:as_binaries` all binaries will be printed in bit syntax.

      When the default `:infer`, the binary will be printed as a string if it
      is printable, otherwise in bit syntax.

    * `:charlists` - when `:as_charlists` all lists will be printed as char
      lists, non-printable elements will be escaped.

      When `:as_lists` all lists will be printed as lists.

      When the default `:infer`, the list will be printed as a charlist if it
      is printable, otherwise as list.

    * `:limit` - limits the number of items that are printed for tuples,
      bitstrings, maps, lists and any other collection of items. It does not
      apply to strings nor charlists and defaults to 50.

    * `:printable_limit` - limits the number of bytes that are printed for strings
      and char lists. Defaults to 4096.

    * `:pretty` - if set to `true` enables pretty printing, defaults to `false`.

    * `:width` - defaults to 80 characters, used when pretty is `true` or when
      printing to IO devices. Set to 0 to force each item to be printed on its
      own line.

    * `:base` - prints integers as `:binary`, `:octal`, `:decimal`, or `:hex`,
      defaults to `:decimal`. When inspecting binaries any `:base` other than
      `:decimal` implies `binaries: :as_binaries`.

    * `:safe` - when `false`, failures while inspecting structs will be raised
      as errors instead of being wrapped in the `Inspect.Error` exception. This
      is useful when debugging failures and crashes for custom inspect
      implementations

    * `:syntax_colors` - when set to a keyword list of colors the output will
      be colorized. The keys are types and the values are the colors to use for
      each type (for example, `[number: :red, atom: :blue]`). Types can include
      `:number`, `:atom`, `regex`, `:tuple`, `:map`, `:list`, and `:reset`.
      Colors can be any `t:IO.ANSI.ansidata/0` as accepted by `IO.ANSI.format/1`.

  """

  # TODO: Remove :char_lists key by 2.0
  defstruct structs: true,
            binaries: :infer,
            charlists: :infer,
            char_lists: :infer,
            limit: 50,
            printable_limit: 4096,
            width: 80,
            base: :decimal,
            pretty: false,
            safe: true,
            syntax_colors: []

  @type color_key :: atom

  # TODO: Remove :char_lists key and :as_char_lists value by 2.0
  @type t :: %__MODULE__{
          structs: boolean,
          binaries: :infer | :as_binaries | :as_strings,
          charlists: :infer | :as_lists | :as_charlists,
          char_lists: :infer | :as_lists | :as_char_lists,
          limit: pos_integer | :infinity,
          printable_limit: pos_integer | :infinity,
          width: pos_integer | :infinity,
          base: :decimal | :binary | :hex | :octal,
          pretty: boolean,
          safe: boolean,
          syntax_colors: [{color_key, IO.ANSI.ansidata()}]
        }
end

defmodule Inspect.Error do
  @moduledoc """
  Raised when a struct cannot be inspected.
  """
  defexception [:message]
end

defmodule Inspect.Algebra do
  @moduledoc ~S"""
  A set of functions for creating and manipulating algebra
  documents.

  This module implements the functionality described in
  ["Strictly Pretty" (2000) by Christian Lindig][0] with small
  additions, like support for binary nodes and a break mode that
  maximises use of horizontal space.

      iex> Inspect.Algebra.empty
      :doc_nil

      iex> "foo"
      "foo"

  With the functions in this module, we can concatenate different
  elements together and render them:

      iex> doc = Inspect.Algebra.concat(Inspect.Algebra.empty, "foo")
      iex> Inspect.Algebra.format(doc, 80)
      ["foo"]

  The functions `nest/2`, `space/2` and `line/2` help you put the
  document together into a rigid structure. However, the document
  algebra gets interesting when using functions like `glue/3` and
  `group/1`. A glue inserts a break between two documents. A group
  indicates a document that must fit the current line, otherwise
  breaks are rendered as new lines. Let's glue two docs together
  with a break, group it and then render it:

      iex> doc = Inspect.Algebra.glue("a", " ", "b")
      iex> doc = Inspect.Algebra.group(doc)
      iex> Inspect.Algebra.format(doc, 80)
      ["a", " ", "b"]

  Notice the break was represented as is, because we haven't reached
  a line limit. Once we do, it is replaced by a newline:

      iex> doc = Inspect.Algebra.glue(String.duplicate("a", 20), " ", "b")
      iex> doc = Inspect.Algebra.group(doc)
      iex> Inspect.Algebra.format(doc, 10)
      ["aaaaaaaaaaaaaaaaaaaa", "\n", "b"]

  This module uses the byte size to compute how much space there is
  left. If your document contains strings, then those need to be
  wrapped in `string/1`, which then relies on `String.length/1` to
  precompute the document size.

  Finally, this module also contains Elixir related functions, a bit
  tied to Elixir formatting, such as `to_doc/2`.

  ## Implementation details

  The original Haskell implementation of the algorithm by [Wadler][1]
  relies on lazy evaluation to unfold document groups on two alternatives:
  `:flat` (breaks as spaces) and `:break` (breaks as newlines).
  Implementing the same logic in a strict language such as Elixir leads
  to an exponential growth of possible documents, unless groups are explicitly
  encoded. Those groups are then reduced to a simple document, where the
  layout is already decided, per [Lindig][0].

  This implementation has two types of breaks: `:strict` and `:flex`. When
  a group does not fit, all strict breaks are treated as breaks. The flex
  breaks however are re-evaluated and may still be rendered as spaces.

  This implementation also adds `force_break/1` and `next_break_fits/2` which
  give more control over the document fitting.

  Custom pretty printers can be implemented using the documents returned
  by this module and by providing their own rendering functions.

    [0]: http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.34.2200
    [1]: http://homepages.inf.ed.ac.uk/wadler/papers/prettier/prettier.pdf

  """

  @container_separator ","
  @tail_separator " |"
  @newline "\n"
  @break :flex
  @next_break_fits :enabled

  # Functional interface to "doc" records

  @type t ::
          binary
          | :doc_nil
          | :doc_line
          | doc_string
          | doc_cons
          | doc_nest
          | doc_break
          | doc_group
          | doc_color
          | doc_force
          | doc_fits
          | doc_collapse

  @typep doc_string :: {:doc_string, t, non_neg_integer}
  defmacrop doc_string(string, length) do
    quote do: {:doc_string, unquote(string), unquote(length)}
  end

  @typep doc_cons :: {:doc_cons, t, t}
  defmacrop doc_cons(left, right) do
    quote do: {:doc_cons, unquote(left), unquote(right)}
  end

  @typep doc_nest :: {:doc_nest, t, :cursor | :reset | non_neg_integer, :always | :break}
  defmacrop doc_nest(doc, indent, always_or_break) do
    quote do: {:doc_nest, unquote(doc), unquote(indent), unquote(always_or_break)}
  end

  @typep doc_break :: {:doc_break, binary, :flex | :strict}
  defmacrop doc_break(break, mode) do
    quote do: {:doc_break, unquote(break), unquote(mode)}
  end

  @typep doc_group :: {:doc_group, t, :inherit | :self}
  defmacrop doc_group(group, mode) do
    quote do: {:doc_group, unquote(group), unquote(mode)}
  end

  @typep doc_fits :: {:doc_fits, t, :enabled | :disabled}
  defmacrop doc_fits(group, mode) do
    quote do: {:doc_fits, unquote(group), unquote(mode)}
  end

  @typep doc_force :: {:doc_force, t}
  defmacrop doc_force(group) do
    quote do: {:doc_force, unquote(group)}
  end

  @typep doc_collapse :: {:doc_collapse, pos_integer()}
  defmacrop doc_collapse(count) do
    quote do: {:doc_collapse, unquote(count)}
  end

  @typep doc_color :: {:doc_color, t, IO.ANSI.ansidata()}
  defmacrop doc_color(doc, color) do
    quote do: {:doc_color, unquote(doc), unquote(color)}
  end

  defmacrop is_doc(doc) do
    if Macro.Env.in_guard?(__CALLER__) do
      do_is_doc(doc)
    else
      var = quote(do: doc)

      quote do
        unquote(var) = unquote(doc)
        unquote(do_is_doc(var))
      end
    end
  end

  @docs [
    :doc_string,
    :doc_cons,
    :doc_nest,
    :doc_break,
    :doc_group,
    :doc_color,
    :doc_force,
    :doc_fits,
    :doc_collapse
  ]

  defp do_is_doc(doc) do
    quote do
      is_binary(unquote(doc)) or unquote(doc) in [:doc_nil, :doc_line] or
        (is_tuple(unquote(doc)) and elem(unquote(doc), 0) in unquote(@docs))
    end
  end

  # Elixir + Inspect.Opts conveniences

  @doc """
  Converts an Elixir term to an algebra document
  according to the `Inspect` protocol.
  """
  @spec to_doc(any, Inspect.Opts.t()) :: t
  def to_doc(term, opts)

  def to_doc(%_{} = struct, %Inspect.Opts{} = opts) do
    if opts.structs do
      try do
        Inspect.inspect(struct, opts)
      rescue
        caught_exception ->
          stacktrace = System.stacktrace()

          # Because we try to raise a nice error message in case
          # we can't inspect a struct, there is a chance the error
          # message itself relies on the struct being printed, so
          # we need to trap the inspected messages to guarantee
          # we won't try to render any failed instruct when building
          # the error message.
          if Process.get(:inspect_trap) do
            Inspect.Map.inspect(struct, opts)
          else
            try do
              Process.put(:inspect_trap, true)

              res = Inspect.Map.inspect(struct, %{opts | syntax_colors: []})
              res = IO.iodata_to_binary(format(res, :infinity))

              message =
                "got #{inspect(caught_exception.__struct__)} with message " <>
                  "#{inspect(Exception.message(caught_exception))} while inspecting #{res}"

              exception = Inspect.Error.exception(message: message)

              if opts.safe do
                Inspect.inspect(exception, opts)
              else
                reraise(exception, stacktrace)
              end
            after
              Process.delete(:inspect_trap)
            end
          end
      end
    else
      Inspect.Map.inspect(struct, opts)
    end
  end

  def to_doc(arg, %Inspect.Opts{} = opts) do
    Inspect.inspect(arg, opts)
  end

  @doc ~S"""
  Wraps `collection` in `left` and `right` according to limit and contents.

  It uses the given `left` and `right` documents as surrounding and the
  separator document `separator` to separate items in `docs`. If all entries
  in the collection are simple documents (texts or strings), then this function
  attempts to put as much as possible on the same line. If they are not simple,
  only one entry is shown per line if they do not fit.

  The limit in the given `Inspect.Opts` is respected and when reached this
  function stops processing and outputs `"..."` instead.

  ## Options

    * `:separator` - the separator used between each doc
    * `:break` - when set to `:strict`, always break between each element. Defaults to
      `:flex` which breaks only when necessary and if all elements are text-based

  ## Examples

      iex> doc = Inspect.Algebra.container_doc("[", Enum.to_list(1..5), "]",
      ...>         %Inspect.Opts{limit: :infinity}, fn i, _opts -> to_string(i) end)
      iex> Inspect.Algebra.format(doc, 5) |> IO.iodata_to_binary
      "[1,\n 2,\n 3,\n 4,\n 5]"

      iex> doc = Inspect.Algebra.container_doc("[", Enum.to_list(1..5), "]",
      ...>         %Inspect.Opts{limit: 3}, fn i, _opts -> to_string(i) end)
      iex> Inspect.Algebra.format(doc, 20) |> IO.iodata_to_binary
      "[1, 2, 3, ...]"

      iex> doc = Inspect.Algebra.container_doc("[", Enum.to_list(1..5), "]",
      ...>         %Inspect.Opts{limit: 3}, fn i, _opts -> to_string(i) end, separator: "!")
      iex> Inspect.Algebra.format(doc, 20) |> IO.iodata_to_binary
      "[1! 2! 3! ...]"

  """
  @spec container_doc(t, [any], t, Inspect.Opts.t(), (term, Inspect.Opts.t() -> t), keyword()) ::
          t
  def container_doc(left, collection, right, inspect, fun, opts \\ [])
      when is_doc(left) and is_list(collection) and is_doc(right) and is_function(fun, 2) and
             is_list(opts) do
    case collection do
      [] ->
        concat(left, right)

      _ ->
        separator = Keyword.get(opts, :separator, @container_separator)
        flex? = Keyword.get(opts, :break, @break) == :flex
        {docs, simple?} = container_each(collection, inspect.limit, inspect, fun, [], flex?)
        docs = fold_doc(docs, &join(&1, &2, simple?, separator))

        case simple? do
          true -> concat(concat(left, nest(docs, :cursor)), right)
          false -> group(glue(nest(glue(left, "", docs), 2), "", right))
        end
    end
  end

  defp container_each([], _limit, _opts, _fun, acc, simple?) do
    {:lists.reverse(acc), simple?}
  end

  defp container_each(_, 0, _opts, _fun, acc, simple?) do
    {:lists.reverse(["..." | acc]), simple?}
  end

  defp container_each([term | terms], limit, opts, fun, acc, simple?) when is_list(terms) do
    limit = decrement(limit)
    doc = fun.(term, %{opts | limit: limit})
    container_each(terms, limit, opts, fun, [doc | acc], simple? and simple?(doc))
  end

  defp container_each([left | right], limit, opts, fun, acc, simple?) do
    limit = decrement(limit)
    left = fun.(left, %{opts | limit: limit})
    right = fun.(right, %{opts | limit: limit})
    simple? = simple? and simple?(left) and simple?(right)

    doc = join(left, right, simple?, @tail_separator)
    {:lists.reverse([doc | acc]), simple?}
  end

  defp decrement(:infinity), do: :infinity
  defp decrement(counter), do: counter - 1

  defp join(:doc_nil, :doc_nil, _, _), do: :doc_nil
  defp join(left, :doc_nil, _, _), do: left
  defp join(:doc_nil, right, _, _), do: right
  defp join(left, right, true, sep), do: flex_glue(concat(left, sep), right)
  defp join(left, right, false, sep), do: glue(concat(left, sep), right)

  defp simple?(doc_cons(left, right)), do: simple?(left) and simple?(right)
  defp simple?(doc_color(doc, _)), do: simple?(doc)
  defp simple?(doc_string(_, _)), do: true
  defp simple?(:doc_nil), do: true
  defp simple?(other), do: is_binary(other)

  @doc false
  # TODO: Deprecate on Elixir v1.8
  def surround(left, doc, right) when is_doc(left) and is_doc(doc) and is_doc(right) do
    concat(concat(left, nest(doc, 1)), right)
  end

  @doc false
  # TODO: Deprecate on Elixir v1.8
  def surround_many(
        left,
        docs,
        right,
        %Inspect.Opts{} = inspect,
        fun,
        separator \\ @container_separator
      )
      when is_doc(left) and is_list(docs) and is_doc(right) and is_function(fun, 2) do
    container_doc(left, docs, right, inspect, fun, separator: separator)
  end

  # Algebra API

  @doc """
  Returns a document entity used to represent nothingness.

  ## Examples

      iex> Inspect.Algebra.empty
      :doc_nil

  """
  @spec empty() :: :doc_nil
  def empty, do: :doc_nil

  @doc ~S"""
  Creates a document represented by string.

  While `Inspect.Algebra` accepts binaries as documents,
  those are counted by binary size. On the other hand,
  `string` documents are measured in terms of graphemes
  towards the document size.

  ## Examples

  The following document has 10 bytes and therefore it
  does not format to width 9 without breaks:

      iex> doc = Inspect.Algebra.glue("olá", " ", "mundo")
      iex> doc = Inspect.Algebra.group(doc)
      iex> Inspect.Algebra.format(doc, 9)
      ["olá", "\n", "mundo"]

  However, if we use `string`, then the string length is
  used, instead of byte size, correctly fitting:

      iex> string = Inspect.Algebra.string("olá")
      iex> doc = Inspect.Algebra.glue(string, " ", "mundo")
      iex> doc = Inspect.Algebra.group(doc)
      iex> Inspect.Algebra.format(doc, 9)
      ["olá", " ", "mundo"]

  """
  @spec string(String.t()) :: doc_string
  def string(string) when is_binary(string) do
    doc_string(string, String.length(string))
  end

  @doc ~S"""
  Concatenates two document entities returning a new document.

  ## Examples

      iex> doc = Inspect.Algebra.concat("hello", "world")
      iex> Inspect.Algebra.format(doc, 80)
      ["hello", "world"]

  """
  @spec concat(t, t) :: t
  def concat(doc1, doc2) when is_doc(doc1) and is_doc(doc2) do
    doc_cons(doc1, doc2)
  end

  @doc ~S"""
  Concatenates a list of documents returning a new document.

  ## Examples

      iex> doc = Inspect.Algebra.concat(["a", "b", "c"])
      iex> Inspect.Algebra.format(doc, 80)
      ["a", "b", "c"]

  """
  @spec concat([t]) :: t
  def concat(docs) when is_list(docs) do
    fold_doc(docs, &concat(&1, &2))
  end

  @doc ~S"""
  Colors a document if the `color_key` has a color in the options.
  """
  @spec color(t, Inspect.Opts.color_key(), Inspect.Opts.t()) :: doc_color
  def color(doc, color_key, %Inspect.Opts{syntax_colors: syntax_colors}) when is_doc(doc) do
    if precolor = Keyword.get(syntax_colors, color_key) do
      postcolor = Keyword.get(syntax_colors, :reset, :reset)
      concat(doc_color(doc, precolor), doc_color(empty(), postcolor))
    else
      doc
    end
  end

  @doc ~S"""
  Nests the given document at the given `level`.

  If `level` is an integer, that's the indentation appended
  to line breaks whenever they occur. If the level is `:cursor`,
  the current position of the "cursor" in the document becomes
  the nesting. If the level is `:reset`, it is set back to 0.

  `mode` can be `:always`, which means nesting always happen,
  or `:break`, which means nesting only happens inside a group
  that has been broken.

  ## Examples

      iex> doc = Inspect.Algebra.nest(Inspect.Algebra.glue("hello", "world"), 5)
      iex> doc = Inspect.Algebra.group(doc)
      iex> Inspect.Algebra.format(doc, 5)
      ["hello", "\n     ", "world"]

  """
  @spec nest(t, non_neg_integer) :: doc_nest
  def nest(doc, level, mode \\ :always)

  def nest(doc, :cursor, mode) when is_doc(doc) and mode in [:always, :break] do
    doc_nest(doc, :cursor, mode)
  end

  def nest(doc, :reset, mode) when is_doc(doc) and mode in [:always, :break] do
    doc_nest(doc, :reset, mode)
  end

  def nest(doc, 0, _mode) when is_doc(doc) do
    doc
  end

  def nest(doc, level, mode)
      when is_doc(doc) and is_integer(level) and level > 0 and mode in [:always, :break] do
    doc_nest(doc, level, mode)
  end

  @doc ~S"""
  Returns a document entity representing a break based on the given
  `string`.

  This break can be rendered as a linebreak or as the given `string`,
  depending on the `mode` of the chosen layout.

  ## Examples

  Let's create a document by concatenating two strings with a break between
  them:

      iex> doc = Inspect.Algebra.concat(["a", Inspect.Algebra.break("\t"), "b"])
      iex> Inspect.Algebra.format(doc, 80)
      ["a", "\t", "b"]

  Notice the break was represented with the given string, because we didn't
  reach a line limit. Once we do, it is replaced by a newline:

      iex> break = Inspect.Algebra.break("\t")
      iex> doc = Inspect.Algebra.concat([String.duplicate("a", 20), break, "b"])
      iex> doc = Inspect.Algebra.group(doc)
      iex> Inspect.Algebra.format(doc, 10)
      ["aaaaaaaaaaaaaaaaaaaa", "\n", "b"]

  """
  @spec break(binary) :: doc_break
  def break(string \\ " ") when is_binary(string) do
    doc_break(string, :strict)
  end

  @doc """
  Collapse any new lines and whitespace following this
  node and emitting up to `max` new lines.
  """
  @spec collapse_lines(pos_integer) :: doc_collapse
  def collapse_lines(max) when is_integer(max) and max > 0 do
    doc_collapse(max)
  end

  @doc """
  Considers the next break as fit.

  `mode` can be `:enabled` or `:disabled`. When `:enabled`,
  it will consider the document as fit as soon as it finds
  the next break, effectively cancelling the break. It will
  also ignore any `force_break/1`.

  When disabled, it behaves as usual and it will ignore
  any further `next_break_fits/2` instruction.
  """
  @spec next_break_fits(t) :: doc_fits
  def next_break_fits(doc, mode \\ @next_break_fits) when is_doc(doc) do
    doc_fits(doc, mode)
  end

  @doc """
  Forces the document to break.
  """
  @spec force_break(t) :: doc_force
  def force_break(doc) when is_doc(doc) do
    doc_force(doc)
  end

  @doc """
  Introduces a flex break.

  A flex break still causes a group to break, like
  a regular break, but it is re-evaluated when the
  documented is processed.

  This function is used by `container_doc/4` and friends
  to the maximum number of entries on the same line.
  """
  @spec flex_break(binary) :: doc_break
  def flex_break(string \\ " ") when is_binary(string) do
    doc_break(string, :flex)
  end

  @doc """
  Glues two documents (`doc1` and `doc2`) inserting a
  `flex_break/1` given by `break_string` between them.

  This function is used by `container_doc/6` and friends
  to the maximum number of entries on the same line.
  """
  @spec flex_glue(t, binary, t) :: t
  def flex_glue(doc1, break_string \\ " ", doc2) when is_binary(break_string) do
    concat(doc1, concat(flex_break(break_string), doc2))
  end

  @doc ~S"""
  Glues two documents (`doc1` and `doc2`) inserting the given
  break `break_string` between them.

  For more information on how the break is inserted, see `break/1`.

  ## Examples

      iex> doc = Inspect.Algebra.glue("hello", "world")
      iex> Inspect.Algebra.format(doc, 80)
      ["hello", " ", "world"]

      iex> doc = Inspect.Algebra.glue("hello", "\t", "world")
      iex> Inspect.Algebra.format(doc, 80)
      ["hello", "\t", "world"]

  """
  @spec glue(t, binary, t) :: t
  def glue(doc1, break_string \\ " ", doc2) when is_binary(break_string) do
    concat(doc1, concat(break(break_string), doc2))
  end

  @doc ~S"""
  Returns a group containing the specified document `doc`.

  Documents in a group are attempted to be rendered together
  to the best of the renderer ability.

  The group mode can also be set to `:inherit`, which means it
  automatically breaks if the parent group has broken too.

  ## Examples

      iex> doc = Inspect.Algebra.group(
      ...>   Inspect.Algebra.concat(
      ...>     Inspect.Algebra.group(
      ...>       Inspect.Algebra.concat(
      ...>         "Hello,",
      ...>         Inspect.Algebra.concat(
      ...>           Inspect.Algebra.break,
      ...>           "A"
      ...>         )
      ...>       )
      ...>     ),
      ...>     Inspect.Algebra.concat(
      ...>       Inspect.Algebra.break,
      ...>       "B"
      ...>     )
      ...> ))
      iex> Inspect.Algebra.format(doc, 80)
      ["Hello,", " ", "A", " ", "B"]
      iex> Inspect.Algebra.format(doc, 6)
      ["Hello,", "\n", "A", "\n", "B"]

  """
  @spec group(t) :: doc_group
  def group(doc, mode \\ :self) when is_doc(doc) do
    doc_group(doc, mode)
  end

  @doc ~S"""
  Inserts a mandatory single space between two documents.

  ## Examples

      iex> doc = Inspect.Algebra.space("Hughes", "Wadler")
      iex> Inspect.Algebra.format(doc, 5)
      ["Hughes", " ", "Wadler"]

  """
  @spec space(t, t) :: t
  def space(doc1, doc2), do: concat(doc1, concat(" ", doc2))

  @doc ~S"""
  A mandatory linebreak.

  A group with linebreaks will fit if all lines in the group fit.

  ## Examples

    iex> doc = Inspect.Algebra.concat(
    ...>   Inspect.Algebra.concat(
    ...>     "Hughes",
    ...>     Inspect.Algebra.line()
    ...>   ), "Wadler"
    ...> )
    iex> Inspect.Algebra.format(doc, 80)
    ["Hughes", "\n", "Wadler"]

  """
  @spec line() :: t
  def line(), do: :doc_line

  @doc ~S"""
  Inserts a mandatory linebreak between two documents.

  See `line/1`.

  ## Examples

      iex> doc = Inspect.Algebra.line("Hughes", "Wadler")
      iex> Inspect.Algebra.format(doc, 80)
      ["Hughes", "\n", "Wadler"]

  """
  @spec line(t, t) :: t
  def line(doc1, doc2), do: concat(doc1, concat(line(), doc2))

  @doc ~S"""
  Folds a list of documents into a document using the given folder function.

  The list of documents is folded "from the right"; in that, this function is
  similar to `List.foldr/3`, except that it doesn't expect an initial
  accumulator and uses the last element of `docs` as the initial accumulator.

  ## Examples

      iex> docs = ["A", "B", "C"]
      iex> docs = Inspect.Algebra.fold_doc(docs, fn(doc, acc) ->
      ...>   Inspect.Algebra.concat([doc, "!", acc])
      ...> end)
      iex> Inspect.Algebra.format(docs, 80)
      ["A", "!", "B", "!", "C"]

  """
  @spec fold_doc([t], (t, t -> t)) :: t
  def fold_doc(docs, folder_fun)

  def fold_doc([], _folder_fun), do: empty()
  def fold_doc([doc], _folder_fun), do: doc

  def fold_doc([doc | docs], folder_fun) when is_function(folder_fun, 2),
    do: folder_fun.(doc, fold_doc(docs, folder_fun))

  @doc ~S"""
  Formats a given document for a given width.

  Takes the maximum width and a document to print as its arguments
  and returns an IO data representation of the best layout for the
  document to fit in the given width.

  The document starts flat (without breaks) until a group is found.

  ## Examples

      iex> doc = Inspect.Algebra.glue("hello", " ", "world")
      iex> doc = Inspect.Algebra.group(doc)
      iex> doc |> Inspect.Algebra.format(30) |> IO.iodata_to_binary()
      "hello world"
      iex> doc |> Inspect.Algebra.format(10) |> IO.iodata_to_binary()
      "hello\nworld"

  """
  @spec format(t, non_neg_integer | :infinity) :: iodata
  def format(doc, width) when is_doc(doc) and (width == :infinity or width >= 0) do
    format(width, 0, [{0, :flat, doc}])
  end

  # Type representing the document mode to be rendered
  #
  #   * break - represents a fitted document with breaks as breaks
  #   * flat - represents a fitted document with breaks as flats
  #   * next_fits - represents a document being fitted that will cancel (fit) the next break
  #   * no_fitting - represents a document being fitted that will not accept next break fits
  #
  @typep mode :: :break | :flat | :next_fits | :no_fitting

  @spec fits?(integer, integer, [{integer, mode, t}]) :: boolean
  defp fits?(w, k, _) when k > w, do: false
  defp fits?(_, _, []), do: true

  # No fitting

  defp fits?(w, k, [{i, _, doc_fits(x, :disabled)} | t]),
    do: fits?(w, k, [{i, :no_fitting, x} | t])

  defp fits?(w, k, [{i, :no_fitting, doc_group(x, _)} | t]),
    do: fits?(w, k, [{i, :no_fitting, x} | t])

  defp fits?(w, k, [{i, :no_fitting, doc_fits(x, _)} | t]),
    do: fits?(w, k, [{i, :no_fitting, x} | t])

  ## Next fits

  defp fits?(w, k, [{i, _, doc_fits(x, :enabled)} | t]), do: fits?(w, k, [{i, :next_fits, x} | t])
  defp fits?(w, k, [{i, :next_fits, doc_force(x)} | t]), do: fits?(w, k, [{i, :next_fits, x} | t])

  defp fits?(w, k, [{i, :next_fits, doc_group(x, _)} | t]),
    do: fits?(w, k, [{i, :next_fits, x} | t])

  defp fits?(_, _, [{_, :next_fits, doc_break(_, _)} | _]), do: true
  defp fits?(_, _, [{_, :next_fits, :doc_line} | _]), do: true

  ## Fitting rules

  defp fits?(w, k, [{_, _, :doc_nil} | t]), do: fits?(w, k, t)
  defp fits?(w, _, [{i, _, :doc_line} | t]), do: fits?(w, i, t)
  defp fits?(w, _, [{i, _, doc_collapse(_)} | t]), do: fits?(w, i, t)
  defp fits?(w, k, [{i, m, doc_cons(x, y)} | t]), do: fits?(w, k, [{i, m, x} | [{i, m, y} | t]])
  defp fits?(w, k, [{i, m, doc_color(x, _)} | t]), do: fits?(w, k, [{i, m, x} | t])
  defp fits?(w, k, [{i, m, doc_nest(x, _, :break)} | t]), do: fits?(w, k, [{i, m, x} | t])

  defp fits?(w, k, [{i, m, doc_nest(x, j, _)} | t]),
    do: fits?(w, k, [{apply_nesting(i, k, j), m, x} | t])

  defp fits?(w, k, [{i, _, doc_group(x, _)} | t]), do: fits?(w, k, [{i, :flat, x} | t])
  defp fits?(w, k, [{_, _, doc_string(_, l)} | t]), do: fits?(w, k + l, t)
  defp fits?(w, k, [{_, _, s} | t]) when is_binary(s), do: fits?(w, k + byte_size(s), t)
  defp fits?(_, _, [{_, _, doc_force(_)} | _]), do: false
  defp fits?(_, _, [{_, :break, doc_break(_, _)} | _]), do: true
  defp fits?(w, k, [{_, _, doc_break(s, _)} | t]), do: fits?(w, k + byte_size(s), t)

  @spec format(integer | :infinity, integer, [{integer, mode, t}]) :: [binary]
  defp format(_, _, []), do: []
  defp format(w, k, [{_, _, :doc_nil} | t]), do: format(w, k, t)
  defp format(w, _, [{i, _, :doc_line} | t]), do: [indent(i) | format(w, i, t)]
  defp format(w, k, [{i, m, doc_cons(x, y)} | t]), do: format(w, k, [{i, m, x} | [{i, m, y} | t]])
  defp format(w, k, [{i, m, doc_color(x, c)} | t]), do: [ansi(c) | format(w, k, [{i, m, x} | t])]
  defp format(w, k, [{_, _, doc_string(s, l)} | t]), do: [s | format(w, k + l, t)]
  defp format(w, k, [{_, _, s} | t]) when is_binary(s), do: [s | format(w, k + byte_size(s), t)]
  defp format(w, k, [{i, m, doc_force(x)} | t]), do: format(w, k, [{i, m, x} | t])
  defp format(w, k, [{i, m, doc_fits(x, _)} | t]), do: format(w, k, [{i, m, x} | t])
  defp format(w, _, [{i, _, doc_collapse(max)} | t]), do: collapse(format(w, i, t), max, 0, i)

  # Flex breaks are not conditional to the mode
  defp format(w, k, [{i, _, doc_break(s, :flex)} | t]) do
    k = k + byte_size(s)

    if w == :infinity or fits?(w, k, t) do
      [s | format(w, k, t)]
    else
      [indent(i) | format(w, i, t)]
    end
  end

  # Strict breaks are conditional to the mode
  defp format(w, k, [{i, mode, doc_break(s, :strict)} | t]) do
    if mode == :break do
      [indent(i) | format(w, i, t)]
    else
      [s | format(w, k + byte_size(s), t)]
    end
  end

  # Nesting is conditional to the mode.
  defp format(w, k, [{i, mode, doc_nest(x, j, nest)} | t]) do
    if nest == :always or (nest == :break and mode == :break) do
      format(w, k, [{apply_nesting(i, k, j), mode, x} | t])
    else
      format(w, k, [{i, mode, x} | t])
    end
  end

  # Groups must do the fitting decision.
  defp format(w, k, [{i, :break, doc_group(x, :inherit)} | t]) do
    format(w, k, [{i, :break, x} | t])
  end

  defp format(w, k, [{i, _, doc_group(x, _)} | t]) do
    if w == :infinity or fits?(w, k, [{i, :flat, x}]) do
      format(w, k, [{i, :flat, x} | t])
    else
      format(w, k, [{i, :break, x} | t])
    end
  end

  defp collapse(["\n" <> _ | t], max, count, i) do
    collapse(t, max, count + 1, i)
  end

  defp collapse(["" | t], max, count, i) do
    collapse(t, max, count, i)
  end

  defp collapse(t, max, count, i) do
    [:binary.copy("\n", min(max, count)) <> :binary.copy(" ", i) | t]
  end

  defp apply_nesting(_, k, :cursor), do: k
  defp apply_nesting(_, _, :reset), do: 0
  defp apply_nesting(i, _, j), do: i + j

  defp ansi(color) do
    IO.ANSI.format_fragment(color, true)
  end

  defp indent(0), do: @newline
  defp indent(i), do: @newline <> :binary.copy(" ", i)
end


Cleanup FarmEvent logs

Cleanup FarmEvent logs

defmodule Farmbot.FarmEvent.Manager do
  @moduledoc """
  Manages execution of FarmEvents.

  ## Rules for FarmEvent execution.
  * Regimen
    * ignore `end_time`.
    * ignore calendar.
    * if start_time is more than 60 seconds passed due, assume it already started, and don't start it again.
  * Sequence
    * if `start_time` is late, check the calendar.
      * for each item in the calendar, check if it's event is more than 60 seconds in the past. if not, execute it.
    * if there is only one event in the calendar, ignore the `end_time`
  """

  # credo:disable-for-this-file Credo.Check.Refactor.FunctionArity

  use GenServer
  use Farmbot.Logger
  alias Farmbot.FarmEvent.Execution
  alias Farmbot.Repo.FarmEvent

  # @checkup_time 100
  @checkup_time 30_000

  def register_events(event_list) do
    GenServer.call(__MODULE__, {:register_events, event_list}, 10_000)
  end

  ## GenServer

  defmodule State do
    @moduledoc false
    defstruct [timer: nil, last_time_index: %{}, events: [], checkup: nil]
  end

  @doc false
  def start_link do
    GenServer.start_link(__MODULE__, [], [name: __MODULE__])
  end

  def init([]) do
    send self(), :checkup
    {:ok, struct(State)}
  end

  def terminate(reason, _state) do
    Logger.error 1, "FarmEvent Manager terminated: #{inspect reason}"
  end

  def handle_call({:register_events, events}, _, state) do
    maybe_farm_event_log "Reindexed FarmEvents"
    if match?({_, _}, state.checkup) do
      Process.exit(state.checkup |> elem(0), {:success, %{state | events: events}})
    end

    if state.timer do
      Process.cancel_timer(state.timer)
      timer = Process.send_after(self(), :checkup, @checkup_time)
      {:reply, :ok, %{state | events: events, timer: timer}}
    else
      {:reply, :ok, %{state | events: events}}
    end
  end

  def handle_info(:checkup, state) do
    checkup = spawn_monitor __MODULE__, :async_checkup, [self(), state]
    {:noreply, %{state | timer: nil, checkup: checkup}}
  end

  def handle_info({:DOWN, _, :process, _, {:success, new_state}}, old_state) do
    timer = Process.send_after(self(), :checkup, @checkup_time)
    {:noreply, %{new_state | timer: timer, checkup: nil}}
  end

  def handle_info({:DOWN, _, :process, _, error}, state) do
    Logger.error 1, "Farmevent checkup process died: #{inspect error}"
    timer = Process.send_after(self(), :checkup, @checkup_time)
    {:noreply, %{state | timer: timer, checkup: nil}}
  end

  def async_checkup(manager, state) do
    now = get_now()
    alias Farmbot.Repo.FarmEvent
    # maybe_farm_event_log "Rebuilding calendar."
    all_events = state.events |> Enum.map(&FarmEvent.build_calendar(&1))
    # maybe_farm_event_log "Rebuilding calendar complete."

    # do checkup is the bulk of the work.
    {late_events, new} = do_checkup(all_events, now, state)

    #TODO(Connor) Conditionally start events based on some state info.
    unless Enum.empty?(late_events) do
      Logger.info 3, "Time for events: #{inspect late_events} to run at: #{now.hour}:#{now.minute}"
      start_events(late_events, now)
    end
    exit({:success, new})
  end

  defp do_checkup(list, time, late_events \\ [], state)

  defp do_checkup([], _now, late_events, state), do: {late_events, state}

  defp do_checkup([farm_event | rest], now, late_events, state) do
    # new_late will be a executable event (Regimen or Sequence.)
    {new_late_event, last_time} = check_event(farm_event, now, state.last_time_index[farm_event.id])

    # update state.
    new_state = %{state | last_time_index: Map.put(state.last_time_index, farm_event.id, last_time)}
    case new_late_event do
      # if `new_late_event` is nil, don't accumulate it.
      nil   -> do_checkup(rest, now, late_events, new_state)
      # if there is a new event, accumulate it.
      event -> do_checkup(rest, now, [event | late_events], new_state)
    end
  end

  defp check_event(%FarmEvent{} = f, now, last_time) do
    # Get the executable out of the database this may fail.
    # mod_list = ["Farmbot", "Repo", f.executable_type]
    mod      = Module.safe_concat([f.executable_type])

    event    = lookup(mod, f.executable_id)

    # build a local start time and end time
    start_time = Timex.parse! f.start_time, "{ISO:Extended}"
    end_time   = Timex.parse! f.end_time,   "{ISO:Extended}"
    # start_time = f.start_time
    # end_time = f.end_time
    # get local bool of if the event is started and finished.
    started?  = Timex.after? now, start_time
    finished? = Timex.after? now, end_time

    case f.executable_type do
      "Elixir.Farmbot.Repo.Regimen"  -> maybe_start_regimen(started?, start_time, last_time, event, now)
      "Elixir.Farmbot.Repo.Sequence" -> maybe_start_sequence(started?, finished?, f, last_time, event, now)
    end
  end

  defp maybe_start_regimen(started?, start_time, last_time, event, now)
  defp maybe_start_regimen(true = _started?, start_time, last_time, event, now) do
    case is_too_old?(now, start_time) do
      true  ->
        maybe_farm_event_log "regimen #{event.name} (#{event.id}) is too old to start or already started."
        {nil, last_time}
      false ->
        maybe_farm_event_log "regimen #{event.name} (#{event.id}) starting."
        {event, now}
    end
  end

  defp maybe_start_regimen(false = _started?, start_time, last_time, event, _) do
    maybe_farm_event_log "regimen #{event.name} (#{event.id}) is not started yet. (#{inspect start_time}) (#{inspect Timex.now()})"
    {nil, last_time}
  end

  defp lookup(module, sr_id) do
    case Farmbot.Repo.current_repo().get(module, sr_id) do
      nil -> raise "Could not find #{module} by id: #{sr_id}"
      item -> item
    end
  end

  # signals the start of a sequence based on the described logic.
  defp maybe_start_sequence(started?, finished?, farm_event, last_time, event, now)

  # We only want to check if the sequence is started, and not finished.
  defp maybe_start_sequence(true = _started?, false = _finished?, farm_event, last_time, event, now) do
    {run?, next_time} = should_run_sequence?(farm_event.calendar, last_time, now)
    case run? do
      true  -> {event, next_time}
      false -> {nil, last_time}
    end
  end

  # if `farm_event.time_unit` is "never" we can't use the `end_time`.
  # if we have no `last_time`, time to execute.
  defp maybe_start_sequence(true = _started?, _, %{time_unit: "never"} = f, nil = _last_time, event, now) do
    maybe_farm_event_log "Ignoring end_time."
    case should_run_sequence?(f.calendar, nil, now) do
      {true, next} -> {event, next}
      {false,   _} -> {nil,    nil}
    end
  end

  # if started is false, the event isn't ready to be executed.
  defp maybe_start_sequence(false = _started?, _fin, _farm_event, last_time, event, _now) do
    maybe_farm_event_log "sequence #{event.name} (#{event.id}) is not started yet."
    {nil, last_time}
  end

  # if the event is finished (but not a "never" time_unit), we don't execute.
  defp maybe_start_sequence(_started?, true = _finished?, _farm_event, last_time, event, _now) do
    Logger.success 3, "sequence #{event.name} (#{event.id}) is finished."
    {nil, last_time}
  end

  # Checks  if we shoudl run a sequence or not. returns {event | nil, time | nil}
  defp should_run_sequence?(calendar, last_time, now)

  # if there is no last time, check if time is passed now within 60 seconds.
  defp should_run_sequence?([first_time | _], nil, now) do

    maybe_farm_event_log "Checking sequence event that hasn't run before #{first_time}"
    # convert the first_time to a DateTime
    dt = Timex.parse! first_time, "{ISO:Extended}"
    # if now is after the time, we are in fact late
    if Timex.after?(now, dt) do
        {true, now}
     else
       # make sure to return nil as the last time because it stil hasnt executed yet.
       maybe_farm_event_log "Sequence Event not ready yet."
      {false, nil}
    end
  end

  defp should_run_sequence?(nil, last_time, now) do
    maybe_farm_event_log "Checking sequence with no calendar."
    if is_nil(last_time) do
      {true, now}
    else
      {false, last_time}
    end
  end

  defp should_run_sequence?(calendar, last_time, now) do
    # get rid of all the items that happened before last_time
    filtered_calendar = Enum.filter(calendar, fn(iso_time) ->
      dt = Timex.parse! iso_time, "{ISO:Extended}"
      # we only want this time if it happened after the last_time
      Timex.after?(dt, last_time)
    end)

    # if after filtering, there are events that need to be run
    # check if they are older than a minute ago,
    case filtered_calendar do
      [iso_time |  _] ->
        dt = Timex.parse! iso_time, "{ISO:Extended}"
        if Timex.after?(now, dt) do
          {true, dt}
          # too_old? = is_too_old?(now, dt)
          # if too_old?, do: {false, last_time}, else: {true, dt}
        else
          maybe_farm_event_log "Sequence Event not ready yet."
          {false, dt}
        end
      [] ->
        maybe_farm_event_log "No items in calendar."
        {false, last_time}
    end
  end

  # Enumeration is complete.
  defp start_events([], _now), do: :ok

  # Enumerate the events to be started.
  defp start_events([event | rest], now) do
    # Spawn to be non blocking here. Maybe link to this process?
    spawn fn() -> Execution.execute_event(event, now) end
    # Continue enumeration.
    start_events(rest, now)
  end

  # is then more than 1 minute in the past?
  defp is_too_old?(now, then) do
    time_str_fun = fn(dt) -> "#{dt.hour}:#{dt.minute}:#{dt.second}" end
    seconds = DateTime.to_unix(now, :second) - DateTime.to_unix(then, :second)
    c = seconds > 60 # not in MS here
    maybe_farm_event_log "is checking #{time_str_fun.(now)} - #{time_str_fun.(then)} = #{seconds} seconds ago. is_too_old? => #{c}"
    c
  end

  defp get_now(), do: Timex.now()

  defp maybe_farm_event_log(message) do
    if Application.get_env(:farmbot, :farm_event_debug_log) do
      Logger.debug 3, message
    else
      :ok
    end
  end
end

defmodule Farmbot.FarmEvent.Manager do
  @moduledoc """
  Manages execution of FarmEvents.

  ## Rules for FarmEvent execution.
  * Regimen
    * ignore `end_time`.
    * ignore calendar.
    * if start_time is more than 60 seconds passed due, assume it already started, and don't start it again.
  * Sequence
    * if `start_time` is late, check the calendar.
      * for each item in the calendar, check if it's event is more than 60 seconds in the past. if not, execute it.
    * if there is only one event in the calendar, ignore the `end_time`
  """

  # credo:disable-for-this-file Credo.Check.Refactor.FunctionArity

  use GenServer
  use Farmbot.Logger
  alias Farmbot.FarmEvent.Execution
  alias Farmbot.Repo.FarmEvent

  # @checkup_time 100
  @checkup_time 30_000

  def register_events(event_list) do
    GenServer.call(__MODULE__, {:register_events, event_list}, 10_000)
  end

  ## GenServer

  defmodule State do
    @moduledoc false
    defstruct [timer: nil, last_time_index: %{}, events: [], checkup: nil]
  end

  @doc false
  def start_link do
    GenServer.start_link(__MODULE__, [], [name: __MODULE__])
  end

  def init([]) do
    send self(), :checkup
    {:ok, struct(State)}
  end

  def terminate(reason, _state) do
    Logger.error 1, "FarmEvent Manager terminated: #{inspect reason}"
  end

  def handle_call({:register_events, events}, _, state) do
    Logger.info 3, "Reindexed FarmEvents"
    if match?({_, _}, state.checkup) do
      Process.exit(state.checkup |> elem(0), {:success, %{state | events: events}})
    end

    if state.timer do
      Process.cancel_timer(state.timer)
      timer = Process.send_after(self(), :checkup, @checkup_time)
      {:reply, :ok, %{state | events: events, timer: timer}}
    else
      {:reply, :ok, %{state | events: events}}
    end
  end

  def handle_info(:checkup, state) do
    Logger.info 1, "starting checkup"
    checkup = spawn_monitor __MODULE__, :async_checkup, [self(), state]
    {:noreply, %{state | timer: nil, checkup: checkup}}
  end

  def handle_info({:DOWN, _, :process, _, {:success, new_state}}, old_state) do
    Logger.info 1, "Checkup process exit success"
    timer = Process.send_after(self(), :checkup, @checkup_time)
    {:noreply, %{new_state | timer: timer, checkup: nil}}
  end

  def handle_info({:DOWN, _, :process, _, error}, state) do
    Logger.error 1, "Checkup process died: #{inspect error}"
    timer = Process.send_after(self(), :checkup, @checkup_time)
    {:noreply, %{state | timer: timer, checkup: nil}}
  end

  def async_checkup(manager, state) do
    now = get_now()
    alias Farmbot.Repo.FarmEvent
    maybe_farm_event_log "Rebuilding calendar."
    all_events = state.events |> Enum.map(&FarmEvent.build_calendar(&1))
    maybe_farm_event_log "Rebuilding calendar complete."

    # do checkup is the bulk of the work.
    {late_events, new} = do_checkup(all_events, now, state)

    #TODO(Connor) Conditionally start events based on some state info.
    unless Enum.empty?(late_events) do
      Logger.info 3, "Time for events: #{inspect late_events} to run at: #{now.hour}:#{now.minute}"
      start_events(late_events, now)
    end
    exit({:success, new})
  end

  defp do_checkup(list, time, late_events \\ [], state)

  defp do_checkup([], _now, late_events, state), do: {late_events, state}

  defp do_checkup([farm_event | rest], now, late_events, state) do
    # new_late will be a executable event (Regimen or Sequence.)
    {new_late_event, last_time} = check_event(farm_event, now, state.last_time_index[farm_event.id])

    # update state.
    new_state = %{state | last_time_index: Map.put(state.last_time_index, farm_event.id, last_time)}
    case new_late_event do
      # if `new_late_event` is nil, don't accumulate it.
      nil   -> do_checkup(rest, now, late_events, new_state)
      # if there is a new event, accumulate it.
      event -> do_checkup(rest, now, [event | late_events], new_state)
    end
  end

  defp check_event(%FarmEvent{} = f, now, last_time) do
    # Get the executable out of the database this may fail.
    # mod_list = ["Farmbot", "Repo", f.executable_type]
    mod      = Module.safe_concat([f.executable_type])

    event    = lookup(mod, f.executable_id)

    # build a local start time and end time
    start_time = Timex.parse! f.start_time, "{ISO:Extended}"
    end_time   = Timex.parse! f.end_time,   "{ISO:Extended}"
    # start_time = f.start_time
    # end_time = f.end_time
    # get local bool of if the event is started and finished.
    started?  = Timex.after? now, start_time
    finished? = Timex.after? now, end_time

    case f.executable_type do
      "Elixir.Farmbot.Repo.Regimen"  -> maybe_start_regimen(started?, start_time, last_time, event, now)
      "Elixir.Farmbot.Repo.Sequence" -> maybe_start_sequence(started?, finished?, f, last_time, event, now)
    end
  end

  defp maybe_start_regimen(started?, start_time, last_time, event, now)
  defp maybe_start_regimen(true = _started?, start_time, last_time, event, now) do
    case is_too_old?(now, start_time) do
      true  ->
        maybe_farm_event_log "regimen #{event.name} (#{event.id}) is too old to start or already started."
        {nil, last_time}
      false ->
        maybe_farm_event_log "regimen #{event.name} (#{event.id}) starting."
        {event, now}
    end
  end

  defp maybe_start_regimen(false = _started?, start_time, last_time, event, _) do
    maybe_farm_event_log "regimen #{event.name} (#{event.id}) is not started yet. (#{inspect start_time}) (#{inspect Timex.now()})"
    {nil, last_time}
  end

  defp lookup(module, sr_id) do
    case Farmbot.Repo.current_repo().get(module, sr_id) do
      nil -> raise "Could not find #{module} by id: #{sr_id}"
      item -> item
    end
  end

  # signals the start of a sequence based on the described logic.
  defp maybe_start_sequence(started?, finished?, farm_event, last_time, event, now)

  # We only want to check if the sequence is started, and not finished.
  defp maybe_start_sequence(true = _started?, false = _finished?, farm_event, last_time, event, now) do
    {run?, next_time} = should_run_sequence?(farm_event.calendar, last_time, now)
    case run? do
      true  -> {event, next_time}
      false -> {nil, last_time}
    end
  end

  # if `farm_event.time_unit` is "never" we can't use the `end_time`.
  # if we have no `last_time`, time to execute.
  defp maybe_start_sequence(true = _started?, _, %{time_unit: "never"} = f, nil = _last_time, event, now) do
    maybe_farm_event_log "Ignoring end_time."
    case should_run_sequence?(f.calendar, nil, now) do
      {true, next} -> {event, next}
      {false,   _} -> {nil,    nil}
    end
  end

  # if started is false, the event isn't ready to be executed.
  defp maybe_start_sequence(false = _started?, _fin, _farm_event, last_time, event, _now) do
    maybe_farm_event_log "sequence #{event.name} (#{event.id}) is not started yet."
    {nil, last_time}
  end

  # if the event is finished (but not a "never" time_unit), we don't execute.
  defp maybe_start_sequence(_started?, true = _finished?, _farm_event, last_time, event, _now) do
    Logger.success 3, "sequence #{event.name} (#{event.id}) is finished."
    {nil, last_time}
  end

  # Checks  if we shoudl run a sequence or not. returns {event | nil, time | nil}
  defp should_run_sequence?(calendar, last_time, now)

  # if there is no last time, check if time is passed now within 60 seconds.
  defp should_run_sequence?([first_time | _], nil, now) do

    maybe_farm_event_log "Checking sequence event that hasn't run before #{first_time}"
    # convert the first_time to a DateTime
    dt = Timex.parse! first_time, "{ISO:Extended}"
    # if now is after the time, we are in fact late
    if Timex.after?(now, dt) do
        {true, now}
     else
       # make sure to return nil as the last time because it stil hasnt executed yet.
       maybe_farm_event_log "Sequence Event not ready yet."
      {false, nil}
    end
  end

  defp should_run_sequence?(nil, last_time, now) do
    maybe_farm_event_log "Checking sequence with no calendar."
    if is_nil(last_time) do
      {true, now}
    else
      {false, last_time}
    end
  end

  defp should_run_sequence?(calendar, last_time, now) do
    # get rid of all the items that happened before last_time
    filtered_calendar = Enum.filter(calendar, fn(iso_time) ->
      dt = Timex.parse! iso_time, "{ISO:Extended}"
      # we only want this time if it happened after the last_time
      Timex.after?(dt, last_time)
    end)

    # if after filtering, there are events that need to be run
    # check if they are older than a minute ago,
    case filtered_calendar do
      [iso_time |  _] ->
        dt = Timex.parse! iso_time, "{ISO:Extended}"
        if Timex.after?(now, dt) do
          {true, dt}
          # too_old? = is_too_old?(now, dt)
          # if too_old?, do: {false, last_time}, else: {true, dt}
        else
          maybe_farm_event_log "Sequence Event not ready yet."
          {false, dt}
        end
      [] ->
        maybe_farm_event_log "No items in calendar."
        {false, last_time}
    end
  end

  # Enumeration is complete.
  defp start_events([], _now), do: :ok

  # Enumerate the events to be started.
  defp start_events([event | rest], now) do
    # Spawn to be non blocking here. Maybe link to this process?
    spawn fn() -> Execution.execute_event(event, now) end
    # Continue enumeration.
    start_events(rest, now)
  end

  # is then more than 1 minute in the past?
  defp is_too_old?(now, then) do
    time_str_fun = fn(dt) -> "#{dt.hour}:#{dt.minute}:#{dt.second}" end
    seconds = DateTime.to_unix(now, :second) - DateTime.to_unix(then, :second)
    c = seconds > 60 # not in MS here
    maybe_farm_event_log "is checking #{time_str_fun.(now)} - #{time_str_fun.(then)} = #{seconds} seconds ago. is_too_old? => #{c}"
    c
  end

  defp get_now(), do: Timex.now()

  defp maybe_farm_event_log(message) do
    if Application.get_env(:farmbot, :farm_event_debug_log) do
      Logger.debug 3, message
    else
      :ok
    end
  end
end