%% -*- mode: erlang; indent-tabs-mode: nil -*-
%% Copyright (c) 2024 Robert Virding
%%
%% 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.

%% File    : lfe_scan.erl
%% Author  : Robert Virding
%% Purpose : Token scanner for the Lisp Flavoured Erlang language.

%%% Much of the basic structure has been taken from erl_scan. A core
%%% difference is that we need to handle both the cases of only one
%%% token and many tokens. For this reason we have added the 'none'
%%% return.
%%%
%%% We chain a state, #lfe_scan{}, through the scanner but we never
%%% use it for anything. It was inherited from erl_scan and we left it
%%% just in case we might need it.

-module(lfe_scan).

-export([string/1,string/2,string/3,token/2,token/3,tokens/2,tokens/3,
         format_error/1]).

-export([start_symbol_char/1,symbol_char/1]).

-export([token_test/1,token_test/2,tokens_test/1,tokens_test/2]).

format_error({illegal,S}) ->
    io_lib:format(<<"illegal ~s">>, [S]);
format_error({illegal_token,S}) ->
    io_lib:format(<<"illegal token ~s">>, [S]);
format_error({illegal_chars,S}) ->
    io_lib:format(<<"illegal characters in ~s">>, [S]);
format_error({bad_format,S}) ->
    io_lib:format(<<"bad of format of ~s">>, [S]);
format_error(bad_tq_string) ->
    "bad format of triple-quoted string";
format_error({user,S}) -> S;
format_error(Other) ->
    lists:flatten(io_lib:write(Other)).

%% Nothing in here yet, but who knows.
-record(lfe_scan, {}).

%% LFE definitions of these types.
-define(WHITE_SPACE(C), (C >= $\000 andalso C =< $\s)).
-define(DIGIT(C), (C >= $0 andalso C =< $9)).
-define(CHAR(C), (C >= O andalso C < 16#110000)).
-define(UNICODE(C),
        (is_integer(C) andalso
         (C >= 0 andalso C < 16#D800 orelse
          C > 16#DFFF andalso C < 16#FFFE orelse
          C > 16#FFFF andalso C =< 16#10FFFF))).

-define(UNI255(C), (is_integer(C) andalso 0 =< C andalso C =< 16#ff)).

%% string(String) ->
%% string(String, StartLine) ->
%% string(String, StartLine, Options) ->
%%     {ok,Tokens,LastLine}  | {error,Error,LastLine}.
%%  Scan a string for the tokens in it.

string(String) ->
    string(String, 1, []).

string(String, StartLocation) ->
    string(String, StartLocation, []).

string(String, StartLocation, _Options) ->
    string1(String, StartLocation, 1, [], #lfe_scan{}).

%% string1(Chars, Line, Column, Tokens, State) ->
%%     {ok,Tokens,LastLine} | {error,Error,LastLine}.

string1(Cs, Line, _Col, Toks, _St) when Cs =:= [] orelse Cs =:= eof ->
    %% No more chars left in the string.
    {ok,lists:reverse(Toks),anno(Line)};
string1(Cs0, Line0, Col0, Toks0, St0) ->
    %% Keep going!
    case scan1(Cs0, Line0, Col0, St0) of
        {more,{Cs1,Line1,Col1,St1,Extra,Fun}} ->
            %% Needs more, try again by appending eof.
            case Fun(Cs1 ++ eof, Line1, Col1, St1, Extra) of
                %% What to do when we get more here.
                {more,{Cs2,Line2,Col2,St2,Extra2,Fun2}} ->
                    Fun2(Cs2, Line2, Col2, St2, Extra2);
                {none,Rest,Line2,Col2,St2} ->
                    string1(Rest, Line2, Col2, Toks0, St2);
                {ok,Tok,_Rest,Line2,_Col2, _St2} ->
                    %% It worked and all is done.
                    {ok,lists:reverse([Tok|Toks0]),anno(Line2)};
                {{error,_,_}=Error,_Rest} ->
                    %% Really bad!
                    Error
            end;
        {none,Rest,Line1,Col1,St1} ->
            %% Nothing from this call so try again.
            string1(Rest, Line1, Col1, Toks0, St1);
        {ok,Tok,Rest,Line1,Col1,St1} ->
            %% It worked but still has chars left, try agiain.
            string1(Rest, Line1, Col1, [Tok|Toks0], St1);
        {{error,_,_}=Error,_Rest} ->
            %% Bad!
            Error
    end.

%% token(Continuation, String) ->
%% token(Continuation, String, StartLine) ->
%%     {more,Continuation} | {done,ReturnVal,RestChars}.
%%  Scan all tokens in the repeated calls threading the continuation
%%  through the calls.

token(Cont, Chars) ->
    token(Cont, Chars, 1, []).

token(Cont, Chars, StartLine) ->
    token(Cont, Chars, StartLine, []).

token([], Chars, Line, _Options) ->
    %% io:format("t4 ~p\n", [{[],Chars,Line,_Options}]),
    token1(Chars, Line, 1, #lfe_scan{}, [], fun scan/5); 
token({lfe_scan_token,Cs,Line,Col,St,Extra,Fun},
       Chars, _Line, _Options) ->
    %% io:format("t4 ~p\n", [{lfe_scan_token,Chars,_Line,_Options}]),
    token1(Cs ++ Chars, Line, Col, St, Extra, Fun).

%% token1(Chars, Line, Column, State, Extra, Fun) ->
%%     {done,{ok,Token,Line},Cs} | {done,Error,Cs} | {more,Continuation}.

token1(eof, Line, Col, St, Extra, Fun) ->
    Fun(eof, Line, Col, St, Extra);
token1([], Line, Col, St, Extra, Fun) ->
    {more,{lfe_scan_token,[],Line,Col,St,Extra,Fun}};
token1(Cs0, Line0, Col0, St0, Extra0, Fun0) ->
    %% io:format("t11 ~p\n", [{Cs0,Fun0,fun scan/5]),
    case Fun0(Cs0, Line0, Col0, St0, Extra0) of
        {more,{Cs1,Line1,Col1,St1,Extra1,Fun1}} ->
            Cont = {lfe_scan_token,Cs1,Line1,Col1,St1,Extra1,Fun1},
            %% io:format("tf1 ~p\n", [{Cs1,Line1,Col1,Fun1,fun scan/5}]),
            {more,Cont};
        {none,Rest,Line1,Col1,St1} ->
            %% Nothing from this call so try again.
            token1(Rest, Line1, Col1, St1, Extra0, fun scan/5);
        {ok,Token,Rest,Line1,_Col1,_St1} ->
            {done,{ok,Token,Line1},Rest};
        {{error,_,_}=Error,Rest} ->
            {done,Error,Rest}
    end.

%% tokens(Continuation, String) ->
%% tokens(Continuation, String, StartLine) ->
%% tokens(Continuation, String, StartLine, Options) ->
%%     {more,Continuation} | {done,ReturnVal,RestChars}.
%%  Scan all tokens in the repeated calls threading the continuation
%%  through the calls.

tokens(Cont, Chars) ->
    %% io:format("ts2 ~p\n", [{Cont,Chars}]),
    tokens(Cont, Chars, 1, []).

tokens(Cont, Chars, StartLine) ->
    %% io:format("ts3 ~p\n", [{Cont,Chars,StartLine}]),
    tokens(Cont, Chars, StartLine, []).

tokens([], Chars, Line, _Options) ->
    %% io:format("ts4 ~p\n", [{[],Chars}]),
    tokens1(Chars, Line, 1, [], #lfe_scan{}, [], fun scan/5); 
tokens({lfe_scan_tokens,Cs,Line,Col,Toks,St,Extra,Fun},
       Chars, _Line, _Options) ->
    %% io:format("ts4 ~p\n", [{Cont,Chars}]),
    tokens1(Cs ++ Chars, Line, Col, Toks, St, Extra, Fun).

%% tokens1(Chars, Line, Column, State, Extra, Fun) ->
%%     {done,{ok,Token,Line},Cs} | {done,Error,Cs} | {more,Continuation}.
%%  We loop inside this function for as long as we can until we need
%%  more characters or there is an error.

tokens1(Cs, Line0, Col0, Toks, St0, Extra0, Fun0) ->
    case Fun0(Cs, Line0, Col0, St0, Extra0) of
        {more,{Cs1,Line1,Col1,St1,Extra1,Fun1}} ->
            Cont = {lfe_scan_tokens,Cs1,Line1,Col1,Toks,St1,Extra1,Fun1},
            {more,Cont};
        {none,Rest,Line1,Col1,St1} ->
            if Rest =:= eof ->
                    {done,{ok,lists:reverse(Toks),anno(Line1)},eof};
               true ->
                    tokens1(Rest, Line1, Col1, Toks, St1, Extra0, fun scan/5)
            end;
        {ok,Tok,Rest,Line1,Col1,St1} ->
            if Rest =:= eof ->
                    {done,{ok,lists:reverse([Tok|Toks]),anno(Line1)},eof};
               true ->
                    tokens1(Rest, Line1, Col1, [Tok|Toks], St1, [], fun scan/5)
            end;
        {{error,_,_}=Error,Rest} ->
            {done,Error,Rest}
    end.

%%
%% Now to the actual scanning and collecting tokens.
%%

%% scan(Chars, Line, Column, State, Extra) ->
%%     {ok,Token,Chars,Line,Column} | {more,Continuation} | ScanError.
%%  Scan one token.

scan(Chars, Line, Col, St, _Extra) ->
    scan1(Chars, Line, Col, St).

%% scan1(Chars, Line, Column, State) ->
%%     {ok,Token,Chars,Line,Column} | {more,Continuation} | None|  ScanError.

scan1_fun(Cs, Line, Col, St, _Extra) ->
    scan1(Cs, Line, Col, St).

%% Strings
scan1([$"|Cs], Line, Col, St) ->
    %% We handle both normal strings and triple quote strings here.
    scan_string(Cs, Line, Col, St);
%% Newlines and white space.
scan1([$\n|Cs], Line, _Col, St) ->
    scan1(Cs, Line+1, 1, St);
scan1([C|Cs], Line, Col, St) when ?WHITE_SPACE(C) ->
    scan1(Cs, Line, Col+1, St);
%% Comments.
scan1([$;|Cs], Line, Col, St) ->
    scan_line_comment(Cs, Line, Col+1, St);
%% These start symbol chars must be handled specially.
scan1([$||Cs], Line, Col, St) ->
    scan_qsymbol(Cs, Line, Col, St);
scan1([$#|Cs], Line, Col, St) ->
    scan_hash(Cs, Line, Col+1, St);
%% Non-existent test tokens which can be useful for testing the parser.
%% scan1([$^|Cs], Line, Col, St) ->
%%     {ok,{'^',anno(Line)},Cs,Line,Col+1,St};
%% scan1([$&|Cs], Line, Col, St) ->
%%     {ok,{'&',anno(Line)},Cs,Line,Col+1,St};
%% We do the one character separators which are also start symbol chars.
scan1([$'|Cs], Line, Col, St) ->
    {ok,{'\'',anno(Line)},Cs,Line,Col+1,St};
scan1([$`|Cs], Line, Col, St) ->
    {ok,{'\`',anno(Line)},Cs,Line,Col+1,St};
scan1([$.|Cs], Line, Col, St) ->
    {ok,{'.',anno(Line)},Cs,Line,Col+1,St};
scan1([$,,$@|Cs], Line, Col, St) ->
    {ok,{',@',anno(Line)},Cs,Line,Col+2,St};
scan1([$,|Cs], Line, Col, St) ->
    {ok,{',',anno(Line)},Cs,Line,Col+1,St};
%% Now it gets a little tricky, everything starts with a start symbol
%% char and is a symbol or single char or a NUMBER!
scan1([C|Cs], Line, Col, St) ->
    case start_symbol_char(C) of
        true ->
            scan_symbol([C|Cs], Line, Col, St);
        false ->
            Tok = {list_to_atom([C]),anno(Line)},
            {ok,Tok,Cs,Line,Col,St}
    end;
scan1([], Line, Col, St) ->
    %% Need more here.
    {more,{[],Line,Col,St,[],fun scan1_fun/5}};
scan1(eof, Line, Col, St) ->
    %% We didn't get anything and nothing left.
    {none,eof,Line,Col,St}.

%% scan_line_comment(Chars, Line, Column, State) ->
%%     {ok,Tokens,Chars,Line,Column} | {more,Continuation} | ScanError.
%%  Skip to the end of the line.
    
scan_line_comment_fun(Cs, Line, Col, St, _Extra) -> 
    scan_line_comment(Cs, Line, Col, St).

scan_line_comment([$\n|Cs], Line, _Col, St) ->
    {none,Cs,Line+1,0,St};
scan_line_comment([_C|Cs], Line, Col, St) ->
    scan_line_comment(Cs, Line, Col+1, St);
scan_line_comment([]=Cs, Line, Col, St) ->
    {more,{Cs,Line,Col,St,[],fun scan_line_comment_fun/5}};
scan_line_comment(eof=Cs, Line, Col, St) ->
    {none,Cs,Line,Col,St}.

%% scan_block_comment(Chars, Line, Column, State) ->
%%     {ok,Tokens,Chars,Line,Column} | {more,Continuation} |  | ScanError.
%%  Skip to the end of the block comment. The first # has already been
%%  scanned.

scan_block_comment_fun(Cs, Line, Col, St, _Extra) ->
    scan_block_comment(Cs, Line, Col, St).

scan_block_comment("|#" ++ Cs, Line, Col, St) ->
    {none,Cs,Line,Col,St};
scan_block_comment("#|" ++ Cs, Line, Col, _St) ->
    scan_error({illegal,"nested block comment"}, Line, Col, Line, Col, Cs);
scan_block_comment([C]=Cs, Line, Col, St) when C =:= $|; C =:= $# ->
    %% Need next character to work these out.
    {more,{Cs,Line,Col,St,[],fun scan_block_comment_fun/5}};
scan_block_comment([$\n|Cs], Line, _Col, St) ->
    scan_block_comment(Cs, Line+1, 0, St);
scan_block_comment([_C|Cs], Line, Col, St) ->
    scan_block_comment(Cs, Line, Col+1, St);
%% [], we need more to know.
scan_block_comment([]=Cs, Line, Col, St) ->
    {more,{Cs,Line,Col,St,[],fun scan_block_comment_fun/5}};
%% Eof so we are really done!
scan_block_comment(eof=Cs, Line, Col, _St) ->
    scan_error({bad_format,"block comment"}, Line, Col, Line, Col, Cs).

%% scan_hash(Chars, Line, Column, State) ->
%%     {ok,Token,Chars,Line,Column} | {more,Continuation} | ScanError.
%%  Scan a hash character symbol. We collect digits before the command
%%  character as some forms need this.

scan_hash(Cs, Line, Col, St) ->
    scan_hash_digits(Cs, Line, Col, [], St).

scan_hash_digits_fun(Cs, Line, Col, St, Digits) ->
    scan_hash_digits(Cs, Line, Col, Digits, St).

scan_hash_digits([C|Cs], Line, Col, Digits, St) when ?DIGIT(C) ->
    scan_hash_digits(Cs, Line, Col+1, Digits ++ [C], St);
%% [], we need more to know.
scan_hash_digits([]=Cs, Line, Col, Digits, St) ->
    {more,{Cs,Line,Col,St,Digits,fun scan_hash_digits_fun/5}};
scan_hash_digits(Cs, Line, Col, Digits, St) ->
    %% We know there is 
    scan_hash1(Cs, Line, Col, Digits, St).

%% scan_hash1_fun(Cs, Line, Col, St, Digits) ->
%%     scan_hash1(Cs, Line, Col, Digits, St).

%% First the tokens which only need one character.
scan_hash1([$\(|Cs], Line, Col, [], St) ->
    {ok,{'#(',Line},Cs,Line,Col,St};
scan_hash1([$.|Cs], Line, Col, [], St) ->
    {ok,{'#.',Line},Cs,Line,Col,St};
scan_hash1([$`|Cs], Line, Col, [], St) ->
    {ok,{'#`',Line},Cs,Line,Col,St};
scan_hash1([$;|Cs], Line, Col, [], St) ->
    {ok,{'#;',Line},Cs,Line,Col,St};
scan_hash1([$||Cs], Line, Col, [], St) ->
    scan_block_comment(Cs, Line, Col+1, St); 
scan_hash1([$"|Cs], Line, Col, [], St) ->
    scan_binary_string(Cs, Line, Col+1, St);
scan_hash1([$'|Cs], Line, Col, [], St) ->
    scan_fun(Cs, Line, Col+1, St);
scan_hash1([$*|Cs], Line, Col, [], St) ->
    scan_bnumber(Cs, 2, Line, Col+1, St);
%% The #B binary number needs to be checked in hash2.
scan_hash1([C|Cs], Line, Col, [], St) when (C =:= $o) or (C =:= $O) ->
    scan_bnumber(Cs, 8, Line, Col+1, St);
scan_hash1([C|Cs], Line, Col, [], St) when (C =:= $d) or (C =:= $D) ->
    scan_bnumber(Cs, 10, Line, Col+1, St);
scan_hash1([C|Cs], Line, Col, [], St) when (C =:= $x) or (C =:= $X) ->
    scan_bnumber(Cs, 16, Line, Col+1, St);
scan_hash1([C|Cs], Line, Col, Digits, St) when (C =:= $r) or (C =:= $R) ->
    Base = list_to_integer([$0|Digits]),
    if Base >=2, Base =< 36 ->
            scan_bnumber(Cs, Base, Line, Col+1, St);
       true ->
            scan_error({bad_format,"#r"}, Line, Col, Line, Col+1, Cs)
    end;
scan_hash1(Cs, Line, Col, Digits, St) ->
    %% Pass the buck!
    scan_hash2(Cs, Line, Col, Digits, St).

%% scan_hash1([_,_|_]=Cs, Line, Col, Digits, St) ->
%%     %% We have two characters and can now go on.
%%     scan_hash2(Cs, Line, Col, Digits, St);
%% scan_hash1([_]=Cs, Line, Col, Digits, St) ->
%%     %% We now need more characters to go on.
%%     {more,{Cs,Line,Col,St,Digits,fun scan_hash2_fun/5}};

%% scan_hash1(eof=Cs, Line, Col, _Digits, _St) ->
%%     scan_error({illegal_chars,"#"}, Line, Col, Line, Col+1, Cs).

scan_hash2_fun(Cs, Line, Col, St, Digits) ->
    scan_hash2(Cs, Line, Col, Digits, St).

%% Check that we don't just have one character.
scan_hash2([_]=Cs, Line, Col, Digits, St) ->
    {more,{Cs,Line,Col,St,Digits,fun scan_hash2_fun/5}};
%% The tokens where we need two characters.
scan_hash2([$\\,C|Cs], Line, Col, [], St) ->
    {ok,{number,Line,C},Cs,Line,Col+2,St};
scan_hash2([$,,$@|Cs], Line, Col, [], St) ->    %Get this before #,
    {ok,{'#,@',Line},Cs,Line,Col+2,St};
scan_hash2([$,|Cs], Line, Col, [], St) ->
    {ok,{'#,',Line},Cs,Line,Col+1,St};
scan_hash2([C,$\(|Cs], Line, Col, [], St) when (C =:= $m) or (C =:= $M) ->
    {ok,{'#M(',Line},Cs,Line,Col+2,St};
scan_hash2([C,$\(|Cs], Line, Col, [], St) when (C =:= $s) or (C =:= $S) ->
    {ok,{'#S(',Line},Cs,Line,Col+2,St};
%% scan_hash2([C,$\(|Cs], Line, Col, [], St) when (C =:= $f) or (C =:= $F) ->
%%     {ok,{'#F(',Line},Cs,Line,Col+2,St};
%% Scan binary tokens, these must come before the based number.
scan_hash2([C,$\(|Cs], Line, Col, [], St) when (C =:= $b) or (C =:= $B) ->
    {ok,{'#B(',Line},Cs,Line,Col,St};
%% Scan based numbers, these must come after binary tokens.
scan_hash2([C,C1|Cs], Line, Col, [], St) when
      (C =:= $b) or (C =:= $B) andalso C1 =/= $( ->
    %% Must not clash with #b(
    scan_bnumber([C1|Cs], 2, Line, Col+1, St);
%% Eof, other chars or illegal digits, so we are really done!
scan_hash2([C|Cs], Line, Col, _Digits, _St) ->
    scan_error({illegal_token,[$#,C]}, Line, Col, Line, Col+1, Cs);
scan_hash2(eof, Line, Col, _Digits, _St) ->
    scan_error({illegal_token,"#"}, Line, Col, Line, Col+1, eof).

%% scan_fun(Chars, Line, Column, State) ->
%%     {ok,Token,Chars,Line,Column} | {more,Continuation} | ScanError.
%%  Scan a fun symbol.

scan_fun(Cs, Line, Col, St) ->
    scan_fun1(Cs, Line, Col, [], St).

scan_fun1_fun(Cs, Line, Col, St, Symcs) ->
    scan_fun1(Cs, Line, Col, Symcs, St).

scan_fun1([C|Cs], Line, Col, Symcs, St) ->
    case symbol_char(C) of
        true ->
            scan_fun1(Cs, Line, Col+1, Symcs ++ [C], St);
        false ->
            scan_fun_ret(Symcs, [C|Cs], Line, Col, St)
    end;
scan_fun1([]=Cs, Line, Col, Symcs, St) ->
    {more,{Cs,Line,Col,St,Symcs,fun scan_fun1_fun/5}};
scan_fun1(eof, Line, Col, Symcs, St) ->
    scan_fun_ret(Symcs, eof, Line, Col, St).

scan_fun_ret(Symcs, Cs, Line, Col, St) ->
    case lists:splitwith(fun (C) -> C =/= $/ end, Symcs) of
        {_Pre,[]} ->
            scan_error({bad_format,[$#,$'|Symcs]}, Line, Col, Line, Col, Cs);
        {Pre,[$/|Ds]=After} ->
            Field = Pre ++ After,
            %% io:format("sfr ~p\n", [{Symcs,Field}]),
            case lists:any(fun (C) -> not ?DIGIT(C) end, Ds) of
                true ->
                    scan_error({bad_format,[$#,$'|Symcs]},
                               Line, Col, Line, Col, Cs);
                false ->
                    Token = {'#\'',Line,Field},
                    {ok,Token,Cs,Line,Col,St}
            end
    end.

%% scan_bnumber(Chars, Base, Line, Column, State) ->
%%     {ok,Token,Chars,Line,Column} | {more,Continuation} | ScanError.
%%  Scan a based number. We stay compatible and scan all symbol
%%  characters then check then build and check.

scan_bnumber(Cs, Base, Line, Col, St) ->
    scan_bnumber_sign(Cs, Line, Col, Base, St).

scan_bnumber_sign_fun(Cs, Line, Col, St, Base) ->
    scan_bnumber_sign(Cs, Line, Col, Base, St).
    
scan_bnumber_sign([$+|Cs], Line, Col, Base, St) ->
    scan_bnumber_digits(Cs, Line, Col, [], Base, +1, St);
scan_bnumber_sign([$-|Cs], Line, Col, Base, St) ->
    scan_bnumber_digits(Cs, Line, Col, [], Base, -1, St);
scan_bnumber_sign([], Line, Col, Base, St) ->
    {more,{[],Line,Col,St,Base,fun scan_bnumber_sign_fun/5}};
scan_bnumber_sign(Cs, Line, Col, Base, St) ->
    scan_bnumber_digits(Cs, Line, Col, [], Base, +1, St).

scan_bnumber_digits_fun(Cs, Line, Col, St, {Digits,Base,Sign}) ->
    scan_bnumber_digits(Cs, Line, Col, Digits, Base, Sign, St).

scan_bnumber_digits([C|Cs], Line, Col, Digits, Base, Sign, St) ->
    case symbol_char(C) of
        true ->
            scan_bnumber_digits(Cs, Line, Col+1, Digits++[C], Base, Sign, St);
        false ->
            scan_bnumber_check([C|Cs], Line, Col, Digits, Base, Sign, St)
    end;
scan_bnumber_digits([]=Cs, Line, Col, Digits, Base, Sign, St) ->
    {more,{Cs,Line,Col,St,{Digits,Base,Sign},fun scan_bnumber_digits_fun/5}};
scan_bnumber_digits(eof=Cs, Line, Col, Digits, Base, Sign, St) ->
    scan_bnumber_check(Cs, Line, Col, Digits, Base, Sign, St).

scan_bnumber_check(Cs, Line, Col, [], _Base, _Sign, _St) ->
    scan_error({bad_format,"based number"}, Line, Col, Line, Col, Cs);
scan_bnumber_check(Cs, Line, Col, Digits, Base, Sign, St) ->
    case base_collect_chars(Digits, Base, 0) of
        {yes,Number} ->
            Token = {number,Line,Sign*Number},
            {ok,Token,Cs,Line,Col,St};
        no ->
            scan_error({illegal_chars,"based number"}, Line, Col, Line, Col, Cs)
    end.

%% base_collect_chars(Chars, Base, NumberSoFar) -> {yes,Number} | no.
%% base_collect_char(Char, Base, NumberSoFar) -> {yes,Number} | no.
%%  Check if Char is number of valid base and if so build new number
%%  with SoFar.

base_collect_chars([C|Cs], Base, SoFar) ->
    if C >= $0, C =< $9, C < Base + $0 ->
            Next = SoFar * Base + (C - $0),
            base_collect_chars(Cs, Base, Next);
       C >= $a, C =< $z, C < Base + $a - 10 ->
            Next = SoFar * Base + (C - $a + 10),
            base_collect_chars(Cs, Base, Next);
       C >= $A, C =< $Z, C < Base + $A - 10 ->
            Next = SoFar * Base + (C - $A + 10),
            base_collect_chars(Cs, Base, Next);
       true -> no
    end;
base_collect_chars([], _Base, SoFar) ->
    {yes,SoFar}.

%% scan_symbol(Chars, Line, Column, State) ->
%%     {ok,Token,Chars,Line,Column} | {more,Continuation} | ScanError.
%%  Scan unquoted "symbols". This is a little tricky as numbers will
%%  also match here. We try to solve this by scanning in all the
%%  matching characters and then first checking whether it is a
%%  number, otherwise it is a symbol.

scan_symbol(Cs, Line, Col, St) ->
    scan_symbol1(Cs, Line, Col, [], St).

scan_symbol1_fun(Cs, Line, Col, St, Symcs) ->
    scan_symbol1(Cs, Line, Col, Symcs, St).

scan_symbol1([C|Cs], Line, Col, Symcs, St) ->
    case symbol_char(C) of
        true ->
            scan_symbol1(Cs, Line, Col+1, Symcs ++ [C], St);
        false ->
            %% Check whether we have an integer, a float or a symbol.
            Token = make_symbol_token(Symcs, Line),
            {ok,Token,[C|Cs],Line,Col+1,St}
    end;
scan_symbol1([]=Cs, Line, Col, Symcs, St) ->
    {more,{Cs,Line,Col,St,Symcs,fun scan_symbol1_fun/5}};
scan_symbol1(eof=Cs, Line, Col, Symcs, St) ->
    Token = make_symbol_token(Symcs, Line),
    {ok,Token,Cs,Line,Col,St}.

make_symbol_token(Chars, Line) ->
    try
        Integer = list_to_integer(Chars),
        {number,Line,Integer}
    catch
        _:_ ->
            try
                Float = list_to_float(Chars),
                {number,Line,Float}
            catch
                _:_ ->
                    Symbol = list_to_atom(Chars),
                    {symbol,Line,Symbol}
            end
    end.

%% start_symbol_char(Char) -> true | false.
%% symbol_char(Char) -> true | false.
%%  Define start symbol chars and symbol chars.

%% start_symbol_char($^) -> false;                 %These 2 are for test cases
%% start_symbol_char($&) -> false;
start_symbol_char($#) -> false;
start_symbol_char($`) -> false;
start_symbol_char($') -> false;                 %'
start_symbol_char($,) -> false;
start_symbol_char($|) -> false;                 %Symbol quote character
start_symbol_char(C) -> symbol_char(C).

symbol_char($() -> false;
symbol_char($)) -> false;
symbol_char($[) -> false;
symbol_char($]) -> false;
symbol_char(${) -> false;
symbol_char($}) -> false;
symbol_char($") -> false;
symbol_char($;) -> false;
symbol_char(C) -> ((C > $\s) and (C =< $~)) orelse (C > $\240).

%% scan_qsymbol(Chars, Line, Column, State) ->
%%     {ok,Token,Chars,Line,Column} | {more,Continuation} | ScanError.
%%  Scan quoted symbols.

scan_qsymbol(Cs, Line, Col, St) ->
    scan_qsymbol1(Cs, Line, Col, Line, Col, [], St).

scan_qsymbol1_fun(Cs, Line, Col, St, {Sline,Scol,Symcs}) ->
    scan_qsymbol1(Cs, Line, Col, Sline, Scol, Symcs, St).

scan_qsymbol1([$\\,C|Cs], Line, Col, Sline, Scol, Symcs, St) ->
    %% Take the quoted character as is.
    scan_qsymbol1(Cs, Line, Col+2, Sline, Scol, Symcs ++ [C], St);
scan_qsymbol1([$||Cs], Line, Col, Sline, _Scol, Symcs, St) ->
    Qsymb = list_to_atom(Symcs),
    {ok,{symbol,Sline,Qsymb},Cs,Line,Col+1,St};
scan_qsymbol1([$\n|Cs], Line, _Col, Sline, Scol, Symcs, St) ->
    scan_qsymbol1(Cs, Line+1, 0, Sline, Scol, Symcs ++ [$\n], St);
scan_qsymbol1([C|Cs], Line, Col, Sline, Scol, Symcs, St) when C =/= $\\ ->
    scan_qsymbol1(Cs, Line, Col+1, Sline, Scol, Symcs ++ [C], St);
scan_qsymbol1(Cs, Line, Col, Sline, Scol, Symcs, St) when Cs =/= eof ->
    {more,{Cs,Line,Col,St,{Sline,Scol,Symcs},fun scan_qsymbol1_fun/5}};
scan_qsymbol1(eof, Line, Col, Sline, Scol, Symcs, _St) ->
    scan_error({illegal_chars,[$| | Symcs]}, Line, Col, Sline, Scol, eof).

%% scan_string(Chars, Line, Column, State) ->
%%     {ok,Token,Chars,Line,Column} | {more,Continuation} | ScanError.
%% scan_binary_string(Chars, Line, Column, State) ->
%%     {ok,Token,Chars,Line,Column} | {more,Continuation} | ScanError.
%%  Scan strings. We have not incremented Col.

scan_string(Cs, Line, Col, St) ->
    scan_string(Cs, Line, Col, string, St).

scan_binary_string(Cs, Line, Col, St) ->
    scan_string(Cs, Line, Col, binary, St).

%% scan_string(Chars, Line, Column, Type, State) ->
%%     {ok,Token,Chars,Line,Column} | {more,Continuation} | ScanError.
%%  Find which string type to use. Note the first " has already been
%%  scanned. Here we work out if is a normal string or a triple quote
%%  string and then pass the buck.

scan_string(Cs, Line, Col, Type, St) ->
    scan_string1(Cs, Line, Col, Type, St).
    
scan_string1_fun(Cs, Line, Col, St, Type) ->
    scan_string1(Cs, Line, Col, Type, St).

scan_string1([C|_]=Cs, Line, Col, Type, St) when C =/= $" ->
    %% Scan a normal single quote string.
    scan_sq_string(Cs, Line, Col, Type, St);
scan_string1([$",$"|Cs], Line, Col, Type, St) ->
    %% This is a triple quoted string.
    scan_tq_string(Cs, Line, Col+3, Line, Col, Type, St);
scan_string1([$",C|_]=Cs, Line, Col, Type, St) when C =/= $" ->
    %% This is a empty normal string which we pass on.
    scan_sq_string(Cs, Line, Col, Type, St);
scan_string1([$"]=Cs, Line, Col, Type, St) ->
    %% We don't know yet.
    {more,{Cs,Line,Col,St,Type,fun scan_string1_fun/5}};
scan_string1([], Line, Col, Type, St) ->
    %% We need more to decide what to do.
    {more,{[],Line,Col,St,Type,fun scan_string1_fun/5}};
scan_string1(Cs, Line, Col, _Type, _St) ->
    scan_error({illegal_chars,[$"]}, Line, Col, Line, Col, Cs).

    %% This is a normal string which we pass on.
    %% scan_sq_string(Cs, Line, Col, Type, St).

%% scan_sq_string(Chars, Line, Column, Type, State) ->
%%     {ok,Token,Chars,Line,Column} | ScanError | ScanMore.
%%  Single quote "normal" strings. Note that the first " has already
%%  been scanned.

scan_sq_string(Chars, Line, Col, Type, St) ->
    scan_sq_string1(Chars, Line, Col+1, Line, Col, [], Type, St).

scan_sq_string1_fun(Cs, Line, Col, St, {Sline,Scol,Symcs,Type}) ->
    scan_sq_string1(Cs, Line, Col, Sline, Scol, Symcs, Type, St).

scan_sq_string1([$\\,C|Cs], Line, Col, Sline, Scol, Symcs, Type, St) ->
    scan_sq_string1(Cs, Line, Col+2, Sline, Scol, 
                    Symcs ++ [escape_char(C)], Type, St);
scan_sq_string1([$\\]=Cs, Line, Col, Sline, Scol, Symcs, Type, St) ->
    {more,{Cs,Line,Col,St,{Sline,Scol,Symcs,Type},fun scan_sq_string1_fun/5}};
scan_sq_string1([$"|Cs], Line, Col, Sline, _Scol, Symcs, Type, St) ->
    %% We have the string.
    Token = string_token(Symcs, Sline, Type),
    {ok,Token,Cs,Line,Col+1,St};
scan_sq_string1([$\n|Cs], Line, _Col, Sline, Scol, Symcs, Type, St) ->
    scan_sq_string1(Cs, Line+1, 1, Sline, Scol, Symcs ++ [$\n], Type, St);
scan_sq_string1([C|Cs], Line, Col, Sline, Scol, Symcs, Type, St) ->
    scan_sq_string1(Cs, Line, Col+1, Sline, Scol, Symcs ++ [C], Type, St);
scan_sq_string1([]=Cs, Line, Col, Sline, Scol, Symcs, Type, St) ->
    {more,{Cs,Line,Col,St,{Sline,Scol,Symcs,Type},fun scan_sq_string1_fun/5}};
scan_sq_string1(eof=Cs, Line, Col, Sline, Scol, Symcs, _Type, _St) ->
    scan_error({illegal_chars,[$" | Symcs]}, Line, Col, Sline, Scol, Cs).

escape_char($b) -> $\b;                %\b = BS
escape_char($t) -> $\t;                %\t = TAB
escape_char($n) -> $\n;                %\n = LF
escape_char($v) -> $\v;                %\v = VT
escape_char($f) -> $\f;                %\f = FF
escape_char($r) -> $\r;                %\r = CR
escape_char($e) -> $\e;                %\e = ESC
escape_char($s) -> $\s;                %\s = SPC
escape_char($d) -> $\d;                %\d = DEL
escape_char(C) -> C.                   %\Other = Other

string_token(String, StartLine, Type) ->
    case Type of
        string -> {string,StartLine,String};
        binary ->
            Binary = unicode:characters_to_binary(String, utf8, utf8),
            {binary,StartLine,Binary}
    end.

%% scan_tq_string(Chars, Line, Column, StartLine, StartCol, Type, State) ->
%%     {ok,Token,Char,Line,Column} | {more,Continuation} | ScanError.
%%  Scan triple quoted strings. Note that the first """ has already
%%  been scanned.

scan_tq_string(Chars, Line, Col, StartLine, StartCol, Type, State) ->
    scan_tq_string_1(Chars, Line, Col, StartLine, StartCol, Type, State).

%% scan_tq_string_1(Chars, Line, Column, StartLine, StartCol, Type, State) ->
%%     {ok,Token,Char,Line,Column} | {more,Continuation} | ScanError.
%%  Scan the first line and check the format.

scan_tq_string_1_fun(Cs, Line, Col, St, {Sline,Scol,Type}) ->
    scan_tq_string_1(Cs, Line, Col, Sline, Scol, Type, St).
    
scan_tq_string_1([$\s|Cs], Line, Col, Sline, Scol, Type, St) ->
    scan_tq_string_1(Cs, Line, Col, Sline, Scol, Type, St);
scan_tq_string_1([$\n|Cs], Line, _Col, Sline, Scol, Type, St) ->
    scan_tq_string_lines(Cs, Line+1, 1, Sline, Scol, [], [], Type, St);
scan_tq_string_1([]=Cs, Line, Col, Sline, Scol, Type, St) ->
    {more,{Cs,Line,Col,St,{Sline,Scol,Type},fun scan_tq_string_1_fun/5}};
scan_tq_string_1(Cs, Line, Col, Sline, Scol, _Type, _St) ->
    %% An error for illegal character or eof.
    scan_error(bad_tq_string, Line, Col, Sline, Scol, Cs).

%% scan_tq_string_lines(Chars, Line, Col, StartLine, StartCol, LineChars, Lines,
%%                      Type, State) ->
%%      {ok,Token,Char,Line,Column,State} | {more,Continuation} | ScanError.
%%  Scan and collect the following lines up to a valid end """.

scan_tq_string_lines_fun(Chars, Line, Col, St, {Sline,Scol,Lcs,Lines,Type}) ->
    scan_tq_string_lines(Chars, Line, Col, Sline, Scol, Lcs, Lines, Type, St).

scan_tq_string_lines([$\n|Cs], Line, _Col, Sline, Scol, Lcs, Lines, Type, St) ->
    scan_tq_string_lines(Cs, Line+1, 0, Sline, Scol,
                         [], Lines ++ [Lcs], Type, St);
scan_tq_string_lines([$"|Cs], Line, Col, Sline, Scol, Lcs, Lines, Type, St) -> 
    scan_tq_string_tq(Cs, Line, Col, Sline, Scol, Lcs, Lines, Type, St);
scan_tq_string_lines([C|Cs], Line, Col, Sline, Scol, Lcs, Lines, Type, St) ->
    scan_tq_string_lines(Cs, Line, Col, Sline, Scol,
                         Lcs ++ [C], Lines, Type, St);
scan_tq_string_lines([]=Cs, Line, Col, Sline, Scol, Lcs, Lines, Type, St) ->
    {more,{Cs,Line,Col,St,{Sline,Scol,Lcs,Lines,Type},
           fun scan_tq_string_lines_fun/5}};
scan_tq_string_lines(eof=Cs, Line, Col, Sline, Scol,
                     _Lcs, _Lines, _Type, _St) ->
    scan_error(bad_tq_string, Line, Col, Sline, Scol, Cs).

%% scan_tq_string_tq(Chars, Line, Col, StartLine, StartCol, LineChars, Lines,
%%                   Type, State) ->
%%      {ok,Token,Char,Line,Column,State} | {more,Continuation} | ScanError.
%%  Check if we have valid end of the """ or whether we must go
%%  on. Note that the first " has already been scanned.

scan_tq_string_tq_fun(Cs, Line, Col, St, {Sline,Scol,Lcs,Lines,Type}) ->
    scan_tq_string_tq(Cs, Line, Col, Sline, Scol, Lcs, Lines, Type, St).

scan_tq_string_tq([$",$"|Cs], Line, Col, Sline, Scol, Lcs, Lines, Type, St) ->
    %% This is a triple quote, check if this is a valid end line.
    case blank_line(Lcs) of
        true ->
            scan_tq_string_end(Cs, Line, Col+3, Sline, Scol,
                               Lcs, Lines, Type, St);
        false ->
            scan_tq_string_lines(Cs, Line, Col+3, Sline, Scol, 
                                 Lcs ++ [$",$",$"], Lines, Type, St)
    end;
scan_tq_string_tq([$",C|Cs], Line, Col, Sline, Scol, Lcs, Lines, Type, St) when
      C =/= $" ->
    %% This is not a triple quote here, it is a normal line. So we
    %% pass the buck, but don't forget the ".
    scan_tq_string_lines([C|Cs], Line, Col, Sline, Scol,
                         Lcs ++ [$"], Lines, Type, St);
scan_tq_string_tq([$"]=Cs, Line, Col, Sline, Scol, Lcs, Lines, Type, St) ->
    {more,{Cs,Line,Col,St,{Sline,Scol,Lcs,Lines,Type},
           fun scan_tq_string_tq_fun/5}};
scan_tq_string_tq([]=Cs, Line, Col, Sline, Scol, Lcs, Lines, Type, St) ->
    {more,{Cs,Line,Col,St,{Sline,Scol,Lcs,Lines,Type},
           fun scan_tq_string_tq_fun/5}};
scan_tq_string_tq(eof, Line, Col, Sline, Scol, _Lcs, _Lines, _Type, _St) ->
    scan_error(bad_tq_string, Line, Col, Sline, Scol, eof);
scan_tq_string_tq(Cs, Line, Col, Sline, Scol, Lcs, Lines, Type, St) ->
    %% This is not a triple quote here, it is a normal line. So we
    %% pass the buck, but don't forget the ".
    scan_tq_string_lines(Cs, Line, Col, Sline, Scol,
                         Lcs ++ [$"], Lines, Type, St).

%% scan_tq_string_end(Chars, Line, Col, StartLine, StartCol, Prefix, Lines,
%%                    Type, State) ->
%%      {ok,Token,Char,Line,Column,State} | ScanError.
%%  Check if we have a valid end line.

scan_tq_string_end(Cs, Line, Col, Sline, _Scol, _Prefix, [], Type, St) ->
    %% No lines so it just is empty.
    Token = string_token([], Sline, Type),
    {ok,Token,Cs,Line,Col,St};
scan_tq_string_end(Cs, Line, Col, Sline, Scol, Prefix, Lines, Type, St) ->
    %% Here we will check what we have.
    case collect_tqstring_lines(Lines, Prefix, []) of
        {yes,CheckedLines} ->
            %% Skip the leading newline added by the fold.
            [_Lc|String] = lists:foldr(fun (Lcs, Scs) -> [$\n|Lcs] ++ Scs end,
                                       [], CheckedLines),
            Token = string_token(String, Sline, Type),
            {ok,Token,Cs,Line,Col,St};
        no ->
            %% io:format("ls ~p\n", [{ste,Cs,Sline,Line,Lines,Prefix}]),
            scan_error(bad_tq_string, Line, Col, Sline, Scol, Cs)
    end.

blank_line(Cs) ->
    lists:all(fun (C) -> C =:= $\s end, Cs).

collect_tqstring_lines([Lcs|Lines], Prefix, Plines) ->
    case check_tqstring_prefix(Prefix, Lcs) of
        {yes,Lcs1} ->
            collect_tqstring_lines(Lines, Prefix, Plines ++ [Lcs1]);
        no -> no
    end;
collect_tqstring_lines([], _Prefix, Plines) ->
    {yes,Plines}.

check_tqstring_prefix([P|Ps], [C|Lcs]) ->
    if P =:= C ->
            check_tqstring_prefix(Ps, Lcs);
       true -> no
    end;
check_tqstring_prefix([], Lcs) ->
    {yes,Lcs}.

%% scan_error(Error, StartLine, StartCol, EndLine, EndCol, RestChars) ->
%%     {Error,Rest}.

scan_error(Error, Line, _Col, EndLine, _EndCol, Rest) ->
    Loc = Line,                                 %location(Line, Col)
    EndLoc = EndLine,                           %location(EndLine, EndCol)
    scan_error(Error, Loc, EndLoc, Rest).

scan_error(Error, ErrorLoc, EndLoc, Rest) ->
    {{error,{ErrorLoc,?MODULE,Error},EndLoc},Rest}.

%% location(Line, no_col) ->
%%     Line;
%% location(Line, Col) when is_integer(Col) ->
%%     {Line,Col}.

%% -compile({inline,[anno/1,incr_column/2,new_column/2,int_column/1]}).

anno(Location) ->
    erl_anno:new(Location).

%% token_test(Chars) -> {done,Done,Rest}.
%% token_test(Continuation, Chars) -> {done,Done,Rest}.
%% tokens_test(Chars) -> {done,Done,Rest}.
%% tokens_test(Continuation, Chars) -> {done,Done,Rest}.
%%  Carefully test the token(s) by steping over them one character at
%%  a time. This guarantees that they can handle the input string
%%  safely and correctly.

token_test(Cs) ->
    token_test([], Cs).

token_test(Cont0, [C|Cs]) ->
    case lfe_scan:token(Cont0, [C]) of
        {more,Cont1} ->
            token_test(Cont1, Cs);
        {done,_Done,_Rest}=Done ->
            Done
    end;
token_test(Cont, []) ->
    io:format("tt ~p\n", [Cont]),
    lfe_scan:token(Cont, eof).

tokens_test(Cs) ->
    tokens_test([], Cs).

tokens_test(Cont0, [C|Cs]) -> 
    case lfe_scan:tokens(Cont0, [C]) of
        {more,Cont1} ->
            tokens_test(Cont1, Cs);
        {done,_Done,_Rest}=Done ->
            Done
    end;
tokens_test(Cont, []) ->
    io:format("tst ~p\n", [Cont]),
    lfe_scan:tokens(Cont, eof).
