%%
%% %CopyrightBegin%
%% 
%% Copyright Ericsson AB 2002-2024. 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.
%% 
%% %CopyrightEnd%
%%

%%
%%----------------------------------------------------------------------
%% Purpose: Megaco message transformation
%% 
%% Usage:   From a base message file, messages for every codec is 
%%          generated. The file should have the following structure:
%% 
%%          {Codec, Messages}.
%% 
%%          Codec    = pretty | compact | ber | per | erlang
%%          Messages = [{Name, binary()}]
%%          Name     = atom
%%          
%%          The function messages/0 is used by the meas and mstone
%%          tools, but messages/1 can also be used if another base
%%          message file is to be used.
%% 
%%          The messages can also be exported to the old format,
%%          e.g. a directory for each of the codec's and the 
%%          each message as a file in those directories.
%%
%%             Pretty text:  pretty
%%             Compact text: compact
%%             Binary ber:   ber
%%             Binary per:   per
%%             Erlang:       erlang
%%          
%% 
%%           <message package>/pretty/<message-files>
%%                             compact/<message-files>
%%                             per/<message-files>
%%                             ber/<message-files>
%%                             erlang/<message-files>
%%         
%%----------------------------------------------------------------------

-module(megaco_codec_transform).
-moduledoc """
Megaco message transformation utility.

This module implements a simple megaco message transformation utility.

_Note_ that this module is _not_ included in the runtime part of the
application.

[](){: #export_messages }
""".

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

-export([
	 codecs/0, 
	 default_message_package/0, 
	 messages/0, messages/1, 
	 export_messages/0, export_messages/1
	]).

-define(DEFAULT_MESSAGE_PACKAGE, time_test).
-define(ALL_CODECS, [pretty, compact, per, ber, erlang]).
-define(V3, v3).

-doc false.
codecs() ->
    ?ALL_CODECS.

-doc false.
default_message_package() ->
    ?DEFAULT_MESSAGE_PACKAGE.

-doc false.
messages() ->
    messages(?DEFAULT_MESSAGE_PACKAGE).

-doc false.
messages(MessagePackage) when is_atom(MessagePackage) ->
    %% Try the CWD first, and if that does not work try the installation directory
    case load_messages(".", MessagePackage) of
	{error, _Reason} ->
	    AppLibDir = code:lib_dir(megaco),
	    Dir = filename:join([AppLibDir, examples, meas]),
	    load_messages(Dir, MessagePackage);
	Else ->
	    Else
    end.

load_messages(Dir, MessagePackage) ->
    %% io:format("try loading messages from ~s~n", [Dir]),
    Filename = filename:join([Dir, atom_to_list(MessagePackage) ++ ".msgs"]),
    case file:consult(Filename) of
	{ok, [{Codec, Msgs}]} when is_atom(Codec) andalso is_list(Msgs) ->
	    case lists:member(Codec, ?ALL_CODECS) of
		true ->
		    messages(Codec, Msgs);
		false ->
		    {error, {unknown_codec, Codec}}
	    end;

	{ok, [{BadCodec, Msgs}]} when is_list(Msgs) ->
	    {error, {bad_codec, BadCodec}};

	%% No codec specified, try with pretty
	{ok, [Msgs]} when is_list(Msgs) ->
	    messages(pretty, Msgs);
	
	{ok, Crap} ->
	    {error, {bad_messages, Crap}};
	
	Error ->
	    %% exit({Error, Dir, MessagePackage, Filename})
            Error
    end.
    
messages(BaseCodec, Msgs) ->
    OutCodecs = lists:delete(BaseCodec, ?ALL_CODECS),
    transform_messages(BaseCodec, Msgs, OutCodecs).
    

-doc(#{equiv => export_messages/1}).
export_messages() ->
    export_messages(?DEFAULT_MESSAGE_PACKAGE).

-doc """
Export the messages in the `MessagePackage` (default is `time_test`).

The output produced by this function is a directory structure with the following
structure:

```text
<message package>/pretty/<message-files>
                  compact/<message-files>
                  per/<message-files>
                  ber/<message-files>
                  erlang/<message-files>
```
""".
export_messages(MessagePackage) when is_atom(MessagePackage) ->
    case messages(MessagePackage) of
	TMsgs when is_list(TMsgs) ->
	    (catch export_messages(MessagePackage, TMsgs));
	Error ->
	    Error
    end.

export_messages(MessagePackage, TMsgs) ->
    case file:make_dir(MessagePackage) of
	ok ->
	    ok;
	{error, eexist} ->
	    ok;
	Error ->
	    throw(Error)
    end,
    do_export_messages(MessagePackage, TMsgs).

do_export_messages(_MessagePackage, []) ->
    ok;
do_export_messages(MessagePackage, [{Codec, Msgs} | TMsgs]) ->
    ems(MessagePackage, Codec, Msgs),
    do_export_messages(MessagePackage, TMsgs).


%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

transform_messages(BaseCodec, BaseMsgs, OutCodecs) ->
    [{BaseCodec, BaseMsgs} | 
     [{Codec, tms(BaseMsgs, BaseCodec, Codec)} || Codec <- OutCodecs]].
    

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

tms(FromMsgs, FromCodec, ToCodec) ->
    [{Name, tm(FromBin, FromCodec, ToCodec)} || {Name, FromBin} <- FromMsgs].

tm(FromBin, FromCodec, ToCodec) ->
    FromMsg = decode_message(FromCodec, FromBin),
    encode_message(ToCodec, FromMsg).


%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

ems(MessagePackage, Codec, Msgs) ->
    Dir = filename:join([MessagePackage, Codec]),
    case file:make_dir(Dir) of
	ok ->
	    ok;
	{error, eexist} ->
	    ok;
	Error ->
	    throw(Error)
    end,
    Extension = extension_of(Codec),
    F = fun({Name, Bin}) -> em(MessagePackage, Codec, Name, Extension, Bin) end,
    lists:foreach(F, Msgs).
   
em(MessagePackage, Codec, Name, Extension, Bin) ->
    Filename = filename:join([MessagePackage, Codec, atom_to_list(Name) ++ Extension]),
    case file:open(Filename, [raw, binary, write]) of
        {ok, Fd} ->
            case file:write(Fd, Bin) of
                ok ->
                    file:close(Fd),
                    ok;
                {error, Reason} ->
                    S = format("failed writing ~w message ~w (~p bytes): ~p",
                               [Codec, Name, byte_size(Bin), Reason]),
                    file:close(Fd),
                    throw({error, S})
            end;

        {error, Reason} ->
            S = format("failed open file ~s: ~p", [Filename, Reason]),
            throw({error, S})
    end.


%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

decode_message(pretty, BinMsg) ->
    Mod  = megaco_pretty_text_encoder,
    Conf = [{version3,?V3}],
    do_decode(Mod, Conf, BinMsg);
decode_message(compact, BinMsg) ->
    Mod  = megaco_compact_text_encoder,
    Conf = [{version3,?V3}],
    do_decode(Mod, Conf, BinMsg);
decode_message(ber, BinMsg) ->
    Mod  = megaco_ber_encoder,
    Conf = [{version3,?V3}],
    do_decode(Mod, Conf, BinMsg);
decode_message(per, BinMsg) ->
    Mod  = megaco_per_encoder,
    Conf = [{version3,?V3}],
    do_decode(Mod, Conf, BinMsg);
decode_message(erlang, BinMsg) ->
    Mod  = megaco_erl_dist_encoder,
    Conf = [{version3,?V3}],
    do_decode(Mod, Conf, BinMsg).


do_decode(Mod, Conf, Bin) ->
    case (catch Mod:decode_message(Conf, Bin)) of
	{ok, Msg} ->
	    Msg;
	 {error, Reason} ->
	    S = format("decode error: ~p", [Reason]),
	    throw({error, S});
	{'EXIT', Reason} ->
	    S = format("decode exit: ~p", [Reason]),
	    throw({error, S});
	Other ->
	    S = format("unknwon decode result: ~p", [Other]),
	    throw({error, S})
    end.


%% encode_message
%% Note: See note above (decode_message)

encode_message(pretty, Msg) ->
    Mod  = megaco_pretty_text_encoder,
    Conf = [{version3,?V3}],
    do_encode(Mod, Conf, Msg);
encode_message(compact, Msg) ->
    Mod  = megaco_compact_text_encoder,
    Conf = [{version3,?V3}],
    do_encode(Mod, Conf, Msg);
encode_message(ber, Msg) ->
    Mod  = megaco_ber_encoder,
    Conf = [{version3,?V3}],
    do_encode(Mod, Conf, Msg);
encode_message(per, Msg) ->
    Mod  = megaco_per_encoder,
    Conf = [{version3,?V3}],
    do_encode(Mod, Conf, Msg);
encode_message(erlang, Msg) ->
    Mod  = megaco_erl_dist_encoder,
    Conf = [{version3,?V3}],
    do_encode(Mod, Conf, Msg).


do_encode(Mod, Conf, Msg) ->
    case (catch Mod:encode_message(Conf, Msg)) of
	{ok, Bin} ->
	    Bin;
	 {error, Reason} ->
	    S = format("encode error: ~p", [Reason]),
	    throw({error, S});
	{'EXIT', Reason} ->
	    S = format("encode exit: ~p", [Reason]),
	    throw({error, S});
	Other ->
	    S = format("unknwon encode result: ~p", [Other]),
	    throw({error, S})
    end.


%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

extension_of(pretty) ->
    ".txt";
extension_of(compact) ->
    ".txt";
extension_of(ber) ->
    ".bin";
extension_of(per) ->
    ".bin";
extension_of(erlang) ->
    ".bin".

%% d(F) ->
%%     d(F, []).
%% d(F, A) ->
%%     io:format(F ++ "~n", A).

format(F, A) ->    
    lists:flatten(io_lib:format(F, A)).
