%%%----------------------------------------------------------------------
%%% File    : esip_server_transaction.erl
%%% Author  : Evgeniy Khramtsov <ekhramtsov@process-one.net>
%%% Purpose : Server transaction layer. See RFC3261 and friends.
%%% 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_server_transaction).

-behaviour(p1_fsm).

%% API
-export([start_link/2, start/2, stop/1, route/2]).

%% p1_fsm callbacks
-export([init/1, handle_event/3, handle_sync_event/4,
	 handle_info/3, terminate/3, code_change/4]).

%% p1_fsm states
-export([trying/2,
	 proceeding/2,
         accepted/2,
	 completed/2,
         confirmed/2]).

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

-define(MAX_TRANSACTION_LIFETIME, timer:minutes(5)).

-record(state, {sock, branch, method, resp, tu}).

%%====================================================================
%% API
%%====================================================================
start_link(SIPSocket, Request) ->
    p1_fsm:start_link(?MODULE, [SIPSocket, Request], []).

start(SIPSocket, Request) ->
    case esip_tmp_sup:start_child(esip_server_transaction_sup,
                                  ?MODULE, p1_fsm, [SIPSocket, Request]) of
        {ok, Pid} ->
            {ok, make_trid(Pid)};
        Err ->
            Err
    end.

route(Pid, R) ->
    p1_fsm:send_event(Pid, R).

stop(Pid) ->
    p1_fsm:send_all_state_event(Pid, stop).

%%====================================================================
%% p1_fsm callbacks
%%====================================================================
init([SIPSock, Request]) ->
    Branch = esip:get_branch(Request#sip.hdrs),
    State = #state{sock = SIPSock, branch = Branch},
    p1_fsm:send_event(self(), Request),
    erlang:send_after(?MAX_TRANSACTION_LIFETIME, self(), timeout),
    esip_transaction:insert(Branch, Request#sip.method, server, self()),
    {ok, trying, State}.

trying(#sip{method = <<"CANCEL">> = Method, type = request} = Req, State) ->
    Resp = esip:make_response(Req,
                              #sip{type = response, status = 200},
                              esip:make_tag()),
    proceeding(Resp, State#state{method = Method});
trying(#sip{type = request, method = Method} = Req, State) ->
    case find_transaction_user(Req, State#state.sock) of
        {dialog, TU} ->
            NewState = State#state{tu = TU, method = Method},
            case pass_to_transaction_user(NewState, Req) of
                #sip{type = response} = Resp ->
                    proceeding(Resp, NewState);
                wait ->
                    maybe_send_trying(Req),
                    {next_state, proceeding, NewState};
                NewTU ->
                    case is_transaction_user(NewTU) of
                        true ->
                            maybe_send_trying(Req),
                            {next_state, proceeding, NewState#state{tu = NewTU}};
                        false ->
                            Resp = esip:make_response(
                                     Req, #sip{status = 500, type = response},
                                     esip:make_tag()),
                            proceeding(Resp, State#state{method = Method})
                    end
            end;
        #sip{type = response} = Resp ->
            proceeding(Resp, State#state{method = Method});
        cseq_out_of_order ->
            Resp = esip:make_response(
                     Req, #sip{status = 500, type = response,
                               reason = <<"CSeq is Out of Order">>},
                     esip:make_tag()),
            proceeding(Resp, State#state{method = Method});
        wait ->
            maybe_send_trying(Req),
            {next_state, proceeding, State#state{method = Method}};
        TU ->
            case is_transaction_user(TU) of
                true ->
                    maybe_send_trying(Req),
                    {next_state, proceeding, State#state{tu = TU,
                                                         method = Method}};
                false ->
                    Resp = esip:make_response(
                             Req, #sip{status = 500, type = response},
                             esip:make_tag()),
                    proceeding(Resp, State#state{method = Method})
            end
    end;
trying(_Event, State) ->
    {next_state, trying, State}.

proceeding({trying, TryingResp}, #state{resp = undefined} = State) ->
    %% TU didn't respond in 200 ms
    case send(State, TryingResp) of
        ok ->
            {next_state, proceeding, State};
        _ ->
            {stop, normal, State}
    end;
proceeding(#sip{type = response, status = Status} = Resp, State) when Status < 200 ->
    update_remote_seqnum(Resp, State),
    case send(State, Resp) of
        ok ->
            {next_state, proceeding, State#state{resp = Resp}};
        _ ->
            {stop, normal, State}
    end;
proceeding(#sip{type = response, status = Status} = Resp,
           #state{method = <<"INVITE">>} = State) when Status < 300 ->
    update_remote_seqnum(Resp, State),
    p1_fsm:send_event_after(64*esip:timer1(), timer_L),
    case send(State, Resp) of
        ok ->
            {next_state, accepted, State#state{resp = Resp}};
        _ ->
            {stop, normal, State}
    end;
proceeding(#sip{type = response, status = Status} = Resp,
           #state{method = <<"INVITE">>} = State) when Status >= 300 ->
    update_remote_seqnum(Resp, State),
    T1 = esip:timer1(),
    if (State#state.sock)#sip_socket.type == udp ->
            p1_fsm:send_event_after(T1, {timer_G, T1});
       true ->
            ok
    end,
    p1_fsm:send_event_after(64*T1, timer_H),
    case send(State, Resp) of
        ok ->
            {next_state, completed, State#state{resp = Resp}};
        _ ->
            {stop, normal, State}
    end;
proceeding(#sip{type = response, status = Status} = Resp, State) when Status >= 200 ->
    update_remote_seqnum(Resp, State),
    if (State#state.sock)#sip_socket.type == udp ->
            p1_fsm:send_event_after(64*esip:timer1(), timer_J),
            case send(State, Resp) of
                ok ->
                    {next_state, completed, State#state{resp = Resp}};
                _ ->
                    {stop, normal, State}
            end;
       true ->
            send(State, Resp),
            {stop, normal, State}
    end;
proceeding(#sip{type = request, method = <<"CANCEL">>} = Req, State) ->
    if State#state.method == <<"INVITE">> ->
            case pass_to_transaction_user(State, Req) of
                #sip{type = response} = Resp ->
                    proceeding(Resp#sip{method = <<"INVITE">>}, State);
                _ ->
                    {next_state, proceeding, State}
            end;
       true ->
            {next_state, proceeding, State}
    end;
proceeding(#sip{type = request, method = Method} = Req,
           #state{method = Method, resp = Resp} = State) ->
    if Resp /= undefined ->
            case send(State, Resp) of
                ok ->
                    {next_state, proceeding, State};
                _ ->
                    {stop, normal, State}
            end;
       Method == <<"INVITE">> ->
            Trying = esip:make_response(Req, #sip{type = response,
                                                  status = 100}),
            case send(State, Trying) of
                ok ->
                    {next_state, proceeding, State};
                _ ->
                    {stop, normal, State}
            end;
       true ->
            {next_state, proceeding, State}
    end;
proceeding(_Event, State) ->
    {next_state, proceeding, State}.

accepted(#sip{type = request, method = <<"ACK">>} = Req, State) ->
    pass_to_transaction_user(State, Req),
    {next_state, accepted, State};
accepted(#sip{type = response, status = Status} = Resp, State)
  when Status >= 200, Status < 300 ->
    send(State, Resp),
    {next_state, accepted, State};
accepted(timer_L, State) ->
    {stop, normal, State};
accepted(_Event, State) ->
    {next_state, accepted, State}.

completed(#sip{type = request, method = <<"ACK">>},
          #state{method = <<"INVITE">>} = State) ->
    if (State#state.sock)#sip_socket.type == udp ->
            p1_fsm:send_event_after(esip:timer4(), timer_I),
            {next_state, confirmed, State};
       true ->
            {stop, normal, State}
    end;
completed(#sip{type = request, method = Method},
          #state{method = Method, resp = Resp} = State) ->
    case send(State, Resp) of
        ok ->
            {next_state, completed, State};
        _ ->
            {stop, normal, State}
    end;
completed(timer_H, State) ->
    pass_to_transaction_user(State, {error, timeout}),
    {stop, normal, State};
completed({timer_G, T}, State) ->
    T2 = esip:timer2(),
    case 2*T < T2 of
        true ->
            p1_fsm:send_event_after(2*T, {timer_G, 2*T});
        false ->
            p1_fsm:send_event_after(T2, {timer_G, T2})
    end,
    case send(State, State#state.resp) of
        ok ->
            {next_state, completed, State};
        _ ->
            {stop, normal, State}
    end;
completed(timer_J, State) ->
    {stop, normal, State};
completed(_Event, State) ->
    {next_state, completed, State}.

confirmed(timer_I, State) ->
    {stop, normal, State};
confirmed(_Event, State) ->
    {next_state, confirmed, State}.

handle_event(stop, _StateName, State) ->
    {stop, normal, State};
handle_event(_Event, StateName, State) ->
    {next_state, StateName, State}.

handle_sync_event(_Event, _From, StateName, State) ->
    {reply, ok, StateName, State}.

handle_info(timeout, _StateName, State) ->
    pass_to_transaction_user(State, {error, timeout}),
    {stop, normal, State};
handle_info(_Info, StateName, State) ->
    {next_state, StateName, State}.

terminate(_Reason, _StateName, #state{branch = Branch, method = Method}) ->
    esip_transaction:delete(Branch, Method, server).

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

%%--------------------------------------------------------------------
%% Internal functions
%%--------------------------------------------------------------------
is_transaction_user(TU) when is_function(TU) ->
    true;
is_transaction_user({M, F, A}) when is_atom(M), is_atom(F), is_list(A) ->
    true;
is_transaction_user(_) ->
    false.

pass_to_transaction_user(#state{tu = TU, sock = Sock}, Req) ->
    TrID = make_trid(),
    case TU of
        F when is_function(F) ->
            esip:callback(F, [Req, Sock, TrID]);
        {M, F, A} ->
            esip:callback(M, F, [Req, Sock, TrID | A]);
        #sip{type = response} = Resp ->
            Resp;
        _ ->
            TU
    end.

find_transaction_user(#sip{method = Method} = Req, SIPSock) ->
    TrID = make_trid(),
    case esip_dialog:id(uas, Req) of
        #dialog_id{local_tag = Tag} = DialogID when Tag /= <<>> ->
            case esip_dialog:lookup(DialogID) of
                {ok, TU, _Dialog} when Method == <<"OPTIONS">> ->
                    {dialog, TU};
                {ok, TU, #dialog{remote_seq_num = RemoteSeqNum}} ->
                    CSeq = esip:get_hdr('cseq', Req#sip.hdrs),
                    if is_integer(RemoteSeqNum), RemoteSeqNum > CSeq ->
                            cseq_out_of_order;
                       true ->
                            {dialog, TU}
                    end;
                _ ->
                    esip:callback(request, [Req, SIPSock, TrID])
            end;
        _ ->
            esip:callback(request, [Req, SIPSock, TrID])
    end.

send(State, Resp) ->
    case esip_transport:send(State#state.sock, Resp) of
        ok ->
            ok;
        Err ->
            pass_to_transaction_user(State, Err),
            Err
    end.

maybe_send_trying(#sip{method = <<"INVITE">>} = Req) ->
    Trying = esip:make_response(Req, #sip{type = response,
                                          status = 100}),
    p1_fsm:send_event_after(200, {trying, Trying});
maybe_send_trying(_) ->
    ok.

update_remote_seqnum(#sip{status = S}, _State) when S == 100; S >= 300 ->
    ok;
update_remote_seqnum(_Resp, #state{method = <<"OPTIONS">>}) ->
    ok;
update_remote_seqnum(#sip{hdrs = Hdrs} = Resp, _State) ->
    DialogID = esip_dialog:id(uas, Resp),
    CSeq = esip:get_hdr('cseq', Hdrs),
    esip_dialog:update_remote_seqnum(DialogID, CSeq).

make_trid() ->
    make_trid(self()).

make_trid(Pid) ->
    #trid{owner = Pid, type = server}.
