%% ``The contents of this file are subject to the Erlang Public License,
%% Version 1.1, (the "License"); you may not use this file except in
%% compliance with the License. You should have received a copy of the
%% Erlang Public License along with this software. If not, it can be
%% retrieved via the world wide web at http://www.erlang.org/.
%% 
%% Software distributed under the License is distributed on an "AS IS"
%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
%% the License for the specific language governing rights 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$
%%
-module(file_io_server).

%% A simple file server for io to one file instance per server instance.

-export([format_error/1]).
-export([start/3, start_link/3]).

-record(state, {handle,owner,mref,buf,read_mode}).

-define(PRIM_FILE, prim_file).
-define(READ_SIZE_LIST, 128).
-define(READ_SIZE_BINARY, (8*1024)).

-define(eat_message(M, T), receive M -> M after T -> timeout end).

%%%-----------------------------------------------------------------
%%% Exported functions

format_error({_Line, ?MODULE, Reason}) ->
    io_lib:format("~w", [Reason]);
format_error({_Line, Mod, Reason}) ->
    Mod:format_error(Reason);
format_error(ErrorId) ->
    erl_posix_msg:message(ErrorId).

start(Owner, FileName, ModeList) 
  when is_pid(Owner), is_list(FileName), is_list(ModeList) ->
    do_start(spawn, Owner, FileName, ModeList).

start_link(Owner, FileName, ModeList) 
  when is_pid(Owner), is_list(FileName), is_list(ModeList) ->
    do_start(spawn_link, Owner, FileName, ModeList).

%%%-----------------------------------------------------------------
%%% Server starter, dispatcher and helpers

do_start(Spawn, Owner, FileName, ModeList) ->
    Self = self(),
    Ref = make_ref(),
    Pid = 
	erlang:Spawn(
	  fun() ->
		  %% process_flag(trap_exit, true),
		  {ReadMode,Opts} = 
		      case lists:member(binary, ModeList) of
			  true ->
			      {binary,ModeList};
			  false ->
			      {list,[binary|ModeList]}
		      end,
		  case ?PRIM_FILE:open(FileName, Opts) of
		      {error, Reason} = Error ->
			  Self ! {Ref, Error},
			  exit(Reason);
		      {ok, Handle} ->
			  %% XXX must I handle R6 nodes here?
			  M = erlang:monitor(process, Owner),
			  Self ! {Ref, ok},
			  server_loop(
			    #state{handle    = Handle,
				   owner     = Owner, 
				   mref      = M, 
				   buf       = <<>>,
				   read_mode = ReadMode})
		  end
	  end),
    Mref = erlang:monitor(process, Pid),
    receive
	{Ref, {error, _Reason} = Error} ->
	    erlang:demonitor(Mref),
	    receive {'DOWN', Mref, _, _, _} -> ok after 0 -> ok end,
	    Error;
	{Ref, ok} ->
	    erlang:demonitor(Mref),
	    receive
		{'DOWN', Mref, _, _, Reason} ->
		    {error, Reason}
	    after 0 ->
		    {ok, Pid}
	    end;
	{'DOWN', Mref, _, _, Reason} ->
	    {error, Reason}
    end.

server_loop(#state{mref = Mref} = State) ->
    receive
	{file_request, From, ReplyAs, Request} when is_pid(From) ->
	    case file_request(Request, State) of
		{reply, Reply, NewState} ->
		    file_reply(From, ReplyAs, Reply),
		    server_loop(NewState);
		{error, Reply, NewState} ->
		    %% error is the same as reply, except that
		    %% it breaks the io_request_loop further down
		    file_reply(From, ReplyAs, Reply),
		    server_loop(NewState);
		{stop, Reason, Reply, _NewState} ->
		    file_reply(From, ReplyAs, Reply),
		    exit(Reason)
	    end;
	{io_request, From, ReplyAs, Request} when is_pid(From) ->
	    case io_request(Request, State) of
		{reply, Reply, NewState} ->
		    io_reply(From, ReplyAs, Reply),
		    server_loop(NewState);
		{error, Reply, NewState} ->
		    %% error is the same as reply, except that
		    %% it breaks the io_request_loop further down
		    io_reply(From, ReplyAs, Reply),
		    server_loop(NewState);
		{stop, Reason, Reply, _NewState} ->
		    io_reply(From, ReplyAs, Reply),
		    exit(Reason)
	    end;
	{'DOWN', Mref, _, _, Reason} ->
	    exit(Reason);
	_ ->
	    server_loop(State)
    end.

file_reply(From, ReplyAs, Reply) ->
    From ! {file_reply, ReplyAs, Reply}.

io_reply(From, ReplyAs, Reply) ->
    From ! {io_reply, ReplyAs, Reply}.

%%%-----------------------------------------------------------------
%%% file requests

file_request({pread,At,Sz}, 
	     #state{handle=Handle,buf=Buf,read_mode=ReadMode}=State) ->
    case position(Handle, At, Buf) of
	{ok,_Offs} ->
	    case ?PRIM_FILE:read(Handle, Sz) of
		{ok,Bin} when ReadMode =:= list ->
		    std_reply({ok,binary_to_list(Bin)}, State);
		Reply ->
		    std_reply(Reply, State)
	    end;
	Reply ->
	    std_reply(Reply, State)
    end;
file_request({pwrite,At,Data}, 
	     #state{handle=Handle,buf=Buf}=State) ->
    case position(Handle, At, Buf) of
	{ok,_Offs} ->
	    std_reply(?PRIM_FILE:write(Handle, Data), State);
	Reply ->
	    std_reply(Reply, State)
    end;
file_request(sync, 
	     #state{handle=Handle}=State) ->
    case ?PRIM_FILE:sync(Handle) of
	{error,_}=Reply ->
	    {stop,normal,Reply,State};
	Reply ->
	    {reply,Reply,State}
    end;
file_request(close, 
	     #state{handle=Handle}=State) ->
    {stop,normal,?PRIM_FILE:close(Handle),State#state{buf= <<>>}};
file_request({position,At}, 
	     #state{handle=Handle,buf=Buf}=State) ->
    std_reply(position(Handle, At, Buf), State);
file_request(truncate, 
	     #state{handle=Handle}=State) ->
    case ?PRIM_FILE:truncate(Handle) of
	{error,_Reason}=Reply ->
	    {stop,normal,Reply,State#state{buf= <<>>}};
	Reply ->
	    {reply,Reply,State}
    end;
file_request(Unknown, 
	     #state{}=State) ->
    Reason = {request, Unknown},
    {error,{error,Reason},State}.

std_reply({error,_}=Reply, State) ->
    {error,Reply,State#state{buf= <<>>}};
std_reply(Reply, State) ->
    {reply,Reply,State#state{buf= <<>>}}.

%%%-----------------------------------------------------------------
%%% I/O request 

io_request({put_chars,Chars}, % binary(Chars) new in R9C
	   #state{buf= <<>>}=State) ->
    put_chars(Chars, State);
io_request({put_chars,Chars}, % binary(Chars) new in R9C
	   #state{handle=Handle,buf=Buf}=State) ->
    case position(Handle, cur, Buf) of
	{error,_}=Reply ->
	    {stop,normal,Reply,State#state{buf= <<>>}};
	_ ->
	    put_chars(Chars, State#state{buf= <<>>})
    end;
io_request({put_chars,Mod,Func,Args}, 
	   #state{}=State) ->
    case catch apply(Mod, Func, Args) of
	Chars when is_list(Chars); is_binary(Chars) ->
	    io_request({put_chars,Chars}, State);
	_ ->
	    {error,{error,Func},State}
    end;
io_request({get_until,_Prompt,Mod,Func,XtraArgs}, 
	   #state{}=State) ->
    get_chars(io_lib, get_until, {Mod, Func, XtraArgs}, State);
io_request({get_chars,_Prompt,N}, % New in R9C
	   #state{}=State) ->
    get_chars(N, State);
io_request({get_chars,_Prompt,Mod,Func,XtraArg}, % New in R9C
	   #state{}=State) ->
    get_chars(Mod, Func, XtraArg, State);
io_request({get_line,_Prompt}, % New in R9C
	   #state{}=State) ->
    get_chars(io_lib, collect_line, [], State);
io_request({setopts, Opts}, % New in R9C
	   #state{}=State) when is_list(Opts) ->
    setopts(Opts, State);
io_request({requests,Requests}, 
	   #state{}=State) when is_list(Requests) ->
    io_request_loop(Requests, {reply,ok,State});
io_request(Unknown, 
	   #state{}=State) ->
    Reason = {request,Unknown},
    {error,{error,Reason},State}.



%% Process a list of requests as long as the results are ok.

io_request_loop([], Result) ->
    Result;
io_request_loop([_Request|_Tail], 
		{stop,_Reason,_Reply,_State}=Result) ->
    Result;
io_request_loop([_Request|_Tail],
		{error,_Reply,_State}=Result) ->
    Result;
io_request_loop([Request|Tail], 
		{reply,_Reply,State}) ->
    io_request_loop(Tail, io_request(Request, State)).



%% I/O request put_chars
%%
put_chars(Chars, #state{handle=Handle}=State) ->
    case ?PRIM_FILE:write(Handle, Chars) of
	{error,_}=Reply ->
	    {stop,normal,Reply,State};
	Reply ->
	    {reply,Reply,State}
    end.


%% Process the I/O request get_chars
%%
get_chars(0, #state{read_mode=ReadMode}=State) ->
    {reply,cast(<<>>, ReadMode),State};
get_chars(N, #state{buf=Buf,read_mode=ReadMode}=State) 
  when is_integer(N), N > 0, N =< byte_size(Buf) ->
    {B1,B2} = split_binary(Buf, N),
    {reply,cast(B1, ReadMode),State#state{buf=B2}};
get_chars(N, #state{handle=Handle,buf=Buf,read_mode=ReadMode}=State) 
  when is_integer(N), N > 0 ->
    BufSize = byte_size(Buf),
    NeedSize = N-BufSize,
    Size = max(NeedSize, ?READ_SIZE_BINARY),
    case ?PRIM_FILE:read(Handle, Size) of
	{ok, B} ->
	    if BufSize+byte_size(B) < N ->
		    std_reply(cat(Buf, B, ReadMode), State);
	       true ->
		    {B1,B2} = split_binary(B, NeedSize),
		    {reply,cat(Buf, B1, ReadMode),State#state{buf=B2}}
	    end;
	eof when BufSize =:= 0 ->
	    {reply,eof,State};
	eof ->
	    std_reply(cast(Buf, ReadMode), State);
	{error,Reason}=Error ->
	    {stop,Reason,Error,State#state{buf= <<>>}}
    end;
get_chars(_N, #state{}=State) ->
    {error,{error,get_chars},State}.

get_chars(Mod, Func, XtraArg, #state{buf= <<>>}=State) ->
    get_chars_empty(Mod, Func, XtraArg, start, State);
get_chars(Mod, Func, XtraArg, #state{buf=Buf}=State) ->
    get_chars_apply(Mod, Func, XtraArg, start, State#state{buf= <<>>}, Buf).

get_chars_empty(Mod, Func, XtraArg, S, 
		#state{handle=Handle,read_mode=ReadMode}=State) ->
    case ?PRIM_FILE:read(Handle, read_size(ReadMode)) of
	{ok,Bin} ->
	    get_chars_apply(Mod, Func, XtraArg, S, State, Bin);
	eof ->
	    get_chars_apply(Mod, Func, XtraArg, S, State, eof);
	{error,Reason}=Error ->
	    {stop,Reason,Error,State}
    end.

get_chars_apply(Mod, Func, XtraArg, S0, 
		#state{read_mode=ReadMode}=State, Data0) ->
    Data1 = case ReadMode of
	       list when is_binary(Data0) -> binary_to_list(Data0);
	       _ -> Data0
	    end,
    case catch Mod:Func(S0, Data1, XtraArg) of
	{stop,Result,Buf} ->
	    {reply,Result,State#state{buf=cast_binary(Buf)}};
	{'EXIT',Reason} ->
	    {stop,Reason,{error,err_func(Mod, Func, XtraArg)},State};
	S1 ->
	    get_chars_empty(Mod, Func, XtraArg, S1, State)
    end.

%% Convert error code to make it look as before
err_func(io_lib, get_until, {_,F,_}) ->
    F;
err_func(_, F, _) ->
    F.



%% Process the I/O request setopts
%%
%% setopts
setopts(Opts0, State) ->
    Opts = proplists:substitute_negations([{list,binary}], Opts0),
    case proplists:get_value(binary, Opts) of
	true ->
	    {ok,ok,State#state{read_mode=binary}};
	false ->
	    {ok,ok,State#state{read_mode=list}};
	_ ->
	    {error,{error,badarg},State}
    end.



%% Concatenate two binaries and convert the result to list or binary
cat(B1, B2, binary) ->
    list_to_binary([B1,B2]);
cat(B1, B2, list) ->
    binary_to_list(B1)++binary_to_list(B2).

%% Cast binary to list or binary
cast(B, binary) ->
    B;
cast(B, list) ->
    binary_to_list(B).

%% Convert buffer to binary
cast_binary(Binary) when is_binary(Binary) ->
    Binary;
cast_binary(List) when is_list(List) ->
    list_to_binary(List);
cast_binary(_EOF) ->
    <<>>.

%% Read size for different read modes
read_size(binary) ->
    ?READ_SIZE_BINARY;
read_size(list) ->
    ?READ_SIZE_LIST.

max(A, B) when A >= B ->
    A;
max(_, B) ->
    B.

%%%-----------------------------------------------------------------
%%% ?PRIM_FILE helpers

%% Compensates ?PRIM_FILE:position/2 for the number of bytes 
%% we have buffered

position(Handle, cur, Buf) ->
    position(Handle, {cur, 0}, Buf);
position(Handle, {cur, Offs}, Buf) when is_binary(Buf) ->
    ?PRIM_FILE:position(Handle, {cur, Offs-byte_size(Buf)});
position(Handle, At, _Buf) ->
    ?PRIM_FILE:position(Handle, At).

