src/t__.erl

%% -*- coding: utf-8 -*-
%% Copyright (c) 2022, Madalin Grigore-Enescu <https://github.com/ergenius> <https://ergenius.com>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
%% copyright notice and this permission notice appear in all copies.
%%
%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
-module(t__).
-author("Madalin Grigore-Enescu").

-include("../include/t__.hrl").

-export([
	application/0,
	language/0,

	config_get/0, config_get/1, config_get_environment/1,
	config_set/1, config_set/2, config_set/3, config_set_cast/2,
	config_delete/0, config_delete/1, config_delete/2, config_delete_cast/1,

	translate/1, translate/2, translate/3, translate/7
]).

-export_type([repository/0, config/0, p/0]).

-type repository() :: #t__repository{}.
-type config() :: #t__config{}.
-type p() :: #t__p{}.

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% application/language
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 

-spec application() -> 't__' | Application when
	Application :: atom().
%% @doc Returns the name of the application to which the calling process belongs.
%% If the specified process does not belong to any application the function returns 't__'.
application() ->
	case application:get_application() of
		{ok, Application} -> Application;
		_ -> t__
	end.

-spec language() -> Language when
	Language :: string().
%% @doc Returns the current language for the calling process.
%% 
%% When you properly setup the language for the process, the expected time complexity for the current implementation 
%% of this macro is O(1) and the worst case time complexity is O(N), where N is the number of items in the process dictionary.
%% 
%% The function will perform the following steps in order to determine the default language:
%% - call erlang:get(t__language)
%% - call application:get_env(t__config)
%% - call application:get_env(t__, t__config)
%% - if no t__config environment key was set for both the current application or the t__ application
%% we return t__ default language.
language() ->
	case erlang:get(t__language) of
		undefined ->
			#t__config{
				language = Language
			} = config_get(),
			Language;
		PDLanguage -> PDLanguage
	end.

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% config
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% 

%%-------------------------------------------------------------
%% config_get
%%-------------------------------------------------------------

-spec config_get() -> Config when
	Config :: t__:config().
%% @doc Returns the current configuration for the calling process.
%% 
%% Please notice that the language config record field may not be the same
%% to the calling process application config because language can also be configured 
%% at the process level. (The process level language overwrites the application
%% configured language).
%%
%% If you want to get the config holding application language, call t__:config/1 function.
%%
%% The function will perform the following steps:
%% - call erlang:get(t__language) to get any calling process specific language that will overwrite 
%% the language returned from the next calls
%% - call application:get_env(t__config)
%% - call application:get_env(t__, t__config)
%% - if no t__config environment key was set for both the current application or the t__ application
%% we return t__ default config
config_get() ->
	PDLanguage = erlang:get(t__language),
	Application = application(),
	Config = config_get(Application),
	%% Process dictionary (may) overwrite application language
	case erlang:is_list(PDLanguage) of
		true -> Config#t__config{language = PDLanguage};
		_ -> Config
	end.

-spec config_get(Application) -> Config when
	Application :: atom(),
	Config :: t__:config().
%% @doc Returns the current configuration for the specified application.
%%
%% The function will perform the following steps:
%% - call application:get_env(t__config)
%% - call application:get_env(t__, t__config)
%% - if no t__config environment key was set for both the current application or the t__ application
%% we return t__ default config
config_get(Application) ->
	case config_get_environment(Application) of
		{ok, Config} -> Config;
		_ ->
			case config_get_environment(t__) of
				{ok, TConfig} -> TConfig;
				_ -> #t__config{}
			end
	end.

%% @doc Returns the value of configuration t__config for Application
config_get_environment(Application) ->
	case application:get_env(Application, t__) of
		{ok, Config = #t__config{}} -> {ok, Config};
		{ok, _InvalidConfig} ->
			?T__LOG(emergency, "The value of configuration t__config for application is invalid!",
				[{application, Application}]),
			erlang:error(invalid_configuration);
		_ -> undefined
	end.

%%-------------------------------------------------------------
%% config_set
%%-------------------------------------------------------------

-spec config_set(Config) -> ok | {error, Error} when
	Config :: t__:config(),
	Error :: term().
%% @doc Sets a new configuration for the calling process application with infinity timeout.
%% @see t__:config_set/3
config_set(Config) -> config_set(application(), Config, infinity).

-spec config_set(Application, Config) -> ok | {error, Error} when
	Application :: atom(),
	Config :: t__:config(),
	Error :: term().
%% @doc Sets a new configuration for the specified application with infinity timeout.
%% @see t__:config_set/3
config_set(Application, Config) -> config_set(Application, Config, infinity).

-spec config_set(Application, Config, Timeout) -> ok | {error, Error} when
	Application :: atom(),
	Config :: t__:config(),
	Timeout :: timeout(),
	Error :: term().
%% @doc Sets a new configuration for the specified application with the specified timeout.
%%
%% There is a blocking gen_server call behind this!
%% That's why there is a config_set_cast version of this function for convenience.
%% However I don't recommend using that because you will not know if the operation failed.
%%
%% Changing applications config is possible but should not be abused.
%% The system was designed for you to be able to change it at the application start or
%% from an administration panel, from time to time.
config_set(Application, Config = #t__config{}, Timeout) -> t__srv:config_set_call(Application, Config, Timeout).

-spec config_set_cast(Application, Config) -> ok | {error, Error} when
	Application :: atom(),
	Config :: t__:config(),
	Error :: term().
%% @doc Sets a new configuration for the specified application asynchronous
%% Sends an asynchronous request to the process handling the implementation 
%% and returns ok immediately.
%%
%% @see t__:config_set/3
config_set_cast(Application, Config = #t__config{}) -> t__srv:config_set_cast(Application, Config).

%%-------------------------------------------------------------
%% config_delete
%%-------------------------------------------------------------

-spec config_delete() -> ok | {error, Error} when
	Error :: term().
%% @doc Deletes configuration for the calling process application with infinity timeout.
%% @see t__:config_delete/2
config_delete() -> config_set(application(), infinity).

-spec config_delete(Application) -> ok | {error, Error} when
	Application :: atom(),
	Error :: term().
%% @doc Deletes configuration for the specified application with infinity timeout.
%% @see t__:config_delete/2
config_delete(Application) -> config_delete(Application, infinity).

-spec config_delete(Application, Timeout) -> ok | {error, Error} when
	Application :: atom(),
	Timeout :: timeout(),
	Error :: term().
%% @doc Deletes configuration for the specified application with the specified timeout.
%%
%% There is a blocking gen_server call behind this!
%% That's why there is a config_delete_cast version of this function for convenience.
%%
%% Deleting applications config is possible but should not be abused.
%% The system was designed for you to be able to change it at the application start or
%% from an administration panel, from time to time.
config_delete(Application, Timeout) -> t__srv:config_delete_call(Application, Timeout).

-spec config_delete_cast(Application) -> ok | {error, Error} when
	Application :: atom(),
	Error :: term().
%% @doc Deletes configuration for the specified application asynchronous
%% Sends an asynchronous request to the process handling the implementation 
%% and returns ok immediately.
%%
%% @see t__:config_delete/2
config_delete_cast(Application) -> t__srv:config_delete_cast(Application).

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% translate
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

-spec translate(Param) -> Msg when
	Param :: term(),
	Msg :: string().
%% @doc Translate a singular or plural term, with or without repository, language and context
%% This is the main translate function that does everything.
% using #t__p
translate(#t__p{
	application = Application,
	repository = Repository,
	language = Language,
	context = Context,
	msg = Msg,
	data = Data,
	reference = Reference
}) ->
	Application1 = case Application of
					   undefined -> erlang:atom_to_list(application());
					   _ -> erlang:atom_to_list(Application)
				   end,
	Repository1 = case Repository of
					  undefined -> "default";
					  _ -> translate_param_cast_term(Repository)
				  end,
	Language1 = case Language of
					undefined -> language();
					_ -> translate_param_cast_term(Language)
				end,
	Context1 = case Context of
				   undefined -> undefined;
				   _ -> translate_param_cast_term(Context)
			   end,
	Reference1 = case Reference of
				   undefined -> undefined;
				   _ -> translate_param_cast_term(Context)
			   end,
	Msg1 = translate_param_cast_msg(Msg),
	true = ((Data =:= undefined) or erlang:is_list(Data)),
	Config = config_get(Application1),
	translate_private(Application1, Config, Repository1, Language1, Context1, Msg1, Data, Reference1);
translate(P) -> translate(P, undefined, undefined).

-spec translate(Param, Data) -> Msg when
	Param :: term(),
	Data :: undefined | list(),
	Msg :: string().
%% @doc Translate with data
%% @see t__:translate/3
translate(P, Data) -> translate(P, Data, undefined).

-spec translate(Param, Data, Reference) -> Msg when
	Param :: term(),
	Data :: undefined | list(),
	Reference :: undefined | string(),
	Msg :: string().
%% @doc Translate with data and reference as separate parameters
% using #t__p
translate(P = #t__p{}, Data, Reference) ->
	translate(P#t__p{data = Data, reference = Reference});
% With proplist
translate(P = [{_K, _V} | _T], Data, Reference) ->
	translate(translate_param_proplists_to_p(P, Data, Reference));
% With repository and term
translate({Repository, {Term}}, Data, Reference) ->
	translate(translate_param_term_to_p(Term, Repository, undefined, Data, Reference));
% With repository, language and term
translate({Repository, Language, {Term}}, Data, Reference) ->
	translate(translate_param_term_to_p(Term, Repository, Language, Data, Reference));
% Without repository and language
translate(Term, Data, Reference) ->
	translate(translate_param_term_to_p(Term, undefined, undefined, Data, Reference)).

-spec translate(Application, Repository, Language, Context, Msg, Data, Reference) -> TranslatedMsg when
	Application :: undefined | atom(),
	Repository :: undefined | string() | atom() | binary(),
	Language :: undefined | string() | atom() | binary(),
	Context :: undefined | string() | atom() | binary(),
	Msg :: string() | atom() | binary(),
	Data :: undefined | list(),
	Reference :: undefined | string(),
	TranslatedMsg :: string().
%% @doc Translate with all separate parameters.
translate(Application, Repository, Language, Context, Msg, Data, Reference) ->
	translate(#t__p{
		application = Application,
		repository = Repository,
		language = Language,
		context = Context,
		msg = Msg,
		data = Data,
		reference = Reference
	}).

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% translate_param_
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

%%-------------------------------------------------------------
%% translate_param_proplists_to_p
%%-------------------------------------------------------------

%% @doc Convert proplists translate parameter to #t__p{}
translate_param_proplists_to_p(P, Data, Reference) ->
	#t__p{
		application = proplists:get_value(application, P, undefined),
		repository = proplists:get_value(repository, P, undefined),
		language = proplists:get_value(language, P, undefined),
		context = proplists:get_value(context, P, undefined),
		msg = proplists:get_value(msg, P, undefined),
		data = proplists:get_value(data, P, Data),
		reference = proplists:get_value(reference, P, Reference)
	}.

%%-------------------------------------------------------------
%% translate_param_term_to_p
%%-------------------------------------------------------------

%% @doc Convert term to #t__p{}
%% Singular terms:
%% ```
%% "I have a joke about Erlang, but it requires a prologue."
%% <<"Erlang is user-friendly, it’s just picky about its friends!">>
%% 'Why can't you trust atoms? Because they make up everything!'
%% '''
%% Plural terms:
%% ```
%% %% ["user", "users"]
%% '''
%% Plural terms with interpolation:
%% ```
%% ["~B user", "~B users"]
%% '''
translate_param_term_to_p(Msg, Repository, Language, Data, Reference) when
	erlang:is_list(Msg); erlang:is_binary(Msg); erlang:is_atom(Msg) -> #t__p{
	repository = Repository,
	language = Language,
	msg = Msg,
	data = Data,
	reference = Reference};
%% Singular with context:
%% ```{"menu", "Save"}'''
%% Plural terms with context:
%% ```{"female", ["File belongs to her", "Files belong to her"]}'''
%% ```{"male", ["File belongs to him", "Files belong to him"]}'''
%% Plural terms with context and interpolation:
%% ```{"female", ["~B file belongs to her", "~B files belong to her"]}'''
%% ```{"male", ["~B file belongs to him", "~B files belong to him"]}'''
translate_param_term_to_p({Context, Msg}, Repository, Language, Data, Reference) ->
	#t__p{
		repository = Repository,
		language = Language,
		context = Context,
		msg = Msg,
		data = Data,
		reference = Reference}.

%%-------------------------------------------------------------
%% translate_param_cast_term
%%-------------------------------------------------------------

%% @doc Convert non-empty string, atom, binary to string
translate_param_cast_term(Term) when is_list(Term), erlang:length(Term) > 0 ->
	true = io_lib:char_list(Term),
	Term;
translate_param_cast_term(Term) when is_binary(Term) ->
	case unicode:characters_to_list(Term) of
		L when is_list(L) ->
			translate_param_cast_term(L); %% Unicode
		_ ->
			translate_param_cast_term(erlang:binary_to_list(Term)) %% bytewise encoded
	end;
translate_param_cast_term(Term) when is_atom(Term) ->
	translate_param_cast_term(erlang:atom_to_list(Term)).

%%-------------------------------------------------------------
%% translate_param_cast_msg
%%-------------------------------------------------------------

%% @doc Convert list of strings, atoms, binaries to list of strings
translate_param_cast_msg([Msg1]) when
	erlang:is_list(Msg1); erlang:is_binary(Msg1); erlang:is_atom(Msg1)
	-> [translate_param_cast_term(Msg1)];
translate_param_cast_msg([Msg1, Msg2]) when
	(erlang:is_list(Msg1) or erlang:is_binary(Msg1) or erlang:is_atom(Msg1));
	(erlang:is_list(Msg2) or erlang:is_binary(Msg2) or erlang:is_atom(Msg2))
	-> [translate_param_cast_term(Msg1), translate_param_cast_term(Msg2)];
translate_param_cast_msg(M) when erlang:is_list(M) -> [M];
translate_param_cast_msg(M) when erlang:is_binary(M) ->
	case unicode:characters_to_list(M) of
		L when is_list(L) -> [L]; %% Unicode
		_ -> [erlang:binary_to_list(M)] %% bytewise encoded
	end;
translate_param_cast_msg(M) when erlang:is_atom(M) -> [erlang:atom_to_list(M)].

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% translate_private
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

%% @doc Private translate function
translate_private(Application, #t__config{dev=Dev}, Repository, Language, Context, Msg, Data, Reference) ->
	EtsTableRepository = t__repository:ets_table(Application, Repository),
	try
		translate_private(Dev, EtsTableRepository, Language, Context, Msg, Data, Reference)
	catch
		Exception1:Reason1 ->
			?T__LOG(error, "Translate exception!",
				[
					{exception, {Exception1, Reason1}},
					{application, Application},
					{repository, Repository},
					{language, Language},
					{context, Context},
					{msg, Msg},
					{data, Data},
					{reference, Reference}
				]),
			translate_private_sp_missing(Msg, Data, Language)
	end.

%% @doc Private translate function for production mode/developer mode
% Production mode
translate_private(false, EtsTable, Language, Context, Msg, Data, _Reference) ->
	Key = {po, Language, Context, Msg},
	case ets:lookup(EtsTable, Key) of
		[{_Key, {_Comments, Value}}] ->
			translate_private_sp(Value, Data, Language);
		_ ->
			translate_private_sp_missing(Msg, Data, Language)
	end;
% developer mode
translate_private(true, EtsTable, Language, Context, Msg, Data, Reference) ->
	PotKey = {pot, Context, Msg},
	case ets:lookup(EtsTable, PotKey) of
		[] -> ets:insert(EtsTable, {PotKey, [Reference]});
		_ -> ok
	end,
	Key = {po, Language, Context, Msg},
	case ets:lookup(EtsTable, Key) of
		[{_Key, {_Comments, Value}}] ->
			translate_private_sp(Value, Data, Language);
		_ ->
			translate_private_sp_missing(Msg, Data, Language)
	end.

%% @doc Detect and translate single or plural terms when the translation is missing
%% Single terms
translate_private_sp_missing([Msg], Data, _Language) ->
	translate_private_format(Msg, Data);
%% Plural terms
translate_private_sp_missing([M1|_T], Data = [N | _], _Language) when N == 1 ->
	translate_private_format(M1, Data);
translate_private_sp_missing([_M1,M2|_T], Data, _Language) ->
	translate_private_format(M2, Data).

%% @doc Detect and translate single or plural terms
%% Single terms
translate_private_sp([Msg], Data, _Language) ->
	translate_private_format(Msg, Data);
%% Plural terms
translate_private_sp(Msg, Data = [N | _], Language) ->
	SelectedMsg = t__plural:select(Language, N, Msg),
	translate_private_format(SelectedMsg, Data).

%% @doc Returns a character list that represents Data formatted in accordance with Format
%% If any exception is raised, returns original Msg unaltered.
translate_private_format(Msg, Data) when erlang:is_list(Data); erlang:length(Data) > 0 ->
	try
		lists:flatten(io_lib:format(Msg, Data))
	catch
		Exception:Reason ->
			?T__LOG(error, "Exception for io_lib:format(Msg, Data)",
				[
					{exception, {Exception, Reason}},
					{msg, Msg},
					{data, Data}
				]),
			Msg
	end;
translate_private_format(Msg, _Data) when erlang:is_list(Msg) -> Msg.