Small improvements to the binary protocol type IDs (#132)

1. Fix the `type_id` typespec to use the correct value range.
2. Add a formal `@stop` type identifier (0) and use it below.

defmodule Thrift.Protocol.Binary do
  @moduledoc """
  Provides a set of high-level functions for working with the Thrift binary
  protocol.

  The Thrift binary protocol uses a fairly simple binary encoding scheme in
  which the length and type of a field are encoded as bytes followed by the
  actual value of the field.
  """

  alias Thrift.TApplicationException

  @typedoc "Binary protocol field type identifier"
  @type type_id :: 2 | 3 | 4 | 6 | 8 | 10 | 11 | 12 | 13 | 14 | 15
  @type deserializable :: :message_begin | :application_exception

  @stop   0
  @bool   2
  @byte   3   # also: i8
  @double 4
  @i16    6
  @i32    8
  @i64    10
  @string 11
  @struct 12
  @map    13
  @set    14
  @list   15

  @spec type_id(Thrift.data_type) :: type_id
  defp type_id(:bool),      do: @bool
  defp type_id(:byte),      do: @byte
  defp type_id(:i16),       do: @i16
  defp type_id(:i32),       do: @i32
  defp type_id(:i64),       do: @i64
  defp type_id(:double),    do: @double
  defp type_id(:string),    do: @string
  defp type_id(:struct),    do: @struct
  defp type_id({:map, _}),  do: @map
  defp type_id({:set, _}),  do: @set
  defp type_id({:list, _}), do: @list

  @typedoc "Binary protocol message type identifier"
  @type message_type_id :: (1..4)

  @spec from_message_type(Thrift.message_type) :: message_type_id
  defp from_message_type(:call),      do: 1
  defp from_message_type(:reply),     do: 2
  defp from_message_type(:exception), do: 3
  defp from_message_type(:oneway),    do: 4

  @spec to_message_type(message_type_id) :: Thrift.message_type
  defp to_message_type(1), do: :call
  defp to_message_type(2), do: :reply
  defp to_message_type(3), do: :exception
  defp to_message_type(4), do: :oneway

  @typedoc "Binary protocol message sequence identifier"
  @type message_seq_id :: non_neg_integer

  @doc """
  Serializes a value as an IO list using Thrift's type-specific encoding rules.
  """
  @spec serialize(Thrift.data_type | :message_begin, any) :: iolist
  def serialize(:bool, false),   do: <<0::8-signed>>
  def serialize(:bool, true),    do: <<1::8-signed>>
  def serialize(:i8, value),     do: <<value::8-signed>>
  def serialize(:i16, value),    do: <<value::16-signed>>
  def serialize(:i32, value),    do: <<value::32-signed>>
  def serialize(:i64, value),    do: <<value::64-signed>>
  def serialize(:double, value), do: <<value::signed-float>>
  def serialize(:string, value), do: [<<byte_size(value)::size(32)>>, value]
  def serialize(:binary, value), do: [<<byte_size(value)::size(32)>>, value]

  def serialize({:list, elem_type}, elems) when is_list(elems) do
    rest = Enum.map(elems, &serialize(elem_type, &1))
    [<<type_id(elem_type)::size(8), Enum.count(elems)::32-signed>>, rest]
  end
  def serialize({:set, elem_type}, %MapSet{} = elems) do
    rest = Enum.map(elems, &serialize(elem_type, &1))
    [<<type_id(elem_type)::size(8), Enum.count(elems)::32-signed>>, rest]
  end
  def serialize({:map, {key_type, val_type}}, map) when is_map(map) do
    elem_count = map_size(map)
    rest = Enum.map(map, fn {key, value} ->
      [serialize(key_type, key), serialize(val_type, value)]
    end)
    [<<type_id(key_type)::size(8), type_id(val_type)::size(8), elem_count::32-signed>>, rest]
  end
  def serialize(:struct, %{__struct__: mod} = struct) do
    mod.serialize(struct, :binary)
  end
  def serialize(:union, %{__struct__: mod} = struct) do
    mod.serialize(struct, :binary)
  end
  def serialize(:message_begin, {message_type, sequence_id, name}) do
    # Taken from https://erikvanoosten.github.io/thrift-missing-specification/#_message_encoding

    <<1::size(1), 1::size(15), 0::size(8),
    # ^^ Strange, I know. We could integrate the 8-bit zero here with the 5 bit zero below.
    0::size(5), from_message_type(message_type)::size(3),
    byte_size(name)::32-signed, name::binary, sequence_id::32-signed>>
  end
  def serialize(:application_exception, %TApplicationException{type: t} = exc) when is_atom(t) do
    serialize(:application_exception, %TApplicationException{exc | type: TApplicationException.exception_type_to_int(t)})
  end
  def serialize(:application_exception, %TApplicationException{message: message, type: type}) do
    <<@string::size(8), 1::16-signed, byte_size(message)::size(32), message::binary,
      @i32::size(8), 2::16-signed, type::32-signed, @stop>>
  end

  @doc """
  Deserializes a Thrift-encoded binary.
  """
  @spec deserialize(deserializable, binary) ::
    {:ok, {Thrift.message_type, message_seq_id, name :: String.t, binary}} |
    {:error, {atom, binary}}
  def deserialize(:message_begin, <<1::size(1), 1::size(15), _::size(8),
                  0::size(5), message_type::size(3),
                  name_size::32-signed, name::binary-size(name_size), sequence_id::32-signed, rest::binary>>) do
    {:ok, {to_message_type(message_type), sequence_id, name, rest}}
  end

  # the old format, see here:
  # https://erikvanoosten.github.io/thrift-missing-specification/#_message_encoding
  def deserialize(:message_begin, <<name_size::32-signed, name::binary-size(name_size),
                  0::size(5), message_type::size(3), sequence_id::32-signed, rest::binary>>) do
    {:ok, {to_message_type(message_type), sequence_id, name, rest}}
  end

  def deserialize(:message_begin, rest) do
    {:error, {:cant_decode_message, rest}}
  end

  def deserialize(:application_exception, binary) when is_binary(binary) do
    do_read_application_exception(binary, %TApplicationException{})
  end

  defp do_read_application_exception(
        <<@string::size(8),
        1::16-unsigned,
        message_size::32-signed,
        message::binary-size(message_size),
        rest::binary>>, accum) do
    # read the message string
    do_read_application_exception(rest, Map.put(accum, :message, message))
  end
  defp do_read_application_exception(
        <<@i32::size(8),
        2::16-unsigned,
        type::32-signed,
        rest::binary>>, accum) do
    # read the type
    exception_type = TApplicationException.exception_type(type)
    do_read_application_exception(rest, Map.put(accum, :type, exception_type))
  end
  defp do_read_application_exception(<<@stop>>, accum) do
    # read the field stop and return
    accum
  end
  defp do_read_application_exception(error, _) do
    message = "Could not decode TApplicationException, remaining was #{inspect error}"
    %TApplicationException{message: message,
                           type: TApplicationException.exception_type(7)}
  end

  @doc """
  Skips over the bytes representing a binary-encoded field.

  This is useful for jumping over unrecognized fields in the serialized byte
  stream.
  """
  @spec skip_field(binary, type_id) :: binary | :error
  def skip_field(<<_, rest::binary>>, unquote(@bool)), do: rest
  def skip_field(<<_, rest::binary>>, unquote(@byte)), do: rest
  def skip_field(<<_::signed-float, rest::binary>>, unquote(@double)), do: rest
  def skip_field(<<_::size(16), rest::binary>>, unquote(@i16)), do: rest
  def skip_field(<<_::size(32), rest::binary>>, unquote(@i32)), do: rest
  def skip_field(<<_::size(64), rest::binary>>, unquote(@i64)), do: rest
  def skip_field(<<length::32-signed, _::binary-size(length), rest::binary>>, unquote(@string)) do
    rest
  end
  def skip_field(<<rest::binary>>, unquote(@struct)) do
    rest |> skip_struct
  end
  def skip_field(<<key_type, val_type, length::size(32), rest::binary>>, unquote(@map)) do
    rest |> skip_map_entry(key_type, val_type, length)
  end
  def skip_field(<<elem_type, length::size(32), rest::binary>>, unquote(@set)) do
    rest |> skip_list_element(elem_type, length)
  end
  def skip_field(<<elem_type, length::size(32), rest::binary>>, unquote(@list)) do
    rest |> skip_list_element(elem_type, length)
  end
  def skip_field(_, _), do: :error

  defp skip_list_element(<<rest::binary>>, _, 0), do: rest
  defp skip_list_element(<<rest::binary>>, elem_type, remaining) do
    rest
    |> skip_field(elem_type)
    |> skip_list_element(elem_type, remaining - 1)
  end
  defp skip_list_element(:error, _, _), do: :error

  defp skip_map_entry(<<rest::binary>>, _, _, 0), do: rest
  defp skip_map_entry(<<rest::binary>>, key_type, val_type, remaining) do
    rest
    |> skip_field(key_type)
    |> skip_field(val_type)
    |> skip_map_entry(key_type, val_type, remaining - 1)
  end
  defp skip_map_entry(:error, _, _, _), do: :error

  defp skip_struct(<<0, rest::binary>>), do: rest
  defp skip_struct(<<type, _id::16-signed, rest::binary>>) do
    rest
    |> skip_field(type)
    |> skip_struct
  end
  defp skip_struct(_), do: :error
end

defmodule Thrift.Protocol.Binary do
  @moduledoc """
  Provides a set of high-level functions for working with the Thrift binary
  protocol.

  The Thrift binary protocol uses a fairly simple binary encoding scheme in
  which the length and type of a field are encoded as bytes followed by the
  actual value of the field.
  """

  alias Thrift.TApplicationException

  @typedoc "Binary protocol field type identifier"
  @type type_id :: (2..15)
  @type deserializable :: :message_begin | :application_exception

  @bool 2
  @byte 3
  @double 4
  @i16 6
  @i32 8
  @i64 10
  @string 11
  @struct 12
  @map 13
  @set 14
  @list 15

  @spec type_id(Thrift.data_type) :: type_id
  defp type_id(:bool),      do: @bool
  defp type_id(:byte),      do: @byte
  defp type_id(:i16),       do: @i16
  defp type_id(:i32),       do: @i32
  defp type_id(:i64),       do: @i64
  defp type_id(:double),    do: @double
  defp type_id(:string),    do: @string
  defp type_id(:struct),    do: @struct
  defp type_id({:map, _}),  do: @map
  defp type_id({:set, _}),  do: @set
  defp type_id({:list, _}), do: @list

  @typedoc "Binary protocol message type identifier"
  @type message_type_id :: (1..4)

  @spec from_message_type(Thrift.message_type) :: message_type_id
  defp from_message_type(:call),      do: 1
  defp from_message_type(:reply),     do: 2
  defp from_message_type(:exception), do: 3
  defp from_message_type(:oneway),    do: 4

  @spec to_message_type(message_type_id) :: Thrift.message_type
  defp to_message_type(1), do: :call
  defp to_message_type(2), do: :reply
  defp to_message_type(3), do: :exception
  defp to_message_type(4), do: :oneway

  @typedoc "Binary protocol message sequence identifier"
  @type message_seq_id :: non_neg_integer

  @doc """
  Serializes a value as an IO list using Thrift's type-specific encoding rules.
  """
  @spec serialize(Thrift.data_type | :message_begin, any) :: iolist
  def serialize(:bool, false),   do: <<0::8-signed>>
  def serialize(:bool, true),    do: <<1::8-signed>>
  def serialize(:i8, value),     do: <<value::8-signed>>
  def serialize(:i16, value),    do: <<value::16-signed>>
  def serialize(:i32, value),    do: <<value::32-signed>>
  def serialize(:i64, value),    do: <<value::64-signed>>
  def serialize(:double, value), do: <<value::signed-float>>
  def serialize(:string, value), do: [<<byte_size(value)::size(32)>>, value]
  def serialize(:binary, value), do: [<<byte_size(value)::size(32)>>, value]

  def serialize({:list, elem_type}, elems) when is_list(elems) do
    rest = Enum.map(elems, &serialize(elem_type, &1))
    [<<type_id(elem_type)::size(8), Enum.count(elems)::32-signed>>, rest]
  end
  def serialize({:set, elem_type}, %MapSet{} = elems) do
    rest = Enum.map(elems, &serialize(elem_type, &1))
    [<<type_id(elem_type)::size(8), Enum.count(elems)::32-signed>>, rest]
  end
  def serialize({:map, {key_type, val_type}}, map) when is_map(map) do
    elem_count = map_size(map)
    rest = Enum.map(map, fn {key, value} ->
      [serialize(key_type, key), serialize(val_type, value)]
    end)
    [<<type_id(key_type)::size(8), type_id(val_type)::size(8), elem_count::32-signed>>, rest]
  end
  def serialize(:struct, %{__struct__: mod} = struct) do
    mod.serialize(struct, :binary)
  end
  def serialize(:union, %{__struct__: mod} = struct) do
    mod.serialize(struct, :binary)
  end
  def serialize(:message_begin, {message_type, sequence_id, name}) do
    # Taken from https://erikvanoosten.github.io/thrift-missing-specification/#_message_encoding

    <<1::size(1), 1::size(15), 0::size(8),
    # ^^ Strange, I know. We could integrate the 8-bit zero here with the 5 bit zero below.
    0::size(5), from_message_type(message_type)::size(3),
    byte_size(name)::32-signed, name::binary, sequence_id::32-signed>>
  end
  def serialize(:application_exception, %TApplicationException{type: t} = exc) when is_atom(t) do
    serialize(:application_exception, %TApplicationException{exc | type: TApplicationException.exception_type_to_int(t)})
  end
  def serialize(:application_exception, %TApplicationException{message: message, type: type}) do
    <<@string::size(8), 1::16-signed, byte_size(message)::size(32), message::binary,
      @i32::size(8), 2::16-signed, type::32-signed, 0::size(8)>>
  end

  @doc """
  Deserializes a Thrift-encoded binary.
  """
  @spec deserialize(deserializable, binary) ::
    {:ok, {Thrift.message_type, message_seq_id, name :: String.t, binary}} |
    {:error, {atom, binary}}
  def deserialize(:message_begin, <<1::size(1), 1::size(15), _::size(8),
                  0::size(5), message_type::size(3),
                  name_size::32-signed, name::binary-size(name_size), sequence_id::32-signed, rest::binary>>) do
    {:ok, {to_message_type(message_type), sequence_id, name, rest}}
  end

  # the old format, see here:
  # https://erikvanoosten.github.io/thrift-missing-specification/#_message_encoding
  def deserialize(:message_begin, <<name_size::32-signed, name::binary-size(name_size),
                  0::size(5), message_type::size(3), sequence_id::32-signed, rest::binary>>) do
    {:ok, {to_message_type(message_type), sequence_id, name, rest}}
  end

  def deserialize(:message_begin, rest) do
    {:error, {:cant_decode_message, rest}}
  end

  def deserialize(:application_exception, binary) when is_binary(binary) do
    do_read_application_exception(binary, %TApplicationException{})
  end

  defp do_read_application_exception(
        <<@string::size(8),
        1::16-unsigned,
        message_size::32-signed,
        message::binary-size(message_size),
        rest::binary>>, accum) do
    # read the message string
    do_read_application_exception(rest, Map.put(accum, :message, message))
  end
  defp do_read_application_exception(
        <<@i32::size(8),
        2::16-unsigned,
        type::32-signed,
        rest::binary>>, accum) do
    # read the type
    exception_type = TApplicationException.exception_type(type)
    do_read_application_exception(rest, Map.put(accum, :type, exception_type))
  end
  defp do_read_application_exception(<<0>>, accum) do
    # read the field stop and return
    accum
  end
  defp do_read_application_exception(error, _) do
    message = "Could not decode TApplicationException, remaining was #{inspect error}"
    %TApplicationException{message: message,
                           type: TApplicationException.exception_type(7)}
  end

  @doc """
  Skips over the bytes representing a binary-encoded field.

  This is useful for jumping over unrecognized fields in the serialized byte
  stream.
  """
  @spec skip_field(binary, type_id) :: binary | :error
  def skip_field(<<_, rest::binary>>, unquote(@bool)), do: rest
  def skip_field(<<_, rest::binary>>, unquote(@byte)), do: rest
  def skip_field(<<_::signed-float, rest::binary>>, unquote(@double)), do: rest
  def skip_field(<<_::size(16), rest::binary>>, unquote(@i16)), do: rest
  def skip_field(<<_::size(32), rest::binary>>, unquote(@i32)), do: rest
  def skip_field(<<_::size(64), rest::binary>>, unquote(@i64)), do: rest
  def skip_field(<<length::32-signed, _::binary-size(length), rest::binary>>, unquote(@string)) do
    rest
  end
  def skip_field(<<rest::binary>>, unquote(@struct)) do
    rest |> skip_struct
  end
  def skip_field(<<key_type, val_type, length::size(32), rest::binary>>, unquote(@map)) do
    rest |> skip_map_entry(key_type, val_type, length)
  end
  def skip_field(<<elem_type, length::size(32), rest::binary>>, unquote(@set)) do
    rest |> skip_list_element(elem_type, length)
  end
  def skip_field(<<elem_type, length::size(32), rest::binary>>, unquote(@list)) do
    rest |> skip_list_element(elem_type, length)
  end
  def skip_field(_, _), do: :error

  defp skip_list_element(<<rest::binary>>, _, 0), do: rest
  defp skip_list_element(<<rest::binary>>, elem_type, remaining) do
    rest
    |> skip_field(elem_type)
    |> skip_list_element(elem_type, remaining - 1)
  end
  defp skip_list_element(:error, _, _), do: :error

  defp skip_map_entry(<<rest::binary>>, _, _, 0), do: rest
  defp skip_map_entry(<<rest::binary>>, key_type, val_type, remaining) do
    rest
    |> skip_field(key_type)
    |> skip_field(val_type)
    |> skip_map_entry(key_type, val_type, remaining - 1)
  end
  defp skip_map_entry(:error, _, _, _), do: :error

  defp skip_struct(<<0, rest::binary>>), do: rest
  defp skip_struct(<<type, _id::16-signed, rest::binary>>) do
    rest
    |> skip_field(type)
    |> skip_struct
  end
  defp skip_struct(_), do: :error
end


Improve naming in strip functions of "escript.build" Mix task

defmodule Mix.Tasks.Escript.Build do
  use Mix.Task
  use Bitwise, only_operators: true

  @shortdoc "Builds an escript for the project"

  @moduledoc ~S"""
  Builds an escript for the project.

  An escript is an executable that can be invoked from the
  command line.  An escript can run on any machine that has
  Erlang installed and by default does not require Elixir to
  be installed, as Elixir is embedded as part of the escript.

  This task guarantees the project and its dependencies are
  compiled and packages them inside an escript. Before invoking
  `mix escript.build`, it is only necessary to define a `:escript`
  key with a `:main_module` option in your `mix.exs` file:

      escript: [main_module: MyApp.CLI]

  By default, this task removes documentation and debugging
  chunks from the compiled `.beam` files to reduce the size of
  the escript. If this is not desired, check the `:strip_beams`
  option.

  > Note: escripts do not support projects and dependencies
  > that need to store or read artifacts from the priv directory.

  ## Command line options

    * `--force`      - forces compilation regardless of modification times
    * `--no-compile` - skips compilation to .beam files

  ## Configuration

  The following option must be specified in your `mix.exs` under `:escript`
  key:

    * `:main_module` - the module to be invoked once the escript starts.
      The module must contain a function named `main/1` that will receive the
      command line arguments as binaries.

  The remaining options can be specified to further customize the escript:

    * `:name` - the name of the generated escript.
      Defaults to app name.

    * `:path` - the path to write the escript to.
      Defaults to app name.

    * `:app` - the app to start with the escript.
      Defaults to app name. Set it to `nil` if no application should
      be started.

    * `:strip_beam` - if `true` strips BEAM code in the escript to remove chunks
      unnecessary at runtime, such as debug information and documentation.
      Defaults to `true`.

    * `:embed_elixir` - if `true` embeds Elixir and its children apps
      (`ex_unit`, `mix`, etc.) mentioned in the `:applications` list inside the
      `application/0` function in `mix.exs`.

      Defaults to `true` for Elixir projects, `false` for Erlang projects.

      Note: if you set this to `false` for an Elixir project, you will have to add paths to Elixir's
      `ebin` directories to `ERL_LIBS` environment variable when running the resulting escript, in
      order for the code loader to be able to find `:elixir` application and its children
      applications (if they are used).

    * `:shebang` - shebang interpreter directive used to execute the escript.
      Defaults to `"#! /usr/bin/env escript\n"`.

    * `:comment` - comment line to follow shebang directive in the escript.
      Defaults to `""`.

    * `:emu_args` - emulator arguments to embed in the escript file.
      Defaults to `""`.

  There is one project-level option that affects how the escript is generated:

    * `language: :elixir | :erlang` - set it to `:erlang` for Erlang projects
      managed by Mix. Doing so will ensure Elixir is not embedded by default.
      Your app will still be started as part of escript loading, with the
      config used during build.

  ## Example

      defmodule MyApp.Mixfile do
        use Mix.Project

        def project do
          [
            app: :my_app,
            version: "0.0.1",
            escript: escript()
          ]
        end

        def escript do
          [main_module: MyApp.CLI]
        end
      end

      defmodule MyApp.CLI do
        def main(_args) do
          IO.puts("Hello from MyApp!")
        end
      end

  """
  @switches [force: :boolean, compile: :boolean,
             deps_check: :boolean, archives_check: :boolean, elixir_version_check: :boolean]

  @spec run(OptionParser.argv) :: :ok | :noop
  def run(args) do
    Mix.Project.get!
    {opts, _} = OptionParser.parse!(args, strict: @switches)

    if Keyword.get(opts, :compile, true) do
      Mix.Task.run :compile, args
    end

    project  = Mix.Project.config
    language = Keyword.get(project, :language, :elixir)

    escriptize(project, language, Keyword.get(opts, :force, false))
  end

  defp escriptize(project, language, force?) do
    escript_opts = project[:escript] || []

    if Mix.Project.umbrella?() do
      Mix.raise "Building escripts for umbrella projects is unsupported"
    end

    script_name = Mix.Local.name_for(:escript, project)
    filename = escript_opts[:path] || script_name
    main = escript_opts[:main_module]
    files = project_files()

    cond do
      !script_name ->
        Mix.raise "Could not generate escript, no name given, " <>
          "set :name escript option or :app in the project settings"

      !main ->
        Mix.raise "Could not generate escript, please set :main_module " <>
          "in your project configuration (under :escript option) to a module that implements main/1"

      not Code.ensure_loaded?(main) ->
        Mix.raise "Could not generate escript, module #{main} defined as " <>
          ":main_module could not be loaded"

      force? or Mix.Utils.stale?(files, [filename]) ->
        app = Keyword.get(escript_opts, :app, project[:app])
        strip_beam? = Keyword.get(escript_opts, :strip_beam, true)
        escript_mod = String.to_atom(Atom.to_string(app) <> "_escript")

        beam_paths =
          [files, deps_files(), core_files(escript_opts, language)]
          |> Stream.concat
          |> prepare_beam_paths()
          |> Map.merge(consolidated_paths(project))

        tuples = gen_main(project, escript_mod, main, app, language) ++
                 read_beams(beam_paths)
        tuples = if strip_beam?, do: strip_beams(tuples), else: tuples

        case :zip.create('mem', tuples, [:memory]) do
          {:ok, {'mem', zip}} ->
            shebang = escript_opts[:shebang] || "#! /usr/bin/env escript\n"
            comment = build_comment(escript_opts[:comment])
            emu_args = build_emu_args(escript_opts[:emu_args], escript_mod)

            script = IO.iodata_to_binary([shebang, comment, emu_args, zip])
            File.mkdir_p!(Path.dirname(filename))
            File.write!(filename, script)
            set_perms(filename)
          {:error, error} ->
            Mix.raise "Error creating escript: #{error}"
        end

        Mix.shell.info "Generated escript #{filename} with MIX_ENV=#{Mix.env}"
        :ok
      true ->
        :noop
    end
  end

  defp project_files() do
    get_files(Mix.Project.app_path)
  end

  defp get_files(app) do
    Path.wildcard("#{app}/ebin/*.{app,beam}") ++
      (Path.wildcard("#{app}/priv/**/*") |> Enum.filter(&File.regular?/1))
  end

  defp set_perms(filename) do
    stat = File.stat!(filename)
    :ok  = File.chmod(filename, stat.mode ||| 0o111)
  end

  defp deps_files() do
    deps = Mix.Dep.cached()
    Enum.flat_map(deps, fn dep -> get_files(dep.opts[:build]) end)
  end

  defp core_files(escript_opts, language) do
    if Keyword.get(escript_opts, :embed_elixir, language == :elixir) do
      Enum.flat_map [:elixir | extra_apps()], &app_files/1
    else
      []
    end
  end

  defp extra_apps() do
    Mix.Project.config()[:app]
    |> extra_apps_in_app_tree()
    |> Enum.uniq()
  end

  defp extra_apps_in_app_tree(app) when app in [:kernel, :stdlib, :elixir] do
    []
  end

  defp extra_apps_in_app_tree(app) when app in [:eex, :ex_unit, :iex, :logger, :mix] do
    [app]
  end

  defp extra_apps_in_app_tree(app) do
    _ = Application.load(app)
    case Application.spec(app) do
      nil ->
        []
      spec ->
        applications = Keyword.get(spec, :applications, []) ++
                       Keyword.get(spec, :included_applications, [])
        Enum.flat_map(applications, &extra_apps_in_app_tree/1)
    end
  end

  defp app_files(app) do
    case :code.where_is_file('#{app}.app') do
      :non_existing -> Mix.raise "Could not find application #{app}"
      file -> get_files(Path.dirname(Path.dirname(file)))
    end
  end

  defp prepare_beam_paths(paths) do
    for path <- paths, into: %{}, do: {Path.basename(path), path}
  end

  defp read_beams(items) do
    items
    |> Enum.map(fn {basename, beam_path} ->
      {String.to_charlist(basename), File.read!(beam_path)}
    end)
  end

  defp strip_beams(tuples) do
    for {basename, maybe_beam} <- tuples do
      case Path.extname(basename) do
        ".beam" -> {basename, strip_beam(maybe_beam)}
        _ -> {basename, maybe_beam}
      end
    end
  end

  defp strip_beam(beam) when is_binary(beam) do
    {:ok, _, all_chunks} = :beam_lib.all_chunks(beam)
    strip_chunks = ['Abst', 'CInf', 'Dbgi', 'ExDc']
    preserved_chunks =
      for {name, _} = chunk <- all_chunks, name not in strip_chunks, do: chunk
    {:ok, content} = :beam_lib.build_module(preserved_chunks)
    compress(content)
  end

  defp compress(binary) do
    {:ok, file} = :ram_file.open(binary, [:write, :binary])
    {:ok, _} = :ram_file.compress(file)
    {:ok, binary} = :ram_file.get_file(file)
    :ok = :ram_file.close(file)
    binary
  end

  defp consolidated_paths(config) do
    if config[:consolidate_protocols] do
      Mix.Project.consolidation_path(config)
      |> Path.join("*")
      |> Path.wildcard()
      |> prepare_beam_paths()
    else
      %{}
    end
  end

  defp build_comment(user_comment) do
    "%% #{user_comment}\n"
  end

  defp build_emu_args(user_args, escript_mod) do
    "%%! -escript main #{escript_mod} #{user_args}\n"
  end

  defp gen_main(project, name, module, app, language) do
    config =
      if File.regular?(project[:config_path]) do
        Macro.escape Mix.Config.read!(project[:config_path])
      else
        []
      end

    module_body = quote do
      @module unquote(module)
      @config unquote(config)
      @app unquote(app)

      @spec main(OptionParser.argv) :: any
      def main(args) do
        unquote(main_body_for(language))
      end

      defp load_config(config) do
        :lists.foreach(fn {app, kw} ->
          :lists.foreach(fn {k, v} ->
            :application.set_env(app, k, v, persistent: true)
          end, kw)
        end, config)
        :ok
      end

      defp start_app(nil) do
        :ok
      end

      defp start_app(app) do
        case :application.ensure_all_started(app) do
          {:ok, _} -> :ok
          {:error, {app, reason}} ->
            formatted_error = case :code.ensure_loaded(Application) do
              {:module, Application} -> Application.format_error(reason)
              {:error, _} -> :io_lib.format('~p', [reason])
            end
            io_error ["Could not start application ",
                      :erlang.atom_to_binary(app, :utf8),
                      ": ", formatted_error, ?\n]
            :erlang.halt(1)
        end
      end

      defp io_error(message) do
        :io.put_chars(:standard_error, message)
      end
    end

    {:module, ^name, binary, _} = Module.create(name, module_body, Macro.Env.location(__ENV__))
    [{'#{name}.beam', binary}]
  end

  defp main_body_for(:elixir) do
    quote do
      erl_version = :erlang.system_info(:otp_release)

      case :string.to_integer(erl_version) do
        {num, _} when num >= 18 -> nil
        _ ->
          io_error ["Incompatible Erlang/OTP release: ", erl_version,
                    ".\nThis escript requires at least Erlang/OTP 18.0\n"]
          :erlang.halt(1)
      end

      case :application.ensure_all_started(:elixir) do
        {:ok, _} ->
          load_config(@config)
          start_app(@app)
          args = Enum.map(args, &List.to_string(&1))
          Kernel.CLI.run fn _ -> @module.main(args) end, true
        error ->
          io_error ["Failed to start Elixir.\n", :io_lib.format('error: ~p~n', [error])]
          :erlang.halt(1)
      end
    end
  end

  defp main_body_for(:erlang) do
    quote do
      load_config(@config)
      start_app(@app)
      @module.main(args)
    end
  end
end

defmodule Mix.Tasks.Escript.Build do
  use Mix.Task
  use Bitwise, only_operators: true

  @shortdoc "Builds an escript for the project"

  @moduledoc ~S"""
  Builds an escript for the project.

  An escript is an executable that can be invoked from the
  command line.  An escript can run on any machine that has
  Erlang installed and by default does not require Elixir to
  be installed, as Elixir is embedded as part of the escript.

  This task guarantees the project and its dependencies are
  compiled and packages them inside an escript. Before invoking
  `mix escript.build`, it is only necessary to define a `:escript`
  key with a `:main_module` option in your `mix.exs` file:

      escript: [main_module: MyApp.CLI]

  By default, this task removes documentation and debugging
  chunks from the compiled `.beam` files to reduce the size of
  the escript. If this is not desired, check the `:strip_beams`
  option.

  > Note: escripts do not support projects and dependencies
  > that need to store or read artifacts from the priv directory.

  ## Command line options

    * `--force`      - forces compilation regardless of modification times
    * `--no-compile` - skips compilation to .beam files

  ## Configuration

  The following option must be specified in your `mix.exs` under `:escript`
  key:

    * `:main_module` - the module to be invoked once the escript starts.
      The module must contain a function named `main/1` that will receive the
      command line arguments as binaries.

  The remaining options can be specified to further customize the escript:

    * `:name` - the name of the generated escript.
      Defaults to app name.

    * `:path` - the path to write the escript to.
      Defaults to app name.

    * `:app` - the app to start with the escript.
      Defaults to app name. Set it to `nil` if no application should
      be started.

    * `:strip_beam` - if `true` strips BEAM code in the escript to remove chunks
      unnecessary at runtime, such as debug information and documentation.
      Defaults to `true`.

    * `:embed_elixir` - if `true` embeds Elixir and its children apps
      (`ex_unit`, `mix`, etc.) mentioned in the `:applications` list inside the
      `application/0` function in `mix.exs`.

      Defaults to `true` for Elixir projects, `false` for Erlang projects.

      Note: if you set this to `false` for an Elixir project, you will have to add paths to Elixir's
      `ebin` directories to `ERL_LIBS` environment variable when running the resulting escript, in
      order for the code loader to be able to find `:elixir` application and its children
      applications (if they are used).

    * `:shebang` - shebang interpreter directive used to execute the escript.
      Defaults to `"#! /usr/bin/env escript\n"`.

    * `:comment` - comment line to follow shebang directive in the escript.
      Defaults to `""`.

    * `:emu_args` - emulator arguments to embed in the escript file.
      Defaults to `""`.

  There is one project-level option that affects how the escript is generated:

    * `language: :elixir | :erlang` - set it to `:erlang` for Erlang projects
      managed by Mix. Doing so will ensure Elixir is not embedded by default.
      Your app will still be started as part of escript loading, with the
      config used during build.

  ## Example

      defmodule MyApp.Mixfile do
        use Mix.Project

        def project do
          [
            app: :my_app,
            version: "0.0.1",
            escript: escript()
          ]
        end

        def escript do
          [main_module: MyApp.CLI]
        end
      end

      defmodule MyApp.CLI do
        def main(_args) do
          IO.puts("Hello from MyApp!")
        end
      end

  """
  @switches [force: :boolean, compile: :boolean,
             deps_check: :boolean, archives_check: :boolean, elixir_version_check: :boolean]

  @spec run(OptionParser.argv) :: :ok | :noop
  def run(args) do
    Mix.Project.get!
    {opts, _} = OptionParser.parse!(args, strict: @switches)

    if Keyword.get(opts, :compile, true) do
      Mix.Task.run :compile, args
    end

    project  = Mix.Project.config
    language = Keyword.get(project, :language, :elixir)

    escriptize(project, language, Keyword.get(opts, :force, false))
  end

  defp escriptize(project, language, force?) do
    escript_opts = project[:escript] || []

    if Mix.Project.umbrella?() do
      Mix.raise "Building escripts for umbrella projects is unsupported"
    end

    script_name = Mix.Local.name_for(:escript, project)
    filename = escript_opts[:path] || script_name
    main = escript_opts[:main_module]
    files = project_files()

    cond do
      !script_name ->
        Mix.raise "Could not generate escript, no name given, " <>
          "set :name escript option or :app in the project settings"

      !main ->
        Mix.raise "Could not generate escript, please set :main_module " <>
          "in your project configuration (under :escript option) to a module that implements main/1"

      not Code.ensure_loaded?(main) ->
        Mix.raise "Could not generate escript, module #{main} defined as " <>
          ":main_module could not be loaded"

      force? or Mix.Utils.stale?(files, [filename]) ->
        app = Keyword.get(escript_opts, :app, project[:app])
        strip_beam? = Keyword.get(escript_opts, :strip_beam, true)
        escript_mod = String.to_atom(Atom.to_string(app) <> "_escript")

        beam_paths =
          [files, deps_files(), core_files(escript_opts, language)]
          |> Stream.concat
          |> prepare_beam_paths()
          |> Map.merge(consolidated_paths(project))

        tuples = gen_main(project, escript_mod, main, app, language) ++
                 read_beams(beam_paths)
        tuples = if strip_beam?, do: strip_beams(tuples), else: tuples

        case :zip.create('mem', tuples, [:memory]) do
          {:ok, {'mem', zip}} ->
            shebang = escript_opts[:shebang] || "#! /usr/bin/env escript\n"
            comment = build_comment(escript_opts[:comment])
            emu_args = build_emu_args(escript_opts[:emu_args], escript_mod)

            script = IO.iodata_to_binary([shebang, comment, emu_args, zip])
            File.mkdir_p!(Path.dirname(filename))
            File.write!(filename, script)
            set_perms(filename)
          {:error, error} ->
            Mix.raise "Error creating escript: #{error}"
        end

        Mix.shell.info "Generated escript #{filename} with MIX_ENV=#{Mix.env}"
        :ok
      true ->
        :noop
    end
  end

  defp project_files() do
    get_files(Mix.Project.app_path)
  end

  defp get_files(app) do
    Path.wildcard("#{app}/ebin/*.{app,beam}") ++
      (Path.wildcard("#{app}/priv/**/*") |> Enum.filter(&File.regular?/1))
  end

  defp set_perms(filename) do
    stat = File.stat!(filename)
    :ok  = File.chmod(filename, stat.mode ||| 0o111)
  end

  defp deps_files() do
    deps = Mix.Dep.cached()
    Enum.flat_map(deps, fn dep -> get_files(dep.opts[:build]) end)
  end

  defp core_files(escript_opts, language) do
    if Keyword.get(escript_opts, :embed_elixir, language == :elixir) do
      Enum.flat_map [:elixir | extra_apps()], &app_files/1
    else
      []
    end
  end

  defp extra_apps() do
    Mix.Project.config()[:app]
    |> extra_apps_in_app_tree()
    |> Enum.uniq()
  end

  defp extra_apps_in_app_tree(app) when app in [:kernel, :stdlib, :elixir] do
    []
  end

  defp extra_apps_in_app_tree(app) when app in [:eex, :ex_unit, :iex, :logger, :mix] do
    [app]
  end

  defp extra_apps_in_app_tree(app) do
    _ = Application.load(app)
    case Application.spec(app) do
      nil ->
        []
      spec ->
        applications = Keyword.get(spec, :applications, []) ++
                       Keyword.get(spec, :included_applications, [])
        Enum.flat_map(applications, &extra_apps_in_app_tree/1)
    end
  end

  defp app_files(app) do
    case :code.where_is_file('#{app}.app') do
      :non_existing -> Mix.raise "Could not find application #{app}"
      file -> get_files(Path.dirname(Path.dirname(file)))
    end
  end

  defp prepare_beam_paths(paths) do
    for path <- paths, into: %{}, do: {Path.basename(path), path}
  end

  defp read_beams(items) do
    items
    |> Enum.map(fn {basename, beam_path} ->
      {String.to_charlist(basename), File.read!(beam_path)}
    end)
  end

  defp strip_beams(tuples) do
    for {basename, maybe_beam} <- tuples do
      case Path.extname(basename) do
        ".beam" -> {basename, strip_beam(maybe_beam)}
        _ -> {basename, maybe_beam}
      end
    end
  end

  defp strip_beam(beam) when is_binary(beam) do
    {:ok, _, all_chunks} = :beam_lib.all_chunks(beam)
    filtered_chunks = ['Abst', 'CInf', 'Dbgi', 'ExDc']
    significant_chunks =
      for {name, _} = pair <- all_chunks, name not in filtered_chunks, do: pair
    {:ok, built_module} = :beam_lib.build_module(significant_chunks)
    compress(built_module)
  end

  defp compress(binary0) do
    {:ok, fd} = :ram_file.open(binary0, [:write, :binary])
    {:ok, _} = :ram_file.compress(fd)
    {:ok, binary} = :ram_file.get_file(fd)
    :ok = :ram_file.close(fd)
    binary
  end

  defp consolidated_paths(config) do
    if config[:consolidate_protocols] do
      Mix.Project.consolidation_path(config)
      |> Path.join("*")
      |> Path.wildcard()
      |> prepare_beam_paths()
    else
      %{}
    end
  end

  defp build_comment(user_comment) do
    "%% #{user_comment}\n"
  end

  defp build_emu_args(user_args, escript_mod) do
    "%%! -escript main #{escript_mod} #{user_args}\n"
  end

  defp gen_main(project, name, module, app, language) do
    config =
      if File.regular?(project[:config_path]) do
        Macro.escape Mix.Config.read!(project[:config_path])
      else
        []
      end

    module_body = quote do
      @module unquote(module)
      @config unquote(config)
      @app unquote(app)

      @spec main(OptionParser.argv) :: any
      def main(args) do
        unquote(main_body_for(language))
      end

      defp load_config(config) do
        :lists.foreach(fn {app, kw} ->
          :lists.foreach(fn {k, v} ->
            :application.set_env(app, k, v, persistent: true)
          end, kw)
        end, config)
        :ok
      end

      defp start_app(nil) do
        :ok
      end

      defp start_app(app) do
        case :application.ensure_all_started(app) do
          {:ok, _} -> :ok
          {:error, {app, reason}} ->
            formatted_error = case :code.ensure_loaded(Application) do
              {:module, Application} -> Application.format_error(reason)
              {:error, _} -> :io_lib.format('~p', [reason])
            end
            io_error ["Could not start application ",
                      :erlang.atom_to_binary(app, :utf8),
                      ": ", formatted_error, ?\n]
            :erlang.halt(1)
        end
      end

      defp io_error(message) do
        :io.put_chars(:standard_error, message)
      end
    end

    {:module, ^name, binary, _} = Module.create(name, module_body, Macro.Env.location(__ENV__))
    [{'#{name}.beam', binary}]
  end

  defp main_body_for(:elixir) do
    quote do
      erl_version = :erlang.system_info(:otp_release)

      case :string.to_integer(erl_version) do
        {num, _} when num >= 18 -> nil
        _ ->
          io_error ["Incompatible Erlang/OTP release: ", erl_version,
                    ".\nThis escript requires at least Erlang/OTP 18.0\n"]
          :erlang.halt(1)
      end

      case :application.ensure_all_started(:elixir) do
        {:ok, _} ->
          load_config(@config)
          start_app(@app)
          args = Enum.map(args, &List.to_string(&1))
          Kernel.CLI.run fn _ -> @module.main(args) end, true
        error ->
          io_error ["Failed to start Elixir.\n", :io_lib.format('error: ~p~n', [error])]
          :erlang.halt(1)
      end
    end
  end

  defp main_body_for(:erlang) do
    quote do
      load_config(@config)
      start_app(@app)
      @module.main(args)
    end
  end
end


Update text output of Serum.Template.Compiler.Include.make_graph/1

defmodule Serum.Template.Compiler.Include do
  @moduledoc false

  _moduledocp = "Provides functions for expanding includes in templates."

  alias Serum.Result
  alias Serum.Template
  alias Serum.Template.Storage, as: TS

  @typep context :: %{
           template: Template.t(),
           stack: [binary()],
           error: Result.t() | nil
         }

  @spec expand(Template.t()) :: Result.t(Template.t())
  def expand(template) do
    initial_context = %{
      template: template,
      stack: [],
      error: nil
    }

    case do_expand(template, initial_context) do
      {new_template, %{error: nil}} -> {:ok, new_template}
      {_, %{error: {:error, _} = error}} -> error
    end
  end

  @spec do_expand(Template.t(), context()) :: {Template.t(), context()}
  defp do_expand(template, context)
  defp do_expand(%Template{include_resolved?: true} = t, ctx), do: {t, ctx}

  defp do_expand(%Template{name: name, type: type, ast: ast} = t, context) do
    next_context = %{context | template: t, stack: [name | context.stack]}

    with :ok <- check_cycle(name, context),
         walk_result = Macro.prewalk(ast, next_context, &prewalk_fun/2),
         {new_ast, %{error: nil} = new_context} <- walk_result do
      new_template = %Template{t | ast: new_ast, include_resolved?: true}

      TS.put(name, type, new_template)

      {new_template, new_context}
    else
      {:error, _} = error -> {t, %{context | error: error}}
      {_, %{error: {:error, _} = error}} -> {t, %{context | error: error}}
    end
  end

  @spec prewalk_fun(Macro.t(), context()) :: {Macro.t(), context()}
  defp prewalk_fun(ast, context)
  defp prewalk_fun(ast, %{error: e} = ctx) when not is_nil(e), do: {ast, ctx}

  defp prewalk_fun({:include, _, [arg]} = ast, context) do
    case TS.get(arg, :include) do
      %Template{} = include ->
        {new_include, new_context} = do_expand(include, context)

        {quote(do: (fn -> unquote(new_include.ast) end).()), new_context}

      nil ->
        message = "include not found: \"#{arg}\""

        {ast, %{context | error: {:error, {message, context.template.file, 0}}}}
    end
  end

  defp prewalk_fun(ast, context), do: {ast, context}

  @spec check_cycle(binary(), context()) :: Result.t()
  defp check_cycle(name, context) do
    if name in context.stack do
      graph =
        context.stack
        |> Enum.reverse()
        |> Enum.drop_while(&(&1 != name))
        |> make_graph()

      message = [
        "cycle detected while expanding includes:\n",
        graph,
        "  Cycles are not allowed when recursively including templates.\n",
        "  Please refactor your templates to break the cycle.\n",
        "  Alternatively, you can use the render/1,2 template helper.\n"
      ]

      {:error, {IO.iodata_to_binary(message), context.template.file, 0}}
    else
      :ok
    end
  end

  endl = [:reset, ?\n]
  top = ["    ", :red, "\u256d\u2500\u2500\u2500\u2500\u2500\u256e", endl]
  arrow = ["    ", :red, "\u2502     \u2193", endl]
  first_text = ["    ", :red, "\u2502    ", :yellow]
  rest_text = ["    ", :red, "\u2502    "]
  bottom1 = ["    ", :red, "\u2502     \u2502", endl]
  bottom2 = ["    ", :red, "\u2570\u2500\u2500\u2500\u2500\u2500\u256f", endl]

  @spec make_graph([binary()]) :: iodata()
  defp make_graph([first | rest]) do
    rest_graph =
      Enum.map(rest, fn name ->
        [
          unquote(arrow),
          unquote(rest_text),
          name,
          unquote(endl)
        ]
      end)

    [
      unquote(top),
      unquote(arrow),
      unquote(first_text),
      first,
      unquote(endl),
      rest_graph,
      unquote(bottom1),
      unquote(bottom2)
    ]
    |> IO.ANSI.format()
  end
end

defmodule Serum.Template.Compiler.Include do
  @moduledoc false

  _moduledocp = "Provides functions for expanding includes in templates."

  alias Serum.Result
  alias Serum.Template
  alias Serum.Template.Storage, as: TS

  @typep context :: %{
           template: Template.t(),
           stack: [binary()],
           error: Result.t() | nil
         }

  @spec expand(Template.t()) :: Result.t(Template.t())
  def expand(template) do
    initial_context = %{
      template: template,
      stack: [],
      error: nil
    }

    case do_expand(template, initial_context) do
      {new_template, %{error: nil}} -> {:ok, new_template}
      {_, %{error: {:error, _} = error}} -> error
    end
  end

  @spec do_expand(Template.t(), context()) :: {Template.t(), context()}
  defp do_expand(template, context)
  defp do_expand(%Template{include_resolved?: true} = t, ctx), do: {t, ctx}

  defp do_expand(%Template{name: name, type: type, ast: ast} = t, context) do
    next_context = %{context | template: t, stack: [name | context.stack]}

    with :ok <- check_cycle(name, context),
         walk_result = Macro.prewalk(ast, next_context, &prewalk_fun/2),
         {new_ast, %{error: nil} = new_context} <- walk_result do
      new_template = %Template{t | ast: new_ast, include_resolved?: true}

      TS.put(name, type, new_template)

      {new_template, new_context}
    else
      {:error, _} = error -> {t, %{context | error: error}}
      {_, %{error: {:error, _} = error}} -> {t, %{context | error: error}}
    end
  end

  @spec prewalk_fun(Macro.t(), context()) :: {Macro.t(), context()}
  defp prewalk_fun(ast, context)
  defp prewalk_fun(ast, %{error: e} = ctx) when not is_nil(e), do: {ast, ctx}

  defp prewalk_fun({:include, _, [arg]} = ast, context) do
    case TS.get(arg, :include) do
      %Template{} = include ->
        {new_include, new_context} = do_expand(include, context)

        {quote(do: (fn -> unquote(new_include.ast) end).()), new_context}

      nil ->
        message = "include not found: \"#{arg}\""

        {ast, %{context | error: {:error, {message, context.template.file, 0}}}}
    end
  end

  defp prewalk_fun(ast, context), do: {ast, context}

  @spec check_cycle(binary(), context()) :: Result.t()
  defp check_cycle(name, context) do
    if name in context.stack do
      graph =
        context.stack
        |> Enum.reverse()
        |> Enum.drop_while(&(&1 != name))
        |> make_graph()

      message = [
        "cycle detected while expanding includes:\n",
        graph,
        "  Cycles are not allowed when recursively including templates.\n",
        "  Please refactor your templates to break the cycle.\n",
        "  Alternatively, you can use the render/1,2 template helper.\n"
      ]

      {:error, {IO.iodata_to_binary(message), context.template.file, 0}}
    else
      :ok
    end
  end

  top = "    \x1b[31m\u256d\u2500\u2500\u2500\u2500\u2500\u256e\x1b[m\n"
  arrow = "    \x1b[31m\u2502     \u2193\x1b[m\n"
  first_text = "    \x1b[31m\u2502    \x1b[33m"
  rest_text = "    \x1b[31m\u2502    "
  bottom1 = "    \x1b[31m\u2502     \u2502\x1b[m\n"
  bottom2 = "    \x1b[31m\u2570\u2500\u2500\u2500\u2500\u2500\u256f\x1b[m\n"

  @spec make_graph([binary()]) :: iodata()
  defp make_graph([first | rest]) do
    rest_graph =
      Enum.map(rest, fn name ->
        [
          unquote(arrow),
          unquote(rest_text),
          name,
          "\x1b[m\n"
        ]
      end)

    [
      unquote(top),
      unquote(arrow),
      unquote(first_text),
      first,
      "\x1b[m\n",
      rest_graph,
      unquote(bottom1),
      unquote(bottom2)
    ]
  end
end


Use actual emojis so phone view and fallback shows them.

defmodule SlackCoder.Github.Notification do
  import SlackCoder.Github.TimeHelper
  import StubAlias
  stub_alias SlackCoder.Users.User
  stub_alias SlackCoder.Users.Supervisor, as: Users
  alias SlackCoder.Travis.Build

  defstruct [:slack_user, :type, :called_out?, :message_for, :message]

  ### NOTIFICATIONS ###

  def conflict(pr) do
    case user_for_pr(pr) |> slack_user_with_monitors do
      [message_for | slack_users] ->
        message = %{
                    attachments: [
                      %{
                        author_name: "✖ MERGE CONFLICTS",
                        color: "#999999",
                        fallback: "✖ MERGE CONFLICTS #{pr.title}",
                        title: pr.title,
                        title_link: pr.html_url
                      }
                    ]
                  }
        notify(slack_users, :conflict, message_for, message, pr)
        pr
      _ -> pr
    end
  end

  def failure(pr) do
    case user_for_pr(pr) |> slack_user_with_monitors do
      [message_for | slack_users] ->
        %{"build_id" => build_id} = Regex.named_captures(~r/(?<build_id>\d+)/, pr.build_url)
        failed_jobs = SlackCoder.Travis.build_info(pr.owner, pr.repo, build_id)
                      |> Enum.filter(&match?(%Build{result: :failure}, &1))
                      |> Enum.map(fn build ->
                        SlackCoder.Travis.job_log(build)
                      end)
        message = %{
                    attachments: [
                      %{
                        author_name: "👎 FAILURE",
                        color: "#FF0000",
                        fallback: "👎 FAILURE #{pr.title}",
                        title: pr.title,
                        title_link: pr.html_url,
                        text: """
                        <#{pr.build_url}|Travis Build>
                        ```
                        #{Enum.join(failed_jobs, "\n")}
                        ```
                        """,
                        mrkdwn_in: ["text"],
                        footer: "#{Enum.sum(Enum.map(failed_jobs, &(Enum.count(&1.rspec))))} rspec, #{Enum.sum(Enum.map(failed_jobs, &(Enum.count(&1.cucumber))))} cucumber"
                      }
                    ]
                  }
        notify(slack_users, :fail, message_for, message, pr)
        pr
      _ -> pr
    end
  end

  def success(pr) do
    case user_for_pr(pr) |> slack_user_with_monitors do
      [message_for | slack_users] ->
        message = %{
                    attachments: [
                      %{
                        fallback: "🎉 SUCCESS #{pr.title}",
                        author_name: "🎉 SUCCESS",
                        color: "#77DD33",
                        title: pr.title,
                        title_link: pr.html_url
                      }
                    ]
                  }
        notify(slack_users, :pass, message_for, message, pr)
        pr
      _ -> pr
    end
  end

  def merged(pr) do
    case user_for_pr(pr) |> slack_user_with_monitors do
      [message_for | slack_users] ->
        message = %{
                    attachments: [
                      %{
                        fallback: "😈 MERGED #{pr.title}",
                        author_name: "😈 MERGED",
                        color: "#9a009a",
                        title: pr.title,
                        title_link: pr.html_url
                      }
                    ]
                  }
        notify(slack_users, :merge, message_for, message, pr)
        pr
      _ -> pr
    end
  end

  def closed(pr) do
    case user_for_pr(pr) |> slack_user_with_monitors do
      [message_for | slack_users] ->
        message = %{
                    attachments: [
                      %{
                        fallback: "😡 CLOSED #{pr.title}",
                        author_name: "😡 CLOSED",
                        color: "#FF4500",
                        title: pr.title,
                        title_link: pr.html_url
                      }
                    ]
                  }
        notify(slack_users, :closed, message_for, message, pr)
        pr
      _ -> pr
    end
  end

  def stale(pr) do
    case user_for_pr(pr) |> slack_user_with_monitors do
      [message_for | slack_users] ->
        stale_hours = Timex.diff(pr.latest_comment, now(), :hours)
        message = %{
                    attachments: [
                      %{
                        fallback: "💩 STALE #{pr.title}",
                        author_name: "💩 STALE",
                        color: "#DDDDDD",
                        title: pr.title,
                        title_link: pr.html_url,
                        text: "Stale for *#{stale_hours}* hours",
                        mrkdwn_in: ["text"]
                      }
                    ]
                  }
        notify(slack_users, :stale, message_for, message, pr)
        pr
      _ -> pr
    end
  end

  def unstale(pr) do
    case user_for_pr(pr) |> slack_user_with_monitors do
      [message_for | slack_users] ->
        message = %{
                    attachments: [
                      %{
                        fallback: "✉ ACTIVE #{pr.title}",
                        author_name: "✉ ACTIVE",
                        color: "#000000",
                        title: pr.title,
                        title_link: pr.html_url
                      }
                    ]
                  }
        notify(slack_users, :unstale, message_for, message, pr)
        pr
      _ -> pr
    end
  end

  def open(pr) do
    case user_for_pr(pr) |> slack_user_with_monitors do
      [message_for | slack_users] ->
        message = %{
                    attachments: [
                      %{
                        fallback: "👀 OPENED #{pr.title}",
                        author_name: "👀 OPENED",
                        color: "#0000FF",
                        title: pr.title,
                        title_link: pr.html_url
                      }
                    ]
                  }
        notify(slack_users, :open, message_for, message, pr)
        pr
      _ -> pr
    end
  end

  ### HELPERS ###

  if Application.get_env(:slack_coder, :notifications)[:always_allow] do
    def can_send_notifications?(), do: true
  else
    @weekdays (Application.get_env(:slack_coder, :notifications)[:days] || [1,2,3,4,5]) |> Enum.map(&Timex.day_name(&1))
    @min_hour Application.get_env(:slack_coder, :notifications)[:min_hour] || 8
    @max_hour Application.get_env(:slack_coder, :notifications)[:max_hour] || 17
    def can_send_notifications?() do
      day_name = now |> Timex.weekday |> Timex.day_name
      day_name in @weekdays && now.hour >= @min_hour && now.hour <= @max_hour
    end
  end

  def notify([slack_user | users], type, message_for, message, pr) do
    notify(slack_user, type, message_for, message, pr)
    notify(users, type, message_for, message, pr)
  end
  def notify([], type, message_for, message, _pr) do
    Task.start fn ->
      notify(%__MODULE__{
        slack_user: message_for,
        type: type,
        message_for: message_for,
        message: message
      })
    end
  end
  def notify(slack_user, type, message_for, message, _pr) do
    Task.start fn ->
      notify(%__MODULE__{
        slack_user: slack_user,
        type: type,
        message_for: message_for,
        message: message
      })
    end
  end

  def notify(notification = %__MODULE__{slack_user: slack_user}) do
    SlackCoder.Slack.send_to(slack_user, notification)
  end

  def slack_user_with_monitors(nil), do: []
  def slack_user_with_monitors(user) do
    message_for = user.slack
    users = user_with_monitors(user, :slack)
    Enum.uniq([message_for | users])
  end

  def github_user_with_monitors(nil), do: []
  def github_user_with_monitors(user) do
    message_for = user.github
    users = user_with_monitors(user, :github)
    Enum.uniq([message_for | users])
  end

  defp user_with_monitors(user, map_to) do
    Users.users
    |> Stream.filter(&(user.github in &1.monitors))
    |> Enum.map(&(Map.get(&1, map_to)))
  end

  def user_for_pr(pr) do
    Users.user(pr.github_user)
    |> User.get
  end

end

defmodule SlackCoder.Github.Notification do
  import SlackCoder.Github.TimeHelper
  import StubAlias
  stub_alias SlackCoder.Users.User
  stub_alias SlackCoder.Users.Supervisor, as: Users
  alias SlackCoder.Travis.Build

  defstruct [:slack_user, :type, :called_out?, :message_for, :message]

  ### NOTIFICATIONS ###

  def conflict(pr) do
    case user_for_pr(pr) |> slack_user_with_monitors do
      [message_for | slack_users] ->
        message = %{
                    attachments: [
                      %{
                        author_name: ":heavy_multiplication_x: MERGE CONFLICTS",
                        color: "#999999",
                        fallback: "MERGE CONFLICTS #{pr.title}",
                        title: pr.title,
                        title_link: pr.html_url
                      }
                    ]
                  }
        notify(slack_users, :conflict, message_for, message, pr)
        pr
      _ -> pr
    end
  end

  def failure(pr) do
    case user_for_pr(pr) |> slack_user_with_monitors do
      [message_for | slack_users] ->
        %{"build_id" => build_id} = Regex.named_captures(~r/(?<build_id>\d+)/, pr.build_url)
        failed_jobs = SlackCoder.Travis.build_info(pr.owner, pr.repo, build_id)
                      |> Enum.filter(&match?(%Build{result: :failure}, &1))
                      |> Enum.map(fn build ->
                        SlackCoder.Travis.job_log(build)
                      end)
        message = %{
                    attachments: [
                      %{
                        author_name: ":-1: FAILURE",
                        color: "#FF0000",
                        fallback: "FAILURE #{pr.title}",
                        title: pr.title,
                        title_link: pr.html_url,
                        text: """
                        <#{pr.build_url}|Travis Build>
                        ```
                        #{Enum.join(failed_jobs, "\n")}
                        ```
                        """,
                        mrkdwn_in: ["text"],
                        footer: "#{Enum.sum(Enum.map(failed_jobs, &(Enum.count(&1.rspec))))} rspec, #{Enum.sum(Enum.map(failed_jobs, &(Enum.count(&1.cucumber))))} cucumber"
                      }
                    ]
                  }
        notify(slack_users, :fail, message_for, message, pr)
        pr
      _ -> pr
    end
  end

  def success(pr) do
    case user_for_pr(pr) |> slack_user_with_monitors do
      [message_for | slack_users] ->
        message = %{
                    attachments: [
                      %{
                        fallback: "SUCCESS #{pr.title}",
                        author_name: ":tada: SUCCESS",
                        color: "#77DD33",
                        title: pr.title,
                        title_link: pr.html_url
                      }
                    ]
                  }
        notify(slack_users, :pass, message_for, message, pr)
        pr
      _ -> pr
    end
  end

  def merged(pr) do
    case user_for_pr(pr) |> slack_user_with_monitors do
      [message_for | slack_users] ->
        message = %{
                    attachments: [
                      %{
                        fallback: "MERGED #{pr.title}",
                        author_name: ":smiling_imp: MERGED",
                        color: "#9a009a",
                        title: pr.title,
                        title_link: pr.html_url
                      }
                    ]
                  }
        notify(slack_users, :merge, message_for, message, pr)
        pr
      _ -> pr
    end
  end

  def closed(pr) do
    case user_for_pr(pr) |> slack_user_with_monitors do
      [message_for | slack_users] ->
        message = %{
                    attachments: [
                      %{
                        fallback: "CLOSED #{pr.title}",
                        author_name: ":rage: CLOSED",
                        color: "#FF4500",
                        title: pr.title,
                        title_link: pr.html_url
                      }
                    ]
                  }
        notify(slack_users, :closed, message_for, message, pr)
        pr
      _ -> pr
    end
  end

  def stale(pr) do
    case user_for_pr(pr) |> slack_user_with_monitors do
      [message_for | slack_users] ->
        stale_hours = Timex.diff(pr.latest_comment, now(), :hours)
        message = %{
                    attachments: [
                      %{
                        fallback: "STALE #{pr.title}",
                        author_name: ":hankey: STALE",
                        color: "#DDDDDD",
                        title: pr.title,
                        title_link: pr.html_url,
                        text: "Stale for *#{stale_hours}* hours",
                        mrkdwn_in: ["text"]
                      }
                    ]
                  }
        notify(slack_users, :stale, message_for, message, pr)
        pr
      _ -> pr
    end
  end

  def unstale(pr) do
    case user_for_pr(pr) |> slack_user_with_monitors do
      [message_for | slack_users] ->
        message = %{
                    attachments: [
                      %{
                        fallback: "ACTIVE #{pr.title}",
                        author_name: ":email: ACTIVE",
                        color: "#000000",
                        title: pr.title,
                        title_link: pr.html_url
                      }
                    ]
                  }
        notify(slack_users, :unstale, message_for, message, pr)
        pr
      _ -> pr
    end
  end

  def open(pr) do
    case user_for_pr(pr) |> slack_user_with_monitors do
      [message_for | slack_users] ->
        message = %{
                    attachments: [
                      %{
                        fallback: "OPENED #{pr.title}",
                        author_name: "👀 OPENED",
                        color: "#0000FF",
                        title: pr.title,
                        title_link: pr.html_url
                      }
                    ]
                  }
        notify(slack_users, :open, message_for, message, pr)
        pr
      _ -> pr
    end
  end

  ### HELPERS ###

  if Application.get_env(:slack_coder, :notifications)[:always_allow] do
    def can_send_notifications?(), do: true
  else
    @weekdays (Application.get_env(:slack_coder, :notifications)[:days] || [1,2,3,4,5]) |> Enum.map(&Timex.day_name(&1))
    @min_hour Application.get_env(:slack_coder, :notifications)[:min_hour] || 8
    @max_hour Application.get_env(:slack_coder, :notifications)[:max_hour] || 17
    def can_send_notifications?() do
      day_name = now |> Timex.weekday |> Timex.day_name
      day_name in @weekdays && now.hour >= @min_hour && now.hour <= @max_hour
    end
  end

  def notify([slack_user | users], type, message_for, message, pr) do
    notify(slack_user, type, message_for, message, pr)
    notify(users, type, message_for, message, pr)
  end
  def notify([], type, message_for, message, _pr) do
    Task.start fn ->
      notify(%__MODULE__{
        slack_user: message_for,
        type: type,
        message_for: message_for,
        message: message
      })
    end
  end
  def notify(slack_user, type, message_for, message, _pr) do
    Task.start fn ->
      notify(%__MODULE__{
        slack_user: slack_user,
        type: type,
        message_for: message_for,
        message: message
      })
    end
  end

  def notify(notification = %__MODULE__{slack_user: slack_user}) do
    SlackCoder.Slack.send_to(slack_user, notification)
  end

  def slack_user_with_monitors(nil), do: []
  def slack_user_with_monitors(user) do
    message_for = user.slack
    users = user_with_monitors(user, :slack)
    Enum.uniq([message_for | users])
  end

  def github_user_with_monitors(nil), do: []
  def github_user_with_monitors(user) do
    message_for = user.github
    users = user_with_monitors(user, :github)
    Enum.uniq([message_for | users])
  end

  defp user_with_monitors(user, map_to) do
    Users.users
    |> Stream.filter(&(user.github in &1.monitors))
    |> Enum.map(&(Map.get(&1, map_to)))
  end

  def user_for_pr(pr) do
    Users.user(pr.github_user)
    |> User.get
  end

end