%% 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: AGPL-3.0-or-later
%%
%% This program is free software; you can redistribute it and/or modify
%% it under the terms of the GNU Affero General Public License as
%% published by the Free Software Foundation; either version 3 of the
%% License, or (at your option) any later version.
%%
%% This program is distributed in the hope that it will be useful,
%% but WITHOUT ANY WARRANTY; without even the implied warranty of
%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
%% GNU General Public License for more details.
%%
%% You should have received a copy of the GNU Affero General Public License
%% along with this program.  If not, see <https://www.gnu.org/licenses/>.
%%
%% Additional Permission under GNU AGPL version 3 section 7:
%%
%% If you modify this Program, or any covered work, by linking or
%% combining it with runtime libraries of Erlang/OTP as released by
%% Ericsson on https://www.erlang.org (or a modified version of these
%% libraries), containing parts covered by the terms of the Erlang Public
%% License (https://www.erlang.org/EPLICENSE), the licensors of this
%% Program grant you additional permission to convey the resulting work
%% without the need to license the runtime libraries of Erlang/OTP under
%% the GNU Affero General Public License. Corresponding Source for a
%% non-source form of such a combination shall include the source code
%% for the parts of the runtime libraries of Erlang/OTP used as well as
%% that of the covered work.

-module(s1ap_utils).

-export([encode_pdu/1,
         decode_pdu/1,
         parse_pdu/1,
         parse_plmn_id/1,
         parse_enb_id/1,
         genb_id_str/1]).

-include_lib("kernel/include/logger.hrl").

-include("S1AP-PDU-Descriptions.hrl").
-include("S1AP-PDU-Contents.hrl").
-include("S1AP-Containers.hrl").
-include("S1AP-Constants.hrl").
-include("S1AP-IEs.hrl").


%% S1AP PDU (decoded)
-type s1ap_pdu() :: {initiatingMessage, #'InitiatingMessage'{}} |
                    {successfulOutcome, #'SuccessfulOutcome'{}} |
                    {unsuccessfulOutcome, #'UnsuccessfulOutcome'{}}.

%% 9.2.1.1 Message Type
-type s1ap_msg_type() :: {Proc :: non_neg_integer(),
                          Type :: initiatingMessage | successfulOutcome | unsuccessfulOutcome}.

%% S1AP PDU (decoded, unrolled)
-type s1ap_pdu_info() :: {MsgType :: s1ap_msg_type(),
                          Content :: proplists:proplist()}.

-export_type([s1ap_pdu/0,
              s1ap_msg_type/0,
              s1ap_pdu_info/0]).


-type enb_id() :: 0..16#fffffff.
-type plmn_id() :: {MCC :: nonempty_string(),
                    MNC :: nonempty_string()}.
-type genb_id() :: #{enb_id => enb_id(),
                     plmn_id => plmn_id()}.

-export_type([enb_id/0,
              plmn_id/0,
              genb_id/0]).


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

%% Encode an S1AP PDU
-spec encode_pdu(s1ap_pdu()) -> {ok, binary()} |
                                {error, {asn1, tuple()}}.
encode_pdu(PDU) ->
    'S1AP-PDU-Descriptions':encode('S1AP-PDU', PDU).


%% Decode an S1AP PDU
-spec decode_pdu(binary()) -> {ok, s1ap_pdu()} |
                              {error, {asn1, tuple()}}.
decode_pdu(Data) ->
    'S1AP-PDU-Descriptions':decode('S1AP-PDU', Data).


%% Parse an S1AP PDU
-spec parse_pdu(binary()) -> s1ap_pdu_info() | {error, term()}.
parse_pdu(Data) ->
    try decode_pdu(Data) of
        {ok, PDU} ->
            {MsgType, IEs} = unroll_pdu(PDU),
            {MsgType, parse_ies(IEs)};
        {error, Error} ->
            ?LOG_ERROR("S1AP PDU decoding failed: ~p", [Error]),
            {error, {decode_pdu, Error}}
    catch
        Exception:Reason:StackTrace ->
            ?LOG_ERROR("An exception occurred: ~p, ~p, ~p", [Exception, Reason, StackTrace]),
            {error, decode_pdu}
    end.


-spec genb_id_str(genb_id()) -> string().
genb_id_str(#{plmn_id := {MCC, MNC},
              enb_id := ENBId}) ->
    MCC ++ "-" ++ MNC ++ "-" ++ integer_to_list(ENBId).


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

%% Unroll a decoded S1AP PDU (procedure code, type, IEs)
-spec unroll_pdu(s1ap_pdu()) -> s1ap_pdu_info().
unroll_pdu({Type = initiatingMessage,
            #'InitiatingMessage'{procedureCode = PC,
                                 value = {_, IEs}}}) -> {{PC, Type}, IEs};
unroll_pdu({Type = successfulOutcome,
            #'SuccessfulOutcome'{procedureCode = PC,
                                 value = {_, IEs}}}) -> {{PC, Type}, IEs};
unroll_pdu({Type = unsuccessfulOutcome,
            #'UnsuccessfulOutcome'{procedureCode = PC,
                                   value = {_, IEs}}}) -> {{PC, Type}, IEs}.


%% Parse PLMN-ID as per 3GPP TS 24.008, Figure 10.5.13
%% | MCC digit 2 | MCC digit 1 |  octet 1
%% | MNC digit 3 | MCC digit 3 |  octet 2
%% | MNC digit 2 | MNC digit 1 |  octet 3
-spec parse_plmn_id(<< _:24 >>) -> plmn_id().
parse_plmn_id(<< MCC2:4, MCC1:4,
                 MNC3:4, MCC3:4,
                 MNC2:4, MNC1:4 >>) ->
    MCC = parse_mcc_mnc(MCC1, MCC2, MCC3),
    MNC = parse_mcc_mnc(MNC1, MNC2, MNC3),
    {MCC, MNC}.


-define(UNHEX(H), H + 48).

parse_mcc_mnc(D1, D2, 16#f) ->
    [?UNHEX(D1), ?UNHEX(D2)];

parse_mcc_mnc(D1, D2, D3) ->
    [?UNHEX(D1), ?UNHEX(D2), ?UNHEX(D3)].


-spec parse_enb_id(tuple()) -> enb_id().
parse_enb_id({'macroENB-ID', << ID:20 >>}) -> ID;
parse_enb_id({'homeENB-ID', << ID:28 >>}) -> ID;
parse_enb_id({'short-macroENB-ID', << ID:18 >>}) -> ID;
parse_enb_id({'long-macroENB-ID', << ID:21 >>}) -> ID.


-type s1ap_ie_id() :: non_neg_integer().
-type s1ap_ie_val() :: tuple().

-spec parse_ie(tuple()) -> proplists:property().
-spec parse_ie(s1ap_ie_id(), s1ap_ie_val()) -> term().

parse_ie(#'ProtocolIE-Field'{id = IEI, value = C}) ->
    {IEI, parse_ie(IEI, C)};

parse_ie(#'ProtocolExtensionField'{id = IEI, extensionValue = C}) ->
    {IEI, parse_ie(IEI, C)};

parse_ie(IE) ->
    ?LOG_ERROR("Unknown IE format: ~p", [IE]),
    {unknown, IE}.


%% 9.2.1.37 Global eNB ID
parse_ie(?'id-Global-ENB-ID',
         #'Global-ENB-ID'{'pLMNidentity' = PLMNId,
                          'eNB-ID' = EnbId}) ->
    #{plmn_id => parse_plmn_id(PLMNId),
      enb_id => parse_enb_id(EnbId)};

%% 9.1.8.4 Supported TAs
%% 9.2.3.7 Broadcast TAC
parse_ie(?'id-SupportedTAs', TAs) ->
    %% TODO: Broadcast PLMNs
    [TAC || #'SupportedTAs-Item'{tAC = << TAC:16 >>} <- TAs];

%% For all other IEIs return the contents as-is.
parse_ie(_IEI, C) -> C.


%% Iterate over the given list of S1AP IEs, calling parse_ie/2 for each.
%% The result is a proplist containing parsed IEs.
-spec parse_ies(list()) -> proplists:proplist().
parse_ies(IEs) ->
    lists:map(fun parse_ie/1, IEs).


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