%%
%% %CopyrightBegin%
%%
%% Copyright Ericsson AB 1997-2021. 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%
%%
%%
-module(asn1ct_tok).

%% Tokenize ASN.1 code (input to parser generated with yecc)   

-export([file/1,format_error/1]).

file(File0) ->
    case file:open(File0, [read])  of
	{error, Reason} ->
	    {error,{File0,file:format_error(Reason)}};
	{ok,Stream} ->
	    try
		process(Stream, 1, [])
	    catch
		throw:{error,Line,Reason} ->
		    File = filename:basename(File0),
		    Error = {structured_error,{File,Line},?MODULE,Reason},
		    {error,[Error]}
	    end
    end.

process(Stream, Lno, R) ->
    process(io:get_line(Stream, ''), Stream, Lno, R).

process(eof, Stream, Lno, Acc) ->
    ok = file:close(Stream),
    lists:reverse([{'$end',Lno}|Acc]);
process(L, Stream, Lno0, Acc) when is_list(L) ->
    try tokenise(Stream, L, Lno0, []) of
	{Lno,[]} ->
	    process(Stream, Lno, Acc);
	{Lno,Ts} ->
	    process(Stream, Lno, Ts++Acc)
    catch
	throw:{error,Reason} ->
	    throw({error,Lno0,Reason})
    end.

format_error(eof_in_comment) ->
    "premature end of file in multi-line comment";
format_error(eol_in_token) ->
    "end of line in token";
format_error({invalid_binary_number,Str}) ->
    io_lib:format("invalid binary number: '~s'", [Str]);
format_error({invalid_hex_number,Str}) ->
    io_lib:format("invalid hex number: '~s'", [Str]);
format_error(Other) ->
    io_lib:format("~p", [Other]).

tokenise(Stream, [$&,H|T], Lno, R) when $A =< H , H =< $Z ->
    {X,T1} = get_name(T, [H]),
    tokenise(Stream, T1, Lno, [{typefieldreference,Lno,X}|R]);
tokenise(Stream, [$&,H|T], Lno, R) when $a =< H , H =< $z ->
    {X,T1} = get_name(T, [H]),
    tokenise(Stream, T1, Lno, [{valuefieldreference,Lno,X}|R]);

tokenise(Stream, "--"++T, Lno, R) ->
    tokenise(Stream, skip_comment(T), Lno, R);

tokenise(Stream, [$-,H|T], Lno, R) when $0 =< H , H =< $9 ->
    {X, T1} = get_number(T, [H]),
    tokenise(Stream, T1, Lno, [{number,Lno,-list_to_integer(X)}|R]);

tokenise(Stream, "/*"++T, Lno0, R) ->
    {Lno,T1} = skip_multiline_comment(Stream, T, Lno0, 0),
    tokenise(Stream, T1, Lno, R);

tokenise(Stream, "::="++T, Lno, R) ->
    tokenise(Stream, T, Lno, [{'::=',Lno}|R]);
tokenise(Stream, ":"++T, Lno, R) ->
    tokenise(Stream, T, Lno, [{':',Lno}|R]);

tokenise(Stream, "'"++T0, Lno, R) ->
    {Thing, T1} = collect_quoted(T0, Lno, []),
    tokenise(Stream, T1, Lno, [Thing|R]);
tokenise(Stream,[$"|T],Lno,R) ->
    {Str,T1} = collect_string(T,Lno),
    tokenise(Stream,T1,Lno,[Str|R]);

tokenise(Stream, "{"++T, Lno, R) ->
    tokenise(Stream, T, Lno, [{'{',Lno}|R]);
tokenise(Stream, "}"++T, Lno, R) ->
    tokenise(Stream, T, Lno, [{'}',Lno}|R]);

%% Even though x.680 specify '[[' and ']]' as lexical items
%% it does not work to have them as such since the single '[' and ']' can
%% be used beside each other in 'WITH SYNTAX' in x.681.
%% The solution chosen here, i.e. to have them as separate lexical items
%% will not detect the cases where there is white space between them
%% which would be an error in the use in ExtensionAdditionGroups.

tokenise(Stream, "]"++T, Lno, R) ->
    tokenise(Stream, T, Lno, [{']',Lno}|R]);
tokenise(Stream, "["++T,Lno,R) ->
    tokenise(Stream, T, Lno, [{'[',Lno}|R]);

tokenise(Stream, ","++T,Lno,R) ->
    tokenise(Stream, T, Lno, [{',',Lno}|R]);

tokenise(Stream, "("++T, Lno, R) ->
    tokenise(Stream, T, Lno, [{'(',Lno}|R]);
tokenise(Stream, ")"++T, Lno, R) ->
    tokenise(Stream, T, Lno, [{')',Lno}|R]);

tokenise(Stream, "..."++T,Lno,R) ->
    tokenise(Stream, T, Lno, [{'...',Lno}|R]);
tokenise(Stream, ".."++T, Lno, R) ->
    tokenise(Stream, T, Lno, [{'..',Lno}|R]);
tokenise(Stream, "."++T, Lno, R) ->
    tokenise(Stream, T, Lno, [{'.',Lno}|R]);

tokenise(Stream, "|"++T, Lno, R) ->
    tokenise(Stream, T, Lno, [{'|',Lno}|R]);

tokenise(Stream, [H|T], Lno, R) when $A =< H , H =< $Z ->
    {X,T1} = get_name(T, [H]),
    case reserved_word(X) of
	true ->
	    tokenise(Stream, T1, Lno, [{X,Lno}|R]);
	false ->
	    tokenise(Stream, T1, Lno, [{typereference,Lno,X}|R]);
	rstrtype ->
	    tokenise(Stream, T1, Lno, [{restrictedcharacterstringtype,Lno,X}|R])
    end;

tokenise(Stream, [H|T], Lno, R) when $a =< H , H =< $z ->
    {X, T1} = get_name(T, [H]),
    tokenise(Stream, T1, Lno, [{identifier,Lno,X}|R]);

tokenise(Stream, [H|T], Lno, R) when $0 =< H , H =< $9 ->
    {X, T1} = get_number(T, [H]),
    tokenise(Stream, T1, Lno, [{number,Lno,list_to_integer(X)}|R]);

tokenise(Stream, [H|T], Lno, R) when H =< $\s ->
    tokenise(Stream, T, Lno, R);

tokenise(Stream, [H|T], Lno, R) ->
    tokenise(Stream, T, Lno, [{list_to_atom([H]),Lno}|R]);
tokenise(_Stream, [], Lno, R) ->
    {Lno+1,R}.


collect_string(L, Lno) ->
    collect_string(L, Lno, []).

collect_string([$"|T], _Lno, Str) ->
    {{cstring,1,lists:reverse(Str)},T};
collect_string([H|T], Lno, Str) ->
    collect_string(T, Lno, [H|Str]);
collect_string([], _, _) ->
    throw({error,missing_quote_at_eof}).
           
%% <name> is letters digits hyphens.
%% Hyphen is not the last character. Hyphen hyphen is NOT allowed.
%%
%% <identifier> ::= <lowercase> <name>

get_name([$-,Char|T]=T0, Acc) ->
    case isalnum(Char) of
	true ->
	    get_name(T, [Char,$-|Acc]);
	false ->
	    {list_to_atom(lists:reverse(Acc)),T0}
    end;
get_name([$-|_]=T, Acc) ->
    {list_to_atom(lists:reverse(Acc)),T};
get_name([Char|T]=T0, Acc) ->
    case isalnum(Char) of
	true ->
	    get_name(T, [Char|Acc]);
	false ->
	    {list_to_atom(lists:reverse(Acc)),T0}
    end;
get_name([], Acc) ->
    {list_to_atom(lists:reverse(Acc)),[]}.
	    
isalnum(H) when $A =< H , H =< $Z ->
    true;
isalnum(H) when $a =< H , H =< $z ->
    true;
isalnum(H) when $0 =< H , H =< $9 ->
    true;
isalnum(_) ->
    false.

isdigit(H) when $0 =< H , H =< $9 ->
    true;
isdigit(_) ->
    false.

get_number([H|T]=T0, L) ->
    case isdigit(H) of
	true ->
	    get_number(T, [H|L]);
	false ->
	    {lists:reverse(L), T0}
    end;
get_number([], L) ->
    {lists:reverse(L), []}.

skip_comment([]) -> [];
skip_comment("--"++T) -> T;
skip_comment([_|T]) -> skip_comment(T).

skip_multiline_comment(Stream, [], Lno, Level) ->
    case io:get_line(Stream, '') of
	eof ->
	    throw({error,eof_in_comment});
	Line ->
	    skip_multiline_comment(Stream, Line, Lno+1, Level)
    end;
skip_multiline_comment(_Stream, "*/"++T, Lno, 0) ->
    {Lno,T};
skip_multiline_comment(Stream, "*/"++T, Lno, Level) ->
    skip_multiline_comment(Stream, T, Lno, Level - 1);
skip_multiline_comment(Stream, "/*"++T, Lno, Level) ->
    skip_multiline_comment(Stream, T, Lno, Level + 1);
skip_multiline_comment(Stream, [_|T], Lno, Level) ->
    skip_multiline_comment(Stream, T, Lno, Level).

collect_quoted("'B"++T, Lno, L) ->
    case validate_bin(L) of
        {ok, Bin} ->
            {{bstring,Lno,Bin}, T};
        false ->
            throw({error,{invalid_binary_number,lists:reverse(L)}})
    end;
collect_quoted("'H"++T, Lno, L) ->
    case validate_hex(L) of
        {ok, Hex} ->
            {{hstring,Lno,Hex}, T};
        false ->
            throw({error,{invalid_hex_number,lists:reverse(L)}})
    end;
collect_quoted([H|T], Lno, L) ->
    collect_quoted(T, Lno,[H|L]);
collect_quoted([], _, _) ->        % This should be allowed FIX later
    throw({error,eol_in_token}).

validate_bin(L) ->
    validate_bin(L,[]).

validate_bin([H|T], A) when H =:= $0; H =:= $1 ->
    validate_bin(T, [H|A]);
validate_bin([$\s|T], A) ->
    validate_bin(T, A);
validate_bin([_|_], _) ->
    false;
validate_bin([], A) ->
    {ok, A}.

validate_hex(L) ->
    validate_hex(L,[]).

validate_hex([H|T], A) when $0 =< H , H =< $9 ->
    validate_hex(T, [H|A]);
validate_hex([H|T], A) when $A =< H , H =< $F ->
    validate_hex(T, [H|A]);
validate_hex([$\s|T], A) ->
    validate_hex(T, A);
validate_hex([_|_], _) ->
    false;
validate_hex([], A) ->
    {ok, A}.

%% reserved_word(A) -> true|false|rstrtype
%% A = atom()
%% returns true if A is a reserved ASN.1 word
%% returns false if A is not a reserved word
%% returns rstrtype if A is a reserved word in the group 
%% 	RestrictedCharacterStringType
reserved_word('ABSENT') -> true;
reserved_word('ALL') -> true;
reserved_word('ANY') -> true;
reserved_word('APPLICATION') -> true;
reserved_word('AUTOMATIC') -> true;
reserved_word('BEGIN') -> true;
reserved_word('BIT') -> true;
reserved_word('BMPString') -> rstrtype;
reserved_word('BOOLEAN') -> true;
reserved_word('BY') -> true;
reserved_word('CHARACTER') -> true;
reserved_word('CHOICE') -> true;
reserved_word('CLASS') -> true;
reserved_word('COMPONENT') -> true;
reserved_word('COMPONENTS') -> true;
reserved_word('CONSTRAINED') -> true;
reserved_word('CONTAINING') -> true;
reserved_word('DEFAULT') -> true;
reserved_word('DEFINED') -> true; % not present in X.680 07/2002
reserved_word('DEFINITIONS') -> true;
reserved_word('EMBEDDED') -> true;
reserved_word('ENCODED') -> true;
reserved_word('END') -> true;
reserved_word('ENUMERATED') -> true;
reserved_word('EXCEPT') -> true;
reserved_word('EXPLICIT') -> true;
reserved_word('EXPORTS') -> true;
reserved_word('EXTENSIBILITY') -> true;
reserved_word('EXTERNAL') -> true;
reserved_word('FALSE') -> true;
reserved_word('FROM') -> true;
reserved_word('GeneralizedTime') -> true;
reserved_word('GeneralString') -> rstrtype;
reserved_word('GraphicString') -> rstrtype;
reserved_word('IA5String') -> rstrtype;
reserved_word('IDENTIFIER') -> true;
reserved_word('IMPLICIT') -> true;
reserved_word('IMPLIED') -> true;
reserved_word('IMPORTS') -> true;
reserved_word('INCLUDES') -> true;
reserved_word('INSTANCE') -> true;
reserved_word('INTEGER') -> true;
reserved_word('INTERSECTION') -> true;
reserved_word('MAX') -> true;
reserved_word('MIN') -> true;
reserved_word('MINUS-INFINITY') -> true;
reserved_word('NULL') -> true;
reserved_word('NumericString') -> rstrtype;
reserved_word('OBJECT') -> true;
reserved_word('ObjectDescriptor') -> true;
reserved_word('OCTET') -> true;
reserved_word('OF') -> true;
reserved_word('OPTIONAL') -> true;
reserved_word('PATTERN') -> true;
reserved_word('PDV') -> true;
reserved_word('PLUS-INFINITY') -> true;
reserved_word('PRESENT') -> true;
reserved_word('PrintableString') -> rstrtype;
reserved_word('PRIVATE') -> true;
reserved_word('REAL') -> true;
reserved_word('RELATIVE-OID') -> true;
reserved_word('SEQUENCE') -> true;
reserved_word('SET') -> true;
reserved_word('SIZE') -> true;
reserved_word('STRING') -> true;
reserved_word('SYNTAX') -> true;
reserved_word('T61String') -> rstrtype;
reserved_word('TAGS') -> true;
reserved_word('TeletexString') -> rstrtype;
reserved_word('TRUE') -> true;
reserved_word('UNION') -> true;
reserved_word('UNIQUE') -> true;
reserved_word('UNIVERSAL') -> true;
reserved_word('UniversalString') -> rstrtype;
reserved_word('UTCTime') -> true;
reserved_word('UTF8String') -> rstrtype;
reserved_word('VideotexString') -> rstrtype;
reserved_word('VisibleString') -> rstrtype;
reserved_word('WITH') -> true;
reserved_word(_) -> false.
