src/iso8583_process.erl

%%% @doc this module is responsible for processing iso8583 messages
%% @author Nuku Ameyibor
-module(iso8583_process).

-export([unpack/2,pack/2,set_field/3,set_field_list/1,set_mti/2,get_field/2,process_data_element/4,create_bitmap/2,
		get_bitmap_subs/3,get_size_send/2,load_specification/1,get_spec_field/2,get_bitmap_type/1,
		convert_base_pad/3,load_specification_mti/1,get_spec_mti/3,check_mandatory_fields/2,
		add_echo_fields/3,validate_specification/1
		]).


%% @doc this is for performing a binary fold kind of like a list fold
-spec fold_bin(Fun, T, Bin) -> T when
	Fun  		:: fun((Input, T) -> {Reminder, T}),
	Bin  		:: binary(),
	Input		:: binary(),
	Reminder	:: binary().
%% base case first
fold_bin(_Fun, Accum, <<>>) -> Accum;
fold_bin(Fun, Accum, Bin) ->
		{NewBin, NewAccum} = Fun(Bin, Accum),
		fold_bin(Fun, NewAccum, NewBin).

	  	
%% @doc this converts data between bases and also pads the data  for our purposes
-spec convert_base_pad(integer(),integer(),binary())->binary().
convert_base_pad(Data_Base_10,Number_pad,Pad_digit)->
        Data_base2 = erlang:integer_to_binary(Data_Base_10,2),
        pad_data_new(Data_base2,Number_pad,Pad_digit,right).


%% @doc creats a new map specification which contains the various data elements and a bitmap from a specification file
%% can throw an error if there are bugs in the specification
-spec load_specification(string() |binary())->map() .
load_specification(Filename)->
		{ok,Spec_data} = file:consult(Filename),
		 Map_spec = maps:new(),
		 lists:foldl(
		 fun({Key,Value},Acc)->
			case Key of 
				bitmap_type->
					case Value of
						hex ->
							maps:put(bitmap_type,Value,Acc);
						binary ->
							maps:put(bitmap_type,Value,Acc);
						Any->
							throw({<<"Bitmap type value can only be hex or binary and not this value">>,Any})						
					end;
				Number when Number >=1,Number =<128,is_integer(Number) ->
					#{pad_info := Pad_info,header_length := Header_length,length_field := Length_field,sub_format:=Format} = Value,
					Fl_vl = fixed_variable(Header_length),
					case validate_specification(Value) of
						true ->
							maps:put(Number,{Length_field,Fl_vl,Header_length,Format,Pad_info},Acc);
						false ->
						    io:format("**rules for field specifications**~n~p~n~p~n~p~n~p~n**end**~nfind error below and check error field in specification file~n",
						              [
						              "variable length fields(header length greater 0) must have pad_info = {none,none},must have  header_length > 0 and length_field > 0",
						              "fixed length fields(header length equal to 0) can have pad_info = {none,none},must have  header_length >= 0 and length_field > 0",
						              "fixed length fields(header length equal to 0) can have pad_info = {left,Binary} with 1 char Binary,must have  header_length >= 0 and length_field > 0",
						              "fixed length fields(header length equal to 0) can have pad_info = {right,Binary} with 1 char Binary,must have header_length >=0 and length_field > 0"
						              ]),
							throw({<<"Check specification file for correct field configuration for this field with this value">>,{Number,Value}})
					end;
				_ ->
					Acc
			end
		end,Map_spec,Spec_data).


%% @doc this is for loading field information for mtis
-spec load_specification_mti(string() |binary())->map().
load_specification_mti(Filename)->
		{ok,Spec_data} = file:consult(Filename),
		Map_spec = maps:new(),
		lists:foldl(
		 fun({Key,Value},Acc)->
			case Key of 
				Key_value when is_binary(Key_value),size(Key_value)=:=4 ->
					Key_list = maps:keys(Value),	
					Result =  lists:map(fun(Key_mti)->lists:member(Key_mti,Key_list) end,[mand,opti,echo,condi]),
					case  lists:member(false,Result) of
						true ->
							throw(<<"field can only be madatory(mand),optional(opti),echoed(echo),conditional(condi)">>);
						 false ->
								maps:put(Key_value,Value,Acc)
					end;	
				_ ->
					Acc
			end
		end,Map_spec,Spec_data).		 


%% @doc this is for validating a specification
-spec validate_specification(map())-> boolean(). 
validate_specification(Spec_info)->
		#{pad_info := Pad_info,header_length := Header_length,length_field := Length_field,sub_format:=_Format} = Spec_info,	
		%%io:format("~nValue is ~p,check header",[Spec_info]),
		{Padleft,Padright} = Pad_info,
		%%Pacharlength = size(Padright),
		Fl_vl = fixed_variable(Header_length),
		case  {Fl_vl,{Padleft,Padright},(is_integer(Header_length) andalso Header_length >=0),(is_integer(Length_field) andalso Length_field > 0)} of
			{vl,{none,none},true,true}->true;%%variable length fields with no padding,integer header,integer length of field
			{fx,{none,none},true,true} -> true;%%fixed length fields with no padding,integer header length,integer length of field
			{fx,{left,Padright},true,true} when is_binary(Padright)  -> true;%%fixed length fields with left padding,integer header,integer length of field
			{fx,{right,Padright},true,true} when is_binary(Padright) -> true;%%fixed length fields with right padding,integer header,integer length of field
			_ -> false %%anything else means an error with the spec for that field
		end.


%% @doc for checking mandatory fields
-spec check_mandatory_fields(list(),map())->true|false.
check_mandatory_fields(List_mandatory_keys,Iso_map)->
		Map_fields = maps:keys(Iso_map),
		Result =  lists:map(fun(Field_mand)->lists:member(Field_mand,Map_fields) end,List_mandatory_keys),
		case lists:member(false,Result) of
			true ->
				false;
			false ->
				true
		end.
		

%% @doc for adding fields which are supposed to be echoed
-spec add_echo_fields(map(),map(),map())->map().
add_echo_fields(Map_transaction,Map_recipient,Specification_mti)->
		{ok,Mti}  = get_field(mti,Map_recipient),
		List_echo_fields = get_spec_mti(echo,Mti,Specification_mti),
		lists:foldl(
		fun(Field,Acc)->
			case maps:get(Field,Map_transaction,error) of 
				error ->
					Acc;
				Field_value ->
					maps:put(Field,Field_value, Acc)
			end
		end,Map_recipient,List_echo_fields).
	
	

%% @doc for getting various specification types
-spec get_spec_mti(atom(),map(),map())->list()|error.
get_spec_mti(Spec_type,Mti,Spec_field_map)->		
		Spec_mti = maps:get(Mti,Spec_field_map),
		maps:get(Spec_type,Spec_mti).


%% @doc finds out if a field is fixed length or variable lengt
-spec fixed_variable(non_neg_integer())->fx|vl.
fixed_variable(Number) when Number =:= 0,is_integer(Number)->fx;
fixed_variable(Number) when Number > 0,is_integer(Number)->vl.


%% @doc gets the specification for a particular field
-spec get_spec_field(non_neg_integer()|mti,map())->tuple().
get_spec_field(Field,Specification)->
		maps:get(Field,Specification).


%% @doc gets the bitmap type
-spec get_bitmap_type(map())->atom().
get_bitmap_type(Specification)->
		maps:get(bitmap_type,Specification).


%% @doc this part accepts a list iso message with the header removed and extracts the mti,bitmap,data elements into a map object 
%% it also accepts a specification which will be used for getting the specifications for the message
%% exceptions can be thrown here if the string for the message hasnt been formatted well but they should be caught in whichever code is calling the system 
%%the data is first converted into a binary before the processing is done . much faster and uses less memory than using lists
-spec unpack(list(),map())->map().
unpack(Rest,Specification)-> 
		Bin_message = erlang:list_to_binary(Rest),
		process_binary(Bin_message,Specification).


%% @doc this function is used to derive various fields given an iso message and the message area(iso 1987,1992,2002,postillion,ascii subfield etc .. works with ascii )
%% can be used for getting various iso message fields as well as getting subfields out of an iso message
%%all data needed to calculate bitmamp should be part of input to this function 
-spec process_binary(binary(),map())->map().
process_binary(Bin_message,Specification)->
		Bitmap_type = get_bitmap_type(Specification),
		{Mti,Bit_mess,Bitmap_Segment,Rest} = get_bitmap_subs(Bitmap_type,Bin_message,Specification),
		<<_:1/binary,Real_bitmap/binary>> = Bit_mess,
		Mti_map = maps:put(mti,Mti,maps:new()),
		Map_Init = maps:put(bit,Bitmap_Segment,Mti_map),
		Result_process = process_data_element(Real_bitmap,2,Rest,Specification),
		maps:merge(Result_process,Map_Init).


%% @doc for processing the message given the bitmap and the binary containing the data elements
-spec process_data_element(binary(),integer(),binary(),map())->map().
process_data_element(Bitmap,Index_start,Data_binary,Specification)->
		Map_Init = maps:new(),
		OutData = fold_bin(
			 fun(<<X:1/binary, Rest_bin/binary>>, {Data_for_use_in,Index_start_in,Current_index_in,Map_out_list_in}) when X =:= <<"1">> ->
					{Flength,Fx_var_fixed,Fx_header_length,_,_} = get_spec_field(Current_index_in,Specification),
					Data_index = case Fx_var_fixed of
						fx -> 
							Data_element_fx_raw = binary:part(Data_for_use_in,Index_start_in,Flength),
							New_Index_vl = Index_start_in+Flength,
							{Data_element_fx_raw,New_Index_vl};
						vl ->
							Header = binary:part(Data_for_use_in,Index_start_in,Fx_header_length),
							Header_value = erlang:binary_to_integer(Header),
							Start_val = Index_start_in + Fx_header_length,
							Data_element_vl_raw = binary:part(Data_for_use_in,Start_val,Header_value),
							New_Index_vl = Start_val+Header_value,
							{Data_element_vl_raw,New_Index_vl}
								end, 
					{Data_element,New_Index} = Data_index,
					NewMap = maps:put(Current_index_in,Data_element,Map_out_list_in),
					Fld_num_out = Current_index_in + 1, 
					{Rest_bin,{Data_for_use_in,New_Index,Fld_num_out,NewMap}};
			    (<<X:1/binary, Rest_bin/binary>>, {Data_for_use_in,Index_start_in,Current_index_in,Map_out_list_in}) when X =:= <<"0">> ->
					Fld_num_out = Current_index_in + 1,					
					{Rest_bin,{Data_for_use_in,Index_start_in,Fld_num_out,Map_out_list_in}}
			end, {Data_binary,0,Index_start,Map_Init},Bitmap),
		{_,_,_,Fldata} = OutData,
		Fldata.


%% @TODO Check for mandatory fields which have to be included in every message.
%% @doc marshalls a message to be sent.
%%pack all the different elements in a message into an iolist
-spec pack(Message_Map::map(),map())->iolist().
pack(Message_Map,Specification)->
		Pred = fun(Key,_) -> erlang:is_integer(Key) andalso (Key >= 65)  andalso (Key =< 128)  end,
		Secondary_bitmap_flag = maps:filter(Pred,Message_Map),
		case erlang:map_size(Secondary_bitmap_flag) of
			0->
				pack_message(primary,Message_Map,Specification);
			_ ->
				pack_message(secondary,Message_Map,Specification)
		end.


%% @doc creates a primary/secondary bitmap out of message map and specification
-spec pack_message(primary|secondary,map(),map())->iolist().
pack_message(primary,Message_Map,Specification)-> 
		{Bitmap_final,Iso_Fields_Binary} = lists:foldl((pack_check_keys(Message_Map,Specification)) ,{<<>>,[]},lists:seq(2,64)),
		Bitmap_final_bit = << 0,Bitmap_final/binary>>,
		Bitmap_final_bit_list = create_bitmap(get_bitmap_type(Specification),Bitmap_final_bit),
		{ok,Mti} = format_data(1,maps:get(mti,Message_Map),Specification),
		Fields_list = lists:reverse(Iso_Fields_Binary),
		[Mti,Bitmap_final_bit_list,Fields_list];


pack_message(secondary,Message_Map,Specification)-> 
		{Bitmap_final,Iso_Fields_Binary} = lists:foldl((pack_check_keys(Message_Map,Specification)) ,{<<>>,[]},lists:seq(2,128)),
		Bitmap_final_bit = << 1,Bitmap_final/binary>>,
		Bitmap_final_bit_list = create_bitmap(get_bitmap_type(Specification),Bitmap_final_bit),
		{ok,Mti} = format_data(1,maps:get(mti,Message_Map),Specification),
		Fields_list = lists:reverse(Iso_Fields_Binary),
		[Mti,Bitmap_final_bit_list,Fields_list].


%% @doc used for setting the bitmap fields for each field based on whether the key exists or not
%%returns anonymous  function which is used for setting up the bitmap
-spec pack_check_keys(maps:iterator()|map(),maps:iterator()|map())->fun().
pack_check_keys(Message_Map,Specification)->
		fun(Field_key,{Bitmap,Iso_Fields})->
			case maps:get(Field_key,Message_Map,error) of
				error ->
					New_Bitmap = << Bitmap/binary,0 >>,
					{New_Bitmap,Iso_Fields};
				Value ->
					New_Bitmap = << Bitmap/binary,1>>,
					{ok,Actual_value} = format_data(Field_key,Value,Specification),
					New_Iso_Fields = [Actual_value|Iso_Fields],
					{New_Bitmap,New_Iso_Fields}
			end
		end.


%% @doc for getting the bitmap,mti,Data fields 
-spec get_bitmap_subs(atom(),binary(),map())-> tuple().
get_bitmap_subs(binary,Bin_message,Specification)->
		{Flength,_,_,_,_} = get_spec_field(1,Specification),
		<<One_dig/integer>> = binary_part(Bin_message,Flength,1),
		Bitsize = 
		case  binary_part(convert_base_pad(One_dig,8,<<"0">>),0,1) of
			<<"0">> -> 8;
			<<"1">> -> 16
	    end,		
		<<Mti:Flength/binary,Bitmap_Segment:Bitsize/binary,Rest/binary>> = Bin_message,
		Bit_mess = << << (convert_base_pad(One,8,<<"0">>))/binary >>  || <<One>> <= Bitmap_Segment >>,
		{Mti,Bit_mess,Bitmap_Segment,Rest};


get_bitmap_subs(hex,Bin_message,Specification)->
		{Flength,_,_,_,_} = get_spec_field(1,Specification),
		One_dig = binary_part(Bin_message,Flength,1),
		Size_base_ten = erlang:binary_to_integer(One_dig,16),
		Bitsize = 
		case  Size_base_ten =< 7 of
			true -> 16;
			false -> 32
		end,
		<<Mti:Flength/binary,Bitmap_Segment:Bitsize/binary,Rest/binary>> = Bin_message,
		Bit_mess = 
		fold_bin(
			 fun(<<X:2/binary, Rest_bin/binary>>,Bin_list_final) ->
				 Base_10  = erlang:binary_to_integer(X,16),
				 Converted_base  = << (convert_base_pad(Base_10,8,<<"0">>))/binary >>,
			    List_oct =  << Bin_list_final/binary,Converted_base/binary  >>,
			    {Rest_bin,List_oct}
		     end,<<>>,Bitmap_Segment),
		{Mti,Bit_mess,Bitmap_Segment,Rest}.



%% @doc for creating the final bitmap
%%this bitmap is an 8/16 byte binary with each byte being represented  by an integer.
%%integer converted to a 2 bit binary represents presence or absence of those fields
-spec create_bitmap(binary|hex,binary())->binary()|list().
create_bitmap(binary,Bitmap_final_bit)->
		fold_bin(
			 fun(<<X:8/binary, Rest_bin/binary>>,Bin_list_final) ->
				List_bin = erlang:binary_to_list(X),
				List_string = lists:foldr(fun(X_fold,Acc)-> C = erlang:integer_to_list(X_fold),[C|Acc]end,[],List_bin),
				Lists_string_app = lists:append(List_string),
				Bitmap_oct = erlang:list_to_integer(Lists_string_app,2),
			    List_oct =  << Bin_list_final/binary,Bitmap_oct/integer  >>,
			    {Rest_bin,List_oct}
		     end,<<>>,Bitmap_final_bit);


%%this is for creating a hexadecimal bitmap  
create_bitmap(hex,Bitmap_final_bit)->
		Bitmap_hex = 
		fold_bin(
			fun(<<X:8/binary, Rest_bin/binary>>,Accum_list) ->
				First_conv = erlang:binary_to_list(X),
				Concat_First_conv  = lists:concat(First_conv),
				Concat_First_conv_base = erlang:list_to_integer(Concat_First_conv,2),				
				List_part = string:right(erlang:integer_to_list(Concat_First_conv_base,16),2,$0),
				{Rest_bin, [List_part | Accum_list]}
			end,[],Bitmap_final_bit),
		lists:append(lists:reverse(Bitmap_hex)).



%% @doc this will be used for formatting the data which is sent 
%%it is done at the setting stage
%%it checks if data is of the correct length and type for numbers and simple strings and binaries
%%%also adds paddings and as well as headers to the various values
%%not full featured but just enough to make it work
-spec format_data(integer()|mti,term(),map())->{ok,term()}.
format_data(Key,Value,Specification)->
		{Flength,Fx_var_fixed,Fx_header_length,Sub_format,{Pad_info,Pad_char}}  = get_spec_field(Key,Specification),
		pad_data_check(Fx_var_fixed,Fx_header_length,Flength,Value,Pad_char,Sub_format,Pad_info).



%% @doc for changing input binary to string based on the data type 
-spec change_binary_string(binary(),term())->binary()|string().
change_binary_string(Input_binary,Data_type)->
	case Data_type of
		"B"->
			Input_binary;
		_ ->
			erlang:binary_to_list(Input_binary)
	end.

%%for padding various fields based on whether its a variable length field or a fixed length field
-spec pad_data_check(fx|vl,integer(),integer(),binary()|list(),char()|atom()|binary(),term(),atom())->{ok,list()}|{ok,binary()}.
pad_data_check(Fx_var_fixed,Fx_header_length,_Flength,Numb_check,_Pad_char,Sub_format,none)->
		case Fx_var_fixed of 
			fx->
				{ok,change_binary_string(Numb_check,Sub_format)};
			vl->
				Size =  erlang:size(Numb_check),
				Fsize = string:right(erlang:integer_to_list(Size),Fx_header_length,$0),							
				Final_list = [Fsize,change_binary_string(Numb_check,Sub_format)],
				{ok,Final_list}
		end;


pad_data_check(Fx_var_fixed,Fx_header_length,Flength,Numb_check,Char_pad,Sub_format,Pad_direction)->
		case Fx_var_fixed of 
			fx->
				Padded_data = pad_data_new(Numb_check,Flength,Char_pad,Pad_direction),
				{ok,change_binary_string(Padded_data,Sub_format)};
			vl->
				Size =  erlang:size(Numb_check),
				Fsize = string:right(erlang:integer_to_list(Size),Fx_header_length,$0),							
				Final_list = [Fsize,change_binary_string(Numb_check,Sub_format)],
				{ok,Final_list}
		end.


%%for padding a binary up to a certain length with one string character or binary character
-spec pad_data_new(binary(),non_neg_integer(),binary(),left|right)->binary().
pad_data_new(Numb_check,Flength,Binary_char_pad,Pad_direction)->
		Pad_size = Flength-size(Numb_check),
		Pad_info = binary:copy(Binary_char_pad,Pad_size),
		case Pad_direction of
			right ->
				<< Pad_info/binary,Numb_check/binary >>;
			left ->
				<< Numb_check/binary,Pad_info/binary >>
		end.


%%this is for setting the mti of a message
-spec set_mti(Iso_Map::map(),Fld_val::binary())->{ok,map()}.
set_mti(Iso_Map,Fld_val)->
		{ok,maps:put(mti,Fld_val,Iso_Map)}.


%% @doc this is for setting a particular field in the message or an mti
%% field will have to be validated and then after field is validated an entry is created as a map for it 
%%padding may be added to the field depending on the type of field as well as if its fixed or vlength
-spec set_field(Iso_Map::map(),Fld_num::pos_integer()|mti ,Fld_val::binary())->{ok,map()}.
set_field(Iso_Map,Fld_num,Fld_val)->
		case Fld_num of 
			mti ->
				{ok,maps:put(mti,Fld_val,Iso_Map)};
			_ ->					
				{ok,maps:put(Fld_num,Fld_val,Iso_Map)}
		end.


%%this is for accepting a list containing the various fields and then creating an creating an output map 
%%which can be fed into the pack function
-spec set_field_list(List::list())->map().
set_field_list(List)->
		First_map = maps:new(),
		lists:foldl(
		fun({Key,Value},Acc)->
			{ok,Map_new_Accum} = set_field(Acc,Key,Value),
			Map_new_Accum
		end,First_map,List).


%% @doc this is for getting a particular field or mti or bitmap in an iso message back
-spec get_field(Fld_num::pos_integer()|mti|bit,Iso_Map::map())->{ok,term()}|error.
get_field(Fld_num,Iso_Map)->
		Val_field = maps:get(Fld_num,Iso_Map,error),
		case Val_field of
			error ->
				error;
			_ ->
				{ok,Val_field}
		end.


%%for getting the final size of the message to be sent 
%%-spec get_size_send(binary(),binary()|list(),list())->non_neg_integer().
-spec get_size_send(iolist(),non_neg_integer())->list(). 
get_size_send(Fields_iolist,Length_max_size)->
	Final_length = erlang:iolist_size(Fields_iolist),
	string:right(erlang:integer_to_list(Final_length),Length_max_size,$0).