%% Copyright (c) 2008-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    : lfescript.erl
%%% Author  : Robert Virding
%%% Purpose : Lisp Flavoured Erlang scripting interface.

-module(lfescript).

%% External API.
-export([script_name/0]).

%% Internal API.
-export([start/0,start/1]).
-export([run/1,run/2]).

-include("lfe.hrl").

%% External API.

script_name() ->
    [Sname|_] = init:get_plain_arguments(),
    Sname.

%% Internal API.

-define(OK_STATUS, 0).
-define(ERROR_STATUS, 127).

-spec start() -> no_return().
-spec start(_Options) -> no_return().
-spec run(_CmdLine) -> no_return().
-spec run(_CmdLine, _Options) -> no_return().
%%  Evaluate the LFE script. All errors which are caught here are
%%  internal errors. Start gets its arguments from the command line
%%  while run gets them as an argument.

start() -> start([]).

start(Lopts) ->
    run(init:get_plain_arguments(), Lopts).

run(CmdLine) -> run(CmdLine, []).

run([File|Args], Lopts) ->
    try
        process_flag(trap_exit, false),
        parse_check_run(File, Args, Lopts)
    catch
        %% Catch program errors.
        throw:Str ->
            lfe_io:format("lfescript: ~s\n", [Str]),
            halt(?ERROR_STATUS);
        ?CATCH(_, Reason, Stack)
            lfe_io:format("lfescript: Internal error: ~p\n", [Reason]),
            lfe_io:format("~p\n", [Stack]),
            halt(?ERROR_STATUS)
    end;
run([], _) ->
    lfe_io:format("lfescript: Missing filename\n", []),
    halt(?ERROR_STATUS).

%% parse_check_run(FileName, Args, Options) -> no_return().
%%  Parse the script file, check the code, build a function
%%  environment and call the main function. This function never checks
%%  return values, it just assumes that when a script error is
%%  detected/occurs the functions will call halt(?ERROR_STATUS).
%%  Non-handled errors are program errors.

parse_check_run(File, Args, Lopts) ->
    Fs0 = parse_file(File, Args, Lopts),
    {Fs1,Fenv0} = expand_macros(Fs0, File, Args, Lopts),
    check_code(Fs1, File, Args, Lopts),
    lists:member("s", Lopts) andalso halt(?OK_STATUS),
    Fenv1 = make_env(Fs1, Fenv0, File, Args, Lopts),
    eval_code(Fenv1, File, Args, Lopts),
    halt(?OK_STATUS).                %Everything worked, just exit

error_exit(File, Es, Ws) ->
    list_errors(File, Es),
    list_warnings(File, Ws),
    halt(?ERROR_STATUS).

list_warnings(File, [{Line,Mod,Error}|Ws]) ->
    Cs = Mod:format_error(Error),
    lfe_io:format("~s:~w: Warning: ~s\n", [File,Line,Cs]),
    list_warnings(File, Ws);
list_warnings(_, []) -> ok.

list_errors(File, [{Line,Mod,Error}|Es]) ->
    Cs = Mod:format_error(Error),
    lfe_io:format("~s:~w: ~s\n", [File,Line,Cs]),
    list_errors(File, Es);
list_errors(_, []) -> ok.

%% parse_file(FileName, Args, Lopts) -> [{Expr,Line}].
%% parse_file(FileName) -> {ok,[{Expr,Line}]} | {error,Error}.
%%  This code is copied from lfe_io. We need to do that as we special
%%  case ignoring the first line.

parse_file(File, _, _) ->
    case parse_file(File) of
        {ok,Fs} -> Fs;
        {error,Error} ->
            error_exit(File, [Error], [])
    end.

parse_file(File) ->
    case file:open(File, [read]) of
        {ok,F} ->
            io:get_line(F, ''),                 %Skip first line
            case io:request(F, {get_until,unicode,'',lfe_scan,tokens,[2]}) of
                {ok,Ts,_} ->
                    Ret = parse_file1(Ts, [], []),
                    file:close(F),
                    Ret;
                {error,Error,_} -> {error,Error}
            end;
        {error,Error} -> {error,{none,file,Error}}
    end.

parse_file1([_|_]=Ts0, Pc0, Ss) ->
    case lfe_parse:sexpr(Pc0, Ts0) of
        {ok,L,S,Ts1} -> parse_file1(Ts1, [], [{S,L}|Ss]);
        {more,Pc1} ->
            %% Need more tokens but there are none, so call again to
            %% generate an error message.
            {error,E,_} = lfe_parse:sexpr(Pc1, {eof,99999}),
            {error,E};
        {error,E,_} -> {error,E}
    end;
parse_file1([], _, Ss) -> {ok,lists:reverse(Ss)}.

%% expand_macros(Forms, File, Args, Lopts) -> {Forms,Fenv}.

expand_macros(Fs0, File, _, _) ->
    case lfe_macro:expand_fileforms(Fs0, lfe_env:new(), true, false) of
        {ok,Fs1,Fenv,Ws} ->
            list_warnings(File, Ws),
            {Fs1,Fenv};
        {error,Es,Ws} -> error_exit(File, Es, Ws)
    end.

%% check_code(Forms, File, Args, Lopts) -> ok.
%%  Call lfe_lint to check the code. Must create a dummy module to
%%  make lfe_lint happy.

check_code(Fs, File, _, _) ->
    Module = [{['define-module',dummy,[],[[export,[main,1]]]],1}|Fs],
    case lfe_lint:module(Module) of
        {ok,dummy,Ws} ->
            list_warnings(File, Ws);
        {error,Es,Ws} -> error_exit(File, Es, Ws)
    end.

%% make_env(Forms, File, Args, Lopts) -> FunctionEnv.

make_env(Forms, Fenv, _, _, _) ->
    {Fbs,null} = lfe_lib:proc_forms(fun collect_function/3, Forms, null),
    lfe_eval:make_letrec_env(Fbs, Fenv).

collect_function(['define-function',F,_Meta,Def], _, St) ->
    Ar = function_arity(Def),
    {[{F,Ar,Def}],St};
%% Ignore everything else including types and eval-when-compile.
collect_function(_Form, _, St) ->
    {[],St}.


function_arity([lambda,As|_]) -> length(As);
function_arity(['match-lambda',[Pats|_]|_]) -> length(Pats).


%% eval_code(Fenv, File, Args, Lopts) -> Res.
%%  Evaluate the code. We must explicitly catch and handle errors in
%%  the script.

eval_code(Fenv, _, Args, _) ->
    try
        lfe_eval:expr([main,[quote,Args]], Fenv)
    catch
        %% Catch all exceptions in the code.
        ?CATCH(Class, Error, Stack)
            %% Report all errors.
            Skip = fun (_M, _F, _A) -> false end,
            Format = fun (T, I) -> lfe_io:prettyprint1(T, 15, I, 80) end,
            Cs = lfe_error:format_exception(Class, Error, Stack, 
                                            Skip, Format, 1),
            io:put_chars(Cs),
            halt(?ERROR_STATUS)
    end.
