% ``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(httpc_manager).

-behaviour(gen_server).

-include("httpc_internal.hrl").
-include("http_internal.hrl").

%% Application API
-export([start_link/1, request/1, cancel_request/1,
	 request_canceled/1, retry_request/1, redirect_request/1,
	 insert_session/1, delete_session/1, set_options/1, store_cookies/2,
	 cookies/1]).

%% gen_server callbacks
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2,
	 code_change/3]).

-record(state, {
	  cancel = [],	 % [{RequestId, HandlerPid, ClientPid}]  
	  handler_db,    % ets() - Entry: {Requestid, HandlerPid, ClientPid}
	  cookie_db,     % {ets(), dets()} - {session_cookie_db, cookie_db} 
	  options = #options{}
	 }).

%%====================================================================
%% Application API
%%====================================================================
%%--------------------------------------------------------------------
%% Function: start_link() -> {ok, Pid}
%%
%% Description: Starts the http request manger process. (Started by
%% the intes supervisor.)
%%--------------------------------------------------------------------
start_link({default, CookieDir}) ->
    gen_server:start_link({local, ?MODULE}, ?MODULE, 
			  [{http_default_cookie_db, CookieDir}], []). 

%%--------------------------------------------------------------------
%% Function: request() -> {ok, Requestid} | {error, Reason}
%%	Request = #request{}
%%
%% Description: Sends a request to the httpc manager process.
%%--------------------------------------------------------------------
request(Request) ->
    call({request, Request}, infinity).

%%--------------------------------------------------------------------
%% Function: retry_request(Request) -> _
%%	Request = #request{}
%%
%% Description: Resends a request to the httpc manager process, intended
%% to be called by the httpc handler process if it has to terminate with
%% a non empty pipeline.
%%--------------------------------------------------------------------
retry_request(Request) ->
    cast({retry_or_redirect_request, Request}).

%%--------------------------------------------------------------------
%% Function: redirect_request(Request) -> _
%%	Request = #request{}
%%
%% Description: Sends an atoumatic redirect request to the httpc
%% manager process, intended to be called by the httpc handler process
%% when the automatic redirect option is set.
%%--------------------------------------------------------------------
redirect_request(Request) ->
    cast({retry_or_redirect_request, Request}).

%%--------------------------------------------------------------------
%% Function: cancel_request(RequestId) -> ok
%%	RequestId - ref()
%%
%% Description: Cancels the request with <RequestId>.
%%--------------------------------------------------------------------
cancel_request(RequestId) ->
    call({cancel_request, RequestId}, infinity).

%%--------------------------------------------------------------------
%% Function: request_canceled(RequestId) -> ok
%%	RequestId - ref()
%%
%% Description: Confirms that a request has been canceld. Intended to
%% be called by the httpc handler process.
%%--------------------------------------------------------------------
request_canceled(RequestId) ->
    cast({request_canceled, RequestId}).

%%--------------------------------------------------------------------
%% Function: insert_session(Session) -> _
%%	Session - #tcp_session{}
%%
%% Description: Inserts session information into the httpc manager table
%% httpc_manager_session_db. Intended to be called by the httpc request
%% handler process.
%%--------------------------------------------------------------------
insert_session(Session) ->
    ets:insert(httpc_manager_session_db, Session).

%%--------------------------------------------------------------------
%% Function: delete_session(SessionId) -> _
%%	SessionId -  {{Host, Port}, HandlerPid}
%% 
%% Description: Deletes session information from the httpc manager table
%% httpc_manager_session_db. Intended to be called by the httpc request
%% handler process.
%%--------------------------------------------------------------------
delete_session(SessionId) ->
    ets:delete(httpc_manager_session_db, SessionId).

%%--------------------------------------------------------------------
%% Function: set_options(Options) -> ok
%%
%% Options = [Option]
%% Option = {proxy, {Proxy, [NoProxy]}} | {max_pipeline_length, integer()} |
%%          {max_sessions, integer()} | {pipeline_timeout, integer()}
%% Proxy = {Host, Port}
%% NoProxy - [Domain | HostName | IPAddress]     
%% Max - integer() 
%% 
%% Description: Sets the options to be used by the client.
%%--------------------------------------------------------------------
set_options(Options) ->
    cast({set_options, Options}).

%%--------------------------------------------------------------------
%% Function: store_cookies(Cookies, Address) -> ok
%%
%% Cookies = [Cookie]
%% Cookie = #http_cookie{}
%% 
%% Description: Stores cookies from the server.
%%--------------------------------------------------------------------
store_cookies([], _) ->
    ok;
store_cookies(Cookies, Address) ->
    cast({store_cookies, {Cookies, Address}}).

%%--------------------------------------------------------------------
%% Function: cookies(Url) -> ok
%%
%% Url = string()
%%
%% Description: Retrieves the cookies that  
%%--------------------------------------------------------------------
cookies(Url) ->
    call({cookies, Url}, infinity).

%%====================================================================
%% gen_server callback functions
%%====================================================================

%%--------------------------------------------------------------------
%% Function: init([Request, Session]) -> {ok, State} | 
%%                       {ok, State, Timeout} | ignore |{stop, Reason}
%% Description: Initiates the httpc_manger process
%%--------------------------------------------------------------------
init([CookiesConf|_Options]) ->
    process_flag(trap_exit, true),
    ets:new(httpc_manager_session_db, 
	    [public, set, named_table, {keypos, #tcp_session.id}]),
    {ok, #state{handler_db = ets:new(handler_db, [protected, set]),
		cookie_db = 
		http_cookie:open_cookie_db({CookiesConf, 
					    http_session_cookie_db})
	       }}.

%%--------------------------------------------------------------------
%% Function: handle_call(Request, From, State) -> {reply, Reply, State} |
%%          {reply, Reply, State, Timeout} |
%%          {noreply, State}               |
%%          {noreply, State, Timeout}      |
%%          {stop, Reason, Reply, State}   | (terminate/2 is called)
%%          {stop, Reason, State}            (terminate/2 is called)
%% Description: Handling call messages
%%--------------------------------------------------------------------
handle_call({request, Request}, _, State) ->
    case (catch handle_request(Request, State)) of
	{reply, Msg, State} ->
	    {reply, Msg, State};
	Error ->
	    {stop, Error, httpc_response:error(Request, Error), State}
    end;
	
handle_call({cancel_request, RequestId}, From, State) ->
    case ets:lookup(State#state.handler_db, RequestId) of
	[] ->
	    ok, %% Nothing to cancel
	      {reply, ok, State};
	[{_, Pid, _}] ->
	    httpc_handler:cancel(RequestId, Pid),
	    {noreply, State#state{cancel = 
				  [{RequestId, Pid, From} |
				   State#state.cancel]}}
    end;

handle_call({cookies, Url}, _, State) ->
    case http_uri:parse(Url) of
	{Scheme, _, Host, Port, Path, _} ->
	    CookieHeaders = 
		http_cookie:header(Scheme, {Host, Port}, 
				   Path, State#state.cookie_db),
	    {reply, CookieHeaders, State};
	Msg ->
	    {reply, Msg, State}
    end;

handle_call(Msg, From, State) ->
    error_logger:error_report("HTTPC_MANAGER recived unkown call: ~p"
			      "from: ~p~n", [Msg, From]),
    {reply, {error, 'API_violation'}, State}.

%%--------------------------------------------------------------------
%% Function: handle_cast(Msg, State) -> {noreply, State} |
%%          {noreply, State, Timeout} |
%%          {stop, Reason, State}            (terminate/2 is called)
%% Description: Handling cast messages
%%--------------------------------------------------------------------
handle_cast({retry_or_redirect_request, {Time, Request}}, State) ->
    {ok, _} = timer:apply_after(Time, ?MODULE, retry_request, [Request]),
    {noreply, State};

handle_cast({retry_or_redirect_request, Request}, State) ->
    case handle_request(Request, State) of
	{reply, {ok, _}, NewState} ->
	    {noreply, NewState};
	{reply, Error, NewState} ->
	    httpc_response:error(Request, Error),
	    {noreply, NewState}
    end;
handle_cast({request_canceled, RequestId}, State) ->
    ets:delete(State#state.handler_db, RequestId),
    case lists:keysearch(RequestId, 1, State#state.cancel) of
	{value, Entry = {RequestId, _, From}} ->
	    gen_server:reply(From, ok),
	    {noreply, 
	     State#state{cancel = lists:delete(Entry, State#state.cancel)}};
	_ ->
	   {noreply, State}
    end;
handle_cast({set_options, Options}, State = #state{options = OldOptions}) ->
    NewOptions = 
	#options{proxy = 
		 http_util:key1search(Options, proxy,
				       OldOptions#options.proxy),
		 pipeline_timeout = 
		 http_util:key1search(Options, pipeline_timeout, 
				       OldOptions#options.pipeline_timeout),
		 max_pipeline_length =
		 http_util:key1search(Options, max_pipeline_length, 
				       OldOptions#options.max_pipeline_length),
		 max_sessions = 
		 http_util:key1search(Options, max_sessions, 
				       OldOptions#options.max_sessions),
		 cookies = http_util:key1search(Options, cookies, 
						 OldOptions#options.cookies),
		 ipv6 = http_util:key1search(Options, ipv6, 
					     OldOptions#options.ipv6),
		 verbose = http_util:key1search(Options, verbose, 
						OldOptions#options.verbose)
		}, 
    case {OldOptions#options.verbose, NewOptions#options.verbose} of
	{Same, Same} ->
	    ok;
	{_, false} ->
	    dbg:stop();
	{false, Level}  ->
	    dbg:tracer(),
	    handle_verbose(Level);
	{_, Level} ->
	    dbg:stop(),
	    dbg:tracer(),
	    handle_verbose(Level)
    end,

    {noreply, State#state{options = NewOptions}};

handle_cast({store_cookies, _}, 
	    State = #state{options = #options{cookies = disabled}}) ->
    {noreply, State};

handle_cast({store_cookies, {Cookies, _}}, State) ->
    ok = do_store_cookies(Cookies, State),
    {noreply, State};

handle_cast(Msg, State) ->
    error_logger:error_report("HTTPC_MANAGER recived unkown cast: ~p", [Msg]),
    {noreply, State}.

%%--------------------------------------------------------------------
%% Function: handle_info(Info, State) -> {noreply, State} |
%%          {noreply, State, Timeout} |
%%          {stop, Reason, State}            (terminate/2 is called)
%% Description: Handling all non call/cast messages
%%---------------------------------------------------------
handle_info({'EXIT', _, _}, State) ->
    %% Handled in DOWN
    {noreply, State};
handle_info({'DOWN', _, _, Pid, _}, State) ->
    ets:match_delete(State#state.handler_db, {'_', Pid, '_'}),

    %% If there where any canceled request, handled by the
    %% the process that now has terminated, the
    %% cancelation can be viewed as sucessfull!
    NewCanceldList = 
	lists:foldl(fun(Entry = {_, HandlerPid, From}, Acc)  ->
			    case HandlerPid of
				Pid ->
				    gen_server:reply(From, ok),
				    lists:delete(Entry, Acc);
				_ ->
				    Acc
			    end 
		    end, State#state.cancel, State#state.cancel),
    {noreply, State#state{cancel = NewCanceldList}};    
handle_info(Info, State) ->
    error_logger:error_report("Unknown message in "
			      "httpc_manager:handle_info ~p~n", [Info]),
    {noreply, State}. 
%%--------------------------------------------------------------------
%% Function: terminate(Reason, State) -> _  (ignored by gen_server)
%% Description: Shutdown the httpc_handler
%%--------------------------------------------------------------------
terminate(_, State) ->
    http_cookie:close_cookie_db(State#state.cookie_db),
    ets:delete(httpc_manager_session_db),
    ets:delete(State#state.handler_db).

%%--------------------------------------------------------------------
%% Func: code_change(_OldVsn, State, Extra) -> {ok, NewState}
%% Purpose: Convert process state when code is changed
%%--------------------------------------------------------------------
code_change(_OldVsn, State, _Extra) ->
    {ok, State}.

%%--------------------------------------------------------------------
%% Internal functions
%%--------------------------------------------------------------------
handle_request(Request, State) ->
    NewRequest = handle_cookies(generate_request_id(Request), State),
    case select_session(Request#request.method, 
			Request#request.address, 
			Request#request.scheme, State) of
	{ok, HandlerPid} ->
	    pipeline(NewRequest, HandlerPid, State);
	no_connection ->
	    start_handler(NewRequest, State);
	{no_session,  OpenSessions} when OpenSessions 
	< State#options.max_sessions ->
	    start_handler(NewRequest, State);
	{no_session, _} ->
	    %% Do not start any more persistent connections
	    %% towards this server.
	    NewHeaders = 
		(NewRequest#request.headers)#http_request_h{connection
							    = "close"},
	    start_handler(NewRequest#request{headers = NewHeaders}, State)
    end,
    {reply, {ok, NewRequest#request.id}, State}.

select_session(Method, HostPort, Scheme, #state{options = 
						#options{max_pipeline_length =
							 Max}}) ->
    case httpc_request:is_idempotent(Method) of
	true ->
	    Candidates = ets:match(httpc_manager_session_db,
				   {'_', {HostPort, '$1'}, 
				    false, Scheme, '_', '$2'}),
	    select_session(Candidates, Max);
	false ->
	    no_connection
    end.
    
select_session(Candidates, MaxPipeline) ->
    case Candidates of 
	[] ->
	  no_connection; 
	_ ->
	    NewCandidates = 
		lists:foldl(
		  fun([Pid, PipelineLength], Acc) when 
			    PipelineLength =< MaxPipeline ->
			  [{Pid, PipelineLength} | Acc];
		     (_, Acc) ->
			  Acc
		  end, [], Candidates),
	    
	    case lists:keysort(2, NewCandidates) of
		[] ->
		    {no_session, length(Candidates)};
		[{HandlerPid, _} | _] ->
		    {ok, HandlerPid}
	    end
    end.
	    
pipeline(Request, HandlerPid, State) ->
    case (catch httpc_handler:send(Request, HandlerPid)) of
	ok ->
	    ets:insert(State#state.handler_db, {Request#request.id, 
						HandlerPid,
						Request#request.from});
	_  -> %timeout pipelining failed 
	    start_handler(Request, State)
    end.

start_handler(Request, State) ->
    {ok, Pid} = httpc_handler:start_link(Request, State#state.options),
    ets:insert(State#state.handler_db, {Request#request.id, 
					Pid, Request#request.from}),
    erlang:monitor(process, Pid).

generate_request_id(Request) ->
    case Request#request.id of
	undefined ->
	    RequestId = make_ref(),
	    Request#request{id = RequestId};
	_ ->
	    %% This is an automatic redirect or a retryed pipelined
	    %% request keep the old id.
	    Request
    end.

handle_cookies(Request, #state{options = #options{cookies = disabled}}) ->
    Request;
handle_cookies(Request = #request{scheme = Scheme, address = Address,
				  path = Path, headers = 
		 Headers = #http_request_h{other = Other}}, 
	       #state{cookie_db = Db}) ->
    case http_cookie:header(Scheme, Address, Path, Db) of
	{"cookie", ""} ->
	    Request;
	CookieHeader ->
	    NewHeaders = 
		Headers#http_request_h{other = [CookieHeader | Other]},
	    Request#request{headers = NewHeaders}
    end.

do_store_cookies([], _) ->
    ok;
do_store_cookies([Cookie | Cookies], State) ->
    ok = http_cookie:insert(Cookie, State#state.cookie_db),
    do_store_cookies(Cookies, State).

call(Msg, Timeout) ->
    gen_server:call(?MODULE, Msg, Timeout).

cast(Msg) ->
   gen_server:cast(?MODULE, Msg).

handle_verbose(debug) ->
    dbg:p(self(), [call]),
    dbg:tp(?MODULE, [{'_', [], [{return_trace}]}]);
handle_verbose(trace) ->
    dbg:p(self(), [call]),
    dbg:tpl(?MODULE, [{'_', [], [{return_trace}]}]);
handle_verbose(_) ->
    ok.  

