%%% -*- erlang -*-
%%%
%%% This file is part of cloudi_x_hackney_lib released under the Apache 2 license.
%%% See the NOTICE for more information.
%%%
%%% Copyright (c) 2012-2014 Benoît Chesneau <benoitc@e-engura.org>
%%% Copyright (c) 2011-2013, Loïc Hoguin <essen@ninenines.eu>
%%% Copyright (c) 2011, Anthony Ramine <nox@dev-extend.eu>

%% @doc module to manipulate HTTP headers

-module(cloudi_x_hackney_headers).

-export([new/0, new/1,
  update/2,
  to_list/1,
  get_value/2, get_value/3,
  store/3,
  insert/3, insert/4,
  delete/2,
  fold/3]).

-export([to_binary/1]).
-export([make_header/2, make_header/3]).
-export([header_value/2]).
-export([parse/2]).
-export([content_type/1]).
-export([content_disposition/1]).

-type disposition() :: {binary(), [{binary(), binary()}]}.

-type headers() :: any().


%% @doc initialise an header dict
-spec new() -> headers().
new() ->
  dict:new().

-spec new(list()) -> headers().
new({dict, _}=D) ->
  D;

new(Headers) when is_list(Headers) ->
  lists:foldl(fun
                ({K, V}, D) ->
                  insert(cloudi_x_hackney_bstr:to_binary(K), cloudi_x_hackney_bstr:to_binary(V), D);
                ({K, V, P}, D) ->
                  insert(cloudi_x_hackney_bstr:to_binary(K), header_value(V, P), D)
              end, dict:new(), Headers).

%% @doc extend the headers with a new list of `{Key, Value}' pair.
update(Headers, KVs) ->
  lists:foldl(fun
                ({K, V}, D) ->
                  insert(cloudi_x_hackney_bstr:to_binary(K), cloudi_x_hackney_bstr:to_binary(V), D);
                ({K, V, P}, D) ->
                  insert(cloudi_x_hackney_bstr:to_binary(K), header_value(V, P), D)
              end, Headers, KVs).

%% convert the header to a list
to_list(Headers) ->
  lists:reverse(dict:fold(fun(_K, KV, Acc) ->
    [KV | Acc]
                          end, [], Headers)).

%% @doc get the value of the header
get_value(Key, Headers) ->
  get_value(Key, Headers, undefined).

get_value(Key, Headers, Default) ->
  case dict:find(cloudi_x_hackney_bstr:to_lower(Key), Headers) of
    {ok, {_K, V}} ->
      V;
    _ ->
      Default
  end.

%% @doc store the pair into the headers, replacing any pre-existing key.
store(Key, Value, Headers) ->
  dict:store(cloudi_x_hackney_bstr:to_lower(Key), {Key, Value}, Headers).


%% @doc Insert the pair into the headers, merging with any pre-existing key.
%% A merge is done with Value = V0 ++ ", " ++ V1.
insert(Key, Value, Headers) ->
  Key1 = cloudi_x_hackney_bstr:to_lower(Key),
  Value1 = case dict:find(Key1, Headers) of
             {ok, {_, OldValue}} ->
               << OldValue/binary, ", ", Value/binary >>;
             _ ->
               Value
           end,
  dict:store(Key1, {Key, Value1}, Headers).

%% @doc same as `insert/3' but allows to add params to the header value.
insert(Key, Value, Params, Headers) ->
  insert(Key, header_value(Value, Params), Headers).

%% @doc Delete the header corresponding to key if it is present.
delete(Key, Headers) ->
  dict:erase(cloudi_x_hackney_bstr:to_lower(Key), Headers).

%% @doc fold the list of headers
fold(Fun, Acc0, Headers) ->
  Wrapper = fun(_K, KV, Acc) ->
    Fun(KV, Acc)
            end,
  dict:fold(Wrapper, Acc0, Headers).

%% @doc return all the headers as a binary that can be sent over the
%% wire.
to_binary(Headers) when is_list(Headers) ->
  HeadersList = lists:foldl(fun
                              ({Name, Value}, Acc) ->
                                [make_header(Name, Value) | Acc];
                              ({Name, Value, Params}, Acc) ->
                                [make_header(Name, Value, Params) | Acc]
                            end, [], Headers),
  iolist_to_binary([
    cloudi_x_hackney_bstr:join(lists:reverse(HeadersList), <<"\r\n">>),
    <<"\r\n\r\n">>]);
to_binary(Headers) ->
  to_binary(to_list(Headers)).

%% @doc Create a binary header
make_header(Name, Value) ->
  Value1 = cloudi_x_hackney_bstr:to_binary(Value),
  << Name/binary, ": ", Value1/binary >>.

make_header(Name, Value, Params) ->
  Value1 = header_value(cloudi_x_hackney_bstr:to_binary(Value), Params),
  << Name/binary, ": ", Value1/binary >>.

%% @doc join value and params in a binary
header_value(Value, Params) when is_list(Value) ->
  header_value(list_to_binary(Value), Params);
header_value(Value, Params) ->
  Params1 = lists:foldl(fun({K, V}, Acc) ->
    K1 = cloudi_x_hackney_bstr:to_binary(K),
    V1 = cloudi_x_hackney_bstr:to_binary(V),
    ParamStr = << K1/binary, "=", V1/binary  >>,
    [ParamStr | Acc]
                        end, [], Params),
  cloudi_x_hackney_bstr:join([Value] ++ lists:reverse(Params1), "; ").

%% @doc Semantically parse headers.
%%
%% When the value isn't found, a proper default value for the type
%% returned is used as a return value.
%% @see parse/3
-spec parse(binary(), list() | headers())
    -> any() | undefined | {error, badarg}.
parse(Name, Headers) when is_list(Headers) ->
  parse(Name, new(Headers));
parse(Name, Headers) ->
  Name1 = cloudi_x_hackney_bstr:to_lower(Name),
  parse(Name1, Headers, header_default(Name1)).


%% @doc Semantically parse headers.
%%
%% When the header is unknown, the value is returned directly without parsing.
-spec parse(binary(), headers(), any())
    -> any() | undefined | {error, badarg}.
parse(Name = <<"accept">>, Headers, Default) ->
  parse(Name, Headers, Default,
    fun (Value) ->
      cloudi_x_hackney_bstr:list(Value, fun media_range/2)
    end);
parse(Name = <<"accept-charset">>, Headers, Default) ->
  parse(Name, Headers, Default,
    fun (Value) ->
      cloudi_x_hackney_bstr:nonempty_list(Value, fun conneg/2)
    end);
parse(Name = <<"accept-encoding">>, Headers, Default) ->
  parse(Name, Headers, Default,
    fun (Value) ->
      cloudi_x_hackney_bstr:list(Value, fun conneg/2)
    end);
parse(Name = <<"accept-language">>, Headers, Default) ->
  parse(Name, Headers, Default,
    fun (Value) ->
      cloudi_x_hackney_bstr:nonempty_list(Value, fun language_range/2)
    end);
parse(Name = <<"authorization">>, Headers, Default) ->
  parse(Name, Headers, Default,
    fun (Value) ->
      cloudi_x_hackney_bstr:token_ci(Value, fun authorization/2)
    end);
parse(Name = <<"content-length">>, Headers, Default) ->
  parse(Name, Headers, Default, fun cloudi_x_hackney_bstr:digits/1);
parse(Name = <<"content-type">>, Headers, Default) ->
  parse(Name, Headers, Default, fun content_type/1);
parse(Name = <<"cookie">>, Headers, Default) ->
  parse(Name, Headers, Default, fun cloudi_x_hackney_cookie:parse_cookie/1);
parse(Name = <<"expect">>, Headers, Default) ->
  parse(Name, Headers, Default,
    fun (Value) ->
      cloudi_x_hackney_bstr:nonempty_list(Value, fun expectation/2)
    end);
parse(Name, Headers, Default)
  when Name =:= <<"if-match">>;
       Name =:= <<"if-none-match">> ->
  parse(Name, Headers, Default, fun entity_tag_match/1);
parse(Name, Headers, Default)
  when Name =:= <<"if-modified-since">>;
       Name =:= <<"if-unmodified-since">> ->
  parse(Name, Headers, Default, fun cloudi_x_hackney_date:parse_http_date/1);
parse(Name = <<"range">>, Headers, Default) ->
  parse(Name, Headers, Default, fun range/1);
parse(Name, Headers, Default)
  when Name =:= <<"sec-websocket-protocol">>;
       Name =:= <<"x-forwarded-for">> ->
  parse(Name, Headers, Default,
    fun (Value) ->
      cloudi_x_hackney_bstr:nonempty_list(Value, fun cloudi_x_hackney_bstr:token/2)
    end);
%% @todo Extension parameters.
parse(Name, Headers, Default)
  when Name =:= <<"transfer-encoding">>;
       Name =:= <<"upgrade">> ->
  parse(Name, Headers, Default,
    fun (Value) ->
      cloudi_x_hackney_bstr:nonempty_list(Value, fun cloudi_x_hackney_bstr:token_ci/2)
    end);
parse(Name = <<"sec-websocket-extensions">>, Headers, Default) ->
  parse(Name, Headers, Default, fun cloudi_x_hackney_bstr:parameterized_tokens/1);
parse(Name, Headers, Default) ->
  get_value(Name, Headers, Default).

parse(Name, Headers, Default, Fun) ->
  case get_value(Name, Headers) of
    undefined ->
      Default;
    Value ->
      case Fun(Value) of
        {error, badarg} ->
          {error, badarg};
        P ->
          P
      end
  end.


%% @doc Parse a content type.
%%
%% We lowercase the charset header as we know it's case insensitive.
-spec content_type(binary()) -> any().
content_type(Data) ->
  media_type(Data,
    fun (Rest, Type, SubType) ->
      cloudi_x_hackney_bstr:params(Rest,
        fun (<<>>, Params) ->
          case lists:keyfind(<<"charset">>, 1, Params) of
            false ->
              {Type, SubType, Params};
            {_, Charset} ->
              Charset2 = cloudi_x_hackney_bstr:to_lower(Charset),
              Params2 = lists:keyreplace(<<"charset">>,
                1, Params,
                {<<"charset">>, Charset2}),
              {Type, SubType, Params2}
          end;
            (_Rest2, _) ->
              {error, badarg}
        end)
    end).

%% @doc Parse a content disposition.
%% @todo Parse the MIME header instead of the HTTP one.
-spec content_disposition(binary()) -> disposition().
content_disposition(Data) ->
  cloudi_x_hackney_bstr:token_ci(Data, fun
                                (_Rest, <<>>) ->
                                  {error, badarg};
                                (Rest, Disposition) ->
                                  cloudi_x_hackney_bstr:params(Rest, fun
                                                              (<<>>, Params) -> {Disposition, Params};
                                                              (_Rest2, _) -> {error, badarg}
                                                            end)
                              end).

%% @doc Parse a media range.
-spec media_range(binary(), fun()) -> any().
media_range(Data, Fun) ->
  media_type(Data,
    fun (Rest, Type, SubType) ->
      media_range_params(Rest, Fun, Type, SubType, [])
    end).

-spec media_range_params(binary(), fun(), binary(), binary(),
  [{binary(), binary()}]) -> any().
media_range_params(Data, Fun, Type, SubType, Acc) ->
  cloudi_x_hackney_bstr:whitespace(Data,
    fun (<< $;, Rest/binary >>) ->
      cloudi_x_hackney_bstr:whitespace(Rest,
        fun (Rest2) ->
          media_range_param_attr(Rest2, Fun, Type, SubType,
            Acc)
        end);
        (Rest) -> Fun(Rest, {{Type, SubType, lists:reverse(Acc)}, 1000,
          []})
    end).

-spec media_range_param_attr(binary(), fun(), binary(), binary(),
  [{binary(), binary()}]) -> any().
media_range_param_attr(Data, Fun, Type, SubType, Acc) ->
  cloudi_x_hackney_bstr:token_ci(Data,
    fun (_Rest, <<>>) -> {error, badarg};
        (<< $=, Rest/binary >>, Attr) ->
          media_range_param_value(Rest, Fun, Type, SubType, Acc, Attr)
    end).

-spec media_range_param_value(binary(), fun(), binary(), binary(),
  [{binary(), binary()}], binary()) -> any().
media_range_param_value(Data, Fun, Type, SubType, Acc, <<"q">>) ->
  qvalue(Data,
    fun (Rest, Quality) ->
      accept_ext(Rest, Fun, Type, SubType, Acc, Quality, [])
    end);
media_range_param_value(Data, Fun, Type, SubType, Acc, Attr) ->
  cloudi_x_hackney_bstr:word(Data,
    fun (Rest, Value) ->
      media_range_params(Rest, Fun,
        Type, SubType, [{Attr, Value}|Acc])
    end).

%% @doc Parse a media type.
-spec media_type(binary(), fun()) -> any().
media_type(Data, Fun) ->
  cloudi_x_hackney_bstr:token_ci(Data,
    fun (_Rest, <<>>) -> {error, badarg};
        (<< $/, Rest/binary >>, Type) ->
          cloudi_x_hackney_bstr:token_ci(Rest,
            fun (_Rest2, <<>>) -> {error, badarg};
                (Rest2, SubType) -> Fun(Rest2, Type, SubType)
            end);
      %% This is a non-strict parsing clause required by some user agents
      %% that use * instead of */* in the list of media types.
        (Rest, <<"*">> = Type) ->
          cloudi_x_hackney_bstr:token_ci(<<"*", Rest/binary>>,
            fun (_Rest2, <<>>) -> {error, badarg};
                (Rest2, SubType) -> Fun(Rest2, Type, SubType)
            end);
        (_Rest, _Type) -> {error, badarg}
    end).

-spec accept_ext(binary(), fun(), binary(), binary(),
  [{binary(), binary()}], 0..1000,
  [{binary(), binary()} | binary()]) -> any().
accept_ext(Data, Fun, Type, SubType, Params, Quality, Acc) ->
  cloudi_x_hackney_bstr:whitespace(Data,
    fun (<< $;, Rest/binary >>) ->
      cloudi_x_hackney_bstr:whitespace(Rest,
        fun (Rest2) ->
          accept_ext_attr(Rest2, Fun,
            Type, SubType, Params, Quality, Acc)
        end);
        (Rest) ->
          Fun(Rest, {{Type, SubType, lists:reverse(Params)},
            Quality, lists:reverse(Acc)})
    end).

-spec accept_ext_attr(binary(), fun(), binary(), binary(),
  [{binary(), binary()}], 0..1000,
  [{binary(), binary()} | binary()]) -> any().
accept_ext_attr(Data, Fun, Type, SubType, Params, Quality, Acc) ->
  cloudi_x_hackney_bstr:token_ci(Data,
    fun (_Rest, <<>>) -> {error, badarg};
        (<< $=, Rest/binary >>, Attr) ->
          accept_ext_value(Rest, Fun, Type, SubType, Params,
            Quality, Acc, Attr);
        (Rest, Attr) ->
          accept_ext(Rest, Fun, Type, SubType, Params,
            Quality, [Attr|Acc])
    end).

-spec accept_ext_value(binary(), fun(), binary(), binary(),
  [{binary(), binary()}], 0..1000,
  [{binary(), binary()} | binary()], binary()) -> any().
accept_ext_value(Data, Fun, Type, SubType, Params, Quality, Acc, Attr) ->
  cloudi_x_hackney_bstr:word(Data,
    fun (Rest, Value) ->
      accept_ext(Rest, Fun,
        Type, SubType, Params, Quality, [{Attr, Value}|Acc])
    end).

%% @doc Parse a conneg header (Accept-Charset, Accept-Encoding),
%% followed by an optional quality value.
-spec conneg(binary(), fun()) -> any().
conneg(Data, Fun) ->
  cloudi_x_hackney_bstr:token_ci(Data,
    fun (_Rest, <<>>) -> {error, badarg};
        (Rest, Conneg) ->
          maybe_qparam(Rest,
            fun (Rest2, Quality) ->
              Fun(Rest2, {Conneg, Quality})
            end)
    end).

%% @doc Parse a language range, followed by an optional quality value.
-spec language_range(binary(), fun()) -> any().
language_range(<< $*, Rest/binary >>, Fun) ->
  language_range_ret(Rest, Fun, '*');
language_range(Data, Fun) ->
  language_tag(Data,
    fun (Rest, LanguageTag) ->
      language_range_ret(Rest, Fun, LanguageTag)
    end).

-spec language_range_ret(binary(), fun(), '*' | {binary(), [binary()]}) -> any().
language_range_ret(Data, Fun, LanguageTag) ->
  maybe_qparam(Data,
    fun (Rest, Quality) ->
      Fun(Rest, {LanguageTag, Quality})
    end).

-spec language_tag(binary(), fun()) -> any().
language_tag(Data, Fun) ->
  cloudi_x_hackney_bstr:alpha(Data,
    fun (_Rest, Tag) when byte_size(Tag) =:= 0; byte_size(Tag) > 8 ->
      {error, badarg};
        (<< $-, Rest/binary >>, Tag) ->
          language_subtag(Rest, Fun, Tag, []);
        (Rest, Tag) ->
          Fun(Rest, Tag)
    end).

-spec language_subtag(binary(), fun(), binary(), [binary()]) -> any().
language_subtag(Data, Fun, Tag, Acc) ->
  cloudi_x_hackney_bstr:alpha(Data,
    fun (_Rest, SubTag) when byte_size(SubTag) =:= 0;
                             byte_size(SubTag) > 8 -> {error, badarg};
        (<< $-, Rest/binary >>, SubTag) ->
          language_subtag(Rest, Fun, Tag, [SubTag|Acc]);
        (Rest, SubTag) ->
          %% Rebuild the full tag now that we know it's correct
          Sub = << << $-, S/binary >> || S <- lists:reverse([SubTag|Acc]) >>,
          Fun(Rest, << Tag/binary, Sub/binary >>)
    end).

-spec maybe_qparam(binary(), fun()) -> any().
maybe_qparam(Data, Fun) ->
  cloudi_x_hackney_bstr:whitespace(Data,
    fun (<< $;, Rest/binary >>) ->
      cloudi_x_hackney_bstr:whitespace(Rest,
        fun (Rest2) ->
          %% This is a non-strict parsing clause required by some user agents
          %% that use the wrong delimiter putting a charset where a qparam is
          %% expected.
          try qparam(Rest2, Fun) of
            Result -> Result
          catch
            error:function_clause ->
              Fun(<<",", Rest2/binary>>, 1000)
          end
        end);
        (Rest) ->
          Fun(Rest, 1000)
    end).

%% @doc Parse a quality parameter string (for example q=0.500).
-spec qparam(binary(), fun()) -> any().
qparam(<< Q, $=, Data/binary >>, Fun) when Q =:= $q; Q =:= $Q ->
  qvalue(Data, Fun).

%% @doc Parse either a list of entity tags or a "*".
-spec entity_tag_match(binary()) -> any().
entity_tag_match(<< $*, Rest/binary >>) ->
  cloudi_x_hackney_bstr:whitespace(Rest,
    fun (<<>>) -> '*';
        (_Any) -> {error, badarg}
    end);
entity_tag_match(Data) ->
  cloudi_x_hackney_bstr:nonempty_list(Data, fun entity_tag/2).

%% @doc Parse an entity-tag.
-spec entity_tag(binary(), fun()) -> any().
entity_tag(<< "W/", Rest/binary >>, Fun) ->
  opaque_tag(Rest, Fun, weak);
entity_tag(Data, Fun) ->
  opaque_tag(Data, Fun, strong).

-spec opaque_tag(binary(), fun(), weak | strong) -> any().
opaque_tag(Data, Fun, Strength) ->
  cloudi_x_hackney_bstr:quoted_string(Data,
    fun (_Rest, <<>>) -> {error, badarg};
        (Rest, OpaqueTag) -> Fun(Rest, {Strength, OpaqueTag})
    end).

%% @doc Parse an expectation.
-spec expectation(binary(), fun()) -> any().
expectation(Data, Fun) ->
  cloudi_x_hackney_bstr:token_ci(Data,
    fun (_Rest, <<>>) -> {error, badarg};
        (<< $=, Rest/binary >>, Expectation) ->
          cloudi_x_hackney_bstr:word(Rest,
            fun (Rest2, ExtValue) ->
              cloudi_x_hackney_bstr:params(Rest2, fun (Rest3, ExtParams) ->
                Fun(Rest3, {Expectation, ExtValue, ExtParams})
                                         end)
            end);
        (Rest, Expectation) ->
          Fun(Rest, Expectation)
    end).



%% @doc Parse a quality value.
-spec qvalue(binary(), fun()) -> any().
qvalue(<< $0, $., Rest/binary >>, Fun) ->
  qvalue(Rest, Fun, 0, 100);
%% Some user agents use q=.x instead of q=0.x
qvalue(<< $., Rest/binary >>, Fun) ->
  qvalue(Rest, Fun, 0, 100);
qvalue(<< $0, Rest/binary >>, Fun) ->
  Fun(Rest, 0);
qvalue(<< $1, $., $0, $0, $0, Rest/binary >>, Fun) ->
  Fun(Rest, 1000);
qvalue(<< $1, $., $0, $0, Rest/binary >>, Fun) ->
  Fun(Rest, 1000);
qvalue(<< $1, $., $0, Rest/binary >>, Fun) ->
  Fun(Rest, 1000);
qvalue(<< $1, Rest/binary >>, Fun) ->
  Fun(Rest, 1000);
qvalue(_Data, _Fun) ->
  {error, badarg}.

-spec qvalue(binary(), fun(), integer(), 1 | 10 | 100) -> any().
qvalue(Data, Fun, Q, 0) ->
  Fun(Data, Q);
qvalue(<< C, Rest/binary >>, Fun, Q, M)
  when C >= $0, C =< $9 ->
  qvalue(Rest, Fun, Q + (C - $0) * M, M div 10);
qvalue(Data, Fun, Q, _M) ->
  Fun(Data, Q).

%% @doc Parse authorization value according rfc 2617.
%% Only Basic authorization is supported so far.
-spec authorization(binary(), binary()) -> {binary(), any()} | {error, badarg}.
authorization(UserPass, Type = <<"basic">>) ->
  cloudi_x_hackney_bstr:whitespace(UserPass,
    fun(D) ->
      authorization_basic_userid(base64:mime_decode(D),
        fun(Rest, Userid) ->
          authorization_basic_password(Rest,
            fun(Password) ->
              {Type, {Userid, Password}}
            end)
        end)
    end);
authorization(String, Type) ->
  cloudi_x_hackney_bstr:whitespace(String, fun(Rest) -> {Type, Rest} end).

%% @doc Parse user credentials.
-spec authorization_basic_userid(binary(), fun()) -> any().
authorization_basic_userid(Data, Fun) ->
  authorization_basic_userid(Data, Fun, <<>>).

authorization_basic_userid(<<>>, _Fun, _Acc) ->
  {error, badarg};
authorization_basic_userid(<<C, _Rest/binary>>, _Fun, Acc)
  when C < 32; C =:= 127; (C =:=$: andalso Acc =:= <<>>) ->
  {error, badarg};
authorization_basic_userid(<<$:, Rest/binary>>, Fun, Acc) ->
  Fun(Rest, Acc);
authorization_basic_userid(<<C, Rest/binary>>, Fun, Acc) ->
  authorization_basic_userid(Rest, Fun, <<Acc/binary, C>>).

-spec authorization_basic_password(binary(), fun()) -> any().
authorization_basic_password(Data, Fun) ->
  authorization_basic_password(Data, Fun, <<>>).

authorization_basic_password(<<>>, _Fun, <<>>) ->
  {error, badarg};
authorization_basic_password(<<C, _Rest/binary>>, _Fun, _Acc)
  when C < 32; C=:= 127 ->
  {error, badarg};
authorization_basic_password(<<>>, Fun, Acc) ->
  Fun(Acc);
authorization_basic_password(<<C, Rest/binary>>, Fun, Acc) ->
  authorization_basic_password(Rest, Fun, <<Acc/binary, C>>).

%% @doc Parse range header according rfc 2616.
-spec range(binary()) -> {Unit, [Range]} | {error, badarg} when
  Unit :: binary(),
  Range :: {non_neg_integer(), non_neg_integer() | infinity} | neg_integer().
range(Data) ->
  cloudi_x_hackney_bstr:token_ci(Data, fun range/2).

range(Data, Token) ->
  cloudi_x_hackney_bstr:whitespace(Data,
    fun(<<"=", Rest/binary>>) ->
      case cloudi_x_hackney_bstr:list(Rest, fun range_beginning/2) of
        {error, badarg} ->
          {error, badarg};
        Ranges ->
          {Token, Ranges}
      end;
       (_) ->
         {error, badarg}
    end).

range_beginning(Data, Fun) ->
  range_digits(Data, suffix,
    fun(D, RangeBeginning) ->
      range_ending(D, Fun, RangeBeginning)
    end).

range_ending(Data, Fun, RangeBeginning) ->
  cloudi_x_hackney_bstr:whitespace(Data,
    fun(<<"-", R/binary>>) ->
      case RangeBeginning of
        suffix ->
          range_digits(R, fun(D, RangeEnding) ->
            Fun(D, -RangeEnding) end);
        _ ->
          range_digits(R, infinity, fun(D, RangeEnding) ->
            Fun(D, {RangeBeginning, RangeEnding})
                                    end)
      end;
       (_) ->
         {error, badarg}
    end).

-spec range_digits(binary(), fun()) -> any().
range_digits(Data, Fun) ->
  cloudi_x_hackney_bstr:whitespace(Data,
    fun(D) ->
      cloudi_x_hackney_bstr:digits(D, Fun)
    end).

-spec range_digits(binary(), any(), fun()) -> any().
range_digits(Data, Default, Fun) ->
  cloudi_x_hackney_bstr:whitespace(Data,
    fun(<< C, Rest/binary >>) when C >= $0, C =< $9 ->
      cloudi_x_hackney_bstr:digits(Rest, Fun, C - $0);
       (_) ->
         Fun(Data, Default)
    end).

-spec header_default(binary()) -> any().
header_default(<<"transfer-encoding">>) ->
  [<<"identity">>];
header_default(_Name) ->
  undefined.
