%%%----------------------------------------------------------------------
%%% File    : esip_transaction.erl
%%% Author  : Evgeniy Khramtsov <ekhramtsov@process-one.net>
%%% Purpose : Route client/server transactions
%%% Created : 15 Jul 2009 by Evgeniy Khramtsov <ekhramtsov@process-one.net>
%%%
%%%
%%% Copyright (C) 2002-2022 ProcessOne, SARL. All Rights Reserved.
%%%
%%% 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.
%%%
%%%----------------------------------------------------------------------

-module(esip_transaction).

-behaviour(gen_server).

%% API
-export([start_link/0, process/2, reply/2, cancel/2,
         request/3, request/4, insert/4, delete/3, stop/1]).

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

-include("esip.hrl").
-include("esip_lib.hrl").

-record(state, {}).

%%====================================================================
%% API
%%====================================================================
start_link() ->
    gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).

process(SIPSock, #sip{method = Method, hdrs = Hdrs, type = request} = Req) ->
    Branch = esip:get_branch(Hdrs),
    case lookup(transaction_key(Branch, Method, server)) of
	{ok, Pid} ->
	    esip_server_transaction:route(Pid, Req);
        error when Method == <<"ACK">> ->
            case esip_dialog:lookup(esip_dialog:id(uas, Req)) of
                {ok, Core, _} ->
                    pass_to_core(Core, Req, SIPSock);
                _Err ->
                    esip:callback(request, [Req, SIPSock])
            end;
	error when Method == <<"CANCEL">> ->
            case lookup({esip:to_lower(Branch), server}) of
                {ok, Pid} ->
                    esip_server_transaction:route(Pid, Req),
                    start_server_transaction(SIPSock, Req);
                error ->
                    case esip:callback(request, [Req, SIPSock]) of
                        #sip{type = response} = Resp ->
                            esip_transport:send(SIPSock, Resp);
                        drop ->
                            ok;
                        _ ->
                            Resp = esip:make_response(
                                     Req,
                                     #sip{type = response, status = 481},
                                     esip:make_tag()),
                            esip_transport:send(SIPSock, Resp)
                    end
            end;
        error ->
            start_server_transaction(SIPSock, Req)
    end;
process(SIPSock, #sip{method = Method, hdrs = Hdrs, type = response} = Resp) ->
    Branch = esip:get_branch(Hdrs),
    case lookup(transaction_key(Branch, Method, client)) of
        {ok, Pid} ->
            esip_client_transaction:route(Pid, Resp);
        _ ->
            esip:callback(response, [Resp, SIPSock])
    end.

reply(#trid{owner = Pid, type = server}, #sip{type = response} = Resp) ->
    esip_server_transaction:route(Pid, Resp);
reply(#sip{method = Method, type = request, hdrs = Hdrs},
      #sip{type = response} = Resp) ->
    Branch = esip:get_branch(Hdrs),
    case lookup(transaction_key(Branch, Method, server)) of
        {ok, Pid} ->
            esip_server_transaction:route(Pid, Resp);
        error ->
            ok
    end;
reply(_, _) ->
    ok.

request(SIPSock, Req, TU) ->
    request(SIPSock, Req, TU, []).

request(SIPSock, #sip{type = request} = Req, TU, Opts) ->
    start_client_transaction(SIPSock, Req, TU, Opts).

cancel(#sip{method = Method, type = request, hdrs = Hdrs}, TU) ->
    Branch = esip:get_branch(Hdrs),
    case lookup(transaction_key(Branch, Method, client)) of
        {ok, Pid} ->
            esip_client_transaction:cancel(Pid, TU);
        error ->
            ok
    end;
cancel(#trid{type = client, owner = Pid}, TU) ->
    esip_client_transaction:cancel(Pid, TU).

stop(#trid{owner = Pid, type = client}) ->
    esip_client_transaction:stop(Pid);
stop(#trid{owner = Pid, type = server}) ->
    esip_server_transaction:stop(Pid).

insert(Branch, Method, Type, Pid) ->
    ets:update_counter(?MODULE, {transaction_number, Type}, 1),
    ets:insert(?MODULE, {transaction_key(Branch, Method, Type), Pid}).

delete(Branch, Method, Type) ->
    ets:update_counter(?MODULE, {transaction_number, Type}, -1),
    ets:delete(?MODULE, transaction_key(Branch, Method, Type)).

%%====================================================================
%% gen_server callbacks
%%====================================================================
init([]) ->
    ets:new(?MODULE, [public, named_table]),
    ets:insert(?MODULE, {{transaction_number, client}, 0}),
    ets:insert(?MODULE, {{transaction_number, server}, 0}),
    ets:insert(?MODULE, {{last_error_report, client}, 0}),
    ets:insert(?MODULE, {{last_error_report, server}, 0}),
    {ok, #state{}}.

handle_call(_Request, _From, State) ->
    {reply, bad_request, State}.

handle_cast(_Msg, State) ->
    {noreply, State}.

handle_info(_Info, State) ->
    {noreply, State}.

terminate(_Reason, _State) ->
    ok.

code_change(_OldVsn, State, _Extra) ->
    {ok, State}.

%%--------------------------------------------------------------------
%% Internal functions
%%--------------------------------------------------------------------
lookup(TransactionKey) ->
    case ets:lookup(?MODULE, TransactionKey) of
	[{_, Pid}] ->
	    {ok, Pid};
	_ ->
	    error
    end.

transaction_key(Branch, <<"CANCEL">>, Type) ->
    {esip:to_lower(Branch), cancel, Type};
transaction_key(Branch, _Method, Type) ->
    {esip:to_lower(Branch), Type}.

pass_to_core(Core, Req, SIPSock) ->
    case Core of
        F when is_function(F) ->
            F(Req, SIPSock, undefined);
        {M, F, A} ->
            apply(M, F, [Req, SIPSock, undefined | A])
    end.

start_server_transaction(SIPSock, Req) ->
    MaxTransactions = esip:get_config_value(max_server_transactions),
    case ets:lookup(?MODULE, {transaction_number, server}) of
        [{_, N}] when N >= MaxTransactions ->
            maybe_report_error(server, N),
            {Status, Reason} = esip:error_status({error, too_many_transactions}),
            esip_transport:send(SIPSock,
                                esip:make_response(
                                  Req, #sip{type = response,
                                            status = Status,
                                            reason = Reason},
                                  esip:make_tag()));
        _ ->
            case esip_server_transaction:start(SIPSock, Req) of
                {ok, _} ->
                    ok;
                Err ->
                    {Status, Reason} = esip:error_status(Err),
                    esip_transport:send(SIPSock,
                                        esip:make_response(
                                          Req, #sip{type = response,
                                                    status = Status,
                                                    reason = Reason},
                                          esip:make_tag()))
            end
    end.

start_client_transaction(SIPSock, Req, TU, Opts) ->
    MaxTransactions = esip:get_config_value(max_client_transactions),
    case ets:lookup(?MODULE, {transaction_number, client}) of
        [{_, N}] when N >= MaxTransactions ->
            maybe_report_error(client, N),
            {error, too_many_transactions};
        _ ->
            esip_client_transaction:start(SIPSock, Req, TU, Opts)
    end.

maybe_report_error(Type, N) ->
    case ets:lookup(?MODULE, {last_error_report, Type}) of
	[{_, Now}] ->
	    NowTime = p1_time_compat:monotonic_time(microsecond),
	    case NowTime - Now of
                T when T > 60000000 ->
		    ets:insert(?MODULE, {{last_error_report, Type}, NowTime}),
                    ?ERROR_MSG("too many ~s transactions: ~p", [Type, N]);
                _ ->
                    ok
            end;
        _ ->
            ok
    end.
