%% Copyright (C) 2025 by sysmocom - s.f.m.c. GmbH <info@sysmocom.de>
%% Author: Vadim Yanitskiy <vyanitskiy@sysmocom.de>
%%
%% All Rights Reserved
%%
%% SPDX-License-Identifier: MPL-2.0
%%
%% This Source Code Form is subject to the terms of the Mozilla Public
%% License, v. 2.0.  If a copy of the MPL was not distributed with this
%% file, You can obtain one at http://mozilla.org/MPL/2.0/.

-module(logger_gsmtap).

-behaviour(gen_server).

%% public API
-export([start/3,
         stop/1,
         log/2]).
%% gen_server callbacks
-export([init/1,
         handle_call/3,
         handle_cast/2,
         handle_info/2,
         terminate/2]).

-define(GSMTAP_PORT, 4729).
-define(GSMTAP_VERSION, 16#02).
-define(GSMTAP_HDR_LEN, 16#04). %% in number of 32bit words
-define(GSMTAP_TYPE_OSMOCORE_LOG, 16#10).


%% ------------------------------------------------------------------
%% public API
%% ------------------------------------------------------------------

-spec start(LAddr, RAddr, AppName) -> gen_server:start_ret()
    when LAddr :: inet:ip_address(),
         RAddr :: inet:ip_address(),
         AppName :: string().
start(LAddr, RAddr, AppName) ->
    gen_server:start(?MODULE, [LAddr, RAddr, AppName], []).


-spec log(pid(), logger:log_event()) -> ok.
log(Pid, LogEvent) ->
    gen_server:cast(Pid, {?FUNCTION_NAME, LogEvent}).


-spec stop(pid()) -> ok.
stop(Pid) ->
    gen_server:stop(Pid).


%% ------------------------------------------------------------------
%% gen_server API
%% ------------------------------------------------------------------

init([LAddr, RAddr, AppName]) ->
    {ok, #{sock_source => create_source(LAddr, RAddr),
           sock_sink => create_sink(RAddr),
           app_name => AppName}}.


handle_call(_Request, _From, S) ->
    {reply, {error, not_implemented}, S}.


handle_cast({log, LogEvent},
            #{sock_source := Sock,
              app_name := AppName} = S) ->
    PDU = gsmtap_pdu(LogEvent#{app_name => AppName}),
    gen_udp:send(Sock, PDU),
    {noreply, S};

handle_cast(_Request, S) ->
    {noreply, S}.


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


terminate(_Reason, S) ->
    close(maps:get(sock_source, S)),
    close(maps:get(sock_sink, S)),
    ok.


%% ------------------------------------------------------------------
%% private API
%% ------------------------------------------------------------------

-spec create_source(LAddr, RAddr) -> gen_udp:socket()
    when LAddr :: inet:ip_address(),
         RAddr :: inet:ip_address().
create_source(LAddr, RAddr) ->
    {ok, Sock} = gen_udp:open(0, [binary,
                                  {ip, LAddr}, %% bind addr
                                  {reuseaddr, true},
                                  {active, false}]),
    ok = gen_udp:connect(Sock, RAddr, ?GSMTAP_PORT),
    Sock.


-spec create_sink(inet:ip_address()) -> gen_udp:socket() | nope.
create_sink(Addr) ->
    %% it's not critical if gen_udp:open/2 fails
    case gen_udp:open(?GSMTAP_PORT, [binary,
                                     {ip, Addr}, %% bind addr
                                     {reuseaddr, true},
                                     {active, false}]) of
        {ok, Sock} -> Sock;
        _ -> nope
    end.


-spec close(nope | gen_udp:socket()) -> ok.
close(nope) ->
    ok;

close(Sock) ->
    gen_udp:close(Sock).


-spec gsmtap_pdu(map()) -> binary().
gsmtap_pdu(#{msg := Msg,
             level := Level,
             app_name := AppName,
             meta := #{pid := Pid,
                       time := Time} = Meta}) ->
    << ?GSMTAP_VERSION,
       ?GSMTAP_HDR_LEN,
       ?GSMTAP_TYPE_OSMOCORE_LOG,
       16#00:(128 - 3 * 8), %% padding
       (Time div 1_000_000):32, %% seconds
       (Time rem 1_000_000):32, %% microseconds
       (charbuf(AppName, 16))/bytes, %% process name
       16#00:32, %% dummy, Pid goes to subsys
       (log_level(Level)),
       16#00:24, %% padding
       (charbuf(pid_to_list(Pid), 16))/bytes, %% subsys
       (charbuf(filename(Meta), 32))/bytes, %% filename
       (maps:get(line, Meta, 0)):32, %% line number
       (list_to_binary(msg2str(Msg)))/bytes %% the message
    >>.


-type log_event_msg() :: {io:format(), [term()]} |
                         {report, logger:report()} |
                         {string, unicode:chardata()}.
-spec msg2str(log_event_msg()) -> string().
msg2str({string, Str}) ->
    Str;

msg2str({report, Report}) ->
    %% TODO: use report_cb() here
    io_lib:format("~p", [Report]);

msg2str({FmtStr, Args}) ->
    io_lib:format(FmtStr, Args).


filename(#{file := FileName}) ->
    filename:basename(FileName);

filename(#{}) -> "(none)".


-spec charbuf(Str0, Size) -> binary()
    when Str0 :: string(),
         Size :: non_neg_integer().
charbuf(Str0, Size) ->
    Str1 = string:slice(Str0, 0, Size - 1), %% truncate, if needed
    Str2 = string:pad(Str1, Size, trailing, 16#00), %% pad, if needed
    list_to_binary(Str2).


-spec log_level(atom()) -> 0..255.
log_level(debug)     -> 1;
log_level(info)      -> 3;
log_level(notice)    -> 5;
log_level(warning)   -> 6; %% XXX: non-standard
log_level(error)     -> 7;
log_level(critical)  -> 8;
log_level(alert)     -> 9; %% XXX: non-standard
log_level(emergency) -> 11; %% XXX: non-standard
log_level(_)         -> 255. %% XXX: non-standard


%% vim:set ts=4 sw=4 et:
