%% ``Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%%     http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%
%% The Initial Developer of the Original Code is Ericsson Utvecklings AB.
%% Portions created by Ericsson are Copyright 1999, Ericsson Utvecklings
%% AB. All Rights Reserved.''
%%
%%     $Id: mod_range.erl,v 1.1 2008/12/17 09:53:35 mikpe Exp $
%%
-module(mod_range).
-export([do/1]).
-include("httpd.hrl").

%% do



do(Info) ->
    ?DEBUG("do -> entry",[]),
    case Info#mod.method of
	"GET" ->
	    case httpd_util:key1search(Info#mod.data,status) of
		%% A status code has been generated!
		{StatusCode,PhraseArgs,Reason} ->
		    {proceed,Info#mod.data};
		%% No status code has been generated!
		undefined ->
		    case httpd_util:key1search(Info#mod.data,response) of
			%% No response has been generated!
			undefined ->
			    case httpd_util:key1search(Info#mod.parsed_header,"range") of
				undefined ->
				    %Not a range response
				    {proceed,Info#mod.data};
				Range ->
				    %%Control that there weren't a if-range field that stopped
				    %%The range request in favor for the whole file
				    case httpd_util:key1search(Info#mod.data,if_range) of
					send_file ->
					    {proceed,Info#mod.data};
					_undefined ->
					    do_get_range(Info,Range)
				    end
			    end;
			%% A response has been generated or sent!
			Response ->
			    {proceed,Info#mod.data}
		    end
	    end;
	%% Not a GET method!
	_ ->
	    {proceed,Info#mod.data}
    end.

do_get_range(Info,Ranges) ->
    ?DEBUG("do_get_range -> Request URI: ~p",[Info#mod.request_uri]),
     Path = mod_alias:path(Info#mod.data, Info#mod.config_db,
			  Info#mod.request_uri),
    {FileInfo, LastModified} =get_modification_date(Path),
    send_range_response(Path,Info,Ranges,FileInfo,LastModified).


send_range_response(Path,Info,Ranges,FileInfo,LastModified)->
    case parse_ranges(Ranges) of
	error->
	    ?ERROR("send_range_response-> Unparsable range request",[]),
	    {proceed,Info#mod.data};
	{multipart,RangeList}->
	    send_multi_range_response(Path,Info,RangeList);
	{Start,Stop}->
	    send_range_response(Path,Info,Start,Stop,FileInfo,LastModified)
    end.
%%More than one range specified
%%Send a multipart reponse to the user
%
%%An example of an multipart range response

% HTTP/1.1 206 Partial Content
% Date:Wed 15 Nov 1995 04:08:23 GMT
% Last-modified:Wed 14 Nov 1995 04:08:23 GMT
% Content-type: multipart/byteranges; boundary="SeparatorString"
%
% --"SeparatorString"
% Content-Type: application/pdf
% Content-Range: bytes 500-600/1010
% .... The data..... 101 bytes
%
% --"SeparatorString"
% Content-Type: application/pdf
% Content-Range: bytes 700-1009/1010
% .... The data.....



send_multi_range_response(Path,Info,RangeList)->
    case file:open(Path, [raw,binary]) of
	{ok, FileDescriptor} ->
	    file:close(FileDescriptor),
	    ?DEBUG("send_multi_range_response -> FileDescriptor: ~p",[FileDescriptor]),
	    Suffix = httpd_util:suffix(Path),
	    PartMimeType = httpd_util:lookup_mime_default(Info#mod.config_db,Suffix,"text/plain"),
	    Date = httpd_util:rfc1123_date(),
	    {FileInfo,LastModified}=get_modification_date(Path),
	    case valid_ranges(RangeList,Path,FileInfo) of
		{ValidRanges,true}->
		    ?DEBUG("send_multi_range_response -> Ranges are valid:",[]),
		    %Apache breaks the standard by sending the size field in the Header.
		    Header = [{code,206},
			      {content_type,"multipart/byteranges;boundary=RangeBoundarySeparator"},
			      {etag,httpd_util:create_etag(FileInfo)},
			      {last_modified,LastModified}
			     ],
		    ?DEBUG("send_multi_range_response -> Valid Ranges: ~p",[RagneList]),
		    Body={fun send_multiranges/4,[ValidRanges,Info,PartMimeType,Path]},
		    {proceed,[{response,{response,Header,Body}}|Info#mod.data]};
		_ ->
		    {proceed, [{status, {416,"Range not valid",bad_range_boundaries }}]}
	    end;
	{error, Reason} ->
	    ?ERROR("do_get -> failed open file: ~p",[Reason]),
	    {proceed,Info#mod.data}
    end.

send_multiranges(ValidRanges,Info,PartMimeType,Path)->
    ?DEBUG("send_multiranges -> Start sending the ranges",[]),
    case file:open(Path, [raw,binary]) of
	{ok,FileDescriptor} ->
	    lists:foreach(fun(Range)->
				  send_multipart_start(Range,Info,PartMimeType,FileDescriptor)
			  end,ValidRanges),
	    file:close(FileDescriptor),
	    %%Sends an end of the multipart
	    httpd_socket:deliver(Info#mod.socket_type,Info#mod.socket,"\r\n--RangeBoundarySeparator--"),
	    sent;
	_ ->
	    close
    end.

send_multipart_start({{Start,End},{StartByte,EndByte,Size}},Info,PartMimeType,FileDescriptor)when StartByte<Size->
    PartHeader=["\r\n--RangeBoundarySeparator\r\n","Content-type: ",PartMimeType,"\r\n",
                "Content-Range:bytes=",integer_to_list(StartByte),"-",integer_to_list(EndByte),"/",
		integer_to_list(Size),"\r\n\r\n"],
    send_part_start(Info#mod.socket_type,Info#mod.socket,PartHeader,FileDescriptor,Start,End);


send_multipart_start({{Start,End},{StartByte,EndByte,Size}},Info,PartMimeType,FileDescriptor)->
    PartHeader=["\r\n--RangeBoundarySeparator\r\n","Content-type: ",PartMimeType,"\r\n",
                "Content-Range:bytes=",integer_to_list(Size-(StartByte-Size)),"-",integer_to_list(EndByte),"/",
		integer_to_list(Size),"\r\n\r\n"],
    send_part_start(Info#mod.socket_type,Info#mod.socket,PartHeader,FileDescriptor,Start,End).

send_part_start(SocketType,Socket,PartHeader,FileDescriptor,Start,End)->
    case httpd_socket:deliver(SocketType,Socket,PartHeader) of
	ok ->
	    send_part_start(SocketType,Socket,FileDescriptor,Start,End);
	_ ->
	    close
    end.

send_range_response(Path,Info,Start,Stop,FileInfo,LastModified)->
    case file:open(Path, [raw,binary]) of
	{ok, FileDescriptor} ->
	    file:close(FileDescriptor),
	    ?DEBUG("send_range_response -> FileDescriptor: ~p",[FileDescriptor]),
	    Suffix = httpd_util:suffix(Path),
	    MimeType = httpd_util:lookup_mime_default(Info#mod.config_db,Suffix,"text/plain"),
	    Date = httpd_util:rfc1123_date(),
	    Size = get_range_size(Start,Stop,FileInfo),
	    case valid_range(Start,Stop,FileInfo) of
		{true,StartByte,EndByte,TotByte}->
		   Head=[{code,206},{content_type, MimeType},
			 {last_modified, LastModified},
			 {etag,httpd_util:create_etag(FileInfo)},
			 {content_range,["bytes=",integer_to_list(StartByte),"-",
					 integer_to_list(EndByte),"/",integer_to_list(TotByte)]},
			 {content_length,Size}],
		    BodyFunc=fun send_range_body/5,
		    Arg=[Info#mod.socket_type, Info#mod.socket,Path,Start,Stop],
		    {proceed,[{response,{response,Head,{BodyFunc,Arg}}}|Info#mod.data]};
		{false,Reason} ->
		    {proceed, [{status, {416,Reason,bad_range_boundaries }}]}
	    end;
	{error, Reason} ->
	    ?ERROR("send_range_response -> failed open file: ~p",[Reason]),
	    {proceed,Info#mod.data}
    end.


send_range_body(SocketType,Socket,Path,Start,End) ->
    ?DEBUG("mod_range -> send_range_body",[]),
    case file:open(Path, [raw,binary]) of
	{ok,FileDescriptor} ->
	    send_part_start(SocketType,Socket,FileDescriptor,Start,End),
	    file:close(FileDescriptor);
	_ ->
	    close
    end.

send_part_start(SocketType,Socket,FileDescriptor,Start,End) ->
    case Start of
	from_end ->
	    file:position(FileDescriptor,{eof,End}),
	    send_body(SocketType,Socket,FileDescriptor);
	from_start ->
	    file:position(FileDescriptor,{bof,End}),
	    send_body(SocketType,Socket,FileDescriptor);
	Byte when integer(Byte) ->
	    file:position(FileDescriptor,{bof,Start}),
	    send_part(SocketType,Socket,FileDescriptor,End)
    end,
    sent.


%%This function could replace send_body by calling it with Start=0 end =FileSize
%% But i gues it would be stupid when we look at performance
send_part(SocketType,Socket,FileDescriptor,End)->
    case file:position(FileDescriptor,{cur,0}) of
	{ok,NewPos} ->
	   if
	       NewPos > End ->
		   ok;
	       true ->
		   Size=get_file_chunk_size(NewPos,End,?FILE_CHUNK_SIZE),
		   case file:read(FileDescriptor,Size) of
		       eof ->
			   ok;
		       {error,Reason} ->
			   ok;
		       {ok,Binary} ->
			   case httpd_socket:deliver(SocketType,Socket,Binary) of
			       socket_closed ->
				   ?LOG("send_range of body -> socket closed while sending",[]),
				   socket_close;
			       _ ->
				   send_part(SocketType,Socket,FileDescriptor,End)
			   end
		   end
	   end;
	_->
	    ok
    end.

%% validate that the range is in the limits of the file
valid_ranges(RangeList,Path,FileInfo)->
    lists:mapfoldl(fun({Start,End},Acc)->
			case Acc of
			    true ->
				case valid_range(Start,End,FileInfo) of
				    {true,StartB,EndB,Size}->
					{{{Start,End},{StartB,EndB,Size}},true};
				    _ ->
					false
				end;
			    _ ->
				{false,false}
			end
		   end,true,RangeList).



valid_range(from_end,End,FileInfo)->
    Size=FileInfo#file_info.size,
    if
	End < Size ->
	    {true,(Size+End),Size-1,Size};
	true ->
	    false
    end;
valid_range(from_start,End,FileInfo)->
  Size=FileInfo#file_info.size,
    if
	End < Size ->
	    {true,End,Size-1,Size};
	true ->
	    false
    end;

valid_range(Start,End,FileInfo)when Start=<End->
    case FileInfo#file_info.size of
	FileSize when Start< FileSize ->
	    case FileInfo#file_info.size of
		Size when End<Size ->
		    {true,Start,End,FileInfo#file_info.size};
		Size ->
		    {true,Start,Size-1,Size}
	    end;
	_->
	    {false,"The size of the range is negative"}
    end;

valid_range(Start,End,FileInfo)->
    {false,"Range starts out of file boundaries"}.
%% Find the modification date of the file
get_modification_date(Path)->
    case file:read_file_info(Path) of
	{ok, FileInfo0} ->
	    {FileInfo0, httpd_util:rfc1123_date(FileInfo0#file_info.mtime)};
	_ ->
	    {#file_info{},""}
    end.

%Calculate the size of the chunk to read

get_file_chunk_size(Position,End,DefaultChunkSize)when (Position+DefaultChunkSize) =< End->
    DefaultChunkSize;
get_file_chunk_size(Position,End,DefaultChunkSize)->
    (End-Position) +1.



%Get the size of the range to send. Remember that
%A range is from startbyte up to endbyte which means that
%the nuber of byte in a range is (StartByte-EndByte)+1

get_range_size(from_end,Stop,FileInfo)->
    integer_to_list(-1*Stop);

get_range_size(from_start,StartByte,FileInfo) ->
    integer_to_list((((FileInfo#file_info.size)-StartByte)));

get_range_size(StartByte,EndByte,FileInfo) ->
    integer_to_list((EndByte-StartByte)+1).

parse_ranges([$\ ,$b,$y,$t,$e,$s,$\=|Ranges])->
    parse_ranges([$b,$y,$t,$e,$s,$\=|Ranges]);
parse_ranges([$b,$y,$t,$e,$s,$\=|Ranges])->
    case string:tokens(Ranges,", ") of
       [Range] ->
	   parse_range(Range);
       [Range1|SplittedRanges]->
	   {multipart,lists:map(fun parse_range/1,[Range1|SplittedRanges])}
    end;
%Bad unit
parse_ranges(Ranges)->
    io:format("Bad Ranges : ~p",[Ranges]),
    error.
%Parse the range  specification from the request to {Start,End}
%Start=End : Numreric string | []

parse_range(Range)->
    format_range(split_range(Range,[],[])).
format_range({[],BytesFromEnd})->
    {from_end,-1*(list_to_integer(BytesFromEnd))};
format_range({StartByte,[]})->
    {from_start,list_to_integer(StartByte)};
format_range({StartByte,EndByte})->
    {list_to_integer(StartByte),list_to_integer(EndByte)}.
%Last case return the splitted range
split_range([],Current,Other)->
    {lists:reverse(Other),lists:reverse(Current)};

split_range([$-|Rest],Current,Other)->
    split_range(Rest,Other,Current);

split_range([N|Rest],Current,End) ->
    split_range(Rest,[N|Current],End).

send_body(SocketType,Socket,FileDescriptor) ->
    case file:read(FileDescriptor,?FILE_CHUNK_SIZE) of
	{ok,Binary} ->
	    ?DEBUG("send_body -> send another chunk: ~p",[size(Binary)]),
	    case httpd_socket:deliver(SocketType,Socket,Binary) of
		socket_closed ->
		    ?LOG("send_body -> socket closed while sending",[]),
		    socket_close;
		_ ->
		    send_body(SocketType,Socket,FileDescriptor)
	    end;
	eof ->
	    ?DEBUG("send_body -> done with this file",[]),
	    eof
    end.
