/*  ASK.PL  */
 
 
:- module ask.
 
 
:- public ask_yn/2,
          ask_number/3,
          ask_number/2,
          ask_tag/3,
          ask_tag/2,
          ask_atom/3,
          ask_atom/2,                     
          ask_term/3,
          ask_term/2.
 
 
/*
SPECIFICATION
-------------
 
PUBLIC ask_yn( Question+, Answer? ):
Question is some text writeable by 'output'. ask_yn asks the question,
and unifies Answer with 'yes' or 'no' accordingly.
 
The reply must be terminated with a newline, and can be one of
    y
    n
    yes
    no
Case is irrelevant. If the answer is not one of these, then ask_yn
will say
    Please reply with y or n.
and re-ask the question.
 
 
PUBLIC ask_number( Question+, Spec+, Answer? ):
Like ask_yn, but asks for a number, unifying Answer with the result.
 
The reply must be a number (integer or real, positive or negative),
terminated by a newline. If it is anything else, ask_number will say so,
and re-ask the question.
 
Spec is either
    n
or
    n( Var, Test, Description )
where Test is a goal that includes at least one occurrence of Var. If
Spec is the latter, then after the number is read, it will be bound
to Var, and Test will be called. If it fails, then ask_number will
write
    Please type <Description>.
and try again.
 
Example:
    ask_number( 'How many parents are still alive?',
                n( X, (X=1,X=2), 'either 1 or 2' ),
                Parents
              ).
 
 
PUBLIC ask_number( Question+, Answer? ):
 
As ask_number( Question, n, Answer ).
 
 
PUBLIC ask_tag( Question+, Spec+, Answer? ):
Like ask_yn, but asks for a "tag", unifying Answer with the result.
 
The reply must be a single token: either a non-negative integer or a
non-empty sequence of printable characters without embedded spaces,
terminated by a newline. If it is anything else, ask_tag will say so,
and re-ask the question.
 
Spec is either
    t
or
    t( Var, Test, Description )
where Test is a goal that includes at least one occurrence of Var. If
Spec is the latter, then after the tag is read, it will be bound
to Var, and Test will be called. If it fails, then ask_tag will
write
    Please type <Description>.
and try again.
 
Example:
    ask_tag( 'How many parents are still alive?',
             t( X, (X=1,X=2), 'either 1 or 2' ),
             Parents
           ).
 
 
PUBLIC ask_tag( Question+, Answer? ):
 
As ask_tag( Question, t, Answer ).
 
 
PUBLIC ask_atom( Question+, Spec+, Answer? ):
Like ask_yn, but asks for a atom, unifying Answer with the result.
 
The reply is formed by doing 'name' on the entire line of input, after
stripping trailing spaces. Therefore, sequences with embedded spaces,
such as
    `Frederick the Great' Part 42
will still be treated as single atoms. The same applies to other layout
characters.
 
Spec is either
    a
or
    a( Var, Test, Description )
where Test is a goal that includes at least one occurrence of Var. If
Spec is the latter, then after the atom is read, it will be bound
to Var, and Test will be called. If it fails, then ask_atom will
write
    Please type <Description>.
and try again.
 
 
PUBLIC ask_atom( Question+, Answer? ):
 
As ask_atom( Question, a, Answer ).
 
 
PUBLIC ask_term( Question+, Spec+, Answer? ):
Like ask_yn, but asks for a term, unifying Answer with the result.
 
The reply is read by read/1, and can span any number of lines. It must
finish with a full stop.
 
Spec is either
    t
or
    t( Var, Test, Description )
where Test is a goal that includes at least one occurrence of Var. If
Spec is the latter, then after the term is read, it will be bound
to Var, and Test will be called. If it fails, then ask_term will
write
    Please type <Description>.
and try again.
 
 
PUBLIC ask_term( Question+, Answer? ):
 
As ask_term( Question, t, Answer ).
*/
 
 
/*
IMPLEMENTATION
--------------
 
The predicates need to set the prompt: it's assumed this can be done
with the predicates in USEFUL.
 
For simplicity, I have not tried to merge similar pieces of code. I could
do so, but suspect this would add more in complexity than is worthwhile.
*/
 
 
:- needs
    arb / 2,
    chars_to_tokens / 2,
    chars_to_uppercase / 2,
    get_prompt / 1,
    is_printable_char / 1,
    output / 1,
    read_line_as_chars / 1,
    set_prompt / 1.
 
 
ask_yn( Question, AnswerOut ) :-
    get_prompt( Old ),
    set_prompt( ' ' ),
    output( Question ),
    read_line_as_chars( YN ),
    set_prompt( Old ),
    chars_to_uppercase( YN, YN_UC ),
    check_yn( Question, YN_UC, Answer ),
    !,
    AnswerOut = Answer.
 
 
check_yn( _, "Y", yes ) :-
    !.
 
check_yn( _, "YES", yes ) :-
    !.
 
check_yn( _, "N", no ) :-
    !.
 
check_yn( _, "NO", no ) :-
    !.
 
check_yn( Question, _, Answer ) :-
    output( 'Please reply with y or n.'~ ),
    ask_yn( Question, Answer ).
 
 
ask_tag( Question, Spec, AnswerOut ) :-
    get_prompt( Old ),
    set_prompt( ' ' ),
    output( Question ),
    read_line_as_chars( T ),
    set_prompt( Old ),
    check_tag( Question, Spec, T, Answer ),
    !,
    AnswerOut = Answer.
 
 
ask_tag( Question, AnswerOut ) :-
    ask_tag( Question, t, AnswerOut ).
 
 
check_tag( Question, Spec, [], Answer ) :-
    !,
    output( 'Please type a name or integer.'~ ),
    ask_tag( Question, Spec, Answer ).
 
check_tag( Question, Spec, T, Answer ) :-
    phrase( any_nonprintable, T ),
    !,
    output( 'Please type a name or integer (not followed by anything else).'~ ),
    ask_tag( Question, Spec, Answer ).
 
check_tag( Question, Spec, T, Answer ) :-
    chars_to_tokens( T, [integer(X)] ),
    !,
    check_tag_against_spec( Question, Spec, X, Answer ).
 
check_tag( Question, Spec, T, Answer ) :-
    name( A, T ),
    !,
    check_tag_against_spec( Question, Spec, A, Answer ).
 
 
any_nonprintable -->
    arb, [C], { not(is_printable_char(C)) }.
 
 
check_tag_against_spec( Question, t, X, X ) :- !.
 
check_tag_against_spec( Question, t(X,Test,_), X, X ) :-
    call( Test ),
    !.
 
check_tag_against_spec( Question, Spec, _, Answer ) :-
    !,
    Spec = t( _, _, Description ),
    output( 'Please type'...Description<>'.'~ ),
    ask_tag( Question, Spec, Answer ).
 
 
ask_number( Question, Spec, AnswerOut ) :-
    get_prompt( Old ),
    set_prompt( ' ' ),
    output( Question ),
    read_line_as_chars( N ),
    set_prompt( Old ),
    check_number( Question, Spec, N, Answer ),
    !,
    AnswerOut = Answer.
 
 
ask_number( Question, AnswerOut ) :-
    ask_number( Question, n, AnswerOut ).   
 
 
check_number( Question, Spec, N, Answer ) :-
    chars_to_tokens( N, Tokens ),
    check_number_token( Question, Spec, Tokens, Answer ).
 
 
check_number_token( Question, Spec, [H], Answer ) :-
    ( H = integer(X) ; H = real(X) ),
    !,
    check_number_against_spec( Question, Spec, X, Answer ).
 
check_number_token( Question, Spec, [H|_], Answer ) :-
    ( H = integer(_) ; H = real(_) ),
    /*  A number, but followed by something else.  */
    !,
    output( 'Please type a number (not followed by anything else).'~ ),
    ask_number( Question, Spec, Answer ).
 
check_number_token( Question, Spec, L, Answer ) :-
    /*  Empty or illegal token.  */
    !,
    output( 'Please type a number.'~ ),
    ask_number( Question, Spec, Answer ).
 
 
check_number_against_spec( Question, n, X, X ) :- !.
 
check_number_against_spec( Question, n(X,Test,_), X, X ) :-
    call( Test ),
    !.
 
check_number_against_spec( Question, Spec, _, Answer ) :-
    !,
    Spec = n( _, _, Description ),
    output( 'Please type'...Description<>'.'~ ),
    ask_number( Question, Spec, Answer ).
 
 
ask_atom( Question, Spec, AnswerOut ) :-
    get_prompt( Old ),
    set_prompt( ' ' ),
    output( Question ),
    read_line_as_chars( N ),
    set_prompt( Old ),
    check_atom( Question, Spec, N, Answer ),
    !,
    AnswerOut = Answer.
 
 
ask_atom( Question, AnswerOut ) :-      
    ask_atom( Question, a, AnswerOut ).
 
 
check_atom( Question, Spec, N, Answer ) :-
    name( A, N ),
    check_atom_against_spec( Question, Spec, A, Answer ).
 
 
check_atom_against_spec( Question, a, X, X ) :- !.
 
check_atom_against_spec( Question, a(X,Test,_), X, X ) :-
    call( Test ),
    !.
 
check_atom_against_spec( Question, Spec, _, Answer ) :-
    !,
    Spec = a( _, _, Description ),
    output( 'Please type'...Description<>'.'~ ),
    ask_atom( Question, Spec, Answer ).
 
 
ask_term( Question, Spec, AnswerOut ) :-
    get_prompt( Old ),
    set_prompt( ' ' ),
    output( Question ),
    read( T ),
    set_prompt( Old ),
    check_term_against_spec( Question, Spec, T, Answer ),
    !,
    AnswerOut = Answer.
 
 
ask_term( Question, AnswerOut ) :-
    ask_term( Question, t, AnswerOut ).
 
 
check_term_against_spec( Question, t, X, X ) :- !.
 
check_term_against_spec( Question, t(X,Test,_), X, X ) :-
    call( Test ),
    !.
 
check_term_against_spec( Question, Spec, _, Answer ) :-
    !,
    Spec = t( _, _, Description ),
    output( 'Please type'...Description<>'.'~ ),
    ask_term( Question, Spec, Answer ).
 
 
:- endmodule.
