%%
%% %CopyrightBegin%
%% 
%% Copyright Ericsson AB 2000-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: Parse and evaluate digit maps
%%----------------------------------------------------------------------
%%
%% digitMap           =  digitString
%%                    /   LWSP "(" LWSP digitStringList LWSP ")" LWSP 
%% digitStringList    = digitString *( LWSP "|" LWSP digitString ) 
%% digitString        = 1*(digitStringElement) 
%% digitStringElement = digitPosition [DOT] 
%% digitPosition      = digitMapLetter / digitMapRange
%% digitMapRange      = ("x" / LWSP "[" LWSP digitLetter LWSP "]" LWSP)
%% digitLetter        = *((DIGIT "-" DIGIT ) / digitMapLetter)
%% digitMapLetter     = DIGIT   ; Basic event symbols 
%%                    / %x41-4B ; a-k
%%                    / %x61-6B ; A-K 
%%                    / "T"     ; Start inter-event timers
%%                    / "S"     ; Short inter-event timers
%%                    / "L"     ; Long  inter-event timers, e.g. 16 sec
%%                    / "Z"     ; Long duration modifier
%% DIGIT              = %x30-39 ; 0-9 
%%                   
%%----------------------------------------------------------------------
%% Example of a digit map:
%% 
%% (0| 00|[1-7]xxx|8xxxxxxx|Fxxxxxxx|Exx|91xxxxxxxxxx|9011x.) 
%% 
%% DM = "(0| 00|[1-7]xxx|8xxxxxxx|Fxxxxxxx|Exx|91xxxxxxxxxx|9011x.)".
%% DM = "xxx | xxL3 | xxS4".
%% megaco:parse_digit_map(DM).
%% megaco:test_digit_event(DM, "1234").
%% megaco:test_digit_event(DM, "12ssss3").
%% megaco:test_digit_event(DM, "12ssss4").
%% megaco:test_digit_event(DM, "12ssss5").
%% 
%%----------------------------------------------------------------------

-module(megaco_digit_map).
-moduledoc """
Digit Map utility module.

This is a Digit Map utility module (types).

## Version note

This module has existed in the megaco app for long time,
but as of 27.0, its also documented.
""".
-moduledoc(#{since => "OTP 27.0"}).

-export([parse/1, eval/1, eval/2, report/2, test/2]). % Public
-export([test_eval/2]).                               % Internal

-export_type([
              value/0,
              kind/0,
              event/0,
              letter/0,
              pause/0,
              one_second/0,
              ten_seconds/0,
              cancel/0
             ]).

-include_lib("megaco/src/app/megaco_internal.hrl").
-include("megaco_message_internal.hrl").
-include_lib("megaco/src/text/megaco_text_tokens.hrl").

-type value()           :: #'DigitMapValue'{}.
-type kind()            :: full | unambiguous.
-type event()           :: letter() | pause() | cancel().
-doc "`$0..$9 | $a..$k | $A..$K`".
-type letter()          :: $0 .. $9 | $a .. $k | $A .. $K.
-type pause()           :: one_second() | ten_seconds().
-doc "`$s | $S`".
-type one_second()      :: $s | $S.
-doc "`$l | $L`".
-type ten_seconds()     :: $l | $L.
-doc "`$z | $Z | cancel`".
-type cancel()          :: $z | $Z | cancel.

-record(state_transition, {mode, next, cont}).

-record(timers, {mode       = state_dependent,
		 start 	    = 0,
		 short 	    = timer_to_millis(3), 
		 long  	    = timer_to_millis(9),
		 duration   = 100,      % (not used) 100 ms <-> 9.9 sec
		 unexpected = reject}). % ignore | reject


%%----------------------------------------------------------------------
%% Parses a digit map body, represented as a list of chars,
%% into a list of state transitions.
%% 
%% Returns {ok, StateTransitionList} | {error, Reason}
%% 
%%----------------------------------------------------------------------

-doc false.
parse(DigitMapBody) when is_list(DigitMapBody) ->
    ?d("parse -> entry with"
       "~n   DigitMapBody: ~p", [DigitMapBody]),
    case parse_digit_map(DigitMapBody) of
	{ok, STL} ->
	    {ok, duration_cleanup(STL, [])};
	{error, Reason} ->
	    {error, Reason}
    end;
parse(_DigitMapBody) ->
    {error, not_a_digit_map_body}.

duration_cleanup([], Acc) ->
    Acc;
duration_cleanup([STL|T], Acc) ->
    #state_transition{cont = Events} = STL,
    Events2 = duration_events_cleanup(Events, []),
    duration_cleanup(T, [STL#state_transition{cont = Events2}|Acc]).

duration_events_cleanup([], Acc) ->
    lists:reverse(Acc);
duration_events_cleanup([duration_event, Event|Events], Acc) ->
    duration_events_cleanup(Events, [{duration_event, Event}|Acc]);
duration_events_cleanup([Event|Events], Acc) ->
    duration_events_cleanup(Events, [Event|Acc]).
    
parse_digit_map(Chars) ->
    parse_digit_map(Chars, 1, [], []).

parse_digit_map(Chars, Line, DS, STL) ->
    ?d("parse_digit_map -> entry with"
       "~n   Chars: ~p"
       "~n   DS:    ~p", [Chars, DS]),
    case megaco_text_scanner:skip_sep_chars(Chars, Line) of
	{[], _Line2} when (DS =/= []) ->
	    case parse_digit_string(DS) of
		{ok, DS2} ->
		    ST = #state_transition{mode = state_dependent,
					   next = start,
					   cont = DS2},
		    STL2 = lists:reverse([ST | STL]),
		    {ok, STL2};
		{error, Reason} ->
		    {error, Reason}
	    end;
	{[Char | Chars2], Line2} ->
	    case Char of
		$( when (DS =:= []) andalso (STL =:= []) ->
		    parse_digit_map(Chars2, Line2, DS, STL);
		$) when (DS =/= []) ->
		    case megaco_text_scanner:skip_sep_chars(Chars2, Line2) of
			{[], _Line3} ->
			    case parse_digit_string(DS) of
				{ok, DS2} ->
				    ST = #state_transition{mode = state_dependent,
							   next = start,
							   cont = DS2},
				    STL2 = lists:reverse([ST | STL]),
				    {ok, STL2};
				{error, Reason} ->
				    {error, Reason}
			    end;
			{Chars3, Line3} ->
			    Trash =  lists:reverse(Chars3),
			    {error, {round_bracket_mismatch, Trash, Line3}}
		    end;
		$| when (DS =/= []) ->
		    case parse_digit_string(DS) of
			{ok, DS2} ->
			    ST = #state_transition{mode = state_dependent,
						   next = start,
						   cont = DS2},
			    parse_digit_map(Chars2, Line2, [], [ST | STL]);
			{error, Reason} ->
			    {error, Reason}
		    end;
		_ when ( Char =/= $( ) andalso 
		       ( Char =/= $| ) andalso 
		       ( Char =/= $) ) ->
		    parse_digit_map(Chars2, Line2, [Char | DS], STL);
		_ ->
		    {error, {round_bracket_mismatch, Line2}}
	    end;
	{[], Line2} ->
	    {error, {digit_string_expected, Line2}}
    end.

parse_digit_string(Chars) ->
    ?d("parse_digit_string -> entry with"
       "~n   Chars: ~p", [Chars]),
    parse_digit_string(Chars, []).

parse_digit_string([Char | Chars], DS) ->
    ?d("parse_digit_string -> entry with"
       "~n   Char:  ~p"
       "~n   Chars: ~p"
       "~n   DS:    ~p", [[Char], Chars, DS]),
    case Char of
	$] ->
	    parse_digit_letter(Chars, [], DS);
	$[ ->
	    {error, square_bracket_mismatch};
	$x ->
	    parse_digit_string(Chars, [{range, $0, $9} | DS]);
	$. ->
	    parse_digit_string(Chars, [zero_or_more | DS]);

	I when (I >= $0) andalso (I =< $9) ->
	    parse_digit_string(Chars, [{single, I} | DS]);

	A when (A >= $a) andalso (A =< $k) ->
	    parse_digit_string(Chars, [{single, A} | DS]);
	A when (A >= $A) andalso (A =< $K) ->
	    parse_digit_string(Chars, [{single, A} | DS]);

	$S ->
	    parse_digit_string(Chars, [use_short_timer | DS]);
	$s ->
	    parse_digit_string(Chars, [use_short_timer | DS]);

	$L ->
	    parse_digit_string(Chars, [use_long_timer | DS]);
	$l ->
	    parse_digit_string(Chars, [use_long_timer | DS]);

        $Z when length(Chars) > 0 ->
            parse_digit_string(Chars, [duration_event | DS]);
        $z when length(Chars) > 0 ->
            parse_digit_string(Chars, [duration_event | DS]);

        $Z ->
            {error, duration_not_allowed_as_last_char};
        $z ->
            {error, duration_not_allowed_as_last_char};

	BadChar ->
	    {error, {illegal_char_in_digit_string, BadChar}}
    end;
parse_digit_string([], DM) ->
    ?d("parse_digit_string -> entry when done with"
       "~n   DM: ~p", [DM]),
    {ok, DM}.


parse_digit_letter([Char | Chars], DL, DS) ->
    ?d("parse_digit_letter -> entry with"
       "~n   Char:  ~p"
       "~n   Chars: ~p"
       "~n   DL:    ~p"
       "~n   DS:    ~p", [[Char], Chars, DL, DS]),
    case Char of
	$[ ->
	    parse_digit_string(Chars, [{letter, DL} | DS]);
	$] ->
	    {error, square_bracket_mismatch};
	To when (To >= $0) andalso (To =< $9) ->
	    case Chars of
		[$-, From | Chars2] when (From >= $0) andalso (From =< $9) ->
		    parse_digit_letter(Chars2, [{range, From, To} | DL], DS);
		_ ->
		    parse_digit_letter(Chars, [{single, To} | DL], DS)
	    end;

	A when (A >= $a) andalso (A =< $k) ->
	    parse_digit_letter(Chars, [{single, A} | DL], DS);
	A when (A >= $A) andalso (A =< $K) ->
	    parse_digit_letter(Chars, [{single, A} | DL], DS);

	$S ->
	    parse_digit_letter(Chars, [use_short_timer | DL], DS);
	$s ->
	    parse_digit_letter(Chars, [use_short_timer | DL], DS);

	$L ->
	    parse_digit_letter(Chars, [use_long_timer | DL], DS);
	$l ->
	    parse_digit_letter(Chars, [use_long_timer | DL], DS);

	$Z ->
	    parse_digit_letter(Chars, [duration_event | DL], DS);
	$z ->
	    parse_digit_letter(Chars, [duration_event | DL], DS);

	BadChar ->
	    {error, {illegal_char_between_square_brackets, BadChar}}
    end;
parse_digit_letter([], _DL, _DS) ->
    {error, square_bracket_mismatch}.


%%----------------------------------------------------------------------
%% Collect digit map letters according to digit map
%% Returns {ok, Letters} | {error, Reason}
%%----------------------------------------------------------------------
     
-doc false.
eval(DMV) when is_record(DMV, 'DigitMapValue') ->
    case parse(DMV#'DigitMapValue'.digitMapBody) of
	{ok, DigitMapBody} ->
	    eval(DigitMapBody, DMV);
	{error, Reason} ->
	    {error, Reason}
    end;
eval(STL) when is_list(STL) ->
     eval(STL, #timers{}).
	
-doc false.
eval(STL, #'DigitMapValue'{startTimer    = Start,
			   shortTimer    = Short,
			   longTimer     = Long,
			   durationTimer = Duration}) ->
    Timers = #timers{start    = timer_to_millis(Start),
		     short    = timer_to_millis(Short),
		     long     = timer_to_millis(Long),
		     duration = duration_to_millis(Duration)},
    eval(STL, Timers);
eval(STL, {ignore, #'DigitMapValue'{startTimer    = Start,
				    shortTimer    = Short,
				    longTimer     = Long,
				    durationTimer = Duration}}) ->
    Timers = #timers{start      = timer_to_millis(Start),
		     short      = timer_to_millis(Short),
		     long       = timer_to_millis(Long),
		     duration   = duration_to_millis(Duration),
		     unexpected = ignore},
    eval(STL, Timers);
eval(STL, {reject, #'DigitMapValue'{startTimer    = Start,
				    shortTimer    = Short,
				    longTimer     = Long,
				    durationTimer = Duration}}) ->
    Timers = #timers{start      = timer_to_millis(Start),
		     short      = timer_to_millis(Short),
		     long       = timer_to_millis(Long),
		     duration   = duration_to_millis(Duration),
		     unexpected = reject},
    eval(STL, Timers);
eval(STL, Timers) when is_list(STL) andalso 
		       is_record(hd(STL), state_transition) andalso
		       is_record(Timers, timers) ->
    ?d("eval -> entry with"
       "~n   STL:    ~p"
       "~n   Timers: ~p", [STL, Timers]),
    case collect(start, mandatory_event, Timers, lists:reverse(STL), []) of
	{error, _} = Error ->
	    ?d("eval -> error:"
	       "~n   Error: ~p", [Error]),
	    Error;
	OK ->
	    ?d("eval -> ok:"
	       "~n   OK: ~p", [OK]),
	    OK
    end;
eval(DigitMapBody, ignore) ->
    eval(DigitMapBody, #timers{unexpected = ignore});
eval(DigitMapBody, reject) ->
    eval(DigitMapBody, #timers{unexpected = reject});
eval(DigitMapBody, Timers) ->
    case parse(DigitMapBody) of
	{ok, STL} ->
	    eval(STL, Timers);
	{error, Reason} ->
	    {error, Reason}
    end.

%% full | unambiguous

collect(Event, State, Timers, STL, Letters) ->
    ?d("collect -> entry with"
       "~n   Event:  ~p"
       "~n   State:  ~p"
       "~n   Timers: ~p"
       "~n   STL:    ~p", [Event, State, Timers, STL]),
    case handle_event(Event, State, Timers, STL, Letters) of
	{completed_full, _Timers2, _STL2, Letters2} ->
	    completed(full, Letters2);
	{completed, _Timers2, _STL2, Letters2} ->
	    completed(unambiguous, Letters2);
	{State2, Timers2, STL2, Letters2} ->
	    ?d("collect -> "
	       "~n   State2:   ~p"
	       "~n   Timers2:  ~p"
	       "~n   Letters2: ~p", [State2, Timers2, Letters2]),
	    MaxWait = choose_timer(State2, Event, Timers2),
	    ?d("collect -> Timer chosen: "
	       "~n   MaxWait: ~p", [MaxWait]),
	    receive
		{?MODULE, _FromPid, Event2} ->
		    ?d("collect -> Got event: "
		       "~n   ~p", [Event2]),
		    collect(Event2, State2, Timers2, STL2, Letters2)
	    after MaxWait ->
		    ?d("collect -> timeout after ~w", [MaxWait]),
		    collect(inter_event_timeout, 
			    State2, Timers2, STL2, Letters2)
	    end;

	{error, Reason} ->
	    ?d("collect -> error: "
	       "~n   Reason: ~p", [Reason]),
	    {error, Reason}
    end.

choose_timer(_State, start, #timers{start = 0}) ->
    ?d("choose_timer(start) -> entry", []),
    infinity;
choose_timer(_State, start, #timers{start = T}) ->
    ?d("choose_timer(start) -> entry with"
       "~n   T: ~p", [T]),
    T;
choose_timer(State, _Event, T) ->
    ?d("choose_timer(~p) -> entry with"
       "~n   State: ~p"
       "~n   T:     ~p", [_Event, State, T]),
    do_choose_timer(State, T).

do_choose_timer(mandatory_event, #timers{mode = state_dependent, long = T}) ->
    T;
do_choose_timer(optional_event, #timers{mode = state_dependent, short = T}) ->
    T;
do_choose_timer(_State, #timers{mode = use_short_timer, short = T}) ->
    T;
do_choose_timer(_State, #timers{mode = use_long_timer, long = T}) ->
    T.

timer_to_millis(asn1_NOVALUE) -> infinity; 
timer_to_millis(infinity)     -> infinity;
timer_to_millis(Seconds)      -> timer:seconds(Seconds).
    
%% Time for duration is in hundreds of milliseconds
duration_to_millis(asn1_NOVALUE) -> 100;
duration_to_millis(Time) when is_integer(Time) -> Time*100.

completed(Kind, {Letters, Event}) when is_list(Letters) ->
    ?d("completed -> entry with"
       "~n   Kind:  ~p"
       "~n   Event: ~s", [Kind, [Event]]),
    {ok, {Kind, duration_letter_cleanup(Letters, []), Event}};
completed(Kind, Letters) when is_list(Letters) ->
    ?d("completed -> entry with"
       "~n   Kind: ~p", [Kind]),
    {ok, {Kind, duration_letter_cleanup(Letters, [])}}.

duration_letter_cleanup([], Acc) ->
    Acc;
duration_letter_cleanup([{long, Letter}|Letters], Acc) ->
    duration_letter_cleanup(Letters, [$Z,Letter|Acc]);
duration_letter_cleanup([Letter|Letters], Acc) ->
    duration_letter_cleanup(Letters, [Letter|Acc]).

unexpected_event(Event, STL, Letters) ->
    Expected = [Next || #state_transition{next = Next} <- STL],
    SoFar    = lists:reverse(Letters),
    Reason   = {unexpected_event, Event, SoFar, Expected},
    {error, Reason}.
    

%%----------------------------------------------------------------------
%% Handles a received event according to digit map
%% State ::= optional_event | mandatory_event
%% 
%% Returns {State, NewSTL, Letters} | {error, Reason}
%%----------------------------------------------------------------------
handle_event(inter_event_timeout, optional_event, Timers, STL, Letters) ->
    {completed_full, Timers, STL, Letters}; % 7.1.14.5 2
handle_event(cancel, _State, _Timers, STL, Letters) ->
    unexpected_event(cancel, STL, Letters);
handle_event(start, _State, Timers, STL, Letters) ->
    {State2, Timers2, STL2} = compute(Timers, STL),
    {State2, Timers2, STL2, Letters};
handle_event(Event, State, Timers, STL, Letters) ->
    ?d("handle_event -> entry when"
       "~n   Event:   ~p"
       "~n   State:   ~p"
       "~n   Timers:  ~p"
       "~n   Letters: ~p", [Event, State, Timers, Letters]),
    {STL2, Collect, KeepDur} = match_event(Event, STL), 
    ?d("handle_event -> match event result: "
       "~n   Collect: ~p"
       "~n   KeepDur: ~p"
       "~n   STL2:    ~p", [Collect, KeepDur, STL2]),
    case STL2 of
	[] when (State =:= optional_event) -> % 7.1.14.5 5
	    ?d("handle_event -> complete-full with event - 7.1.14.5 5", []),
	    {completed_full, Timers, [], {Letters, Event}};
	[] when (Timers#timers.unexpected =:= ignore) ->
	    ok = io:format("<WARNING> Ignoring unexpected event: ~p~n"
			   "Expected: ~p~n",
			   [Event, STL]),
	    {State, Timers, STL, Letters};
	[] when (Timers#timers.unexpected =:= reject) ->
	    ?d("handle_event -> unexpected (reject)", []),
	    unexpected_event(Event, STL, Letters);
	_ ->
	    {State3, Timers2, STL3} = compute(Timers, STL2),
	    ?d("handle_event -> computed: "
	       "~n   State3:  ~p"
	       "~n   Timers2: ~p"
	       "~n   STL3:    ~p", [State3, Timers2, STL3]),
	    case Collect of
		true when (KeepDur =:= true) -> 
		    {State3, Timers2, STL3, [Event | Letters]};
		true -> 
		    case Event of
			{long, ActualEvent} ->
			    {State3, Timers2, STL3, [ActualEvent | Letters]};
			_ ->
			    {State3, Timers2, STL3, [Event | Letters]}
		    end;
		false -> 
		    {State3, Timers2, STL3, Letters}
	    end
    end.

match_event(Event, STL) ->
    MatchingDuration = matching_duration_event(Event, STL),
    match_event(Event, STL, [], false, false, MatchingDuration).

match_event(Event, [ST | OldSTL], NewSTL, Collect, KeepDur, MatchingDuration)
  when is_record(ST, state_transition) ->
    ?d("match_event -> entry with"
       "~n   Event:            ~p"
       "~n   ST:               ~p"
       "~n   NewSTL:           ~p"
       "~n   Collect:          ~p"
       "~n   KeepDur:          ~p"
       "~n   MatchingDuration: ~p", 
       [Event, ST, NewSTL, Collect, KeepDur, MatchingDuration]),
    case ST#state_transition.next of
	{single, Event} ->
	    ?d("match_event -> keep ST (1)", []),
	    match_event(Event, OldSTL, [ST | NewSTL], true, KeepDur,
			MatchingDuration);

        {single, Single} when (Event =:= {long, Single}) andalso 
			      (MatchingDuration =:= false) ->
	    %% Chap 7.1.14.5 point 4
	    ?d("match_event -> keep ST - change to ordinary event (2)", []),
            match_event(Event, OldSTL, [ST | NewSTL], true, KeepDur,
			MatchingDuration);

	{range, From, To} when (Event >= From) andalso (Event =< To) ->
	    ?d("match_event -> keep ST (3)", []),
	    ST2 = ST#state_transition{next = {single, Event}},
	    match_event(Event, OldSTL, [ST2 | NewSTL], true, KeepDur,
			MatchingDuration);

	{range, From, To} ->
	    case Event of
		{long, R} when (R >= From) andalso 
		               (R =< To)   andalso 
                               (MatchingDuration =:= false) ->
		    ?d("match_event -> keep ST (4)", []),
		    ST2 = ST#state_transition{next = {single, R}},
		    match_event(Event, OldSTL, [ST2 | NewSTL], true, true,
				MatchingDuration);
		_ ->
		    ?d("match_event -> drop ST - "
		       "change to ordinary event (5)", []),
		    match_event(Event, OldSTL, NewSTL, Collect, KeepDur,
				MatchingDuration) 
	    end;

        {duration_event, {single, Single}} when (Event =:= {long, Single}) ->
	    ?d("match_event -> keep ST (5)", []),
            match_event(Event, OldSTL, [ST | NewSTL], true, true,
			MatchingDuration);

        {duration_event, {range, From, To}} ->
	    case Event of
		{long, R} when (R >= From) andalso (R =< To) ->
		    ?d("match_event -> keep ST (6)", []),
		    match_event(Event, OldSTL, [ST | NewSTL], true, true,
				MatchingDuration);
		_ ->
		    ?d("match_event -> drop ST (7)", []),
		    match_event(Event, OldSTL, NewSTL, Collect, KeepDur,
				MatchingDuration) 
	    end;

	Event ->
	    ?d("match_event -> keep ST (8)", []),
	    match_event(Event, OldSTL, [ST | NewSTL], Collect, KeepDur,
			MatchingDuration);

	{letter, Letters} ->
	    case match_letter(Event, Letters, MatchingDuration) of
		{true, ChangedEvent} ->
		    ?d("match_event -> keep ST (9)", []),
		    ST2 = ST#state_transition{next = ChangedEvent},
		    match_event(Event, OldSTL, [ST2 | NewSTL], true, KeepDur,
				MatchingDuration);
		true ->
		    ?d("match_event -> keep ST (10)", []),
		    match_event(Event, OldSTL, [ST | NewSTL], true, KeepDur,
				MatchingDuration);
		false ->
		    ?d("match_event -> drop ST (11)", []),
		    match_event(Event, OldSTL, NewSTL, Collect, KeepDur,
				MatchingDuration)
	    end;

	_ ->
	    ?d("match_event -> drop ST (12)", []),
	    match_event(Event, OldSTL, NewSTL, Collect, KeepDur,
			MatchingDuration)
    end;
match_event(Event, [H | T], NewSTL, Collect, KeepDur0, MatchingDuration) 
  when is_list(H) ->
    ?d("match_event -> entry with"
       "~n   Event:            ~p"
       "~n   H:                ~p"
       "~n   NewSTL:           ~p"
       "~n   Collect:          ~p"
       "~n   KeepDur0:         ~p"
       "~n   MatchingDuration: ~p", 
       [Event, H, NewSTL, Collect, KeepDur0, MatchingDuration]),
    {NewSTL2, _Letters, KeepDur} = 
	match_event(Event, H, NewSTL, Collect, KeepDur0, MatchingDuration),
    ?d("match_event -> "
       "~n   NewSTLs: ~p", [NewSTL2]),
    match_event(Event, T, NewSTL2, Collect, KeepDur,
		MatchingDuration);
match_event(_Event, [], NewSTL, Collect, KeepDur, _MatchingDuration) ->
    ?d("match_event -> entry with"
       "~n   NewSTL:  ~p"
       "~n   Collect: ~p"
       "~n   KeepDur: ~p", [NewSTL, Collect, KeepDur]),
    {lists:reverse(NewSTL), Collect, KeepDur}.
    

match_letter(_Event, [], _MatchingDuration) ->
    false;
match_letter(Event, [Letter | Letters], MatchingDuration) ->
    ?d("match_letter -> entry with"
       "~n   Event:            ~p"
       "~n   Letter:           ~p", 
       [Event, Letter]),
    case Letter of
	{single, Event} ->
	    ?d("match_letter -> keep ST (1)", []),
	    true;

        {single, Single} when (Event =:= {long, Single}) andalso 
			      (MatchingDuration =:= false) ->
	    %% Chap 7.1.14.5 point 4
	    ?d("match_letter -> keep ST - change to ordinary event (2)", []),
            true;

	{range, From, To} when (Event >= From) andalso (Event =< To) ->
	    ?d("match_letter -> keep ST (3)", []),
	    {true, {single, Event}};

	{range, From, To} ->
	    case Event of
		{long, R} when (R >= From) andalso 
		               (R =< To)   andalso 
                               (MatchingDuration =:= false) ->
		    ?d("match_letter -> keep ST (4)", []),
		    {true, {single, R}};
		_ ->
		    ?d("match_letter -> drop ST - "
		       "change to ordinary event (5)", []),
		    match_letter(Event, Letters, MatchingDuration)
	    end;

        {duration_event, {single, Single}} when (Event =:= {long, Single}) ->
	    ?d("match_letter -> keep ST (5)", []),
            true;

        {duration_event, {range, From, To}} ->
	    case Event of
		{long, R} when (R >= From) andalso (R =< To) ->
		    ?d("match_letter -> keep ST (6)", []),
		    true;
		_ ->
		    ?d("match_letter -> drop ST (7)", []),
		    match_letter(Event, Letters, MatchingDuration)
	    end;
	
	_ ->
	    ?d("match_letter -> drop ST (8)", []),	    
	    match_letter(Event, Letters, MatchingDuration)

    end.
	

    

matching_duration_event({long, Event}, STL) ->
    Nexts = [Next || #state_transition{next = Next} <- STL],
    mde(Event, Nexts);
matching_duration_event(_Event, _STL) ->
    false.


mde(_, []) ->
    false;
mde(Event, [{duration_event, {single, Event}}|_]) ->
    true;
mde(Event, [{duration_event, {range, From, To}}|_]) 
  when Event >= From, Event =< To ->
    true;
mde(Event, [_|Nexts]) ->
    mde(Event, Nexts).


%%----------------------------------------------------------------------
%% Compute new state transitions
%% Returns {State, Timers, NewSTL}
%%----------------------------------------------------------------------
compute(Timers, OldSTL) ->
    ?d("compute -> entry with"
       "~n   Timers: ~p"
       "~n   OldSTL: ~p", [Timers, OldSTL]),
    {State, GlobalMode, NewSTL} = 
	compute(mandatory_event, state_dependent, OldSTL, []),
    ?d("compute -> "
       "~n   State:      ~p"
       "~n   GlobalMode: ~p"
       "~n   NewSTL:     ~p", [State, GlobalMode, NewSTL]),
    Timers2 = Timers#timers{mode = GlobalMode},
    ?d("compute -> "
       "~n   Timers2: ~p", [Timers2]),
    {State, Timers2, NewSTL}.

compute(State, GlobalMode, [ST | OldSTL], NewSTL) 
  when is_record(ST, state_transition) ->
    ?d("compute(~w) -> entry with"
       "~n   GlobalMode: ~p"
       "~n   ST:         ~p"
       "~n   NewSTL:     ~p", [State, GlobalMode, ST, NewSTL]),
    Cont = ST#state_transition.cont,
    Mode = ST#state_transition.mode,
    {State2, GlobalMode2, NewSTL2} =
	compute_cont(Cont, Mode, GlobalMode, State, NewSTL),
    compute(State2, GlobalMode2, OldSTL, NewSTL2);
compute(State, GlobalMode, [H | T], NewSTL) when is_list(H) ->
    ?d("compute(~w) -> entry with"
       "~n   GlobalMode: ~p"
       "~n   H:          ~p"
       "~n   NewSTL:     ~p", [State, GlobalMode, H, NewSTL]),
    {State2, GlobalMode2, NewSTL2} = compute(State, GlobalMode, H, NewSTL),
    compute(State2, GlobalMode2, T, NewSTL2);
compute(State, GlobalMode, [], NewSTL) ->
    ?d("compute(~w) -> entry with"
       "~n   GlobalMode: ~p"
       "~n   NewSTL:     ~p", [State, GlobalMode, NewSTL]),
    case NewSTL of
	[] -> {completed, GlobalMode, NewSTL};
	_  -> {State,     GlobalMode, NewSTL}
    end.

compute_cont([Next | Cont] = All, Mode, GlobalMode, State, STL) ->
    ?d("compute_cont -> entry with"
       "~n   Next:       ~p"
       "~n   Mode:       ~p"
       "~n   GlobalMode: ~p", [Next, Mode, GlobalMode]),
    case Next of
	%% Retain long timer if that has already been chosen
	use_short_timer when GlobalMode =:= use_long_timer ->
	    compute_cont(Cont, Mode, GlobalMode, State, STL);
	use_short_timer ->
	    Mode2 = use_short_timer,
	    compute_cont(Cont, Mode2, GlobalMode, State, STL);
	use_long_timer ->
	    Mode2 = use_long_timer,
	    compute_cont(Cont, Mode2, GlobalMode, State, STL);
	[] ->
	    %% Skip empty list
	    case Cont of
		[zero_or_more | Cont2] ->
		    compute_cont(Cont2, Mode, GlobalMode, State, STL);
		_ ->
		    compute_cont(Cont, Mode, GlobalMode, State, STL)
	    end;
	_ ->
	    GlobalMode2 =
		case Mode of
		    state_dependent -> GlobalMode;
		    _               -> Mode
		end,
	    case Cont of
		[zero_or_more | Cont2] ->
		    ST = make_cont(Mode, Next, All),
		    compute_cont(Cont2, Mode, GlobalMode2, State, [ST | STL]);
		_ ->
		    ST = make_cont(Mode, Next, Cont),
		    {State, GlobalMode2, [ST | STL]}
	    end
    end;
compute_cont([], GlobalMode, _Mode, _State, STL) ->
    {optional_event, GlobalMode, STL}.

make_cont(Mode, [Next | Cont2], Cont) ->
    #state_transition{mode = Mode, next = Next, cont = [Cont2 | Cont]};
make_cont(Mode, Next, Cont) ->
    #state_transition{mode = Mode, next = Next, cont = Cont}.


%%----------------------------------------------------------------------
%% Send one or more events to event collector process
%% 
%% Events ::= Event* | Event
%% Event  ::= $0-$9 | $a-$k | $A-$K | $S | $L | $Z
%% $S means sleep one second
%% $L means sleep ten seconds
%% $Z means cancel
%% Returns ok | {error, Reason}
%%----------------------------------------------------------------------

-doc false.
report(Pid, [H | T])->
    case report(Pid, H) of
	ok ->
	    report(Pid, T);
	{error, Reason} ->
	    {error, Reason}
    end;
report(_Pid, [])->
    ok;
report(Pid, Event) when is_pid(Pid) ->
    case Event of
	I when I >= $0, I =< $9 -> cast(Pid, Event);
	A when A >= $a, A =< $k -> cast(Pid, Event);
	A when A >= $A, A =< $K -> cast(Pid, Event);
	cancel                  -> cast(Pid, Event);
	$Z                      -> cast(Pid, cancel);
	$z                      -> cast(Pid, cancel);
	$R                      -> timer:sleep(100);  % 100 ms
	$r                      -> timer:sleep(100);  % 100 ms
	$S                      -> sleep(1);  % 1 sec (1000 ms)
	$s                      -> sleep(1);  % 1 sec (1000 ms)
	$L                      -> sleep(10); % 10 sec (10000 ms)
	$l                      -> sleep(10); % 10 sec (10000 ms)
        {long, I} when (I >= $0) and (I =< $9) -> cast(Pid, {long, I});
        {long, A} when (A >= $a) and (A =< $k) -> cast(Pid, {long, A});
        {long, A} when (A >= $A) and (A =< $K) -> cast(Pid, {long, A});
%%         {long, I} when (I >= $0) and (I =< $9) -> long(Pid, I);
%%         {long, A} when (A >= $a) and (A =< $k) -> long(Pid, A);
%%         {long, A} when (A >= $A) and (A =< $K) -> long(Pid, A);
 	_                       -> {error, {illegal_event, Event}}
    end.

%% long(Pid, Event) ->
%%     cast(Pid, long),
%%     cast(Pid, Event).
%% 
sleep(Sec) ->
    timer:sleep(timer:seconds(Sec)),
    ok.

cast(Pid, Event) ->
    Pid ! {?MODULE, self(), Event},
    ok.

%%----------------------------------------------------------------------
%% Feed digit map collector with events
%% Returns: {ok, Letters} | {error, Reason}
%%----------------------------------------------------------------------

-doc false.
test(DigitMap, Events) ->
    Self = self(),
    Pid = spawn_link(?MODULE, test_eval, [DigitMap, Self]),
    _ = report(Pid, Events),
    receive
	{Self, Pid, Res} ->
	    Res;
	{'EXIT', Pid, Reason} ->
	    {error, {'EXIT', Reason}}
    end.

-doc false.
test_eval(DigitMap, Parent) ->
    Res = eval(DigitMap),
    unlink(Parent),
    Parent ! {Parent, self(), Res},
    exit(normal).
