-module(elli_util).
-include("elli.hrl").
-include("elli_util.hrl").

-include_lib("kernel/include/file.hrl").

-export([normalize_range/2
        , encode_range/2
        , file_size/1
        ]).

-export_type([range/0]).

-type range() :: {Offset::non_neg_integer(), Length::non_neg_integer()}.

-spec normalize_range(RangeOrSet, Size) -> Normalized when
      RangeOrSet :: any(),
      Size       :: integer(),
      Normalized :: range() | undefined | invalid_range.
%% @doc: If a valid byte-range, or byte-range-set of size 1
%% is supplied, returns a normalized range in the format
%% {Offset, Length}. Returns undefined when an empty byte-range-set
%% is supplied and the atom `invalid_range' in all other cases.
normalize_range({suffix, Length}, Size)
  when is_integer(Length), Length > 0 ->
    Length0 = erlang:min(Length, Size),
    {Size - Length0, Length0};
normalize_range({offset, Offset}, Size)
  when is_integer(Offset), Offset >= 0, Offset < Size ->
    {Offset, Size - Offset};
normalize_range({bytes, First, Last}, Size)
  when is_integer(First), is_integer(Last), First =< Last ->
    normalize_range({First, Last - First + 1}, Size);
normalize_range({Offset, Length}, Size)
  when is_integer(Offset), is_integer(Length),
       Offset >= 0, Length >= 0, Offset < Size ->
    Length0 = erlang:min(Length, Size - Offset),
    {Offset, Length0};
normalize_range([ByteRange], Size) ->
    normalize_range(ByteRange, Size);
normalize_range([], _Size) -> undefined;
normalize_range(_, _Size)  -> invalid_range.


-spec encode_range(Range::range() | invalid_range,
                   Size::non_neg_integer()) -> ByteRange::iolist().
%% @doc: Encode Range to a Content-Range value.
encode_range(Range, Size) ->
    [<<"bytes ">>, encode_range_bytes(Range),
     <<"/">>, integer_to_binary(Size)].

encode_range_bytes({Offset, Length}) ->
    [integer_to_binary(Offset),
     <<"-">>,
     integer_to_binary(Offset + Length - 1)];
encode_range_bytes(invalid_range) -> <<"*">>.


-spec file_size(Filename) -> Size | {error, Reason} when
      Filename :: file:name_all(),
      Size     :: non_neg_integer(),
      Reason   :: file:posix() | badarg | invalid_file.
%% @doc: Get the size in bytes of the file.
file_size(Filename) ->
    case file:read_file_info(Filename) of
        {ok, #file_info{type = regular, access = Perm, size = Size}}
          when Perm =:= read orelse Perm =:= read_write ->
            Size;
        {error, Reason} -> {error, Reason};
        _               -> {error, invalid_file}
    end.
