defmodule Sh do
  defmodule CommandNotFound do
    defexception [:command]

    def message(%{command: command}) do
      "Command not found: #{command}"
    end
  end

  defmodule AbnormalExit do
    defexception [:output, :status]

    def message(%{status: status}) do
      "exited with non-zero status (#{status})"
    end
  end

  def unquote(:"$handle_undefined_function")(program, args) do
    command = to_string(program)
      |> String.replace("_", "-")
      |> System.find_executable
      || raise CommandNotFound, command: program

    if is_list(List.last(args)) do
      { args, [opts] } = Enum.split(args, -1)
      args = process_options(opts) ++ args
    end

    # http://www.erlang.org/doc/man/erlang.html#open_port-2
    opts = ~w(exit_status stderr_to_stdout in binary eof hide)a ++ [args: args]
    port = Port.open({ :spawn_executable, command }, opts)

    loop(port, "")
  end

  defp loop(port, acc) do
    receive do
      { ^port, { :data, data } } ->
        loop(port, acc <> data)
      { ^port, :eof } ->
        send port, { self, :close }
        receive do
          { ^port, { :exit_status, 0 } } ->
            acc
          { ^port, { :exit_status, status } } ->
            raise %AbnormalExit{output: acc, status: status}
        end
    end
  end

  defp process_options(opts) do
    Enum.reduce Enum.reverse(opts), [], fn { key, value }, acc ->
      key = to_string(key)
      if String.length(key) == 1 do
        if value do
          if value != true do
            ["-#{key}", to_string(value) | acc]
          else
            ["-#{key}" | acc]
          end
        else
          acc
        end
      else
        key = String.replace(key, "_", "-")
        if value do
          if value != true do
            ["--#{key}=#{value}" | acc]
          else
            ["--#{key}" | acc]
          end
        else
          acc
        end
      end
    end
  end
end
