Add example using file extension in Path.basename/1 (#11583)

defmodule Path do
  @moduledoc """
  This module provides conveniences for manipulating or
  retrieving file system paths.

  The functions in this module may receive chardata as
  arguments and will always return a string encoded in UTF-8. Chardata
  is a string or a list of characters and strings, see `t:IO.chardata/0`.
  If a binary is given, in whatever encoding, its encoding will be kept.

  The majority of the functions in this module do not
  interact with the file system, except for a few functions
  that require it (like `wildcard/2` and `expand/1`).
  """

  @typedoc """
  A path.
  """
  @type t :: IO.chardata()

  @doc """
  Converts the given path to an absolute one.

  Unlike `expand/1`, no attempt is made to resolve `..`, `.`, or `~`.

  ## Examples

  ### Unix-like operating systems

      Path.absname("foo")
      #=> "/usr/local/foo"

      Path.absname("../x")
      #=> "/usr/local/../x"

  ### Windows

      Path.absname("foo")
      #=> "D:/usr/local/foo"

      Path.absname("../x")
      #=> "D:/usr/local/../x"

  """
  @spec absname(t) :: binary
  def absname(path) do
    absname(path, File.cwd!())
  end

  @doc """
  Builds a path from `relative_to` to `path`.

  If `path` is already an absolute path, `relative_to` is ignored. See also
  `relative_to/2`.

  Unlike `expand/2`, no attempt is made to
  resolve `..`, `.` or `~`.

  ## Examples

      iex> Path.absname("foo", "bar")
      "bar/foo"

      iex> Path.absname("../x", "bar")
      "bar/../x"

  """
  @spec absname(t, t) :: binary
  def absname(path, relative_to) do
    path = IO.chardata_to_string(path)

    case type(path) do
      :relative ->
        absname_join([relative_to, path])

      :absolute ->
        absname_join([path])

      :volumerelative ->
        relative_to = IO.chardata_to_string(relative_to)
        absname_vr(split(path), split(relative_to), relative_to)
    end
  end

  # Absolute path on current drive
  defp absname_vr(["/" | rest], [volume | _], _relative), do: absname_join([volume | rest])

  # Relative to current directory on current drive
  defp absname_vr([<<x, ?:>> | rest], [<<x, _::binary>> | _], relative),
    do: absname(absname_join(rest), relative)

  # Relative to current directory on another drive
  defp absname_vr([<<x, ?:>> | name], _, _relative) do
    cwd =
      case :file.get_cwd([x, ?:]) do
        {:ok, dir} -> IO.chardata_to_string(dir)
        {:error, _} -> <<x, ?:, ?/>>
      end

    absname(absname_join(name), cwd)
  end

  @slash [?/, ?\\]

  defp absname_join([]), do: ""
  defp absname_join(list), do: absname_join(list, major_os_type())

  defp absname_join([name1, name2 | rest], os_type) do
    joined = do_absname_join(IO.chardata_to_string(name1), relative(name2), [], os_type)
    absname_join([joined | rest], os_type)
  end

  defp absname_join([name], os_type) do
    do_absname_join(IO.chardata_to_string(name), <<>>, [], os_type)
  end

  defp do_absname_join(<<uc_letter, ?:, rest::binary>>, relativename, [], :win32)
       when uc_letter in ?A..?Z,
       do: do_absname_join(rest, relativename, [?:, uc_letter + ?a - ?A], :win32)

  defp do_absname_join(<<c1, c2, rest::binary>>, relativename, [], :win32)
       when c1 in @slash and c2 in @slash,
       do: do_absname_join(rest, relativename, '//', :win32)

  defp do_absname_join(<<?\\, rest::binary>>, relativename, result, :win32),
    do: do_absname_join(<<?/, rest::binary>>, relativename, result, :win32)

  defp do_absname_join(<<?/, rest::binary>>, relativename, [?., ?/ | result], os_type),
    do: do_absname_join(rest, relativename, [?/ | result], os_type)

  defp do_absname_join(<<?/, rest::binary>>, relativename, [?/ | result], os_type),
    do: do_absname_join(rest, relativename, [?/ | result], os_type)

  defp do_absname_join(<<>>, <<>>, result, os_type),
    do: IO.iodata_to_binary(reverse_maybe_remove_dir_sep(result, os_type))

  defp do_absname_join(<<>>, relativename, [?: | rest], :win32),
    do: do_absname_join(relativename, <<>>, [?: | rest], :win32)

  defp do_absname_join(<<>>, relativename, [?/ | result], os_type),
    do: do_absname_join(relativename, <<>>, [?/ | result], os_type)

  defp do_absname_join(<<>>, relativename, result, os_type),
    do: do_absname_join(relativename, <<>>, [?/ | result], os_type)

  defp do_absname_join(<<char, rest::binary>>, relativename, result, os_type),
    do: do_absname_join(rest, relativename, [char | result], os_type)

  defp reverse_maybe_remove_dir_sep([?/, ?:, letter], :win32), do: [letter, ?:, ?/]
  defp reverse_maybe_remove_dir_sep([?/], _), do: [?/]
  defp reverse_maybe_remove_dir_sep([?/ | name], _), do: :lists.reverse(name)
  defp reverse_maybe_remove_dir_sep(name, _), do: :lists.reverse(name)

  @doc """
  Converts the path to an absolute one, expanding
  any `.` and `..` components and a leading `~`.

  ## Examples

      Path.expand("/foo/bar/../baz")
      #=> "/foo/baz"

  """
  @spec expand(t) :: binary
  def expand(path) do
    expand_dot(absname(expand_home(path), File.cwd!()))
  end

  @doc """
  Expands the path relative to the path given as the second argument
  expanding any `.` and `..` characters.

  If the path is already an absolute path, `relative_to` is ignored.

  Note that this function treats a `path` with a leading `~` as
  an absolute one.

  The second argument is first expanded to an absolute path.

  ## Examples

      # Assuming that the absolute path to baz is /quux/baz
      Path.expand("foo/bar/../bar", "baz")
      #=> "/quux/baz/foo/bar"

      Path.expand("foo/bar/../bar", "/baz")
      #=> "/baz/foo/bar"

      Path.expand("/foo/bar/../bar", "/baz")
      #=> "/foo/bar"

  """
  @spec expand(t, t) :: binary
  def expand(path, relative_to) do
    expand_dot(absname(absname(expand_home(path), expand_home(relative_to)), File.cwd!()))
  end

  @doc """
  Returns the path type.

  ## Examples

  ### Unix-like operating systems

      Path.type("/")                #=> :absolute
      Path.type("/usr/local/bin")   #=> :absolute
      Path.type("usr/local/bin")    #=> :relative
      Path.type("../usr/local/bin") #=> :relative
      Path.type("~/file")           #=> :relative

  ### Windows

      Path.type("D:/usr/local/bin") #=> :absolute
      Path.type("usr/local/bin")    #=> :relative
      Path.type("D:bar.ex")         #=> :volumerelative
      Path.type("/bar/foo.ex")      #=> :volumerelative

  """
  @spec type(t) :: :absolute | :relative | :volumerelative
  def type(name)
      when is_list(name)
      when is_binary(name) do
    pathtype(name, major_os_type()) |> elem(0)
  end

  @doc """
  Forces the path to be a relative path.

  ## Examples

  ### Unix-like operating systems

      Path.relative("/usr/local/bin")   #=> "usr/local/bin"
      Path.relative("usr/local/bin")    #=> "usr/local/bin"
      Path.relative("../usr/local/bin") #=> "../usr/local/bin"

  ### Windows

      Path.relative("D:/usr/local/bin") #=> "usr/local/bin"
      Path.relative("usr/local/bin")    #=> "usr/local/bin"
      Path.relative("D:bar.ex")         #=> "bar.ex"
      Path.relative("/bar/foo.ex")      #=> "bar/foo.ex"

  """
  @spec relative(t) :: binary
  def relative(name) do
    relative(name, major_os_type())
  end

  defp relative(name, os_type) do
    pathtype(name, os_type)
    |> elem(1)
    |> IO.chardata_to_string()
  end

  defp pathtype(name, os_type) do
    case os_type do
      :win32 -> win32_pathtype(name)
      _ -> unix_pathtype(name)
    end
  end

  defp unix_pathtype(path) when path in ["/", '/'], do: {:absolute, "."}
  defp unix_pathtype(<<?/, relative::binary>>), do: {:absolute, relative}
  defp unix_pathtype([?/ | relative]), do: {:absolute, relative}
  defp unix_pathtype([list | rest]) when is_list(list), do: unix_pathtype(list ++ rest)
  defp unix_pathtype(relative), do: {:relative, relative}

  defp win32_pathtype([list | rest]) when is_list(list), do: win32_pathtype(list ++ rest)

  defp win32_pathtype([char, list | rest]) when is_list(list),
    do: win32_pathtype([char | list ++ rest])

  defp win32_pathtype(<<c1, c2, relative::binary>>) when c1 in @slash and c2 in @slash,
    do: {:absolute, relative}

  defp win32_pathtype(<<char, relative::binary>>) when char in @slash,
    do: {:volumerelative, relative}

  defp win32_pathtype(<<_letter, ?:, char, relative::binary>>) when char in @slash,
    do: {:absolute, relative}

  defp win32_pathtype(<<_letter, ?:, relative::binary>>), do: {:volumerelative, relative}

  defp win32_pathtype([c1, c2 | relative]) when c1 in @slash and c2 in @slash,
    do: {:absolute, relative}

  defp win32_pathtype([char | relative]) when char in @slash, do: {:volumerelative, relative}

  defp win32_pathtype([c1, c2, list | rest]) when is_list(list),
    do: win32_pathtype([c1, c2 | list ++ rest])

  defp win32_pathtype([_letter, ?:, char | relative]) when char in @slash,
    do: {:absolute, relative}

  defp win32_pathtype([_letter, ?: | relative]), do: {:volumerelative, relative}
  defp win32_pathtype(relative), do: {:relative, relative}

  @doc """
  Returns the given `path` relative to the given `from` path.

  In other words, this function tries to strip the `from` prefix from `path`.

  This function does not query the file system, so it assumes
  no symlinks between the paths.

  In case a direct relative path cannot be found, it returns
  the original path.

  ## Examples

      iex> Path.relative_to("/usr/local/foo", "/usr/local")
      "foo"

      iex> Path.relative_to("/usr/local/foo", "/")
      "usr/local/foo"

      iex> Path.relative_to("/usr/local/foo", "/etc")
      "/usr/local/foo"

      iex> Path.relative_to("/usr/local/foo", "/usr/local/foo")
      "."

  """
  @spec relative_to(t, t) :: binary
  def relative_to(path, from) do
    path = IO.chardata_to_string(path)
    relative_to(split(path), split(from), path)
  end

  defp relative_to(path, path, _original) do
    "."
  end

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

  defp relative_to([_ | _] = l1, [], _original) do
    join(l1)
  end

  defp relative_to(_, _, original) do
    original
  end

  @doc """
  Convenience to get the path relative to the current working
  directory.

  If, for some reason, the current working directory
  cannot be retrieved, this function returns the given `path`.
  """
  @spec relative_to_cwd(t) :: binary
  def relative_to_cwd(path) do
    case :file.get_cwd() do
      {:ok, base} -> relative_to(path, IO.chardata_to_string(base))
      _ -> path
    end
  end

  @doc """
  Returns the last component of the path or the path
  itself if it does not contain any directory separators.

  ## Examples

      iex> Path.basename("foo")
      "foo"

      iex> Path.basename("foo/bar")
      "bar"

      iex> Path.basename("lib/module/submodule.ex")
      "submodule.ex"

      iex> Path.basename("/")
      ""

  """
  @spec basename(t) :: binary
  def basename(path) do
    :filename.basename(IO.chardata_to_string(path))
  end

  @doc """
  Returns the last component of `path` with the `extension`
  stripped.

  This function should be used to remove a specific
  extension which may or may not be there.

  ## Examples

      iex> Path.basename("~/foo/bar.ex", ".ex")
      "bar"

      iex> Path.basename("~/foo/bar.exs", ".ex")
      "bar.exs"

      iex> Path.basename("~/foo/bar.old.ex", ".ex")
      "bar.old"

  """
  @spec basename(t, t) :: binary
  def basename(path, extension) do
    :filename.basename(IO.chardata_to_string(path), IO.chardata_to_string(extension))
  end

  @doc """
  Returns the directory component of `path`.

  ## Examples

      iex> Path.dirname("/foo/bar.ex")
      "/foo"

      iex> Path.dirname("/foo/bar/baz.ex")
      "/foo/bar"

      iex> Path.dirname("/foo/bar/")
      "/foo/bar"

      iex> Path.dirname("bar.ex")
      "."

  """
  @spec dirname(t) :: binary
  def dirname(path) do
    :filename.dirname(IO.chardata_to_string(path))
  end

  @doc """
  Returns the extension of the last component of `path`.

  The behaviour of this function changed in Erlang/OTP 24 for filenames
  starting with a dot and without an extension. For example, for a file
  named `.gitignore`, `extname/1` now returns an empty string, while it
  would return `".gitignore"` in previous Erlang/OTP versions. This was
  done to match the behaviour of `rootname/1`, which would return
  `".gitignore"` as its name (and therefore it cannot also be an extension).

  See `basename/1` and `rootname/1` for related functions to extract
  information from paths.

  ## Examples

      iex> Path.extname("foo.erl")
      ".erl"

      iex> Path.extname("~/foo/bar")
      ""

  """
  @spec extname(t) :: binary
  def extname(path) do
    :filename.extension(IO.chardata_to_string(path))
  end

  @doc """
  Returns the `path` with the `extension` stripped.

  ## Examples

      iex> Path.rootname("/foo/bar")
      "/foo/bar"

      iex> Path.rootname("/foo/bar.ex")
      "/foo/bar"

  """
  @spec rootname(t) :: binary
  def rootname(path) do
    :filename.rootname(IO.chardata_to_string(path))
  end

  @doc """
  Returns the `path` with the `extension` stripped.

  This function should be used to remove a specific extension which may
  or may not be there.

  ## Examples

      iex> Path.rootname("/foo/bar.erl", ".erl")
      "/foo/bar"

      iex> Path.rootname("/foo/bar.erl", ".ex")
      "/foo/bar.erl"

  """
  @spec rootname(t, t) :: binary
  def rootname(path, extension) do
    :filename.rootname(IO.chardata_to_string(path), IO.chardata_to_string(extension))
  end

  @doc """
  Joins a list of paths.

  This function should be used to convert a list of paths to a path.
  Note that any trailing slash is removed when joining.

  Raises an error if the given list of paths is empty.

  ## Examples

      iex> Path.join(["~", "foo"])
      "~/foo"

      iex> Path.join(["foo"])
      "foo"

      iex> Path.join(["/", "foo", "bar/"])
      "/foo/bar"

  """
  @spec join(nonempty_list(t)) :: binary
  def join([name1, name2 | rest]), do: join([join(name1, name2) | rest])
  def join([name]), do: IO.chardata_to_string(name)

  @doc """
  Joins two paths.

  The right path will always be expanded to its relative format
  and any trailing slash will be removed when joining.

  ## Examples

      iex> Path.join("foo", "bar")
      "foo/bar"

      iex> Path.join("/foo", "/bar/")
      "/foo/bar"

  The functions in this module support chardata, so giving a list will
  treat it as a single entity:

      iex> Path.join("foo", ["bar", "fiz"])
      "foo/barfiz"

      iex> Path.join(["foo", "bar"], "fiz")
      "foobar/fiz"

  Use `join/1` if you need to join a list of paths instead.
  """
  @spec join(t, t) :: binary
  def join(left, right) do
    left = IO.chardata_to_string(left)
    os_type = major_os_type()
    do_join(left, right, os_type) |> remove_dir_sep(os_type)
  end

  defp do_join(left, "/", os_type), do: remove_dir_sep(left, os_type)
  defp do_join("", right, os_type), do: relative(right, os_type)
  defp do_join("/", right, os_type), do: "/" <> relative(right, os_type)

  defp do_join(left, right, os_type),
    do: remove_dir_sep(left, os_type) <> "/" <> relative(right, os_type)

  defp remove_dir_sep("", _os_type), do: ""
  defp remove_dir_sep("/", _os_type), do: "/"

  defp remove_dir_sep(bin, os_type) do
    last = :binary.last(bin)

    if last == ?/ or (last == ?\\ and os_type == :win32) do
      binary_part(bin, 0, byte_size(bin) - 1)
    else
      bin
    end
  end

  @doc ~S"""
  Splits the path into a list at the path separator.

  If an empty string is given, returns an empty list.

  On Windows, path is split on both `"\"` and `"/"` separators
  and the driver letter, if there is one, is always returned
  in lowercase.

  ## Examples

      iex> Path.split("")
      []

      iex> Path.split("foo")
      ["foo"]

      iex> Path.split("/foo/bar")
      ["/", "foo", "bar"]

  """
  @spec split(t) :: [binary]
  def split(path) do
    :filename.split(IO.chardata_to_string(path))
  end

  defmodule Wildcard do
    @moduledoc false

    def read_link_info(file) do
      :file.read_link_info(file)
    end

    def list_dir(dir) do
      case :file.list_dir(dir) do
        {:ok, files} -> {:ok, for(file <- files, hd(file) != ?., do: file)}
        other -> other
      end
    end
  end

  @doc """
  Traverses paths according to the given `glob` expression and returns a
  list of matches.

  The wildcard looks like an ordinary path, except that the following
  "wildcard characters" are interpreted in a special way:

    * `?` - matches one character.

    * `*` - matches any number of characters up to the end of the filename, the
      next dot, or the next slash.

    * `**` - two adjacent `*`'s used as a single pattern will match all
      files and zero or more directories and subdirectories.

    * `[char1,char2,...]` - matches any of the characters listed; two
      characters separated by a hyphen will match a range of characters.
      Do not add spaces before and after the comma as it would then match
      paths containing the space character itself.

    * `{item1,item2,...}` - matches one of the alternatives.
      Do not add spaces before and after the comma as it would then match
      paths containing the space character itself.

  Other characters represent themselves. Only paths that have
  exactly the same character in the same position will match. Note
  that matching is case-sensitive: `"a"` will not match `"A"`.

  Directory separators must always be written as `/`, even on Windows.
  You may call `Path.expand/1` to normalize the path before invoking
  this function.

  By default, the patterns `*` and `?` do not match files starting
  with a dot `.`. See the `:match_dot` option in the "Options" section
  below.

  ## Options

    * `:match_dot` - (boolean) if `false`, the special wildcard characters `*` and `?`
      will not match files starting with a dot (`.`). If `true`, files starting with
      a `.` will not be treated specially. Defaults to `false`.

  ## Examples

  Imagine you have a directory called `projects` with three Elixir projects
  inside of it: `elixir`, `ex_doc`, and `plug`. You can find all `.beam` files
  inside the `ebin` directory of each project as follows:

      Path.wildcard("projects/*/ebin/**/*.beam")

  If you want to search for both `.beam` and `.app` files, you could do:

      Path.wildcard("projects/*/ebin/**/*.{beam,app}")

  """
  @spec wildcard(t, keyword) :: [binary]
  def wildcard(glob, opts \\ []) when is_list(opts) do
    mod = if Keyword.get(opts, :match_dot), do: :file, else: Path.Wildcard

    glob
    |> chardata_to_list!()
    |> :filelib.wildcard(mod)
    |> Enum.map(&IO.chardata_to_string/1)
  end

  defp chardata_to_list!(chardata) do
    case :unicode.characters_to_list(chardata) do
      result when is_list(result) ->
        if 0 in result do
          raise ArgumentError,
                "cannot execute Path.wildcard/2 for path with null byte, got: #{inspect(chardata)}"
        else
          result
        end

      {:error, encoded, rest} ->
        raise UnicodeConversionError, encoded: encoded, rest: rest, kind: :invalid

      {:incomplete, encoded, rest} ->
        raise UnicodeConversionError, encoded: encoded, rest: rest, kind: :incomplete
    end
  end

  defp expand_home(type) do
    case IO.chardata_to_string(type) do
      "~" <> rest -> resolve_home(rest)
      rest -> rest
    end
  end

  defp resolve_home(""), do: System.user_home!()

  defp resolve_home(rest) do
    case {rest, major_os_type()} do
      {"\\" <> _, :win32} -> System.user_home!() <> rest
      {"/" <> _, _} -> System.user_home!() <> rest
      _ -> "~" <> rest
    end
  end

  # expand_dot the given path by expanding "..", "." and "~".
  defp expand_dot(<<"/", rest::binary>>), do: "/" <> do_expand_dot(rest)

  defp expand_dot(<<letter, ":/", rest::binary>>) when letter in ?a..?z,
    do: <<letter, ":/">> <> do_expand_dot(rest)

  defp expand_dot(path), do: do_expand_dot(path)

  defp do_expand_dot(path), do: do_expand_dot(:binary.split(path, "/", [:global]), [])
  defp do_expand_dot([".." | t], [_, _ | acc]), do: do_expand_dot(t, acc)
  defp do_expand_dot([".." | t], []), do: do_expand_dot(t, [])
  defp do_expand_dot(["." | t], acc), do: do_expand_dot(t, acc)
  defp do_expand_dot([h | t], acc), do: do_expand_dot(t, ["/", h | acc])
  defp do_expand_dot([], []), do: ""
  defp do_expand_dot([], ["/" | acc]), do: IO.iodata_to_binary(:lists.reverse(acc))

  defp major_os_type do
    :os.type() |> elem(0)
  end

  @doc """
  Returns a relative path that is protected from directory-traversal attacks.

  The given relative path is sanitized by eliminating `..` and `.` components.

  This function checks that, after expanding those components, the path is still "safe".
  Paths are considered unsafe if either of these is true:

    * The path is not relative, such as `"/foo/bar"`.

    * A `..` component would make it so that the path would travers up above
      the root of `relative_to`.

    * A symbolic link in the path points to something above the root of `relative_to`.

  ## Examples

      iex> Path.safe_relative_to("deps/my_dep/app.beam", "deps")
      {:ok, "deps/my_dep/app.beam"}

      iex> Path.safe_relative_to("deps/my_dep/./build/../app.beam", "deps")
      {:ok, "deps/my_dep/app.beam"}

      iex> Path.safe_relative_to("my_dep/../..", "deps")
      :error

      iex> Path.safe_relative_to("/usr/local", ".")
      :error

  """
  @doc since: "1.14.0"
  @spec safe_relative_to(t, t) :: {:ok, binary} | :error
  def safe_relative_to(path, relative_to) do
    path = IO.chardata_to_string(path)

    case :filelib.safe_relative_path(path, relative_to) do
      :unsafe -> :error
      relative_path -> {:ok, IO.chardata_to_string(relative_path)}
    end
  end

  @doc """
  Returns a path relative to the current working directory that is
  protected from directory-traversal attacks.

  Same as `safe_relative_to/2` with the current working directory as
  the second argument. If there is an issue retrieving the current working
  directory, this function raises an error.

  ## Examples

      iex> Path.safe_relative("foo")
      {:ok, "foo"}

      iex> Path.safe_relative("foo/../bar")
      {:ok, "bar"}

      iex> Path.safe_relative("foo/../..")
      :error

      iex> Path.safe_relative("/usr/local")
      :error

  """
  @doc since: "1.14.0"
  @spec safe_relative(t) :: {:ok, binary} | :error
  def safe_relative(path) do
    safe_relative_to(path, File.cwd!())
  end
end

defmodule Path do
  @moduledoc """
  This module provides conveniences for manipulating or
  retrieving file system paths.

  The functions in this module may receive chardata as
  arguments and will always return a string encoded in UTF-8. Chardata
  is a string or a list of characters and strings, see `t:IO.chardata/0`.
  If a binary is given, in whatever encoding, its encoding will be kept.

  The majority of the functions in this module do not
  interact with the file system, except for a few functions
  that require it (like `wildcard/2` and `expand/1`).
  """

  @typedoc """
  A path.
  """
  @type t :: IO.chardata()

  @doc """
  Converts the given path to an absolute one.

  Unlike `expand/1`, no attempt is made to resolve `..`, `.`, or `~`.

  ## Examples

  ### Unix-like operating systems

      Path.absname("foo")
      #=> "/usr/local/foo"

      Path.absname("../x")
      #=> "/usr/local/../x"

  ### Windows

      Path.absname("foo")
      #=> "D:/usr/local/foo"

      Path.absname("../x")
      #=> "D:/usr/local/../x"

  """
  @spec absname(t) :: binary
  def absname(path) do
    absname(path, File.cwd!())
  end

  @doc """
  Builds a path from `relative_to` to `path`.

  If `path` is already an absolute path, `relative_to` is ignored. See also
  `relative_to/2`.

  Unlike `expand/2`, no attempt is made to
  resolve `..`, `.` or `~`.

  ## Examples

      iex> Path.absname("foo", "bar")
      "bar/foo"

      iex> Path.absname("../x", "bar")
      "bar/../x"

  """
  @spec absname(t, t) :: binary
  def absname(path, relative_to) do
    path = IO.chardata_to_string(path)

    case type(path) do
      :relative ->
        absname_join([relative_to, path])

      :absolute ->
        absname_join([path])

      :volumerelative ->
        relative_to = IO.chardata_to_string(relative_to)
        absname_vr(split(path), split(relative_to), relative_to)
    end
  end

  # Absolute path on current drive
  defp absname_vr(["/" | rest], [volume | _], _relative), do: absname_join([volume | rest])

  # Relative to current directory on current drive
  defp absname_vr([<<x, ?:>> | rest], [<<x, _::binary>> | _], relative),
    do: absname(absname_join(rest), relative)

  # Relative to current directory on another drive
  defp absname_vr([<<x, ?:>> | name], _, _relative) do
    cwd =
      case :file.get_cwd([x, ?:]) do
        {:ok, dir} -> IO.chardata_to_string(dir)
        {:error, _} -> <<x, ?:, ?/>>
      end

    absname(absname_join(name), cwd)
  end

  @slash [?/, ?\\]

  defp absname_join([]), do: ""
  defp absname_join(list), do: absname_join(list, major_os_type())

  defp absname_join([name1, name2 | rest], os_type) do
    joined = do_absname_join(IO.chardata_to_string(name1), relative(name2), [], os_type)
    absname_join([joined | rest], os_type)
  end

  defp absname_join([name], os_type) do
    do_absname_join(IO.chardata_to_string(name), <<>>, [], os_type)
  end

  defp do_absname_join(<<uc_letter, ?:, rest::binary>>, relativename, [], :win32)
       when uc_letter in ?A..?Z,
       do: do_absname_join(rest, relativename, [?:, uc_letter + ?a - ?A], :win32)

  defp do_absname_join(<<c1, c2, rest::binary>>, relativename, [], :win32)
       when c1 in @slash and c2 in @slash,
       do: do_absname_join(rest, relativename, '//', :win32)

  defp do_absname_join(<<?\\, rest::binary>>, relativename, result, :win32),
    do: do_absname_join(<<?/, rest::binary>>, relativename, result, :win32)

  defp do_absname_join(<<?/, rest::binary>>, relativename, [?., ?/ | result], os_type),
    do: do_absname_join(rest, relativename, [?/ | result], os_type)

  defp do_absname_join(<<?/, rest::binary>>, relativename, [?/ | result], os_type),
    do: do_absname_join(rest, relativename, [?/ | result], os_type)

  defp do_absname_join(<<>>, <<>>, result, os_type),
    do: IO.iodata_to_binary(reverse_maybe_remove_dir_sep(result, os_type))

  defp do_absname_join(<<>>, relativename, [?: | rest], :win32),
    do: do_absname_join(relativename, <<>>, [?: | rest], :win32)

  defp do_absname_join(<<>>, relativename, [?/ | result], os_type),
    do: do_absname_join(relativename, <<>>, [?/ | result], os_type)

  defp do_absname_join(<<>>, relativename, result, os_type),
    do: do_absname_join(relativename, <<>>, [?/ | result], os_type)

  defp do_absname_join(<<char, rest::binary>>, relativename, result, os_type),
    do: do_absname_join(rest, relativename, [char | result], os_type)

  defp reverse_maybe_remove_dir_sep([?/, ?:, letter], :win32), do: [letter, ?:, ?/]
  defp reverse_maybe_remove_dir_sep([?/], _), do: [?/]
  defp reverse_maybe_remove_dir_sep([?/ | name], _), do: :lists.reverse(name)
  defp reverse_maybe_remove_dir_sep(name, _), do: :lists.reverse(name)

  @doc """
  Converts the path to an absolute one, expanding
  any `.` and `..` components and a leading `~`.

  ## Examples

      Path.expand("/foo/bar/../baz")
      #=> "/foo/baz"

  """
  @spec expand(t) :: binary
  def expand(path) do
    expand_dot(absname(expand_home(path), File.cwd!()))
  end

  @doc """
  Expands the path relative to the path given as the second argument
  expanding any `.` and `..` characters.

  If the path is already an absolute path, `relative_to` is ignored.

  Note that this function treats a `path` with a leading `~` as
  an absolute one.

  The second argument is first expanded to an absolute path.

  ## Examples

      # Assuming that the absolute path to baz is /quux/baz
      Path.expand("foo/bar/../bar", "baz")
      #=> "/quux/baz/foo/bar"

      Path.expand("foo/bar/../bar", "/baz")
      #=> "/baz/foo/bar"

      Path.expand("/foo/bar/../bar", "/baz")
      #=> "/foo/bar"

  """
  @spec expand(t, t) :: binary
  def expand(path, relative_to) do
    expand_dot(absname(absname(expand_home(path), expand_home(relative_to)), File.cwd!()))
  end

  @doc """
  Returns the path type.

  ## Examples

  ### Unix-like operating systems

      Path.type("/")                #=> :absolute
      Path.type("/usr/local/bin")   #=> :absolute
      Path.type("usr/local/bin")    #=> :relative
      Path.type("../usr/local/bin") #=> :relative
      Path.type("~/file")           #=> :relative

  ### Windows

      Path.type("D:/usr/local/bin") #=> :absolute
      Path.type("usr/local/bin")    #=> :relative
      Path.type("D:bar.ex")         #=> :volumerelative
      Path.type("/bar/foo.ex")      #=> :volumerelative

  """
  @spec type(t) :: :absolute | :relative | :volumerelative
  def type(name)
      when is_list(name)
      when is_binary(name) do
    pathtype(name, major_os_type()) |> elem(0)
  end

  @doc """
  Forces the path to be a relative path.

  ## Examples

  ### Unix-like operating systems

      Path.relative("/usr/local/bin")   #=> "usr/local/bin"
      Path.relative("usr/local/bin")    #=> "usr/local/bin"
      Path.relative("../usr/local/bin") #=> "../usr/local/bin"

  ### Windows

      Path.relative("D:/usr/local/bin") #=> "usr/local/bin"
      Path.relative("usr/local/bin")    #=> "usr/local/bin"
      Path.relative("D:bar.ex")         #=> "bar.ex"
      Path.relative("/bar/foo.ex")      #=> "bar/foo.ex"

  """
  @spec relative(t) :: binary
  def relative(name) do
    relative(name, major_os_type())
  end

  defp relative(name, os_type) do
    pathtype(name, os_type)
    |> elem(1)
    |> IO.chardata_to_string()
  end

  defp pathtype(name, os_type) do
    case os_type do
      :win32 -> win32_pathtype(name)
      _ -> unix_pathtype(name)
    end
  end

  defp unix_pathtype(path) when path in ["/", '/'], do: {:absolute, "."}
  defp unix_pathtype(<<?/, relative::binary>>), do: {:absolute, relative}
  defp unix_pathtype([?/ | relative]), do: {:absolute, relative}
  defp unix_pathtype([list | rest]) when is_list(list), do: unix_pathtype(list ++ rest)
  defp unix_pathtype(relative), do: {:relative, relative}

  defp win32_pathtype([list | rest]) when is_list(list), do: win32_pathtype(list ++ rest)

  defp win32_pathtype([char, list | rest]) when is_list(list),
    do: win32_pathtype([char | list ++ rest])

  defp win32_pathtype(<<c1, c2, relative::binary>>) when c1 in @slash and c2 in @slash,
    do: {:absolute, relative}

  defp win32_pathtype(<<char, relative::binary>>) when char in @slash,
    do: {:volumerelative, relative}

  defp win32_pathtype(<<_letter, ?:, char, relative::binary>>) when char in @slash,
    do: {:absolute, relative}

  defp win32_pathtype(<<_letter, ?:, relative::binary>>), do: {:volumerelative, relative}

  defp win32_pathtype([c1, c2 | relative]) when c1 in @slash and c2 in @slash,
    do: {:absolute, relative}

  defp win32_pathtype([char | relative]) when char in @slash, do: {:volumerelative, relative}

  defp win32_pathtype([c1, c2, list | rest]) when is_list(list),
    do: win32_pathtype([c1, c2 | list ++ rest])

  defp win32_pathtype([_letter, ?:, char | relative]) when char in @slash,
    do: {:absolute, relative}

  defp win32_pathtype([_letter, ?: | relative]), do: {:volumerelative, relative}
  defp win32_pathtype(relative), do: {:relative, relative}

  @doc """
  Returns the given `path` relative to the given `from` path.

  In other words, this function tries to strip the `from` prefix from `path`.

  This function does not query the file system, so it assumes
  no symlinks between the paths.

  In case a direct relative path cannot be found, it returns
  the original path.

  ## Examples

      iex> Path.relative_to("/usr/local/foo", "/usr/local")
      "foo"

      iex> Path.relative_to("/usr/local/foo", "/")
      "usr/local/foo"

      iex> Path.relative_to("/usr/local/foo", "/etc")
      "/usr/local/foo"

      iex> Path.relative_to("/usr/local/foo", "/usr/local/foo")
      "."

  """
  @spec relative_to(t, t) :: binary
  def relative_to(path, from) do
    path = IO.chardata_to_string(path)
    relative_to(split(path), split(from), path)
  end

  defp relative_to(path, path, _original) do
    "."
  end

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

  defp relative_to([_ | _] = l1, [], _original) do
    join(l1)
  end

  defp relative_to(_, _, original) do
    original
  end

  @doc """
  Convenience to get the path relative to the current working
  directory.

  If, for some reason, the current working directory
  cannot be retrieved, this function returns the given `path`.
  """
  @spec relative_to_cwd(t) :: binary
  def relative_to_cwd(path) do
    case :file.get_cwd() do
      {:ok, base} -> relative_to(path, IO.chardata_to_string(base))
      _ -> path
    end
  end

  @doc """
  Returns the last component of the path or the path
  itself if it does not contain any directory separators.

  ## Examples

      iex> Path.basename("foo")
      "foo"

      iex> Path.basename("foo/bar")
      "bar"

      iex> Path.basename("/")
      ""

  """
  @spec basename(t) :: binary
  def basename(path) do
    :filename.basename(IO.chardata_to_string(path))
  end

  @doc """
  Returns the last component of `path` with the `extension`
  stripped.

  This function should be used to remove a specific
  extension which may or may not be there.

  ## Examples

      iex> Path.basename("~/foo/bar.ex", ".ex")
      "bar"

      iex> Path.basename("~/foo/bar.exs", ".ex")
      "bar.exs"

      iex> Path.basename("~/foo/bar.old.ex", ".ex")
      "bar.old"

  """
  @spec basename(t, t) :: binary
  def basename(path, extension) do
    :filename.basename(IO.chardata_to_string(path), IO.chardata_to_string(extension))
  end

  @doc """
  Returns the directory component of `path`.

  ## Examples

      iex> Path.dirname("/foo/bar.ex")
      "/foo"

      iex> Path.dirname("/foo/bar/baz.ex")
      "/foo/bar"

      iex> Path.dirname("/foo/bar/")
      "/foo/bar"

      iex> Path.dirname("bar.ex")
      "."

  """
  @spec dirname(t) :: binary
  def dirname(path) do
    :filename.dirname(IO.chardata_to_string(path))
  end

  @doc """
  Returns the extension of the last component of `path`.

  The behaviour of this function changed in Erlang/OTP 24 for filenames
  starting with a dot and without an extension. For example, for a file
  named `.gitignore`, `extname/1` now returns an empty string, while it
  would return `".gitignore"` in previous Erlang/OTP versions. This was
  done to match the behaviour of `rootname/1`, which would return
  `".gitignore"` as its name (and therefore it cannot also be an extension).

  See `basename/1` and `rootname/1` for related functions to extract
  information from paths.

  ## Examples

      iex> Path.extname("foo.erl")
      ".erl"

      iex> Path.extname("~/foo/bar")
      ""

  """
  @spec extname(t) :: binary
  def extname(path) do
    :filename.extension(IO.chardata_to_string(path))
  end

  @doc """
  Returns the `path` with the `extension` stripped.

  ## Examples

      iex> Path.rootname("/foo/bar")
      "/foo/bar"

      iex> Path.rootname("/foo/bar.ex")
      "/foo/bar"

  """
  @spec rootname(t) :: binary
  def rootname(path) do
    :filename.rootname(IO.chardata_to_string(path))
  end

  @doc """
  Returns the `path` with the `extension` stripped.

  This function should be used to remove a specific extension which may
  or may not be there.

  ## Examples

      iex> Path.rootname("/foo/bar.erl", ".erl")
      "/foo/bar"

      iex> Path.rootname("/foo/bar.erl", ".ex")
      "/foo/bar.erl"

  """
  @spec rootname(t, t) :: binary
  def rootname(path, extension) do
    :filename.rootname(IO.chardata_to_string(path), IO.chardata_to_string(extension))
  end

  @doc """
  Joins a list of paths.

  This function should be used to convert a list of paths to a path.
  Note that any trailing slash is removed when joining.

  Raises an error if the given list of paths is empty.

  ## Examples

      iex> Path.join(["~", "foo"])
      "~/foo"

      iex> Path.join(["foo"])
      "foo"

      iex> Path.join(["/", "foo", "bar/"])
      "/foo/bar"

  """
  @spec join(nonempty_list(t)) :: binary
  def join([name1, name2 | rest]), do: join([join(name1, name2) | rest])
  def join([name]), do: IO.chardata_to_string(name)

  @doc """
  Joins two paths.

  The right path will always be expanded to its relative format
  and any trailing slash will be removed when joining.

  ## Examples

      iex> Path.join("foo", "bar")
      "foo/bar"

      iex> Path.join("/foo", "/bar/")
      "/foo/bar"

  The functions in this module support chardata, so giving a list will
  treat it as a single entity:

      iex> Path.join("foo", ["bar", "fiz"])
      "foo/barfiz"

      iex> Path.join(["foo", "bar"], "fiz")
      "foobar/fiz"

  Use `join/1` if you need to join a list of paths instead.
  """
  @spec join(t, t) :: binary
  def join(left, right) do
    left = IO.chardata_to_string(left)
    os_type = major_os_type()
    do_join(left, right, os_type) |> remove_dir_sep(os_type)
  end

  defp do_join(left, "/", os_type), do: remove_dir_sep(left, os_type)
  defp do_join("", right, os_type), do: relative(right, os_type)
  defp do_join("/", right, os_type), do: "/" <> relative(right, os_type)

  defp do_join(left, right, os_type),
    do: remove_dir_sep(left, os_type) <> "/" <> relative(right, os_type)

  defp remove_dir_sep("", _os_type), do: ""
  defp remove_dir_sep("/", _os_type), do: "/"

  defp remove_dir_sep(bin, os_type) do
    last = :binary.last(bin)

    if last == ?/ or (last == ?\\ and os_type == :win32) do
      binary_part(bin, 0, byte_size(bin) - 1)
    else
      bin
    end
  end

  @doc ~S"""
  Splits the path into a list at the path separator.

  If an empty string is given, returns an empty list.

  On Windows, path is split on both `"\"` and `"/"` separators
  and the driver letter, if there is one, is always returned
  in lowercase.

  ## Examples

      iex> Path.split("")
      []

      iex> Path.split("foo")
      ["foo"]

      iex> Path.split("/foo/bar")
      ["/", "foo", "bar"]

  """
  @spec split(t) :: [binary]
  def split(path) do
    :filename.split(IO.chardata_to_string(path))
  end

  defmodule Wildcard do
    @moduledoc false

    def read_link_info(file) do
      :file.read_link_info(file)
    end

    def list_dir(dir) do
      case :file.list_dir(dir) do
        {:ok, files} -> {:ok, for(file <- files, hd(file) != ?., do: file)}
        other -> other
      end
    end
  end

  @doc """
  Traverses paths according to the given `glob` expression and returns a
  list of matches.

  The wildcard looks like an ordinary path, except that the following
  "wildcard characters" are interpreted in a special way:

    * `?` - matches one character.

    * `*` - matches any number of characters up to the end of the filename, the
      next dot, or the next slash.

    * `**` - two adjacent `*`'s used as a single pattern will match all
      files and zero or more directories and subdirectories.

    * `[char1,char2,...]` - matches any of the characters listed; two
      characters separated by a hyphen will match a range of characters.
      Do not add spaces before and after the comma as it would then match
      paths containing the space character itself.

    * `{item1,item2,...}` - matches one of the alternatives.
      Do not add spaces before and after the comma as it would then match
      paths containing the space character itself.

  Other characters represent themselves. Only paths that have
  exactly the same character in the same position will match. Note
  that matching is case-sensitive: `"a"` will not match `"A"`.

  Directory separators must always be written as `/`, even on Windows.
  You may call `Path.expand/1` to normalize the path before invoking
  this function.

  By default, the patterns `*` and `?` do not match files starting
  with a dot `.`. See the `:match_dot` option in the "Options" section
  below.

  ## Options

    * `:match_dot` - (boolean) if `false`, the special wildcard characters `*` and `?`
      will not match files starting with a dot (`.`). If `true`, files starting with
      a `.` will not be treated specially. Defaults to `false`.

  ## Examples

  Imagine you have a directory called `projects` with three Elixir projects
  inside of it: `elixir`, `ex_doc`, and `plug`. You can find all `.beam` files
  inside the `ebin` directory of each project as follows:

      Path.wildcard("projects/*/ebin/**/*.beam")

  If you want to search for both `.beam` and `.app` files, you could do:

      Path.wildcard("projects/*/ebin/**/*.{beam,app}")

  """
  @spec wildcard(t, keyword) :: [binary]
  def wildcard(glob, opts \\ []) when is_list(opts) do
    mod = if Keyword.get(opts, :match_dot), do: :file, else: Path.Wildcard

    glob
    |> chardata_to_list!()
    |> :filelib.wildcard(mod)
    |> Enum.map(&IO.chardata_to_string/1)
  end

  defp chardata_to_list!(chardata) do
    case :unicode.characters_to_list(chardata) do
      result when is_list(result) ->
        if 0 in result do
          raise ArgumentError,
                "cannot execute Path.wildcard/2 for path with null byte, got: #{inspect(chardata)}"
        else
          result
        end

      {:error, encoded, rest} ->
        raise UnicodeConversionError, encoded: encoded, rest: rest, kind: :invalid

      {:incomplete, encoded, rest} ->
        raise UnicodeConversionError, encoded: encoded, rest: rest, kind: :incomplete
    end
  end

  defp expand_home(type) do
    case IO.chardata_to_string(type) do
      "~" <> rest -> resolve_home(rest)
      rest -> rest
    end
  end

  defp resolve_home(""), do: System.user_home!()

  defp resolve_home(rest) do
    case {rest, major_os_type()} do
      {"\\" <> _, :win32} -> System.user_home!() <> rest
      {"/" <> _, _} -> System.user_home!() <> rest
      _ -> "~" <> rest
    end
  end

  # expand_dot the given path by expanding "..", "." and "~".
  defp expand_dot(<<"/", rest::binary>>), do: "/" <> do_expand_dot(rest)

  defp expand_dot(<<letter, ":/", rest::binary>>) when letter in ?a..?z,
    do: <<letter, ":/">> <> do_expand_dot(rest)

  defp expand_dot(path), do: do_expand_dot(path)

  defp do_expand_dot(path), do: do_expand_dot(:binary.split(path, "/", [:global]), [])
  defp do_expand_dot([".." | t], [_, _ | acc]), do: do_expand_dot(t, acc)
  defp do_expand_dot([".." | t], []), do: do_expand_dot(t, [])
  defp do_expand_dot(["." | t], acc), do: do_expand_dot(t, acc)
  defp do_expand_dot([h | t], acc), do: do_expand_dot(t, ["/", h | acc])
  defp do_expand_dot([], []), do: ""
  defp do_expand_dot([], ["/" | acc]), do: IO.iodata_to_binary(:lists.reverse(acc))

  defp major_os_type do
    :os.type() |> elem(0)
  end

  @doc """
  Returns a relative path that is protected from directory-traversal attacks.

  The given relative path is sanitized by eliminating `..` and `.` components.

  This function checks that, after expanding those components, the path is still "safe".
  Paths are considered unsafe if either of these is true:

    * The path is not relative, such as `"/foo/bar"`.

    * A `..` component would make it so that the path would travers up above
      the root of `relative_to`.

    * A symbolic link in the path points to something above the root of `relative_to`.

  ## Examples

      iex> Path.safe_relative_to("deps/my_dep/app.beam", "deps")
      {:ok, "deps/my_dep/app.beam"}

      iex> Path.safe_relative_to("deps/my_dep/./build/../app.beam", "deps")
      {:ok, "deps/my_dep/app.beam"}

      iex> Path.safe_relative_to("my_dep/../..", "deps")
      :error

      iex> Path.safe_relative_to("/usr/local", ".")
      :error

  """
  @doc since: "1.14.0"
  @spec safe_relative_to(t, t) :: {:ok, binary} | :error
  def safe_relative_to(path, relative_to) do
    path = IO.chardata_to_string(path)

    case :filelib.safe_relative_path(path, relative_to) do
      :unsafe -> :error
      relative_path -> {:ok, IO.chardata_to_string(relative_path)}
    end
  end

  @doc """
  Returns a path relative to the current working directory that is
  protected from directory-traversal attacks.

  Same as `safe_relative_to/2` with the current working directory as
  the second argument. If there is an issue retrieving the current working
  directory, this function raises an error.

  ## Examples

      iex> Path.safe_relative("foo")
      {:ok, "foo"}

      iex> Path.safe_relative("foo/../bar")
      {:ok, "bar"}

      iex> Path.safe_relative("foo/../..")
      :error

      iex> Path.safe_relative("/usr/local")
      :error

  """
  @doc since: "1.14.0"
  @spec safe_relative(t) :: {:ok, binary} | :error
  def safe_relative(path) do
    safe_relative_to(path, File.cwd!())
  end
end


More log output and var renaming

More log output and var renaming

defmodule AmqpOne.Encoding do
  @moduledoc """
  This module contains the logic for encoding and decoding typed
  AMQP streams.
  """
  require Logger
  alias AmqpOne.TypeManager
  alias AmqpOne.TypeManager.{Type, Encoding, Field, Descriptor, Choice}

  def parse(<<0x4 :: size(4), type :: size(4), _rest :: binary>>) do
    [decode(<<0x4::size(4), type>>)]
  end

  @doc """
  Encodes an Elixir value according to the AMQP type specification.

  Generally, this approach tries to minimize the amount of data, but
  for array types this is not possible. Here, all encodings use the
  same constructor and thus an array of `ulong` values consists of
  64-bit only. The data compression format of using one or two bytes
  for small values (`0` or `< 256`) cannot be used.

  This is the reason to use the extra parameter `in_array`, which,
  if true, requires an uncompressed data fornat without a constructor.
  The constructor is computed explicitely in the array construction
  branch of the encoder.
  """
  @spec typed_encoder(any, Type.t | Descriptor.t | Encoding.t | Field.t, boolean) :: iodata
  def typed_encoder(value, type, in_array \\ false)
  def typed_encoder(value, %Type{class: :primitive} = t, in_array) do
    Logger.debug "call primitive_encoder() of value #{inspect value} and type #{inspect t}"
    primitive_encoder(value, t, in_array)
  end
  def typed_encoder(%{} = value, %Type{class: :composite} = t, in_array) do
    # a composite has a descriptor and a list of fields
    con = if in_array, do: [], # no con required due to array
        else: typed_encoder(value, t.descriptor)
    field_count = Enum.count(t.fields)
    list_elements = t.fields |> Enum.map(fn(field) ->
      val = Map.fetch!(value, field.name)
      Logger.debug "Encode field #{inspect field.name} with value #{inspect val}"
      typed_encoder(val, field)
    end)
    field_size = IO.iodata_length(list_elements)
    list_con = if field_size > 255 do
      <<0xd0, field_size :: size(32), field_count :: size(32)>>
    else
      <<0xc0, field_size :: size(8), field_count :: size(8)>>
    end
    [con, list_con, list_elements]
  end
  def typed_encoder(value, %Type{class: :restricted, choices: nil} = t, _in_array) do
    # no choices means no enumeration, but a subtype of an existing type
    source_type = TypeManager.type_spec(t.source) |> encoding_type
    if t.descriptor == nil do
      [typed_encoder(value, source_type)]
    else
      [typed_encoder(value, t.descriptor), typed_encoder(value, source_type)]
    end
  end
  def typed_encoder(_value, [%Descriptor{name: name} = _d], _in_array) do
    # constructor = 0, descriptor name as string
    # Logger.info "encode descriptor with name #{inspect name}"
    [<<0x0>>, typed_encoder(name, TypeManager.type_spec("symbol"))]
  end
  # fields cannot be in an array
  # mandatory null must be encoded explicitely
  def typed_encoder(nil, %Field{mandatory: true}, false), do: <<0x40>>
  def typed_encoder(nil, %Field{mandatory: false}, false), do: [<<0x40>>]
  def typed_encoder(value, %Field{multiple: true, type: type}, false) when is_list(value) do
    # encode an array of values
    t = TypeManager.type_spec(type) |> encoding_type
    elems = value
    |> Enum.map(&(typed_encoder(&1, t, true)))
    |> IO.iodata_to_binary
    array_size = byte_size(elems)
    array_count = length(value)
    constructor = uncompressed_encoding(t).code
    # put the fields together
    array_head = if array_count > 255 do
      <<0xf0, array_size :: size(32), array_count :: size(32)>>
    else
      <<0xe0, array_size :: size(8), array_count :: size(8)>>
    end
    [array_head, constructor, elems]
  end
  def typed_encoder(value, %Field{multiple: false, type: type}, false) do
    t = TypeManager.type_spec(type)
    typed_encoder(value, t)
  end
  def typed_encoder(value, type_name, in_array) when is_binary(type_name) do
    typed_encoder(value, TypeManager.type_spec(type_name), in_array)
  end

  @doc """
  The constructor makes only sense for not constructor encoded values,
  i.e. not for booleans, but for strings
  """
  @spec uncompressed_encoding(Type.t) :: Encoding.t
  def uncompressed_encoding(%Type{class: :primitive} = t) do
    t.encodings
    |> Enum.max_by(fn e -> e.width end)
  end

  @doc """
  Returns the encodable type, i.e. wanders the paths of restricted types
  along
  """
  @spec encoding_type(Type.t) :: Type.t
  @spec encoding_type([Type.t]) :: [Type.t]
  def encoding_type(%Type{class: :restricted, choices: nil} = t) do
    t1 = TypeManager.type_spec(t.source)
    encoding_type(t1)
  end
  def encoding_type(%Type{} = t), do: t
  def encoding_type(types) when is_list(types) do
    types
    |> Enum.map(fn t -> encoding_type(t) end)
  end

  defp enc(encodings, width) do
    Enum.find(encodings, fn e -> e.width==width end)
  end

  def primitive_encoder(value, type, in_array \\ false)
  def primitive_encoder(value, %Type{class: :primitive, name: name} = t, in_array)
        when name in ["ubyte", "uint", "ulong"] do
    case value do
      0 when not in_array -> <<enc(t.encodings, 0).code>>
      x when x < 256 and not in_array ->
        e = enc(t.encodings, 1)
        s = e.width * 8
        e.code <> <<value :: size(s)>>
      _ when not in_array ->
          e = uncompressed_encoding(t)
          s = e.width * 8
          e.code <> <<value :: size(s)>>
      _ ->
          e = uncompressed_encoding(t)
          s = e.width * 8
          <<value :: size(s)>>
    end
  end
  def primitive_encoder(value, %Type{class: :primitive, name: name} = t, false)
        when name in ["ushort"] do
    e = uncompressed_encoding(t)
    s = e.width * 8
    # Logger.debug "byte/short: e.code = #{e.code}, binary? #{is_binary(e.code)}"
    e.code <> <<value :: size(s)-integer>>
  end

  def primitive_encoder(value, %Type{class: :primitive, name: name} = t, in_array)
        when name in [ "int", "long"] do
    case value do
      x when x >-127 and x < 128 and not in_array ->
        e = enc(t.encodings, 1)
        s = e.width * 8
        e.code <> <<value :: size(s)-signed>>
      _ when not in_array ->
          e = uncompressed_encoding(t)
          s = e.width * 8
          e.code <> <<value :: size(s)-signed>>
      _ ->
          e = uncompressed_encoding(t)
          s = e.width * 8
          <<value :: size(s)-signed>>
    end
  end
  def primitive_encoder(value, %Type{class: :primitive, name: name} = t, false)
        when name in [ "byte", "short"] do
    e = uncompressed_encoding(t)
    s = e.width * 8
    # Logger.debug "byte/short: e.code = #{e.code}, binary? #{is_binary(e.code)}"
    e.code <> <<value :: size(s)-signed-integer>>
  end
  def primitive_encoder(value, %Type{class: :primitive, name: name} = t, true)
        when name in [ "byte", "short"] do
    e = uncompressed_encoding(t)
    s = e.width * 8
    <<e.code, value :: size(s)-signed>>
  end
  def primitive_encoder(value, %Type{class: :primitive, name: "double"}, in_array) do
    # float in Erlang/Elixir = double in IEEE 754
    if in_array, do: <<value :: float>>, else: <<0x82, value :: float>>
  end
  def primitive_encoder(nil, %Type{class: :primitive, name: "null"}, _in_array) do
    <<0x40>>
  end
  def primitive_encoder(value, %Type{class: :primitive, name: "boolean"}, false) do
    if value == true, do: <<0x41>>, else: <<0x42>>
  end
  def primitive_encoder(value, %Type{class: :primitive, name: "boolean"}, true) do
    if value == true, do: <<1 :: size(8)>>, else: <<0 :: size(8)>>
  end
  def primitive_encoder(value, %Type{class: :primitive, name: "char"}, in_array) do
    if in_array, do: <<value :: utf32>>, else: <<0x73, value :: utf32>>
  end
  def primitive_encoder(value, %Type{class: :primitive, name: "timestamp"}, in_array) do
    # timestamp = 64 bit unsigned integer
    if in_array, do: <<value :: size(64)>>, else: <<0x83, value :: size(64)>>
  end
  def primitive_encoder(value, %Type{class: :primitive, name: name} = t, in_array)
      when name in ["string", "binary", "symbol"] do
    case byte_size(value) do
      s when s < 255 and not in_array ->
        e = enc(t.encodings, 1)
        e.code <> <<s>> <> value
      s when not in_array ->
        e = enc(t.encodings, 4)
        e.code <> <<s :: size(32)>> <> value
      s ->
        # Logger.debug "string: bytesize: #{s}, value= #{inspect value}"
        <<s :: integer-size(32)>> <> value
    end
  end
  def primitive_encoder(value, %Type{class: :primitive, name: "list"}, in_array) do
    elems = value
    |> Enum.map(&(typed_encoder(&1, type_of(&1), false)))
    |> IO.iodata_to_binary
    size = byte_size(elems)
    count = length(value)
    # put the list together
    case size do
      0 -> <<0x45>>
      x when x > 255 or in_array ->
        if in_array, do: <<size :: size(32), count :: size(32)>> <> elems,
          else: <<0xd0, size :: size(32), count :: size(32)>> <> elems
      _x -> <<0xc0, size :: size(8), count :: size(8)>>  <> elems
    end
  end
  def primitive_encoder(value, %Type{class: :primitive, name: "map"}, in_array) do
    elems = value
    |> Enum.map(fn {k, v} ->
      [typed_encoder(k, type_of(k), false), typed_encoder(v, type_of(v), false)] end)
    |> IO.iodata_to_binary
    size = byte_size(elems)
    count = 2 * Enum.count value
    # put the list together
    case size do
      x when x > 255 ->
        if in_array,
          do: <<size :: size(32), count :: size(32)>> <> elems,
          else: <<0xd1, size :: size(32), count :: size(32)>> <> elems
      _x -> <<0xc1, size :: size(8), count :: size(8)>>  <> elems
    end
  end
  def primitive_encoder(value, %Type{class: :primitive, name: "array"}, in_array) do
    type = List.first(value) |> type_of
    elems = value
    |> Enum.map(&(typed_encoder(&1, type, true)))
    |> IO.iodata_to_binary
    size = byte_size(elems)
    count = length(value)
    constructor = uncompressed_encoding(type)
    # put the array together
    head = case size do
      x when x > 255 or in_array ->
        if in_array do
           <<size :: size(32), count :: size(32)>>
         else
           <<0xf0, size :: size(32), count :: size(32)>>#
         end
      _x -> <<0xe0, size :: size(8), count :: size(8)>>
    end
    head <> constructor.code <> elems
  end

  @doc "Tries to identify the type of an Elixir value according to AMQP"
  def type_of(nil), do: TypeManager.type_spec("null")
  def type_of(b) when b in [true, false], do: TypeManager.type_spec("boolean")
  def type_of(a) when is_atom(a), do: TypeManager.type_spec("symbol")
  def type_of(l) when is_list(l), do: TypeManager.type_spec("list")
  def type_of(m) when is_map(m) do
    # and is a short-cut: returns false or the result of the 2nd argument
    case Map.has_key?(m, :__struct__) and TypeManager.type_spec(m.__struct__) do
      false -> TypeManager.type_spec("map")
      nil   -> TypeManager.type_spec("map")
      type  -> type
    end
  end
  def type_of(i) when is_integer(i) do
    if i >= 0,
      do: TypeManager.type_spec("ulong"),
      else: TypeManager.type_spec("long")
  end
  def type_of(f) when is_float(f), do: TypeManager.type_spec("double")

  def decode(<<0x40>>), do: nil
  def decode(<<0x41>>), do: true
  def decode(<<0x42>>), do: false

  @spec decode_bin(binary) :: {any, binary}
  def decode_bin(<<>>), do: nil
  def decode_bin(<<head :: binary-size(1), rest :: binary>>), do: decode_bin(head, rest)

  @spec decode_bin(constructor :: binary, data :: binary)  :: {any, binary}
  def decode_bin(<<0x40>>, <<rest :: binary>>), do: {nil, rest}
  def decode_bin(<<0x41>>, <<rest :: binary>>), do: {true, rest}
  def decode_bin(<<0x42>>, <<rest :: binary>>), do: {false, rest}
  def decode_bin(<<0x56>>, <<0, rest :: binary>>), do: {false, rest}
  def decode_bin(<<0x56>>, <<1, rest :: binary>>), do: {true, rest}
  # ubyte
  def decode_bin(<<0x50>>, <<val :: integer, rest :: binary>>), do: {val, rest}
  # ushort
  def decode_bin(<<0x60>>, <<val :: integer-size(16), rest :: binary>>), do: {val, rest}
  # uint
  def decode_bin(<<0x70>>, <<val :: integer-size(32), rest :: binary>>), do: {val, rest}
  def decode_bin(<<0x52>>, <<val :: integer, rest :: binary>>), do: {val, rest}
  def decode_bin(<<0x43>>, <<rest :: binary>>), do: {0, rest}
  # ulong
  def decode_bin(<<0x80>>, <<val :: integer-size(64), rest :: binary>>), do: {val, rest}
  def decode_bin(<<0x53>>, <<val :: integer, rest :: binary>>), do: {val, rest}
  def decode_bin(<<0x44>>, <<rest :: binary>>), do: {0, rest}
  # byte
  def decode_bin(<<0x51>>, <<val :: signed-integer, rest :: binary>>), do: {val, rest}
  # short
  def decode_bin(<<0x61>>, <<val :: signed-integer-size(16), rest :: binary>>), do: {val, rest}
  # int
  def decode_bin(<<0x71>>, <<val :: signed-integer-size(32), rest :: binary>>), do: {val, rest}
  def decode_bin(<<0x54>>, <<val :: signed-integer, rest :: binary>>), do: {val, rest}
  # long
  def decode_bin(<<0x81>>, <<val :: signed-integer-size(64), rest :: binary>>), do: {val, rest}
  def decode_bin(<<0x55>>, <<val :: signed-integer, rest :: binary>>), do: {val, rest}
  # 32 and 64 bit floats
  def decode_bin(<<0x72>>, <<val :: float-size(32), rest :: binary>>), do: {val, rest}
  def decode_bin(<<0x82>>, <<val :: float-size(64), rest :: binary>>), do: {val, rest}
  # 32, 64, 128 bit decimals encodings IEEE 754-2008 are not supported
  # UTF-32 char
  def decode_bin(<<0x73>>, <<val :: utf32, rest :: binary>>), do: {val, rest}
  # 64 bit timestamp as milliseconds since Unix epoch
  def decode_bin(<<0x83>>, <<val :: signed-integer-size(64), rest :: binary>>), do: {val, rest}
  # 16 byte long UUID, is taken directly as binary, could be used by UUID.info
  def decode_bin(<<0x98>>, <<val :: binary-size(16), rest :: binary>>), do: {val, rest}
  # a binary = sequence of octects
  def decode_bin(<<0xa0>>, <<size :: integer, val :: binary-size(size), rest :: binary>>), do: {val, rest}
  def decode_bin(<<0xb0>>, <<size :: integer-size(32), val :: binary-size(size), rest :: binary>>), do: {val, rest}
  # an utf8 encoded string, it's size is in bytes, not in graphemes, so it is essentially a simple binary
  def decode_bin(<<0xa1>>, <<size :: integer, val :: binary-size(size), rest :: binary>>), do: {val, rest}
  def decode_bin(<<0xb1>>, <<size :: integer-size(32), val :: binary-size(size), rest :: binary>>), do: {val, rest}
  # symbols
  def decode_bin(<<0xa3>>, <<size :: integer, val :: binary-size(size), rest :: binary>>), do: {val, rest}
  def decode_bin(<<0xb3>>, <<size :: integer-size(32), val :: binary-size(size), rest :: binary>>), do: {val, rest}
  # lists
  def decode_bin(<<0x45>>, rest), do: {[], rest}
  def decode_bin(<<0xc0>>, <<size :: integer, count :: integer, val :: binary-size(size), rest :: binary>>),
    do: {decode_list(val, count), rest}
  def decode_bin(<<0xd0>>, <<size :: integer-size(32), count :: integer-size(32),
      val :: binary-size(size), rest :: binary>>), do: {decode_list(val, count), rest}
  # maps
  def decode_bin(<<0xc1>>, <<size :: integer, count :: integer, val :: binary-size(size), rest :: binary>>),
    do: {decode_map(val, count), rest}
  def decode_bin(<<0xd1>>, <<size :: integer-size(32), count :: integer-size(32), val :: binary-size(size), rest :: binary>>),
    do: {decode_map(val, count), rest}
  # arrays
  def decode_bin(<<0xe0>>, <<size :: integer, count :: integer, con :: integer, rest :: binary>>) do
    # identify type - could be one byte for a primitive byte,
    # or something larger
    decode_array(con, size, count, rest)
  end
  def decode_bin(<<0xf0>>, <<size :: integer-size(32), count :: integer-size(32),
      con :: integer, rest :: binary>>), do: decode_array(con, size, count, rest)
  # composites
  def decode_bin(<<0x00>>, <<rest :: binary>>) do
    {descriptor, value_bin} = decode_bin(rest)
    type = TypeManager.type_spec(descriptor)
    typed_decoder(value_bin, type)
  end

  def decode_list(value, 0), do: {[], value}
  def decode_list(value, count) do
    {list, <<>>} = 1..count |>
    Enum.reduce({[], value}, fn _, {l, bytes} ->
      {elem, remaining} = decode_bin(bytes)
      {[elem | l], remaining}
    end)
    Enum.reverse list
  end

  def decode_map(_value, 0), do: %{}
  def decode_map(value, count) do
    {map, <<>>} = 1..(div(count, 2)) |>
    Enum.reduce({%{}, value}, fn _, {map, bytes} ->
      {key, rem1} = decode_bin(bytes)
      {value, remaining} = decode_bin(rem1)
      {Map.put(map, key, value), remaining}
    end)
    map
  end

  def decode_array(_con, _size, 0, <<>> = value_bytes), do: {[], value_bytes}
  def decode_array(_con, _size, 0, value_bytes ), do: {[], value_bytes}
  def decode_array(con, size, count, value_bytes) do
    Logger.debug "decode array: size= #{size}, count = #{count}"
    {type, bin} = if con == 0x00 do
      {t, b} = decode_bin(value_bytes)
    else
      {nil, value_bytes}
    end
    <<value :: binary-size(size), rest :: binary>> = bin
    {list, <<>>} = 1..count |>
    Enum.reduce({[], value}, fn _, {l, bytes} ->
      {elem, remaining} =
        if con == 0x00 do
          typed_decoder(bytes, type)
        else
          decode_bin(<<con>>, bytes)
        end
      {[elem | l], remaining}
    end)
    {Enum.reverse(list), rest}
  end

  @spec typed_decoder(binary, Type.t) :: {any, binary}
  def typed_decoder(binary, %Type{class: :primitive}) do
    decode_bin(binary)
  end
  def typed_decoder(<<con, value_bin :: binary>>, %Type{class: :composite} = t) do
    {_size, count, value_string} = case con do
      0xc0 ->
        <<s, c, vs :: binary>> = value_bin
        {s, c, vs}
      0xd0 ->
        <<s :: size(32), c :: size(32), vs :: binary>> = value_bin
        {s, c, vs}
    end
    initial_value = case TypeManager.struct_for_type(t) do
      nil -> %{} # unknown struct
      s   -> s
    end
    t.fields
    |> Stream.take(count)
    |> Enum.reduce({initial_value, value_string}, fn field, {map, bin} ->
      {field_val, rest} = typed_decoder(bin, field)
      {Map.put(map, field.name, field_val), rest}
    end)
  end
  def typed_decoder(<<con, value :: binary>> = field_bin, %Field{type: "*"} = f) do
    Logger.debug "field is = #{inspect f}"
    # Type "*" means that any value is allowed,
    # but with a potential constraint from the requires-attributes, defining
    # the set of allowed types. I am not sure, how a static pascal-like type
    # system should handle this, but in Elixir we do not have too many problems.
    # 1. Find all required types, might be nested due to provided types
    all_types = f.requires
    |> Enum.map(fn t -> TypeManager.type_spec(t) |> encoding_type end)
    |> List.flatten
    # 2. Find all types which have the correct constructure `con`
    type = all_types
    |> Stream.map(fn t ->
      if Enum.find_value(t.encodings, nil, fn e -> e.code == con end) == nil,
      do: nil, else: t
    end)
    |> Enum.reject(&(&1 == nil))
    case type do
      [] -> decode_bin(field_bin)
      [t] -> typed_decoder(field_bin, t)
    end
  end
  def typed_decoder(<<con, value :: binary>> = field_bin, %Field{} = f) do
    Logger.debug "field is = #{inspect f}"
    type = TypeManager.type_spec(f.type) |> encoding_type
    Logger.debug "effective type = #{inspect type}"
    Logger.debug "Constructor = 0x#{Integer.to_string(con, 16)}"
    is_array = f.multiple
    case con do
      _ when is_array and value == "" -> nil
      x when x in [0xe0, 0xf0] and is_array -> decode_bin(field_bin)
      _ when not is_array -> decode_bin(<<con>>, value)
      # array but not a multiple crashes, non-array and multiple also!
    end
  end

end

defmodule AmqpOne.Encoding do
  @moduledoc """
  This module contains the logic for encoding and decoding typed
  AMQP streams.
  """
  require Logger
  alias AmqpOne.TypeManager
  alias AmqpOne.TypeManager.{Type, Encoding, Field, Descriptor, Choice}

  def parse(<<0x4 :: size(4), type :: size(4), _rest :: binary>>) do
    [decode(<<0x4::size(4), type>>)]
  end

  @doc """
  Encodes an Elixir value according to the AMQP type specification.

  Generally, this approach tries to minimize the amount of data, but
  for array types this is not possible. Here, all encodings use the
  same constructor and thus an array of `ulong` values consists of
  64-bit only. The data compression format of using one or two bytes
  for small values (`0` or `< 256`) cannot be used.

  This is the reason to use the extra parameter `in_array`, which,
  if true, requires an uncompressed data fornat without a constructor.
  The constructor is computed explicitely in the array construction
  branch of the encoder.
  """
  @spec typed_encoder(any, Type.t | Descriptor.t | Encoding.t | Field.t, boolean) :: iodata
  def typed_encoder(value, type, in_array \\ false)
  def typed_encoder(value, %Type{class: :primitive} = t, in_array) do
    primitive_encoder(value, t, in_array)
  end
  def typed_encoder(%{} = value, %Type{class: :composite} = t, in_array) do
    # a composite has a descriptor and a list of fields
    con = if in_array, do: [], # no con required due to array
        else: typed_encoder(value, t.descriptor)
    field_count = Enum.count(t.fields)
    list_elements = t.fields |> Enum.map(fn(field) ->
      val = Map.fetch!(value, field.name)
      Logger.debug "Encode field #{inspect field.name} with value #{inspect val}"
      typed_encoder(val, field)
    end)
    field_size = IO.iodata_length(list_elements)
    list_con = if field_size > 255 do
      <<0xd0, field_size :: size(32), field_count :: size(32)>>
    else
      <<0xc0, field_size :: size(8), field_count :: size(8)>>
    end
    [con, list_con, list_elements]
  end
  def typed_encoder(value, %Type{class: :restricted, choices: nil} = t, _in_array) do
    # no choices means no enumeration, but a subtype of an existing type
    source_type = TypeManager.type_spec(t.source)
    if t.descriptor == nil do
      [typed_encoder(value, source_type)]
    else
      [typed_encoder(value, t.descriptor), typed_encoder(value, source_type)]
    end
  end
  def typed_encoder(_value, [%Descriptor{name: name} = _d], _in_array) do
    # constructor = 0, descriptor name as string
    # Logger.info "encode descriptor with name #{inspect name}"
    [<<0x0>>, typed_encoder(name, TypeManager.type_spec("symbol"))]
  end
  # fields cannot be in an array
  # mandatory null must be encoded explicitely
  def typed_encoder(nil, %Field{mandatory: true}, false), do: <<0x40>>
  def typed_encoder(nil, %Field{mandatory: false}, false), do: [<<0x40>>]
  def typed_encoder(value, %Field{multiple: true, type: type}, false) when is_list(value) do
    # encode an array of values
    t = TypeManager.type_spec(type) |> encoding_type
    elems = value
    |> Enum.map(&(typed_encoder(&1, t, true)))
    |> IO.iodata_to_binary
    array_size = byte_size(elems)
    array_count = length(value)
    constructor = uncompressed_encoding(t).code
    # put the fields together
    array_head = if array_count > 255 do
      <<0xf0, array_size :: size(32), array_count :: size(32)>>
    else
      <<0xe0, array_size :: size(8), array_count :: size(8)>>
    end
    [array_head, constructor, elems]
  end
  def typed_encoder(value, %Field{multiple: false, type: type}, false) do
    t = TypeManager.type_spec(type)
    typed_encoder(value, t)
  end
  def typed_encoder(value, type_name, in_array) when is_binary(type_name) do
    typed_encoder(value, TypeManager.type_spec(type_name), in_array)
  end

  @doc """
  The constructor makes only sense for not constructor encoded values,
  i.e. not for booleans, but for strings
  """
  @spec uncompressed_encoding(Type.t) :: Encoding.t
  def uncompressed_encoding(%Type{class: :primitive} = t) do
    t.encodings
    |> Enum.max_by(fn e -> e.width end)
  end

  @doc """
  Returns the encodable type, i.e. wanders the paths of restricted types
  along
  """
  @spec encoding_type(Type.t) :: Type.t
  @spec encoding_type([Type.t]) :: [Type.t]
  def encoding_type(%Type{class: :restricted, choices: nil} = t) do
    t1 = TypeManager.type_spec(t.source)
    encoding_type(t1)
  end
  def encoding_type(%Type{} = t), do: t
  def encoding_type(types) when is_list(types) do
    types
    |> Enum.map(fn t -> encoding_type(t) end)
  end

  defp enc(encodings, width) do
    Enum.find(encodings, fn e -> e.width==width end)
  end

  def primitive_encoder(value, type, in_array \\ false)
  def primitive_encoder(value, %Type{class: :primitive, name: name} = t, in_array)
        when name in ["ubyte", "uint", "ulong"] do
    case value do
      0 when not in_array -> <<enc(t.encodings, 0).code>>
      x when x < 256 and not in_array ->
        e = enc(t.encodings, 1)
        s = e.width * 8
        e.code <> <<value :: size(s)>>
      _ when not in_array ->
          e = uncompressed_encoding(t)
          s = e.width * 8
          e.code <> <<value :: size(s)>>
      _ ->
          e = uncompressed_encoding(t)
          s = e.width * 8
          <<value :: size(s)>>
    end
  end
  def primitive_encoder(value, %Type{class: :primitive, name: name} = t, false)
        when name in ["ushort"] do
    e = uncompressed_encoding(t)
    s = e.width * 8
    # Logger.debug "byte/short: e.code = #{e.code}, binary? #{is_binary(e.code)}"
    e.code <> <<value :: size(s)-integer>>
  end

  def primitive_encoder(value, %Type{class: :primitive, name: name} = t, in_array)
        when name in [ "int", "long"] do
    case value do
      x when x >-127 and x < 128 and not in_array ->
        e = enc(t.encodings, 1)
        s = e.width * 8
        e.code <> <<value :: size(s)-signed>>
      _ when not in_array ->
          e = uncompressed_encoding(t)
          s = e.width * 8
          e.code <> <<value :: size(s)-signed>>
      _ ->
          e = uncompressed_encoding(t)
          s = e.width * 8
          <<value :: size(s)-signed>>
    end
  end
  def primitive_encoder(value, %Type{class: :primitive, name: name} = t, false)
        when name in [ "byte", "short"] do
    e = uncompressed_encoding(t)
    s = e.width * 8
    # Logger.debug "byte/short: e.code = #{e.code}, binary? #{is_binary(e.code)}"
    e.code <> <<value :: size(s)-signed-integer>>
  end
  def primitive_encoder(value, %Type{class: :primitive, name: name} = t, true)
        when name in [ "byte", "short"] do
    e = uncompressed_encoding(t)
    s = e.width * 8
    <<e.code, value :: size(s)-signed>>
  end
  def primitive_encoder(value, %Type{class: :primitive, name: "double"}, in_array) do
    # float in Erlang/Elixir = double in IEEE 754
    if in_array, do: <<value :: float>>, else: <<0x82, value :: float>>
  end
  def primitive_encoder(nil, %Type{class: :primitive, name: "null"}, _in_array) do
    <<0x40>>
  end
  def primitive_encoder(value, %Type{class: :primitive, name: "boolean"}, false) do
    if value == true, do: <<0x41>>, else: <<0x42>>
  end
  def primitive_encoder(value, %Type{class: :primitive, name: "boolean"}, true) do
    if value == true, do: <<1 :: size(8)>>, else: <<0 :: size(8)>>
  end
  def primitive_encoder(value, %Type{class: :primitive, name: "char"}, in_array) do
    if in_array, do: <<value :: utf32>>, else: <<0x73, value :: utf32>>
  end
  def primitive_encoder(value, %Type{class: :primitive, name: "timestamp"}, in_array) do
    # timestamp = 64 bit unsigned integer
    if in_array, do: <<value :: size(64)>>, else: <<0x83, value :: size(64)>>
  end
  def primitive_encoder(value, %Type{class: :primitive, name: name} = t, in_array)
      when name in ["string", "binary", "symbol"] do
    case byte_size(value) do
      s when s < 255 and not in_array ->
        e = enc(t.encodings, 1)
        e.code <> <<s>> <> value
      s when not in_array ->
        e = enc(t.encodings, 4)
        e.code <> <<s :: size(32)>> <> value
      s ->
        # Logger.debug "string: bytesize: #{s}, value= #{inspect value}"
        <<s :: integer-size(32)>> <> value
    end
  end
  def primitive_encoder(value, %Type{class: :primitive, name: "list"}, in_array) do
    elems = value
    |> Enum.map(&(typed_encoder(&1, type_of(&1), false)))
    |> IO.iodata_to_binary
    size = byte_size(elems)
    count = length(value)
    # put the list together
    case size do
      0 -> <<0x45>>
      x when x > 255 or in_array ->
        if in_array, do: <<size :: size(32), count :: size(32)>> <> elems,
          else: <<0xd0, size :: size(32), count :: size(32)>> <> elems
      _x -> <<0xc0, size :: size(8), count :: size(8)>>  <> elems
    end
  end
  def primitive_encoder(value, %Type{class: :primitive, name: "map"}, in_array) do
    elems = value
    |> Enum.map(fn {k, v} ->
      [typed_encoder(k, type_of(k), false), typed_encoder(v, type_of(v), false)] end)
    |> IO.iodata_to_binary
    size = byte_size(elems)
    count = 2 * Enum.count value
    # put the list together
    case size do
      x when x > 255 ->
        if in_array,
          do: <<size :: size(32), count :: size(32)>> <> elems,
          else: <<0xd1, size :: size(32), count :: size(32)>> <> elems
      _x -> <<0xc1, size :: size(8), count :: size(8)>>  <> elems
    end
  end
  def primitive_encoder(value, %Type{class: :primitive, name: "array"}, in_array) do
    type = List.first(value) |> type_of
    elems = value
    |> Enum.map(&(typed_encoder(&1, type, true)))
    |> IO.iodata_to_binary
    size = byte_size(elems)
    count = length(value)
    constructor = uncompressed_encoding(type)
    # put the array together
    head = case size do
      x when x > 255 or in_array ->
        if in_array do
           <<size :: size(32), count :: size(32)>>
         else
           <<0xf0, size :: size(32), count :: size(32)>>#
         end
      _x -> <<0xe0, size :: size(8), count :: size(8)>>
    end
    head <> constructor.code <> elems
  end

  @doc "Tries to identify the type of an Elixir value according to AMQP"
  def type_of(nil), do: TypeManager.type_spec("null")
  def type_of(b) when b in [true, false], do: TypeManager.type_spec("boolean")
  def type_of(a) when is_atom(a), do: TypeManager.type_spec("symbol")
  def type_of(l) when is_list(l), do: TypeManager.type_spec("list")
  def type_of(m) when is_map(m) do
    # and is a short-cut: returns false or the result of the 2nd argument
    case Map.has_key?(m, :__struct__) and TypeManager.type_spec(m.__struct__) do
      false -> TypeManager.type_spec("map")
      nil   -> TypeManager.type_spec("map")
      type  -> type
    end
  end
  def type_of(i) when is_integer(i) do
    if i >= 0,
      do: TypeManager.type_spec("ulong"),
      else: TypeManager.type_spec("long")
  end
  def type_of(f) when is_float(f), do: TypeManager.type_spec("double")

  def decode(<<0x40>>), do: nil
  def decode(<<0x41>>), do: true
  def decode(<<0x42>>), do: false

  @spec decode_bin(binary) :: {any, binary}
  def decode_bin(<<>>), do: nil
  def decode_bin(<<head :: binary-size(1), rest :: binary>>), do: decode_bin(head, rest)

  @spec decode_bin(constructor :: binary, data :: binary)  :: {any, binary}
  def decode_bin(<<0x40>>, <<rest :: binary>>), do: {nil, rest}
  def decode_bin(<<0x41>>, <<rest :: binary>>), do: {true, rest}
  def decode_bin(<<0x42>>, <<rest :: binary>>), do: {false, rest}
  def decode_bin(<<0x56>>, <<0, rest :: binary>>), do: {false, rest}
  def decode_bin(<<0x56>>, <<1, rest :: binary>>), do: {true, rest}
  # ubyte
  def decode_bin(<<0x50>>, <<val :: integer, rest :: binary>>), do: {val, rest}
  # ushort
  def decode_bin(<<0x60>>, <<val :: integer-size(16), rest :: binary>>), do: {val, rest}
  # uint
  def decode_bin(<<0x70>>, <<val :: integer-size(32), rest :: binary>>), do: {val, rest}
  def decode_bin(<<0x52>>, <<val :: integer, rest :: binary>>), do: {val, rest}
  def decode_bin(<<0x43>>, <<rest :: binary>>), do: {0, rest}
  # ulong
  def decode_bin(<<0x80>>, <<val :: integer-size(64), rest :: binary>>), do: {val, rest}
  def decode_bin(<<0x53>>, <<val :: integer, rest :: binary>>), do: {val, rest}
  def decode_bin(<<0x44>>, <<rest :: binary>>), do: {0, rest}
  # byte
  def decode_bin(<<0x51>>, <<val :: signed-integer, rest :: binary>>), do: {val, rest}
  # short
  def decode_bin(<<0x61>>, <<val :: signed-integer-size(16), rest :: binary>>), do: {val, rest}
  # int
  def decode_bin(<<0x71>>, <<val :: signed-integer-size(32), rest :: binary>>), do: {val, rest}
  def decode_bin(<<0x54>>, <<val :: signed-integer, rest :: binary>>), do: {val, rest}
  # long
  def decode_bin(<<0x81>>, <<val :: signed-integer-size(64), rest :: binary>>), do: {val, rest}
  def decode_bin(<<0x55>>, <<val :: signed-integer, rest :: binary>>), do: {val, rest}
  # 32 and 64 bit floats
  def decode_bin(<<0x72>>, <<val :: float-size(32), rest :: binary>>), do: {val, rest}
  def decode_bin(<<0x82>>, <<val :: float-size(64), rest :: binary>>), do: {val, rest}
  # 32, 64, 128 bit decimals encodings IEEE 754-2008 are not supported
  # UTF-32 char
  def decode_bin(<<0x73>>, <<val :: utf32, rest :: binary>>), do: {val, rest}
  # 64 bit timestamp as milliseconds since Unix epoch
  def decode_bin(<<0x83>>, <<val :: signed-integer-size(64), rest :: binary>>), do: {val, rest}
  # 16 byte long UUID, is taken directly as binary, could be used by UUID.info
  def decode_bin(<<0x98>>, <<val :: binary-size(16), rest :: binary>>), do: {val, rest}
  # a binary = sequence of octects
  def decode_bin(<<0xa0>>, <<size :: integer, val :: binary-size(size), rest :: binary>>), do: {val, rest}
  def decode_bin(<<0xb0>>, <<size :: integer-size(32), val :: binary-size(size), rest :: binary>>), do: {val, rest}
  # an utf8 encoded string, it's size is in bytes, not in graphemes, so it is essentially a simple binary
  def decode_bin(<<0xa1>>, <<size :: integer, val :: binary-size(size), rest :: binary>>), do: {val, rest}
  def decode_bin(<<0xb1>>, <<size :: integer-size(32), val :: binary-size(size), rest :: binary>>), do: {val, rest}
  # symbols
  def decode_bin(<<0xa3>>, <<size :: integer, val :: binary-size(size), rest :: binary>>), do: {val, rest}
  def decode_bin(<<0xb3>>, <<size :: integer-size(32), val :: binary-size(size), rest :: binary>>), do: {val, rest}
  # lists
  def decode_bin(<<0x45>>, rest), do: {[], rest}
  def decode_bin(<<0xc0>>, <<size :: integer, count :: integer, val :: binary-size(size), rest :: binary>>),
    do: {decode_list(val, count), rest}
  def decode_bin(<<0xd0>>, <<size :: integer-size(32), count :: integer-size(32),
      val :: binary-size(size), rest :: binary>>), do: {decode_list(val, count), rest}
  # maps
  def decode_bin(<<0xc1>>, <<size :: integer, count :: integer, val :: binary-size(size), rest :: binary>>),
    do: {decode_map(val, count), rest}
  def decode_bin(<<0xd1>>, <<size :: integer-size(32), count :: integer-size(32), val :: binary-size(size), rest :: binary>>),
    do: {decode_map(val, count), rest}
  # arrays
  def decode_bin(<<0xe0>>, <<size :: integer, count :: integer, con :: integer, rest :: binary>>) do
    # identify type - could be one byte for a primitive byte,
    # or something larger
    decode_array(con, size, count, rest)
  end
  def decode_bin(<<0xf0>>, <<size :: integer-size(32), count :: integer-size(32),
      con :: integer, rest :: binary>>), do: decode_array(con, size, count, rest)
  # composites
  def decode_bin(<<0x00>>, <<rest :: binary>>) do
    {descriptor, value_bin} = decode_bin(rest)
    type = TypeManager.type_spec(descriptor)
    typed_decoder(value_bin, type)
  end

  def decode_list(value, 0), do: {[], value}
  def decode_list(value, count) do
    {list, <<>>} = 1..count |>
    Enum.reduce({[], value}, fn _, {l, bytes} ->
      {elem, remaining} = decode_bin(bytes)
      {[elem | l], remaining}
    end)
    Enum.reverse list
  end

  def decode_map(_value, 0), do: %{}
  def decode_map(value, count) do
    {map, <<>>} = 1..(div(count, 2)) |>
    Enum.reduce({%{}, value}, fn _, {map, bytes} ->
      {key, rem1} = decode_bin(bytes)
      {value, remaining} = decode_bin(rem1)
      {Map.put(map, key, value), remaining}
    end)
    map
  end

  def decode_array(_con, _size, 0, <<>> = value_bytes), do: {[], value_bytes}
  def decode_array(_con, _size, 0, value_bytes ), do: {[], value_bytes}
  def decode_array(con, size, count, value_bytes) do
    Logger.debug "decode array: size= #{size}, count = #{count}"
    {type, bin} = if con == 0x00 do
      {t, b} = decode_bin(value_bytes)
    else
      {nil, value_bytes}
    end
    <<value :: binary-size(size), rest :: binary>> = bin
    {list, <<>>} = 1..count |>
    Enum.reduce({[], value}, fn _, {l, bytes} ->
      {elem, remaining} =
        if con == 0x00 do
          typed_decoder(bytes, type)
        else
          decode_bin(<<con>>, bytes)
        end
      {[elem | l], remaining}
    end)
    {Enum.reverse(list), rest}
  end

  @spec typed_decoder(binary, Type.t) :: {any, binary}
  def typed_decoder(binary, %Type{class: :primitive}) do
    decode_bin(binary)
  end
  def typed_decoder(<<con, value_bin :: binary>>, %Type{class: :composite} = t) do
    {_size, count, values} = case con do
      0xc0 ->
        <<s, c, vs :: binary>> = value_bin
        {s, c, vs}
      0xd0 ->
        <<s :: size(32), c :: size(32), vs :: binary>> = value_bin
        {s, c, vs}
    end
    initial_value = case TypeManager.struct_for_type(t) do
      nil -> %{} # unknown struct
      s   -> s
    end
    t.fields
    |> Stream.take(count)
    |> Enum.reduce({initial_value, values}, fn field, {map, bin} ->
      {field_val, rest} = typed_decoder(bin, field)
      {Map.put(map, field.name, field_val), rest}
    end)
  end
  def typed_decoder(<<con, value :: binary>> = field_bin, %Field{type: "*"} = f) do
    Logger.debug "field is = #{inspect f}"
    # Type "*" means that any value is allowed,
    # but with a potential constraint from the requires-attributes, defining
    # the set of allowed types. I am not sure, how a static pascal-like type
    # system should handle this, but in Elixir we do not have too many problems.
    # 1. Find all required types, might be nested due to provided types
    all_types = f.requires
    |> Enum.map(fn t -> TypeManager.type_spec(t) |> encoding_type end)
    |> List.flatten
    # 2. Find all types which have the correct constructure `con`
    type = all_types
    |> Stream.map(fn t ->
      if Enum.find_value(t.encodings, nil, fn e -> e.code == con end) == nil,
      do: nil, else: t
    end)
    |> Enum.reject(&(&1 == nil))
    case type do
      [] -> decode_bin(field_bin)
      [t] -> typed_decoder(field_bin, t)
    end
  end
  def typed_decoder(<<con, value :: binary>> = field_bin, %Field{} = f) do
    Logger.debug "field is = #{inspect f}"
    type = TypeManager.type_spec(f.type) |> encoding_type
    Logger.debug "effective type = #{inspect type}"
    Logger.debug "Constructor = 0x#{Integer.to_string(con, 16)}"
    is_array = f.multiple
    case con do
      _ when is_array and value == "" -> nil
      x when x in [0xe0, 0xf0] and is_array -> decode_bin(field_bin)
      _ when not is_array -> decode_bin(<<con>>, value)
      # array but not a multiple crashes, non-array and multiple also!
    end
  end

end


Remove optional parens

Remove optional parens

defmodule Fomobot.History do
  defmodule Entry do
    defstruct [:time, :from_user, :body]

    @doc """
    Creates a new Fomobot.History.Entry from a given message.
    """
    def new(message) do
      %Fomobot.History.Entry{
        time: :erlang.monotonic_time(),
        from_user: message.from.resource,
        body: message.body
      }
    end
  end

  defstruct entries: %{},
            last_modified: %{},
            size: 10

  alias Fomobot.History

  def new(size \\ nil) do
    size = size || Application.get_env(:fomobot, :history_size)

    %History{
      size: size
    }
  end

  def add_message(room, message) do
    Agent.get_and_update(:room_histories, &add_message(&1, room, message))
  end

  def add_message(history = %History{}, room, message) do
    history = history
              |> trim_size(room)
              |> enqueue(room, message)

    is_fomo_event = history
                    |> fomo_event?(room)

    {{is_fomo_event, history |> entries(room)}, history}
  end

  def entries(history, room) do
    history.entries[room] || EQueue.new
  end

  def size(history, room) do
     history
     |> entries(room)
     |> EQueue.length
  end

  def trim_size(history, room) do
    room_history = history |> entries(room)

  if (room_history |> EQueue.length) >= history.size do
    {:value, _dropped_item, room_history_resized} = EQueue.pop(room_history)
      put_in history.entries[room], room_history_resized
    else
      put_in history.entries[room], room_history
    end
  end

  def enqueue(history, room, message) do
    last_notified = if history |> fomo_event?(room) do
      :erlang.monotonic_time
    else
      history.last_notified[room]
    end

    history = put_in history.last_notified[room],
                     last_notified

    put_in history.entries[room],
           history |> entries(room) |> EQueue.push(History.Entry.new(message))
  end

  defp fomo_event?(history, room) do
    history |> exceeds_user_threshold?(room) and
    history |> exceeds_density_threshold?(room) and
    not recently_notified?(history.last_notified[room])
  end

  defp exceeds_user_threshold?(history, room) do
    history |> uniq_user_count(room) >= Application.get_env(:fomobot, :user_threshold)
  end

  defp uniq_user_count(history, room) do
    history
    |> entries(room)
    |> Enum.map(&(&1.from_user))
    |> Enum.uniq
    |> length
  end

  defp exceeds_density_threshold?(history, room) do
    history |> density(room) >= Application.get_env(:fomobot, :density_threshold)
  end

  defp density(history, room) do
    if history |> size(room) < history.size do
      0
    else
      60 * history.size / history |> secs_elapsed(room)
    end
  end

  defp secs_elapsed(history, room) do
    room_history  = history |> entries(room)
    earliest_time = :queue.head(room_history)[:time]
    latest_time   = :queue.last(room_history)[:time]
    System.convert_time_unit(latest_time - earliest_time, :native, :seconds)
  end

  defp recently_notified?(nil) do
    false
  end

  defp recently_notified?(last_notified) do
    elapsed_mins = System.convert_time_unit(:erlang.monotonic_time - last_notified, :native, :seconds) / 60
    elapsed_mins < Application.get_env(:fomobot, :debounce_mins)
  end
end

defmodule Fomobot.History do
  defmodule Entry do
    defstruct [:time, :from_user, :body]

    @doc """
    Creates a new Fomobot.History.Entry from a given message.
    """
    def new(message) do
      %Fomobot.History.Entry{
        time: :erlang.monotonic_time(),
        from_user: message.from.resource,
        body: message.body
      }
    end
  end

  defstruct entries: %{},
            last_modified: %{},
            size: 10

  alias Fomobot.History

  def new(size \\ nil) do
    size = size || Application.get_env(:fomobot, :history_size)

    %History{
      size: size
    }
  end

  def add_message(room, message) do
    Agent.get_and_update(:room_histories, &add_message(&1, room, message))
  end

  def add_message(history = %History{}, room, message) do
    history = history
              |> trim_size(room)
              |> enqueue(room, message)

    is_fomo_event = history
                    |> fomo_event?(room)

    {{is_fomo_event, history |> entries(room)}, history}
  end

  def entries(history, room) do
    history.entries[room] || EQueue.new
  end

  def size(history, room) do
     history
     |> entries(room)
     |> EQueue.length
  end

  def trim_size(history, room) do
    room_history = history |> entries(room)

  if (room_history |> EQueue.length) >= history.size do
    {:value, _dropped_item, room_history_resized} = EQueue.pop(room_history)
      put_in history.entries[room], room_history_resized
    else
      put_in history.entries[room], room_history
    end
  end

  def enqueue(history, room, message) do
    last_notified = if history |> fomo_event?(room) do
      :erlang.monotonic_time()
    else
      history.last_notified[room]
    end

    history = put_in history.last_notified[room],
                     last_notified

    put_in history.entries[room],
           history |> entries(room) |> EQueue.push(History.Entry.new(message))
  end

  defp fomo_event?(history, room) do
    history |> exceeds_user_threshold?(room) and
    history |> exceeds_density_threshold?(room) and
    not recently_notified?(history.last_notified[room])
  end

  defp exceeds_user_threshold?(history, room) do
    history |> uniq_user_count(room) >= Application.get_env(:fomobot, :user_threshold)
  end

  defp uniq_user_count(history, room) do
    history
    |> entries(room)
    |> Enum.map(&(&1.from_user))
    |> Enum.uniq
    |> length
  end

  defp exceeds_density_threshold?(history, room) do
    history |> density(room) >= Application.get_env(:fomobot, :density_threshold)
  end

  defp density(history, room) do
    if history |> size(room) < history.size do
      0
    else
      60 * history.size / history |> secs_elapsed(room)
    end
  end

  defp secs_elapsed(history, room) do
    room_history  = history |> entries(room)
    earliest_time = :queue.head(room_history)[:time]
    latest_time   = :queue.last(room_history)[:time]
    System.convert_time_unit(latest_time - earliest_time, :native, :seconds)
  end

  defp recently_notified?(nil) do
    false
  end

  defp recently_notified?(last_notified) do
    elapsed_mins = System.convert_time_unit(:erlang.monotonic_time() - last_notified, :native, :seconds) / 60
    elapsed_mins < Application.get_env(:fomobot, :debounce_mins)
  end
end