Improve specs and docs for String module

defmodule String do
  @moduledoc %B"""
  A String in Elixir is a UTF-8 encoded binary.

  ## String and binary operations

  The functions in this module act according to the
  Unicode Standard, version 6.2.0. For example,
  `titlecase`, `downcase`, `strip` are provided by this
  module.

  Besides this module, Elixir provides more low-level
  operations that works directly with binaries. Some
  of those can be found in the `Kernel` module, as:

  * `binary_part/2` and `binary_part/3` - retrieves part of the binary
  * `bit_size/1` and `byte_size/1` - size related functions
  * `is_bitstring/1` and `is_binary/1` - type checking function
  * Plus a bunch of conversion functions, like `binary_to_atom/2`,
    `binary_to_integer/2`, `binary_to_term/1` and their opposite
    like `integer_to_binary/2`

  Finally, [the `:binary` module](http://erlang.org/doc/man/binary.html)
  provides a couple other functions that works on the byte level.

  ## Codepoints and graphemes

  As per the Unicode Standard, a codepoint is an Unicode
  Character, which may be represented by one or more bytes.
  For example, the character "é" is represented with two
  bytes:

      iex> string = "é"
      ...> byte_size(string)
      2

  Furthermore, this module also presents the concept of
  graphemes, which are multiple characters that may be
  "perceived as a single character" by readers. For example,
  the same "é" character written above could be represented
  by the letter "e" followed by the accent ́:

      iex> string = "\x{0065}\x{0301}"
      ...> byte_size(string)
      3

  Although the example above is made of two characters, it is
  perceived by users as one.

  Graphemes can also be two characters that are interpreted
  as one by some languages. For example, some languages may
  consider "ch" as a grapheme. However, since this information
  depends on the locale, it is not taken into account by this
  module.

  In general, the functions in this module rely on the Unicode
  Standard, but does not contain any of the locale specific
  behaviour.

  ## Integer codepoints

  Although codepoints could be represented as integers, this
  module represents all codepoints as strings. For example:

      iex> String.codepoints("josé")
      ["j", "o", "s", "é"]

  There are a couple of ways to retrieve a character integer
  codepoint. One may use the `?` special macro:

      iex> ?j
      106
      iex> ?é
      233

  Or also via pattern matching:

      iex> << eacute :: utf8 >> = "é"
      ...> eacute
      233

  As we have seen above, codepoints can be inserted into
  a string by their hexadecimal code:

      "jos\x{0065}\x{0301}" #=>
      "josé"

  ## Self-synchronization

  The UTF-8 encoding is self-synchronizing. This means that
  if malformed data (i.e., data that is not possible according
  to the definition of the encoding) is encountered, only one
  codepoint needs to be rejected.

  This module relies on this behaviour to ignore such invalid
  characters. For example, `String.length` is going to return
  a correct result even if an invalid codepoint is fed into it.

  In other words, this module expects invalid data to be detected
  when retrieving data from the external source. For example, a
  driver that reads strings from a database will be the one
  responsible to check the validity of the encoding.
  """

  @type t :: binary
  @type codepoint :: t
  @type grapheme :: t

  @doc """
  Checks if a string is printable considering it is encoded
  as UTF-8. Returns true if so, false otherwise.

  ## Examples

      iex> String.printable?("abc")
      true

  """
  @spec printable?(t) :: boolean

  def printable?(<< h :: utf8, t :: binary >>)
      when h in ?\040..?\176
      when h in 0xA0..0xD7FF
      when h in 0xE000..0xFFFD
      when h in 0x10000..0x10FFFF do
    printable?(t)
  end

  def printable?(<<?\n, t :: binary>>), do: printable?(t)
  def printable?(<<?\r, t :: binary>>), do: printable?(t)
  def printable?(<<?\t, t :: binary>>), do: printable?(t)
  def printable?(<<?\v, t :: binary>>), do: printable?(t)
  def printable?(<<?\b, t :: binary>>), do: printable?(t)
  def printable?(<<?\f, t :: binary>>), do: printable?(t)
  def printable?(<<?\e, t :: binary>>), do: printable?(t)
  def printable?(<<?\a, t :: binary>>), do: printable?(t)

  def printable?(<<>>), do: true
  def printable?(_),    do: false

  @doc """
  Splits a string on sub strings at each Unicode whitespace
  occurrence with leading and trailing whitespace ignored.

  ## Examples

      iex> String.split("foo bar")
      ["foo", "bar"]
      iex> String.split("foo" <> <<194,133>> <> "bar")
      ["foo", "bar"]
      iex> String.split(" foo bar ")
      ["foo", "bar"]

  """
  @spec split(t) :: [t]
  defdelegate split(binary), to: String.Unicode

  @doc """
  Divides a string into sub strings based on a pattern,
  returning a list of these sub string. The pattern can
  be a string, a list of strings or a regular expression.

  The string is split into as many parts as possible by
  default, unless the `global` option is set to false.

  ## Examples

      iex> String.split("a,b,c", ",")
      ["a", "b", "c"]
      iex> String.split("a,b,c", ",", global: false)
      ["a", "b,c"]

      iex> String.split("1,2 3,4", [" ", ","])
      ["1", "2", "3", "4"]

      iex> String.split("a,b,c", %r{,})
      ["a", "b", "c"]
      iex> String.split("a,b,c", %r{,}, global: false)
      ["a", "b,c"]
      iex> String.split("a,b", %r{\\.})
      ["a,b"]

  """
  @spec split(t, t | [t] | Regex.t) :: [t]
  @spec split(t, t | [t] | Regex.t, Keyword.t) :: [t]
  def split(binary, pattern, options // [])

  def split(binary, pattern, options) when is_regex(pattern) do
    Regex.split(pattern, binary, global: options[:global])
  end

  def split(binary, pattern, options) do
    opts = if options[:global] != false, do: [:global], else: []
    :binary.split(binary, pattern, opts)
  end

  @doc """
  Convert all characters on the given string to upcase.

  ## Examples

      iex> String.upcase("abcd")
      "ABCD"
      iex> String.upcase("ab 123 xpto")
      "AB 123 XPTO"
      iex> String.upcase("josé")
      "JOSÉ"

  """
  @spec upcase(t) :: t
  defdelegate upcase(binary), to: String.Unicode

  @doc """
  Convert all characters on the given string to downcase.

  ## Examples

      iex> String.downcase("ABCD")
      "abcd"
      iex> String.downcase("AB 123 XPTO")
      "ab 123 xpto"
      iex> String.downcase("JOSÉ")
      "josé"

  """
  @spec downcase(t) :: t
  defdelegate downcase(binary), to: String.Unicode

  @doc """
  Converts the first character in the given string to
  titlecase and the remaining to downcase.

  This relies on the titlecase information provided
  by the Unicode Standard. Note this function makes
  no attempt in capitalizing all words in the string
  (usually known as titlecase).

  ## Examples

      iex> String.capitalize("abcd")
      "Abcd"
      iex> String.capitalize("ﬁn")
      "Fin"
      iex> String.capitalize("josé")
      "José"

  """
  @spec capitalize(t) :: t
  def capitalize(string) when is_binary(string) do
    { char, rest } = String.Unicode.titlecase_once(string)
    char <> downcase(rest)
  end

  @doc """
  Returns a string where trailing Unicode whitespace
  has been removed.

  ## Examples

      iex> String.rstrip("   abc  ")
      "   abc"

  """
  @spec rstrip(t) :: t
  defdelegate rstrip(binary), to: String.Unicode

  @doc """
  Returns a string where trailing `char` have been removed.

  ## Examples

      iex> String.rstrip("   abc _", ?_)
      "   abc "

  """
  @spec rstrip(t, char) :: t

  def rstrip("", _char), do: ""

  # Do a quick check before we traverse the whole
  # binary. :binary.last is a fast operation (it
  # does not traverse the whole binary).
  def rstrip(string, char) do
    if :binary.last(string) == char do
      do_rstrip(string, "", char)
    else
      string
    end
  end

  defp do_rstrip(<<char, string :: binary>>, buffer, char) do
    do_rstrip(string, <<char, buffer :: binary>>, char)
  end

  defp do_rstrip(<<char, string :: binary>>, buffer, another_char) do
    <<buffer :: binary, char, do_rstrip(string, "", another_char) :: binary>>
  end

  defp do_rstrip(<<>>, _, _) do
    <<>>
  end

  @doc """
  Returns a string where leading Unicode whitespace
  has been removed.

  ## Examples

      iex> String.lstrip("   abc  ")
      "abc  "

  """
  defdelegate lstrip(binary), to: String.Unicode

  @doc """
  Returns a string where leading `char` have been removed.

  ## Examples

      iex> String.lstrip("_  abc  _", ?_)
      "  abc  _"

  """

  @spec lstrip(t, char) :: t

  def lstrip(<<char, rest :: binary>>, char) do
    <<lstrip(rest, char) :: binary>>
  end

  def lstrip(other, _char) do
    other
  end

  @doc """
  Returns a string where leading/trailing Unicode whitespace
  has been removed.

  ## Examples

      iex> String.strip("   abc  ")
      "abc"

  """
  @spec strip(t) :: t

  def strip(string) do
    rstrip(lstrip(string))
  end

  @doc """
  Returns a string where leading/trailing `char` have been
  removed.

  ## Examples

      iex> String.strip("a  abc  a", ?a)
      "  abc  "

  """
  @spec strip(t, char) :: t

  def strip(string, char) do
    rstrip(lstrip(string, char), char)
  end

  @doc """
  Returns a new binary based on `subject` by replacing the parts
  matching `pattern` for `replacement`. By default, it replaces
  all entries, except if the `global` option is set to false.

  If the replaced part must be used in `replacement`, then the
  position or the positions where it is to be inserted must be
  specified by using the option `insert_replaced`.

  ## Examples

      iex> String.replace("a,b,c", ",", "-")
      "a-b-c"
      iex> String.replace("a,b,c", ",", "-", global: false)
      "a-b,c"
      iex> String.replace("a,b,c", "b", "[]", insert_replaced: 1)
      "a,[b],c"
      iex> String.replace("a,b,c", ",", "[]", insert_replaced: 2)
      "a[],b[],c"
      iex> String.replace("a,b,c", ",", "[]", insert_replaced: [1,1])
      "a[,,]b[,,]c"

  """
  @spec replace(t, t, t) :: t
  @spec replace(t, t, t, Keyword.t) :: t

  def replace(subject, pattern, replacement, options // []) do
    opts = translate_replace_options(options)
    :binary.replace(subject, pattern, replacement, opts)
  end

  defp translate_replace_options(options) do
    opts = if options[:global] != false, do: [:global], else: []

    if insert = options[:insert_replaced] do
      opts = [{:insert_replaced,insert}|opts]
    end

    opts
  end

  @doc """
  Returns a binary `subject` duplicated `n` times.

  ## Examples

      iex> String.duplicate("abc", 1)
      "abc"
      iex> String.duplicate("abc", 2)
      "abcabc"

  """
  @spec duplicate(t, pos_integer) :: t
  def duplicate(subject, n) when is_integer(n) and n > 0 do
    :binary.copy(subject, n)
  end

  @doc """
  Returns all codepoints in the string.

  ## Examples

      iex> String.codepoints("josé")
      ["j", "o", "s", "é"]
      iex> String.codepoints("оптими зации")
      ["о","п","т","и","м","и"," ","з","а","ц","и","и"]
      iex> String.codepoints("ἅἪῼ")
      ["ἅ","Ἢ","ῼ"]

  """
  @spec codepoints(t) :: [codepoint]
  defdelegate codepoints(string), to: String.Unicode

  @doc """
  Returns the next codepoint in a String.

  The result is a tuple with the codepoint and the
  remaining of the string or `:no_codepoint` in case
  the string reached its end.

  As the other functions in the String module, this
  function does not check for the validity of the codepoint.
  That said, if an invalid codepoint is found, it will
  be returned by this function.

  ## Examples

      iex> String.next_codepoint("josé")
      { "j", "osé" }

  """
  @spec next_codepoint(t) :: {codepoint, t} | :no_codepoint
  defdelegate next_codepoint(string), to: String.Unicode

  @doc %B"""
  Checks whether `str` is a valid codepoint.

  Note that the empty string is considered invalid, as are
  strings containing multiple codepoints.

  ## Examples

      iex> String.valid_codepoint?("a")
      true
      iex> String.valid_codepoint?("ø")
      true
      iex> String.valid_codepoint?("\xffff")
      false
      iex> String.valid_codepoint?("asdf")
      false

  """
  @spec valid_codepoint?(codepoint) :: boolean
  def valid_codepoint?(<<_ :: utf8>>), do: true
  def valid_codepoint?(_), do: false

  @doc """
  Returns unicode graphemes in the string.

  ## Examples

      iex> String.graphemes("Ā̀stute")
      ["Ā̀","s","t","u","t","e"]

  """
  @spec graphemes(t) :: [grapheme]
  defdelegate graphemes(string), to: String.Unicode

  @doc """
  Returns the next grapheme in a String.

  The result is a tuple with the grapheme and the
  remaining of the string or `:no_grapheme` in case
  the String reached its end.

  ## Examples

      iex> String.next_grapheme("josé")
      { "j", "osé" }

  """
  @spec next_grapheme(t) :: { grapheme, t } | :no_grapheme
  defdelegate next_grapheme(string), to: String.Unicode

  @doc """
  Returns the first grapheme from an utf8 string,
  nil if the string is empty.

  ## Examples

      iex> String.first("elixir")
      "e"
      iex> String.first("եոգլի")
      "ե"

  """
  @spec first(t) :: grapheme | nil
  def first(string) do
    case next_grapheme(string) do
      { char, _ } -> char
      :no_grapheme -> nil
    end
  end

  @doc """
  Returns the last grapheme from an utf8 string,
  nil if the string is empty.

  ## Examples

      iex> String.last("elixir")
      "r"
      iex> String.last("եոգլի")
      "ի"

  """
  @spec last(t) :: grapheme | nil
  def last(string) do
    do_last(next_grapheme(string), nil)
  end

  defp do_last({char, rest}, _) do
    do_last(next_grapheme(rest), char)
  end

  defp do_last(:no_grapheme, last_char), do: last_char

  @doc """
  Returns the number of unicode graphemes in an utf8 string.

  ## Examples

      iex> String.length("elixir")
      6
      iex> String.length("եոգլի")
      5

  """
  @spec length(t) :: non_neg_integer
  def length(string) do
    do_length(next_grapheme(string))
  end

  defp do_length({_, rest}) do
    1 + do_length(next_grapheme(rest))
  end

  defp do_length(:no_grapheme), do: 0

  @doc """
  Returns the grapheme in the `position` of the given utf8 `string`.
  If `position` is greater than `string` length, than it returns `nil`.

  ## Examples

      iex> String.at("elixir", 0)
      "e"
      iex> String.at("elixir", 1)
      "l"
      iex> String.at("elixir", 10)
      nil
      iex> String.at("elixir", -1)
      "r"
      iex> String.at("elixir", -10)
      nil

  """
  @spec at(t, integer) :: grapheme | nil

  def at(string, position) when position >= 0 do
    do_at(next_grapheme(string), position, 0)
  end

  def at(string, position) when position < 0 do
    real_pos = do_length(next_grapheme(string)) - abs(position)
    case real_pos >= 0 do
      true  -> do_at(next_grapheme(string), real_pos, 0)
      false -> nil
    end
  end

  defp do_at({_ , rest}, desired_pos, current_pos) when desired_pos > current_pos do
    do_at(next_grapheme(rest), desired_pos, current_pos + 1)
  end

  defp do_at({char, _}, desired_pos, current_pos) when desired_pos == current_pos do
    char
  end

  defp do_at(:no_grapheme, _, _), do: nil

  @doc """
  Returns a substring starting at the offset given by the first, and
  a length given by the second.
  If the offset is greater than string length, than it returns nil.

  ## Examples

      iex> String.slice("elixir", 1, 3)
      "lix"
      iex> String.slice("elixir", 1, 10)
      "lixir"
      iex> String.slice("elixir", 10, 3)
      nil
      iex> String.slice("elixir", -4, 4)
      "ixir"
      iex> String.slice("elixir", -10, 3)
      nil

  """
  @spec slice(t, integer, integer) :: grapheme | nil

  def slice(string, start, len) when start >= 0 do
    do_slice(next_grapheme(string), start, start + len - 1, 0, "")
  end

  def slice(string, start, len) when start < 0 do
    real_start_pos = do_length(next_grapheme(string)) - abs(start)
    case real_start_pos >= 0 do
      true -> do_slice(next_grapheme(string), real_start_pos, real_start_pos + len - 1, 0, "")
      false -> nil
    end
  end

  defp do_slice(_, start_pos, last_pos, _, _) when start_pos > last_pos do
    nil
  end

  defp do_slice({_, rest}, start_pos, last_pos, current_pos, acc) when current_pos < start_pos do
    do_slice(next_grapheme(rest), start_pos, last_pos, current_pos + 1, acc)
  end

  defp do_slice({char, rest}, start_pos, last_pos, current_pos, acc) when current_pos >= start_pos and current_pos < last_pos do
    do_slice(next_grapheme(rest), start_pos, last_pos, current_pos + 1, acc <> char)
  end

  defp do_slice({char, _}, start_pos, last_pos, current_pos, acc) when current_pos >= start_pos and current_pos == last_pos do
    acc <> char
  end

  defp do_slice(:no_grapheme, _, _, _, acc) do
    case acc do
      "" -> nil
      _ -> acc
    end
  end
end

defmodule String do
  @moduledoc %B"""
  A String in Elixir is a UTF-8 encoded binary.

  ## String and binary operations

  The functions in this module act according to the
  Unicode Standard, version 6.2.0. For example,
  `titlecase`, `downcase`, `strip` are provided by this
  module.

  Besides this module, Elixir provides more low-level
  operations that works directly with binaries. Some
  of those can be found in the `Kernel` module, as:

  * `binary_part/2` and `binary_part/3` - retrieves part of the binary
  * `bit_size/1` and `byte_size/1` - size related functions
  * `is_bitstring/1` and `is_binary/1` - type checking function
  * Plus a bunch of conversion functions, like `binary_to_atom/2`,
    `binary_to_integer/2`, `binary_to_term/1` and their opposite
    like `integer_to_binary/2`

  Finally, [the `:binary` module](http://erlang.org/doc/man/binary.html)
  provides a couple other functions that works on the byte level.

  ## Codepoints and graphemes

  As per the Unicode Standard, a codepoint is an Unicode
  Character, which may be represented by one or more bytes.
  For example, the character "é" is represented with two
  bytes:

      iex> string = "é"
      ...> byte_size(string)
      2

  Furthermore, this module also presents the concept of
  graphemes, which are multiple characters that may be
  "perceived as a single character" by readers. For example,
  the same "é" character written above could be represented
  by the letter "e" followed by the accent ́:

      iex> string = "\x{0065}\x{0301}"
      ...> byte_size(string)
      3

  Although the example above is made of two characters, it is
  perceived by users as one.

  Graphemes can also be two characters that are interpreted
  as one by some languages. For example, some languages may
  consider "ch" as a grapheme. However, since this information
  depends on the locale, it is not taken into account by this
  module.

  In general, the functions in this module rely on the Unicode
  Standard, but does not contain any of the locale specific
  behaviour.

  ## Integer codepoints

  Although codepoints could be represented as integers, this
  module represents all codepoints as strings. For example:

      iex> String.codepoints("josé")
      ["j", "o", "s", "é"]

  There are a couple of ways to retrieve a character integer
  codepoint. One may use the `?` special macro:

      iex> ?j
      106
      iex> ?é
      233

  Or also via pattern matching:

      iex> << eacute :: utf8 >> = "é"
      ...> eacute
      233

  As we have seen above, codepoints can be inserted into
  a string by their hexadecimal code:

      "jos\x{0065}\x{0301}" #=>
      "josé"

  ## Self-synchronization

  The UTF-8 encoding is self-synchronizing. This means that
  if malformed data (i.e., data that is not possible according
  to the definition of the encoding) is encountered, only one
  codepoint needs to be rejected.

  This module relies on this behaviour to ignore such invalid
  characters. For example, `String.length` is going to return
  a correct result even if an invalid codepoint is fed into it.

  In other words, this module expects invalid data to be detected
  when retrieving data from the external source. For example, a
  driver that reads strings from a database will be the one
  responsible to check the validity of the encoding.
  """

  @type t :: binary
  @type codepoint :: t
  @type grapheme :: t

  @doc """
  Checks if a string is printable considering it is encoded
  as UTF-8. Returns true if so, false otherwise.

  ## Examples

      iex> String.printable?("abc")
      true

  """
  @spec printable?(t) :: boolean

  def printable?(<< h :: utf8, t :: binary >>)
      when h in ?\040..?\176
      when h in 0xA0..0xD7FF
      when h in 0xE000..0xFFFD
      when h in 0x10000..0x10FFFF do
    printable?(t)
  end

  def printable?(<<?\n, t :: binary>>), do: printable?(t)
  def printable?(<<?\r, t :: binary>>), do: printable?(t)
  def printable?(<<?\t, t :: binary>>), do: printable?(t)
  def printable?(<<?\v, t :: binary>>), do: printable?(t)
  def printable?(<<?\b, t :: binary>>), do: printable?(t)
  def printable?(<<?\f, t :: binary>>), do: printable?(t)
  def printable?(<<?\e, t :: binary>>), do: printable?(t)
  def printable?(<<?\a, t :: binary>>), do: printable?(t)

  def printable?(<<>>), do: true
  def printable?(_),    do: false

  @doc """
  Splits a string on sub strings at each Unicode whitespace
  occurrence with leading and trailing whitespace ignored.

  ## Examples

      iex> String.split("foo bar")
      ["foo", "bar"]
      iex> String.split("foo" <> <<194,133>> <> "bar")
      ["foo", "bar"]
      iex> String.split(" foo bar ")
      ["foo", "bar"]

  """
  @spec split(t) :: [t]
  defdelegate split(binary), to: String.Unicode

  @doc """
  Divides a string into sub strings based on a pattern,
  returning a list of these sub string. The pattern can
  be a string, a list of strings or a regular expression.

  The string is split into as many parts as possible by
  default, unless the `global` option is set to false.

  ## Examples

      iex> String.split("a,b,c", ",")
      ["a", "b", "c"]
      iex> String.split("a,b,c", ",", global: false)
      ["a", "b,c"]

      iex> String.split("1,2 3,4", [" ", ","])
      ["1", "2", "3", "4"]

      iex> String.split("a,b,c", %r{,})
      ["a", "b", "c"]
      iex> String.split("a,b,c", %r{,}, global: false)
      ["a", "b,c"]
      iex> String.split("a,b", %r{\\.})
      ["a,b"]

  """
  @spec split(t, t | [t] | Regex.t) :: [t]
  @spec split(t, t | [t] | Regex.t, Keyword.t) :: [t]
  def split(binary, pattern, options // [])

  def split(binary, pattern, options) when is_regex(pattern) do
    Regex.split(pattern, binary, global: options[:global])
  end

  def split(binary, pattern, options) do
    opts = if options[:global] != false, do: [:global], else: []
    :binary.split(binary, pattern, opts)
  end

  @doc """
  Convert all characters on the given string to upcase.

  ## Examples

      iex> String.upcase("abcd")
      "ABCD"
      iex> String.upcase("ab 123 xpto")
      "AB 123 XPTO"
      iex> String.upcase("josé")
      "JOSÉ"

  """
  @spec upcase(t) :: t
  defdelegate upcase(binary), to: String.Unicode

  @doc """
  Convert all characters on the given string to downcase.

  ## Examples

      iex> String.downcase("ABCD")
      "abcd"
      iex> String.downcase("AB 123 XPTO")
      "ab 123 xpto"
      iex> String.downcase("JOSÉ")
      "josé"

  """
  @spec downcase(t) :: t
  defdelegate downcase(binary), to: String.Unicode

  @doc """
  Converts the first character in the given string to
  titlecase and the remaining to downcase.

  This relies on the titlecase information provided
  by the Unicode Standard. Note this function makes
  no attempt in capitalizing all words in the string
  (usually known as titlecase).

  ## Examples

      iex> String.capitalize("abcd")
      "Abcd"
      iex> String.capitalize("ﬁn")
      "Fin"
      iex> String.capitalize("josé")
      "José"

  """
  @spec capitalize(t) :: t
  def capitalize(string) when is_binary(string) do
    { char, rest } = String.Unicode.titlecase_once(string)
    char <> downcase(rest)
  end

  @doc """
  Returns a string where trailing Unicode whitespace
  has been removed.

  ## Examples

      iex> String.rstrip("   abc  ")
      "   abc"

  """
  @spec rstrip(t) :: t
  defdelegate rstrip(binary), to: String.Unicode

  @doc """
  Returns a string where trailing `char` have been removed.

  ## Examples

      iex> String.rstrip("   abc _", ?_)
      "   abc "

  """
  @spec rstrip(t, char) :: t

  def rstrip("", _char), do: ""

  # Do a quick check before we traverse the whole
  # binary. :binary.last is a fast operation (it
  # does not traverse the whole binary).
  def rstrip(string, char) do
    if :binary.last(string) == char do
      do_rstrip(string, "", char)
    else
      string
    end
  end

  defp do_rstrip(<<char, string :: binary>>, buffer, char) do
    do_rstrip(string, <<char, buffer :: binary>>, char)
  end

  defp do_rstrip(<<char, string :: binary>>, buffer, another_char) do
    <<buffer :: binary, char, do_rstrip(string, "", another_char) :: binary>>
  end

  defp do_rstrip(<<>>, _, _) do
    <<>>
  end

  @doc """
  Returns a string where leading Unicode whitespace
  has been removed.

  ## Examples

      iex> String.lstrip("   abc  ")
      "abc  "

  """
  defdelegate lstrip(binary), to: String.Unicode

  @doc """
  Returns a string where leading `char` have been removed.

  ## Examples

      iex> String.lstrip("_  abc  _", ?_)
      "  abc  _"

  """

  @spec lstrip(t, char) :: t

  def lstrip(<<char, rest :: binary>>, char) do
    <<lstrip(rest, char) :: binary>>
  end

  def lstrip(other, _char) do
    other
  end

  @doc """
  Returns a string where leading/trailing Unicode whitespace
  has been removed.

  ## Examples

      iex> String.strip("   abc  ")
      "abc"

  """
  @spec strip(t) :: t

  def strip(string) do
    rstrip(lstrip(string))
  end

  @doc """
  Returns a string where leading/trailing `char` have been
  removed.

  ## Examples

      iex> String.strip("a  abc  a", ?a)
      "  abc  "

  """
  @spec strip(t, char) :: t

  def strip(string, char) do
    rstrip(lstrip(string, char), char)
  end

  @doc """
  Returns a new binary based on `subject` by replacing the parts
  matching `pattern` for `replacement`. By default, it replaces
  all entries, except if the `global` option is set to false.

  If the replaced part must be used in `replacement`, then the
  position or the positions where it is to be inserted must be
  specified by using the option `insert_replaced`.

  ## Examples

      iex> String.replace("a,b,c", ",", "-")
      "a-b-c"
      iex> String.replace("a,b,c", ",", "-", global: false)
      "a-b,c"
      iex> String.replace("a,b,c", "b", "[]", insert_replaced: 1)
      "a,[b],c"
      iex> String.replace("a,b,c", ",", "[]", insert_replaced: 2)
      "a[],b[],c"
      iex> String.replace("a,b,c", ",", "[]", insert_replaced: [1,1])
      "a[,,]b[,,]c"

  """
  @spec replace(t, t, t) :: t
  @spec replace(t, t, t, Keyword.t) :: t

  def replace(subject, pattern, replacement, options // []) do
    opts = translate_replace_options(options)
    :binary.replace(subject, pattern, replacement, opts)
  end

  defp translate_replace_options(options) do
    opts = if options[:global] != false, do: [:global], else: []

    if insert = options[:insert_replaced] do
      opts = [{:insert_replaced,insert}|opts]
    end

    opts
  end

  @doc """
  Returns a binary `subject` duplicated `n` times.

  ## Examples

      iex> String.duplicate("abc", 1)
      "abc"
      iex> String.duplicate("abc", 2)
      "abcabc"

  """
  @spec duplicate(t, pos_integer) :: t
  def duplicate(subject, n) when is_integer(n) and n > 0 do
    :binary.copy(subject, n)
  end

  @doc """
  Returns all codepoints in the string.

  ## Examples

      iex> String.codepoints("josé")
      ["j", "o", "s", "é"]
      iex> String.codepoints("оптими зации")
      ["о","п","т","и","м","и"," ","з","а","ц","и","и"]
      iex> String.codepoints("ἅἪῼ")
      ["ἅ","Ἢ","ῼ"]

  """
  @spec codepoints(t) :: [codepoint]
  defdelegate codepoints(string), to: String.Unicode

  @doc """
  Returns the next codepoint in a String.

  The result is a tuple with the codepoint and the
  remaining of the string or `:no_codepoint` in case
  the string reached its end.

  As the other functions in the String module, this
  function does not check for the validity of the codepoint.
  That said, if an invalid codepoint is found, it will
  be returned by this function.

  ## Examples

      iex> String.next_codepoint("josé")
      { "j", "osé" }

  """
  @spec next_codepoint(t) :: {codepoint, t} | :no_codepoint
  defdelegate next_codepoint(string), to: String.Unicode

  @doc %B"""
  Checks whether `str` is a valid codepoint.

  Note that the empty string is considered invalid, as are
  strings containing multiple codepoints.

  ## Examples

      iex> String.valid_codepoint?("a")
      true
      iex> String.valid_codepoint?("ø")
      true
      iex> String.valid_codepoint?("\xffff")
      false
      iex> String.valid_codepoint?("asdf")
      false

  """
  @spec valid_codepoint?(codepoint) :: boolean
  def valid_codepoint?(<<_ :: utf8>>), do: true
  def valid_codepoint?(_), do: false

  @doc """
  Returns unicode graphemes in the string.

  ## Examples

      iex> String.graphemes("Ā̀stute")
      ["Ā̀","s","t","u","t","e"]

  """
  @spec graphemes(t) :: [grapheme]
  defdelegate graphemes(string), to: String.Unicode

  @doc """
  Returns the next grapheme in a String.

  The result is a tuple with the grapheme and the
  remaining of the string or `:no_grapheme` in case
  the String reached its end.

  ## Examples

      iex> String.next_grapheme("josé")
      { "j", "osé" }

  """
  @spec next_grapheme(t) :: grapheme | :no_grapheme
  defdelegate next_grapheme(string), to: String.Unicode

  @doc """
  Returns the first grapheme from an utf8 string.

  ## Examples

      iex> String.first("elixir")
      "e"
      iex> String.first("եոգլի")
      "ե"

  """
  @spec first(t) :: grapheme | nil
  def first(string) do
    case next_grapheme(string) do
      { char, _ } -> char
      :no_grapheme -> nil
    end
  end

  @doc """
  Returns the last grapheme from an utf8 string.

  ## Examples

      iex> String.last("elixir")
      "r"
      iex> String.last("եոգլի")
      "ի"

  """
  @spec last(t) :: grapheme | nil
  def last(string) do
    do_last(next_grapheme(string), nil)
  end

  defp do_last({char, rest}, _) do
    do_last(next_grapheme(rest), char)
  end

  defp do_last(:no_grapheme, last_char), do: last_char

  @doc """
  Returns the number of unicode graphemes in an utf8 string.

  ## Examples

      iex> String.length("elixir")
      6
      iex> String.length("եոգլի")
      5

  """
  @spec length(t) :: non_neg_integer
  def length(string) do
    do_length(next_grapheme(string))
  end

  defp do_length({_, rest}) do
    1 + do_length(next_grapheme(rest))
  end

  defp do_length(:no_grapheme), do: 0

  @doc """
  Returns the grapheme in the `position` of the given utf8 `string`.
  If `position` is greater than `string` length, than it returns `nil`.

  ## Examples

      iex> String.at("elixir", 0)
      "e"
      iex> String.at("elixir", 1)
      "l"
      iex> String.at("elixir", 10)
      nil
      iex> String.at("elixir", -1)
      "r"
      iex> String.at("elixir", -10)
      nil

  """
  @spec at(t, integer) :: grapheme | nil

  def at(string, position) when position >= 0 do
    do_at(next_grapheme(string), position, 0)
  end

  def at(string, position) when position < 0 do
    real_pos = do_length(next_grapheme(string)) - abs(position)
    case real_pos >= 0 do
      true  -> do_at(next_grapheme(string), real_pos, 0)
      false -> nil
    end
  end

  defp do_at({_ , rest}, desired_pos, current_pos) when desired_pos > current_pos do
    do_at(next_grapheme(rest), desired_pos, current_pos + 1)
  end

  defp do_at({char, _}, desired_pos, current_pos) when desired_pos == current_pos do
    char
  end

  defp do_at(:no_grapheme, _, _), do: nil

  @doc """
  Returns a substring starting at the offset given by the first, and
  a length given by the second.
  If the offset is greater than string length, than it returns nil.

  ## Examples

      iex> String.slice("elixir", 1, 3)
      "lix"
      iex> String.slice("elixir", 1, 10)
      "lixir"
      iex> String.slice("elixir", 10, 3)
      nil
      iex> String.slice("elixir", -4, 4)
      "ixir"
      iex> String.slice("elixir", -10, 3)
      nil

  """
  @spec slice(t, integer, integer) :: grapheme | nil

  def slice(string, start, len) when start >= 0 do
    do_slice(next_grapheme(string), start, start + len - 1, 0, "")
  end

  def slice(string, start, len) when start < 0 do
    real_start_pos = do_length(next_grapheme(string)) - abs(start)
    case real_start_pos >= 0 do
      true -> do_slice(next_grapheme(string), real_start_pos, real_start_pos + len - 1, 0, "")
      false -> nil
    end
  end

  defp do_slice(_, start_pos, last_pos, _, _) when start_pos > last_pos do
    nil
  end

  defp do_slice({_, rest}, start_pos, last_pos, current_pos, acc) when current_pos < start_pos do
    do_slice(next_grapheme(rest), start_pos, last_pos, current_pos + 1, acc)
  end

  defp do_slice({char, rest}, start_pos, last_pos, current_pos, acc) when current_pos >= start_pos and current_pos < last_pos do
    do_slice(next_grapheme(rest), start_pos, last_pos, current_pos + 1, acc <> char)
  end

  defp do_slice({char, _}, start_pos, last_pos, current_pos, acc) when current_pos >= start_pos and current_pos == last_pos do
    acc <> char
  end

  defp do_slice(:no_grapheme, _, _, _, acc) do
    case acc do
      "" -> nil
      _ -> acc
    end
  end
end


Provide defaults in test

Provide defaults in test

defmodule RebaseTest do
  use ExUnit.Case

  test "works" do
    token = System.get_env("TEST_TOKEN") || raise("TEST_TOKEN missing")
    org = System.get_env("TEST_ORG") || "wojtekmach"
    repo = System.get_env("TEST_REPO") || "mr-rebase-test"
    ref = System.get_env("TEST_REF") || "test2"

    Rebase.call(GitHub.client(token), org, repo, ref)
  end
end

defmodule RebaseTest do
  use ExUnit.Case

  test "works" do
    token = System.get_env("TEST_TOKEN") || raise("TEST_TOKEN missing")
    org = System.get_env("TEST_ORG") || raise("TEST_ORG missing")
    repo = System.get_env("TEST_REPO") || raise("TEST_REPO missing")
    ref = System.get_env("TEST_REF") || raise("TEST_REF missing")

    Rebase.call(GitHub.client(token), org, repo, ref)
  end
end


Sync channel works. Next: Log channel, total AMQP removal

defmodule FarmbotExt.MQTT.SyncHandler do
  require FarmbotCore.Logger
  require FarmbotTelemetry

  alias FarmbotCore.{BotState, JSON, Leds, Asset}
  alias FarmbotExt.API.{EagerLoader, Preloader}
  alias FarmbotExt.MQTT
  alias __MODULE__, as: State
  defstruct client_id: "NOT_SET", username: "NOT_SET", preloaded: false

  use GenServer

  @known_kinds ~w(
    Device
    FarmEvent
    FarmwareEnv
    FarmwareInstallation
    FbosConfig
    FirmwareConfig
    Peripheral
    PinBinding
    Point
    PointGroup
    Regimen
    Sensor
    Sequence
    Tool
    )

  def start_link(default, opts \\ [name: __MODULE__]) do
    GenServer.start_link(__MODULE__, default, opts)
  end

  def init(opts) do
    send(self(), :preload)

    state = %State{
      client_id: Keyword.fetch!(opts, :client_id),
      username: Keyword.fetch!(opts, :username)
    }

    {:ok, state}
  end

  def handle_info(:preload, state) do
    _ = Leds.green(:really_fast_blink)

    with :ok <- Preloader.preload_all() do
      _ = Leds.green(:solid)
      BotState.set_sync_status("synced")

      {:noreply, %{state | preloaded: true}}
    else
      reason ->
        BotState.set_sync_status("sync_error")
        _ = Leds.green(:slow_blink)
        FarmbotCore.Logger.error(1, "Error preloading. #{inspect(reason)}")
        FarmbotTelemetry.event(:asset_sync, :preload_error, nil, error: inspect(reason))
        FarmbotExt.Time.send_after(self(), :preload, 5000)

        {:noreply, state}
    end
  end

  def handle_info(
        {:inbound, [_, _, "sync", kind, id_str], json},
        %{preloaded: true} = state
      )
      when kind in @known_kinds do
    data = JSON.decode!(json)
    id = data["id"] || String.to_integer(id_str)
    handle_asset(kind, id, data["body"])
    rpc_reply(state, data["args"]["label"])
    {:noreply, state}
  end

  def handle_info({:inbound, _, _}, %{preloaded: false} = state) do
    send(self(), :preload)
    {:noreply, state}
  end

  def handle_info(other, state) do
    IO.puts("UNKNOWN SYNC MSG: #{inspect(other)}")
    {:noreply, state}
  end

  def rpc_reply(state, label) do
    topic = "bot/#{state.username}/from_device"
    json = JSON.encode!(%{args: %{label: label}, kind: "rpc_ok"})
    MQTT.publish(state.client_id, topic, json)
  end

  def terminate(_reason, _state) do
    try do
      EagerLoader.Supervisor.drop_all_cache()
    catch
      _, _ ->
        :ok
    end
  end

  def handle_asset(asset_kind, id, params) do
    :ok = BotState.set_sync_status("syncing")
    _ = Leds.green(:really_fast_blink)
    Asset.Command.update(asset_kind, id, params)
    :ok = BotState.set_sync_status("synced")
    _ = Leds.green(:solid)
  end
end

defmodule FarmbotExt.MQTT.SyncHandler do
  use GenServer
  alias __MODULE__, as: State
  defstruct client_id: "NOT_SET"
  # alias FarmbotExt.MQTT
  # require Logger

  def start_link(default, opts \\ [name: __MODULE__]) do
    GenServer.start_link(__MODULE__, default, opts)
  end

  def init(opts) do
    {:ok, %State{client_id: Keyword.fetch!(opts, :client_id)}}
  end

  def handle_info({:inbound, [_, _, "sync", resource, id], json}, state) do
    IO.puts("=== AUTO SYNC MESSAGE: #{inspect({resource, id})}")
    IO.puts(inspect(json))
    {:noreply, state}
  end

  def handle_info(_other, state), do: {:noreply, state}
end


Use iodata instead of binaries when building dot graph format

defmodule Mix.Utils do
  @moduledoc false

  @doc """
  Gets the Mix home.

  It defaults to `~/.mix` unless the `MIX_HOME`
  environment variable is set.

  Developers should only store entries in the
  `MIX_HOME` directory which are guaranteed to
  work across multiple Elixir versions, as it is
  not recommended to swap the `MIX_HOME` directory
  as configuration and other important data may be
  stored there.
  """
  def mix_home do
    System.get_env("MIX_HOME") || Path.expand("~/.mix")
  end

  @doc """
  Gets all paths defined in the MIX_PATH env variable.

  `MIX_PATH` may contain multiple paths. If on Windows, those
  paths should be separated by `;`, if on Unix systems, use `:`.
  """
  def mix_paths do
    if path = System.get_env("MIX_PATH") do
      String.split(path, path_separator())
    else
      []
    end
  end

  defp path_separator do
    case :os.type() do
      {:win32, _} -> ";"
      {:unix, _} -> ":"
    end
  end

  @doc """
  Parses a string into module, function and arity.

  It returns `{:ok, mfa_list}`, where a `mfa_list` is
  `[module, function, arity]`, `[module, function]` or `[module]`,
  or the atom `:error`.

      iex> Mix.Utils.parse_mfa("Foo.bar/1")
      {:ok, [Foo, :bar, 1]}
      iex> Mix.Utils.parse_mfa(":foo.bar/1")
      {:ok, [:foo, :bar, 1]}
      iex> Mix.Utils.parse_mfa(":foo.bar")
      {:ok, [:foo, :bar]}
      iex> Mix.Utils.parse_mfa(":foo")
      {:ok, [:foo]}
      iex> Mix.Utils.parse_mfa("Foo")
      {:ok, [Foo]}

      iex> Mix.Utils.parse_mfa("Foo.")
      :error
      iex> Mix.Utils.parse_mfa("Foo.bar.baz")
      :error
      iex> Mix.Utils.parse_mfa("Foo.bar/2/2")
      :error
  """
  def parse_mfa(mfa) do
    with {:ok, quoted} <- Code.string_to_quoted(mfa),
         [_ | _] = mfa_list <- quoted_to_mfa(quoted) do
      {:ok, mfa_list}
    else
      _ -> :error
    end
  end

  defp quoted_to_mfa({:/, _, [dispatch, arity]}) when is_integer(arity) do
    quoted_to_mf(dispatch, [arity])
  end

  defp quoted_to_mfa(dispatch) do
    quoted_to_mf(dispatch, [])
  end

  defp quoted_to_mf({{:., _, [module, fun]}, _, []}, acc) when is_atom(fun) do
    quoted_to_m(module, [fun | acc])
  end

  defp quoted_to_mf(module, acc) do
    quoted_to_m(module, acc)
  end

  defp quoted_to_m({:__aliases__, _, aliases}, acc) do
    [Module.concat(aliases) | acc]
  end

  defp quoted_to_m(atom, acc) when is_atom(atom) do
    [atom | acc]
  end

  defp quoted_to_m(_, _acc) do
    []
  end

  @doc """
  Takes a `command` name and attempts to load a module
  with the command name converted to a module name
  in the given `at` scope.

  Returns `{:module, module}` in case a module
  exists and is loaded, `{:error, reason}` otherwise.

  ## Examples

      iex> Mix.Utils.command_to_module("compile", Mix.Tasks)
      {:module, Mix.Tasks.Compile}

  """
  def command_to_module(command, at \\ Elixir) do
    module = Module.concat(at, command_to_module_name(command))
    Code.ensure_loaded(module)
  end

  @doc """
  Returns `true` if any of the `sources` are stale
  compared to the given `targets`.
  """
  def stale?(sources, targets) do
    Enum.any?(stale_stream(sources, targets))
  end

  @doc """
  Extracts all stale `sources` compared to the given `targets`.
  """
  def extract_stale(_sources, []), do: []
  def extract_stale([], _targets), do: []

  def extract_stale(sources, targets) do
    stale_stream(sources, targets) |> Enum.to_list()
  end

  defp stale_stream(sources, targets) do
    modified_target = targets |> Enum.map(&last_modified/1) |> Enum.min()

    Stream.filter(sources, fn source ->
      last_modified(source) > modified_target
    end)
  end

  @doc """
  Returns the date the given path was last modified.

  If the path does not exist, it returns the Unix epoch
  (1970-01-01 00:00:00).
  """
  def last_modified(path)

  def last_modified({{_, _, _}, {_, _, _}} = timestamp) do
    timestamp
  end

  def last_modified(path) do
    {mtime, _size} = last_modified_and_size(path)
    mtime
  end

  @doc false
  def last_modified_and_size(path) do
    now = :calendar.universal_time()

    case :elixir_utils.read_mtime_and_size(path) do
      {:ok, mtime, size} when mtime > now ->
        message =
          "warning: mtime (modified time) for \"#{path}\" was set to the future, resetting to now"

        Mix.shell().error(message)

        File.touch!(path, now)
        {mtime, size}

      {:ok, mtime, size} ->
        {mtime, size}

      {:error, _} ->
        {{{1970, 1, 1}, {0, 0, 0}}, 0}
    end
  end

  @doc """
  Prints n files are being compiled with the given extension.
  """
  def compiling_n(1, ext), do: Mix.shell().info("Compiling 1 file (.#{ext})")
  def compiling_n(n, ext), do: Mix.shell().info("Compiling #{n} files (.#{ext})")

  @doc """
  Extracts files from a list of paths.

  `exts_or_pattern` may be a list of extensions or a
  `Path.wildcard/1` pattern.

  If the path in `paths` is a file, it is included in
  the return result. If it is a directory, it is searched
  recursively for files with the given extensions or matching
  the given patterns.
  """
  def extract_files(paths, exts_or_pattern)

  def extract_files(paths, exts) when is_list(exts) do
    extract_files(paths, "*.{#{Enum.join(exts, ",")}}")
  end

  def extract_files(paths, pattern) do
    Enum.flat_map(paths, fn path ->
      case :elixir_utils.read_file_type(path) do
        {:ok, :directory} -> Path.wildcard("#{path}/**/#{pattern}")
        {:ok, :regular} -> [path]
        _ -> []
      end
    end)
    |> Enum.uniq()
  end

  @type tree_node :: {name :: String.Chars.t(), edge_info :: String.Chars.t()}

  @doc """
  Prints the given tree according to the callback.

  The callback will be invoked for each node and it
  must return a `{printed, children}` tuple.
  """
  @spec print_tree([tree_node], (tree_node -> {tree_node, [tree_node]}), keyword) :: :ok
  def print_tree(nodes, callback, opts \\ []) do
    pretty? =
      case Keyword.get(opts, :format) do
        "pretty" -> true
        "plain" -> false
        _other -> elem(:os.type(), 0) != :win32
      end

    print_tree(nodes, _depth = [], _parent = nil, _seen = MapSet.new(), pretty?, callback)
    :ok
  end

  defp print_tree(_nodes = [], _depth, _parent, seen, _pretty, _callback) do
    seen
  end

  defp print_tree([node | nodes], depth, parent, seen, pretty?, callback) do
    {{name, info}, children} = callback.(node)
    key = {parent, name}

    if MapSet.member?(seen, key) do
      seen
    else
      info = if(info, do: " #{info}", else: "")
      Mix.shell().info("#{depth(pretty?, depth)}#{prefix(pretty?, depth, nodes)}#{name}#{info}")

      seen =
        print_tree(
          children,
          [nodes != [] | depth],
          name,
          MapSet.put(seen, key),
          pretty?,
          callback
        )

      print_tree(nodes, depth, parent, seen, pretty?, callback)
    end
  end

  defp depth(_pretty?, []), do: ""
  defp depth(pretty?, depth), do: Enum.reverse(depth) |> tl |> Enum.map(&entry(pretty?, &1))

  defp entry(false, true), do: "|   "
  defp entry(false, false), do: "    "
  defp entry(true, true), do: "│   "
  defp entry(true, false), do: "    "

  defp prefix(false, [], _), do: ""
  defp prefix(false, _, []), do: "`-- "
  defp prefix(false, _, _), do: "|-- "
  defp prefix(true, [], _), do: ""
  defp prefix(true, _, []), do: "└── "
  defp prefix(true, _, _), do: "├── "

  @doc """
  Outputs the given tree according to the callback as a DOT graph.

  The callback will be invoked for each node and it
  must return a `{printed, children}` tuple.
  """
  @spec write_dot_graph!(
          Path.t(),
          String.t(),
          [tree_node],
          (tree_node -> {tree_node, [tree_node]}),
          keyword
        ) :: :ok
  def write_dot_graph!(path, title, nodes, callback, _opts \\ []) do
    {dot, _} = build_dot_graph(make_ref(), nodes, MapSet.new(), callback)
    File.write!(path, ["digraph ", quoted(title), " {\n", dot, "}\n"])
  end

  defp build_dot_graph(_parent, [], seen, _callback), do: {[], seen}

  defp build_dot_graph(parent, [node | nodes], seen, callback) do
    {{name, edge_info}, children} = callback.(node)
    key = {parent, name}

    if MapSet.member?(seen, key) do
      {[], seen}
    else
      seen = MapSet.put(seen, key)
      current = build_dot_current(parent, name, edge_info)
      {children, seen} = build_dot_graph(name, children, seen, callback)
      {siblings, seen} = build_dot_graph(parent, nodes, seen, callback)
      {[current, children | siblings], seen}
    end
  end

  defp build_dot_current(parent, name, edge_info) do
    edge_info =
      if edge_info do
        [" [label=", quoted(edge_info), "]"]
      else
        []
      end

    parent =
      if is_reference(parent) do
        []
      else
        [quoted(parent), " -> "]
      end

    ["  ", parent, quoted(name), edge_info, ?\n]
  end

  defp quoted(data), do: [?", to_string(data), ?"]

  @doc false
  def underscore(value) do
    IO.warn("Mix.Utils.underscore/1 is deprecated, use Macro.underscore/1 instead")
    Macro.underscore(value)
  end

  @doc false
  def camelize(value) do
    IO.warn("Mix.Utils.camelize/1 is deprecated, use Macro.camelize/1 instead")
    Macro.camelize(value)
  end

  @doc """
  Takes a module and converts it to a command.

  The nesting argument can be given in order to remove
  the nesting of a module.

  ## Examples

      iex> Mix.Utils.module_name_to_command(Mix.Tasks.Compile, 2)
      "compile"

      iex> Mix.Utils.module_name_to_command("Mix.Tasks.Compile.Elixir", 2)
      "compile.elixir"

  """
  def module_name_to_command(module, nesting \\ 0)

  def module_name_to_command(module, nesting) when is_atom(module) do
    module_name_to_command(inspect(module), nesting)
  end

  def module_name_to_command(module, nesting) do
    module
    |> to_string()
    |> String.split(".")
    |> Enum.drop(nesting)
    |> Enum.map_join(".", &Macro.underscore/1)
  end

  @doc """
  Takes a command and converts it to the module name format.

  ## Examples

      iex> Mix.Utils.command_to_module_name("compile.elixir")
      "Compile.Elixir"

  """
  def command_to_module_name(command) do
    command
    |> to_string()
    |> String.split(".")
    |> Enum.map_join(".", &Macro.camelize/1)
  end

  @doc """
  Symlinks directory `source` to `target` or copies it recursively
  in case symlink fails.

  Expects source and target to be absolute paths as it generates
  a relative symlink.
  """
  def symlink_or_copy(source, target) do
    if File.exists?(source) do
      # Relative symbolic links on Windows are broken
      link =
        case :os.type() do
          {:win32, _} -> source
          _ -> make_relative_path(source, target)
        end
        |> String.to_charlist()

      case :file.read_link(target) do
        {:ok, ^link} ->
          :ok

        {:ok, _} ->
          File.rm!(target)
          do_symlink_or_copy(source, target, link)

        {:error, :enoent} ->
          do_symlink_or_copy(source, target, link)

        {:error, _} ->
          unless File.dir?(target) do
            File.rm_rf!(target)
          end

          do_symlink_or_copy(source, target, link)
      end
    else
      {:error, :enoent}
    end
  end

  defp do_symlink_or_copy(source, target, link) do
    case :file.make_symlink(link, target) do
      :ok ->
        :ok

      {:error, _} ->
        file =
          File.cp_r!(source, target, fn orig, dest ->
            File.stat!(orig).mtime > File.stat!(dest).mtime
          end)

        {:ok, file}
    end
  end

  # Make a relative path between the two given paths.
  # Expects both paths to be fully expanded.
  defp make_relative_path(source, target) do
    do_make_relative_path(Path.split(source), Path.split(target))
  end

  defp do_make_relative_path([h | t1], [h | t2]) do
    do_make_relative_path(t1, t2)
  end

  defp do_make_relative_path(source, target) do
    base = List.duplicate("..", max(length(target) - 1, 0))
    Path.join(base ++ source)
  end

  @doc """
  Opens and reads content from either a URL or a local filesystem path.

  Returns the contents as a `{:ok, binary}`, `:badpath` for invalid
  paths or `{:local, message}` for local errors and `{:remote, message}`
  for remote ones.

  ## Options

    * `:sha512` - checks against the given SHA-512 checksum. Returns
      `{:checksum, message}` in case it fails
  """
  @spec read_path(String.t(), keyword) ::
          {:ok, binary}
          | :badpath
          | {:remote, String.t()}
          | {:local, String.t()}
          | {:checksum, String.t()}
  def read_path(path, opts \\ []) do
    cond do
      url?(path) ->
        read_httpc(path) |> checksum(opts)

      file?(path) ->
        read_file(path) |> checksum(opts)

      true ->
        :badpath
    end
  end

  @checksums [:sha512]

  defp checksum({:ok, binary} = return, opts) do
    Enum.find_value(@checksums, return, fn hash ->
      with expected when expected != nil <- opts[hash],
           actual when actual != expected <- hexhash(binary, hash) do
        message = """
        Data does not match the given SHA-512 checksum.

        Expected: #{expected}
          Actual: #{actual}
        """

        {:checksum, message}
      else
        _ -> nil
      end
    end)
  end

  defp checksum({_, _} = error, _opts) do
    error
  end

  defp hexhash(binary, hash) do
    Base.encode16(:crypto.hash(hash, binary), case: :lower)
  end

  @doc """
  Prompts the user to overwrite the file if it exists. Returns
  the user input.
  """
  def can_write?(path) do
    if File.exists?(path) do
      full = Path.expand(path)
      Mix.shell().yes?(Path.relative_to_cwd(full) <> " already exists, overwrite?")
    else
      true
    end
  end

  defp read_file(path) do
    try do
      {:ok, File.read!(path)}
    rescue
      e in [File.Error] -> {:local, Exception.message(e)}
    end
  end

  defp read_httpc(path) do
    {:ok, _} = Application.ensure_all_started(:ssl)
    {:ok, _} = Application.ensure_all_started(:inets)

    # Starting an HTTP client profile allows us to scope
    # the effects of using an HTTP proxy to this function
    {:ok, _pid} = :inets.start(:httpc, [{:profile, :mix}])

    headers = [{'user-agent', 'Mix/#{System.version()}'}]
    request = {:binary.bin_to_list(path), headers}

    # We are using relaxed: true because some servers is returning a Location
    # header with relative paths, which does not follow the spec. This would
    # cause the request to fail with {:error, :no_scheme} unless :relaxed
    # is given.
    #
    # If a proxy environment variable was supplied add a proxy to httpc.
    http_options = [relaxed: true] ++ proxy_config(path)

    case :httpc.request(:get, request, http_options, [body_format: :binary], :mix) do
      {:ok, {{_, status, _}, _, body}} when status in 200..299 ->
        {:ok, body}

      {:ok, {{_, status, _}, _, _}} ->
        {:remote, "httpc request failed with: {:bad_status_code, #{status}}"}

      {:error, reason} ->
        {:remote, "httpc request failed with: #{inspect(reason)}"}
    end
  after
    :inets.stop(:httpc, :mix)
  end

  defp file?(path) do
    File.regular?(path)
  end

  defp url?(path) do
    URI.parse(path).scheme in ["http", "https"]
  end

  def proxy_config(url) do
    {http_proxy, https_proxy} = proxy_env()

    proxy_auth(URI.parse(url), http_proxy, https_proxy)
  end

  defp proxy_env do
    http_proxy = System.get_env("HTTP_PROXY") || System.get_env("http_proxy")
    https_proxy = System.get_env("HTTPS_PROXY") || System.get_env("https_proxy")
    no_proxy = no_proxy_env() |> no_proxy_list()

    {proxy_setup(:http, http_proxy, no_proxy), proxy_setup(:https, https_proxy, no_proxy)}
  end

  defp no_proxy_env() do
    System.get_env("NO_PROXY") || System.get_env("no_proxy")
  end

  defp no_proxy_list(nil) do
    []
  end

  defp no_proxy_list(no_proxy) do
    no_proxy
    |> String.split(",")
    |> Enum.map(&String.to_charlist/1)
  end

  defp proxy_setup(scheme, proxy, no_proxy) do
    uri = URI.parse(proxy || "")

    if uri.host && uri.port do
      host = String.to_charlist(uri.host)
      :httpc.set_options([{proxy_scheme(scheme), {{host, uri.port}, no_proxy}}], :mix)
    end

    uri
  end

  defp proxy_scheme(scheme) do
    case scheme do
      :http -> :proxy
      :https -> :https_proxy
    end
  end

  defp proxy_auth(%URI{scheme: "http"}, http_proxy, _https_proxy), do: proxy_auth(http_proxy)
  defp proxy_auth(%URI{scheme: "https"}, _http_proxy, https_proxy), do: proxy_auth(https_proxy)

  defp proxy_auth(nil), do: []
  defp proxy_auth(%URI{userinfo: nil}), do: []

  defp proxy_auth(%URI{userinfo: auth}) do
    destructure [user, pass], String.split(auth, ":", parts: 2)

    user = String.to_charlist(user)
    pass = String.to_charlist(pass || "")

    [proxy_auth: {user, pass}]
  end
end

defmodule Mix.Utils do
  @moduledoc false

  @doc """
  Gets the Mix home.

  It defaults to `~/.mix` unless the `MIX_HOME`
  environment variable is set.

  Developers should only store entries in the
  `MIX_HOME` directory which are guaranteed to
  work across multiple Elixir versions, as it is
  not recommended to swap the `MIX_HOME` directory
  as configuration and other important data may be
  stored there.
  """
  def mix_home do
    System.get_env("MIX_HOME") || Path.expand("~/.mix")
  end

  @doc """
  Gets all paths defined in the MIX_PATH env variable.

  `MIX_PATH` may contain multiple paths. If on Windows, those
  paths should be separated by `;`, if on Unix systems, use `:`.
  """
  def mix_paths do
    if path = System.get_env("MIX_PATH") do
      String.split(path, path_separator())
    else
      []
    end
  end

  defp path_separator do
    case :os.type() do
      {:win32, _} -> ";"
      {:unix, _} -> ":"
    end
  end

  @doc """
  Parses a string into module, function and arity.

  It returns `{:ok, mfa_list}`, where a `mfa_list` is
  `[module, function, arity]`, `[module, function]` or `[module]`,
  or the atom `:error`.

      iex> Mix.Utils.parse_mfa("Foo.bar/1")
      {:ok, [Foo, :bar, 1]}
      iex> Mix.Utils.parse_mfa(":foo.bar/1")
      {:ok, [:foo, :bar, 1]}
      iex> Mix.Utils.parse_mfa(":foo.bar")
      {:ok, [:foo, :bar]}
      iex> Mix.Utils.parse_mfa(":foo")
      {:ok, [:foo]}
      iex> Mix.Utils.parse_mfa("Foo")
      {:ok, [Foo]}

      iex> Mix.Utils.parse_mfa("Foo.")
      :error
      iex> Mix.Utils.parse_mfa("Foo.bar.baz")
      :error
      iex> Mix.Utils.parse_mfa("Foo.bar/2/2")
      :error
  """
  def parse_mfa(mfa) do
    with {:ok, quoted} <- Code.string_to_quoted(mfa),
         [_ | _] = mfa_list <- quoted_to_mfa(quoted) do
      {:ok, mfa_list}
    else
      _ -> :error
    end
  end

  defp quoted_to_mfa({:/, _, [dispatch, arity]}) when is_integer(arity) do
    quoted_to_mf(dispatch, [arity])
  end

  defp quoted_to_mfa(dispatch) do
    quoted_to_mf(dispatch, [])
  end

  defp quoted_to_mf({{:., _, [module, fun]}, _, []}, acc) when is_atom(fun) do
    quoted_to_m(module, [fun | acc])
  end

  defp quoted_to_mf(module, acc) do
    quoted_to_m(module, acc)
  end

  defp quoted_to_m({:__aliases__, _, aliases}, acc) do
    [Module.concat(aliases) | acc]
  end

  defp quoted_to_m(atom, acc) when is_atom(atom) do
    [atom | acc]
  end

  defp quoted_to_m(_, _acc) do
    []
  end

  @doc """
  Takes a `command` name and attempts to load a module
  with the command name converted to a module name
  in the given `at` scope.

  Returns `{:module, module}` in case a module
  exists and is loaded, `{:error, reason}` otherwise.

  ## Examples

      iex> Mix.Utils.command_to_module("compile", Mix.Tasks)
      {:module, Mix.Tasks.Compile}

  """
  def command_to_module(command, at \\ Elixir) do
    module = Module.concat(at, command_to_module_name(command))
    Code.ensure_loaded(module)
  end

  @doc """
  Returns `true` if any of the `sources` are stale
  compared to the given `targets`.
  """
  def stale?(sources, targets) do
    Enum.any?(stale_stream(sources, targets))
  end

  @doc """
  Extracts all stale `sources` compared to the given `targets`.
  """
  def extract_stale(_sources, []), do: []
  def extract_stale([], _targets), do: []

  def extract_stale(sources, targets) do
    stale_stream(sources, targets) |> Enum.to_list()
  end

  defp stale_stream(sources, targets) do
    modified_target = targets |> Enum.map(&last_modified/1) |> Enum.min()

    Stream.filter(sources, fn source ->
      last_modified(source) > modified_target
    end)
  end

  @doc """
  Returns the date the given path was last modified.

  If the path does not exist, it returns the Unix epoch
  (1970-01-01 00:00:00).
  """
  def last_modified(path)

  def last_modified({{_, _, _}, {_, _, _}} = timestamp) do
    timestamp
  end

  def last_modified(path) do
    {mtime, _size} = last_modified_and_size(path)
    mtime
  end

  @doc false
  def last_modified_and_size(path) do
    now = :calendar.universal_time()

    case :elixir_utils.read_mtime_and_size(path) do
      {:ok, mtime, size} when mtime > now ->
        message =
          "warning: mtime (modified time) for \"#{path}\" was set to the future, resetting to now"

        Mix.shell().error(message)

        File.touch!(path, now)
        {mtime, size}

      {:ok, mtime, size} ->
        {mtime, size}

      {:error, _} ->
        {{{1970, 1, 1}, {0, 0, 0}}, 0}
    end
  end

  @doc """
  Prints n files are being compiled with the given extension.
  """
  def compiling_n(1, ext), do: Mix.shell().info("Compiling 1 file (.#{ext})")
  def compiling_n(n, ext), do: Mix.shell().info("Compiling #{n} files (.#{ext})")

  @doc """
  Extracts files from a list of paths.

  `exts_or_pattern` may be a list of extensions or a
  `Path.wildcard/1` pattern.

  If the path in `paths` is a file, it is included in
  the return result. If it is a directory, it is searched
  recursively for files with the given extensions or matching
  the given patterns.
  """
  def extract_files(paths, exts_or_pattern)

  def extract_files(paths, exts) when is_list(exts) do
    extract_files(paths, "*.{#{Enum.join(exts, ",")}}")
  end

  def extract_files(paths, pattern) do
    Enum.flat_map(paths, fn path ->
      case :elixir_utils.read_file_type(path) do
        {:ok, :directory} -> Path.wildcard("#{path}/**/#{pattern}")
        {:ok, :regular} -> [path]
        _ -> []
      end
    end)
    |> Enum.uniq()
  end

  @type tree_node :: {name :: String.Chars.t(), edge_info :: String.Chars.t()}

  @doc """
  Prints the given tree according to the callback.

  The callback will be invoked for each node and it
  must return a `{printed, children}` tuple.
  """
  @spec print_tree([tree_node], (tree_node -> {tree_node, [tree_node]}), keyword) :: :ok
  def print_tree(nodes, callback, opts \\ []) do
    pretty? =
      case Keyword.get(opts, :format) do
        "pretty" -> true
        "plain" -> false
        _other -> elem(:os.type(), 0) != :win32
      end

    print_tree(nodes, _depth = [], _parent = nil, _seen = MapSet.new(), pretty?, callback)
    :ok
  end

  defp print_tree(_nodes = [], _depth, _parent, seen, _pretty, _callback) do
    seen
  end

  defp print_tree([node | nodes], depth, parent, seen, pretty?, callback) do
    {{name, info}, children} = callback.(node)
    key = {parent, name}

    if MapSet.member?(seen, key) do
      seen
    else
      info = if(info, do: " #{info}", else: "")
      Mix.shell().info("#{depth(pretty?, depth)}#{prefix(pretty?, depth, nodes)}#{name}#{info}")

      seen =
        print_tree(
          children,
          [nodes != [] | depth],
          name,
          MapSet.put(seen, key),
          pretty?,
          callback
        )

      print_tree(nodes, depth, parent, seen, pretty?, callback)
    end
  end

  defp depth(_pretty?, []), do: ""
  defp depth(pretty?, depth), do: Enum.reverse(depth) |> tl |> Enum.map(&entry(pretty?, &1))

  defp entry(false, true), do: "|   "
  defp entry(false, false), do: "    "
  defp entry(true, true), do: "│   "
  defp entry(true, false), do: "    "

  defp prefix(false, [], _), do: ""
  defp prefix(false, _, []), do: "`-- "
  defp prefix(false, _, _), do: "|-- "
  defp prefix(true, [], _), do: ""
  defp prefix(true, _, []), do: "└── "
  defp prefix(true, _, _), do: "├── "

  @doc """
  Outputs the given tree according to the callback as a DOT graph.

  The callback will be invoked for each node and it
  must return a `{printed, children}` tuple.
  """
  @spec write_dot_graph!(
          Path.t(),
          String.t(),
          [tree_node],
          (tree_node -> {tree_node, [tree_node]}),
          keyword
        ) :: :ok
  def write_dot_graph!(path, title, nodes, callback, _opts \\ []) do
    {dot, _} = build_dot_graph(make_ref(), nodes, MapSet.new(), callback)
    File.write!(path, "digraph \"#{title}\" {\n#{dot}}\n")
  end

  defp build_dot_graph(_parent, [], seen, _callback), do: {"", seen}

  defp build_dot_graph(parent, [node | nodes], seen, callback) do
    {{name, edge_info}, children} = callback.(node)
    key = {parent, name}

    if MapSet.member?(seen, key) do
      {"", seen}
    else
      seen = MapSet.put(seen, key)
      current = build_dot_current(parent, name, edge_info)
      {children, seen} = build_dot_graph(name, children, seen, callback)
      {siblings, seen} = build_dot_graph(parent, nodes, seen, callback)
      {current <> children <> siblings, seen}
    end
  end

  defp build_dot_current(parent, name, edge_info) do
    edge_info =
      if edge_info do
        ~s( [label="#{edge_info}"])
      end

    parent =
      unless is_reference(parent) do
        ~s("#{parent}" -> )
      end

    ~s(  #{parent}"#{name}"#{edge_info}\n)
  end

  @doc false
  def underscore(value) do
    IO.warn("Mix.Utils.underscore/1 is deprecated, use Macro.underscore/1 instead")
    Macro.underscore(value)
  end

  @doc false
  def camelize(value) do
    IO.warn("Mix.Utils.camelize/1 is deprecated, use Macro.camelize/1 instead")
    Macro.camelize(value)
  end

  @doc """
  Takes a module and converts it to a command.

  The nesting argument can be given in order to remove
  the nesting of a module.

  ## Examples

      iex> Mix.Utils.module_name_to_command(Mix.Tasks.Compile, 2)
      "compile"

      iex> Mix.Utils.module_name_to_command("Mix.Tasks.Compile.Elixir", 2)
      "compile.elixir"

  """
  def module_name_to_command(module, nesting \\ 0)

  def module_name_to_command(module, nesting) when is_atom(module) do
    module_name_to_command(inspect(module), nesting)
  end

  def module_name_to_command(module, nesting) do
    module
    |> to_string()
    |> String.split(".")
    |> Enum.drop(nesting)
    |> Enum.map_join(".", &Macro.underscore/1)
  end

  @doc """
  Takes a command and converts it to the module name format.

  ## Examples

      iex> Mix.Utils.command_to_module_name("compile.elixir")
      "Compile.Elixir"

  """
  def command_to_module_name(command) do
    command
    |> to_string()
    |> String.split(".")
    |> Enum.map_join(".", &Macro.camelize/1)
  end

  @doc """
  Symlinks directory `source` to `target` or copies it recursively
  in case symlink fails.

  Expects source and target to be absolute paths as it generates
  a relative symlink.
  """
  def symlink_or_copy(source, target) do
    if File.exists?(source) do
      # Relative symbolic links on Windows are broken
      link =
        case :os.type() do
          {:win32, _} -> source
          _ -> make_relative_path(source, target)
        end
        |> String.to_charlist()

      case :file.read_link(target) do
        {:ok, ^link} ->
          :ok

        {:ok, _} ->
          File.rm!(target)
          do_symlink_or_copy(source, target, link)

        {:error, :enoent} ->
          do_symlink_or_copy(source, target, link)

        {:error, _} ->
          unless File.dir?(target) do
            File.rm_rf!(target)
          end

          do_symlink_or_copy(source, target, link)
      end
    else
      {:error, :enoent}
    end
  end

  defp do_symlink_or_copy(source, target, link) do
    case :file.make_symlink(link, target) do
      :ok ->
        :ok

      {:error, _} ->
        file =
          File.cp_r!(source, target, fn orig, dest ->
            File.stat!(orig).mtime > File.stat!(dest).mtime
          end)

        {:ok, file}
    end
  end

  # Make a relative path between the two given paths.
  # Expects both paths to be fully expanded.
  defp make_relative_path(source, target) do
    do_make_relative_path(Path.split(source), Path.split(target))
  end

  defp do_make_relative_path([h | t1], [h | t2]) do
    do_make_relative_path(t1, t2)
  end

  defp do_make_relative_path(source, target) do
    base = List.duplicate("..", max(length(target) - 1, 0))
    Path.join(base ++ source)
  end

  @doc """
  Opens and reads content from either a URL or a local filesystem path.

  Returns the contents as a `{:ok, binary}`, `:badpath` for invalid
  paths or `{:local, message}` for local errors and `{:remote, message}`
  for remote ones.

  ## Options

    * `:sha512` - checks against the given SHA-512 checksum. Returns
      `{:checksum, message}` in case it fails
  """
  @spec read_path(String.t(), keyword) ::
          {:ok, binary}
          | :badpath
          | {:remote, String.t()}
          | {:local, String.t()}
          | {:checksum, String.t()}
  def read_path(path, opts \\ []) do
    cond do
      url?(path) ->
        read_httpc(path) |> checksum(opts)

      file?(path) ->
        read_file(path) |> checksum(opts)

      true ->
        :badpath
    end
  end

  @checksums [:sha512]

  defp checksum({:ok, binary} = return, opts) do
    Enum.find_value(@checksums, return, fn hash ->
      with expected when expected != nil <- opts[hash],
           actual when actual != expected <- hexhash(binary, hash) do
        message = """
        Data does not match the given SHA-512 checksum.

        Expected: #{expected}
          Actual: #{actual}
        """

        {:checksum, message}
      else
        _ -> nil
      end
    end)
  end

  defp checksum({_, _} = error, _opts) do
    error
  end

  defp hexhash(binary, hash) do
    Base.encode16(:crypto.hash(hash, binary), case: :lower)
  end

  @doc """
  Prompts the user to overwrite the file if it exists. Returns
  the user input.
  """
  def can_write?(path) do
    if File.exists?(path) do
      full = Path.expand(path)
      Mix.shell().yes?(Path.relative_to_cwd(full) <> " already exists, overwrite?")
    else
      true
    end
  end

  defp read_file(path) do
    try do
      {:ok, File.read!(path)}
    rescue
      e in [File.Error] -> {:local, Exception.message(e)}
    end
  end

  defp read_httpc(path) do
    {:ok, _} = Application.ensure_all_started(:ssl)
    {:ok, _} = Application.ensure_all_started(:inets)

    # Starting an HTTP client profile allows us to scope
    # the effects of using an HTTP proxy to this function
    {:ok, _pid} = :inets.start(:httpc, [{:profile, :mix}])

    headers = [{'user-agent', 'Mix/#{System.version()}'}]
    request = {:binary.bin_to_list(path), headers}

    # We are using relaxed: true because some servers is returning a Location
    # header with relative paths, which does not follow the spec. This would
    # cause the request to fail with {:error, :no_scheme} unless :relaxed
    # is given.
    #
    # If a proxy environment variable was supplied add a proxy to httpc.
    http_options = [relaxed: true] ++ proxy_config(path)

    case :httpc.request(:get, request, http_options, [body_format: :binary], :mix) do
      {:ok, {{_, status, _}, _, body}} when status in 200..299 ->
        {:ok, body}

      {:ok, {{_, status, _}, _, _}} ->
        {:remote, "httpc request failed with: {:bad_status_code, #{status}}"}

      {:error, reason} ->
        {:remote, "httpc request failed with: #{inspect(reason)}"}
    end
  after
    :inets.stop(:httpc, :mix)
  end

  defp file?(path) do
    File.regular?(path)
  end

  defp url?(path) do
    URI.parse(path).scheme in ["http", "https"]
  end

  def proxy_config(url) do
    {http_proxy, https_proxy} = proxy_env()

    proxy_auth(URI.parse(url), http_proxy, https_proxy)
  end

  defp proxy_env do
    http_proxy = System.get_env("HTTP_PROXY") || System.get_env("http_proxy")
    https_proxy = System.get_env("HTTPS_PROXY") || System.get_env("https_proxy")
    no_proxy = no_proxy_env() |> no_proxy_list()

    {proxy_setup(:http, http_proxy, no_proxy), proxy_setup(:https, https_proxy, no_proxy)}
  end

  defp no_proxy_env() do
    System.get_env("NO_PROXY") || System.get_env("no_proxy")
  end

  defp no_proxy_list(nil) do
    []
  end

  defp no_proxy_list(no_proxy) do
    no_proxy
    |> String.split(",")
    |> Enum.map(&String.to_charlist/1)
  end

  defp proxy_setup(scheme, proxy, no_proxy) do
    uri = URI.parse(proxy || "")

    if uri.host && uri.port do
      host = String.to_charlist(uri.host)
      :httpc.set_options([{proxy_scheme(scheme), {{host, uri.port}, no_proxy}}], :mix)
    end

    uri
  end

  defp proxy_scheme(scheme) do
    case scheme do
      :http -> :proxy
      :https -> :https_proxy
    end
  end

  defp proxy_auth(%URI{scheme: "http"}, http_proxy, _https_proxy), do: proxy_auth(http_proxy)
  defp proxy_auth(%URI{scheme: "https"}, _http_proxy, https_proxy), do: proxy_auth(https_proxy)

  defp proxy_auth(nil), do: []
  defp proxy_auth(%URI{userinfo: nil}), do: []

  defp proxy_auth(%URI{userinfo: auth}) do
    destructure [user, pass], String.split(auth, ":", parts: 2)

    user = String.to_charlist(user)
    pass = String.to_charlist(pass || "")

    [proxy_auth: {user, pass}]
  end
end