%%
%% %CopyrightBegin%
%%
%% Copyright Ericsson AB 1997-2013. All Rights Reserved.
%%
%% The contents of this file are subject to the Erlang Public License,
%% Version 1.1, (the "License"); you may not use this file except in
%% compliance with the License. You should have received a copy of the
%% Erlang Public License along with this software. If not, it can be
%% retrieved online at http://www.erlang.org/.
%%
%% Software distributed under the License is distributed on an "AS IS"
%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
%% the License for the specific language governing rights and limitations
%% under the License.
%%
%% %CopyrightEnd%
%%
-module(erl_compile).

-include("erl_compile.hrl").
-include("file.hrl").

-export([compile_cmdline/1]).

-export_type([cmd_line_arg/0]).

%% Mapping from extension to {M,F} to run the correct compiler.

compiler(".erl") ->    {compile,         compile};
compiler(".S") ->      {compile,         compile_asm};
compiler(".beam") ->   {compile,         compile_beam};
compiler(".core") ->   {compile,         compile_core};
compiler(".mib") ->    {snmpc,           compile};
compiler(".bin") ->    {snmpc,           mib_to_hrl};
compiler(".xrl") ->    {leex,            compile};
compiler(".yrl") ->    {yecc,            compile};
compiler(".script") -> {systools,        script2boot};
compiler(".rel") ->    {systools,        compile_rel};
compiler(".idl") ->    {ic,              compile};
compiler(".asn1") ->   {asn1ct,          compile_asn1};
compiler(".asn") ->    {asn1ct,          compile_asn};
compiler(".py") ->     {asn1ct,          compile_py};
compiler(_) ->         no.

%% Entry from command line.

-type cmd_line_arg() :: atom() | string().

-spec compile_cmdline([cmd_line_arg()]) -> no_return().

compile_cmdline(List) ->
    case compile(List) of
	ok -> my_halt(0);
	error -> my_halt(1);
	_ -> my_halt(2)
    end.

my_halt(Reason) ->
    erlang:halt(Reason).

%% Run the the compiler in a separate process, trapping EXITs.

compile(List) ->
    process_flag(trap_exit, true),
    Pid = spawn_link(fun() -> compiler_runner(List) end),
    receive
	{'EXIT', Pid, {compiler_result, Result}} ->
	    Result;
	{'EXIT', Pid, Reason} ->
	    io:format("Runtime error: ~tp~n", [Reason]),
	    error
    end.

-spec compiler_runner([cmd_line_arg()]) -> no_return().

compiler_runner(List) ->
    %% We don't want the current directory in the code path.
    %% Remove it.
    Path = [D || D <- code:get_path(), D =/= "."],
    true = code:set_path(Path),
    exit({compiler_result, compile1(List)}).

%% Parses the first part of the option list.

compile1(['@cwd', Cwd|Rest]) ->
    CwdL = atom_to_list(Cwd),
    compile1(Rest, CwdL, #options{outdir=CwdL, cwd=CwdL});
compile1(Args) ->
    %% From R13B02, the @cwd argument is optional.
    {ok, Cwd} = file:get_cwd(),
    compile1(Args, Cwd, #options{outdir=Cwd, cwd=Cwd}).

%% Parses all options.

compile1(['@i', Dir|Rest], Cwd, Opts) ->
    AbsDir = filename:absname(Dir, Cwd),
    compile1(Rest, Cwd, Opts#options{includes=[AbsDir|Opts#options.includes]});
compile1(['@outdir', Dir|Rest], Cwd, Opts) ->
    AbsName = filename:absname(Dir, Cwd),
    case file_or_directory(AbsName) of
	file ->
	    compile1(Rest, Cwd, Opts#options{outfile=AbsName});
	directory ->
	    compile1(Rest, Cwd, Opts#options{outdir=AbsName})
    end;
compile1(['@d', Name|Rest], Cwd, Opts) ->
    Defines = Opts#options.defines,
    compile1(Rest, Cwd, Opts#options{defines=[Name|Defines]});
compile1(['@dv', Name, Term|Rest], Cwd, Opts) ->
    Defines = Opts#options.defines,
    Value = make_term(atom_to_list(Term)),
    compile1(Rest, Cwd, Opts#options{defines=[{Name, Value}|Defines]});
compile1(['@warn', Level0|Rest], Cwd, Opts) ->
    case catch list_to_integer(atom_to_list(Level0)) of
	Level when is_integer(Level) ->
	    compile1(Rest, Cwd, Opts#options{warning=Level});
	_ ->
	    compile1(Rest, Cwd, Opts)
    end;
compile1(['@verbose', false|Rest], Cwd, Opts) ->
    compile1(Rest, Cwd, Opts#options{verbose=false});
compile1(['@verbose', true|Rest], Cwd, Opts) ->
    compile1(Rest, Cwd, Opts#options{verbose=true});
compile1(['@optimize', Atom|Rest], Cwd, Opts) ->
    Term = make_term(atom_to_list(Atom)),
    compile1(Rest, Cwd, Opts#options{optimize=Term});
compile1(['@option', Atom|Rest], Cwd, Opts) ->
    Term = make_term(atom_to_list(Atom)),
    Specific = Opts#options.specific,
    compile1(Rest, Cwd, Opts#options{specific=[Term|Specific]});
compile1(['@output_type', OutputType|Rest], Cwd, Opts) ->
    compile1(Rest, Cwd, Opts#options{output_type=OutputType});
compile1(['@files'|Rest], Cwd, Opts) ->
    Includes = lists:reverse(Opts#options.includes),
    compile2(Rest, Cwd, Opts#options{includes=Includes}).

compile2(Files, Cwd, Opts) ->
    case {Opts#options.outfile, length(Files)} of
	{"", _} ->
	    compile3(Files, Cwd, Opts);
	{[_|_], 1} ->
	    compile3(Files, Cwd, Opts);
	{[_|_], _N} ->
	    io:format("Output file name given, but more than one input file.~n"),
	    error
    end.

%% Compiles the list of files, until done or compilation fails.

compile3([File|Rest], Cwd, Options) ->
    Ext = filename:extension(File),
    Root = filename:rootname(File),
    InFile = filename:absname(Root, Cwd),
    OutFile =
	case Options#options.outfile of
	    "" ->
		filename:join(Options#options.outdir, filename:basename(Root));
	    Outfile ->
		filename:rootname(Outfile)
	end,
    case compile_file(Ext, InFile, OutFile, Options) of
	ok ->
	    compile3(Rest, Cwd, Options);
	Other ->
	    Other
    end;
compile3([], _Cwd, _Options) -> ok.

%% Invokes the appropriate compiler, depending on the file extension.

compile_file("", Input, _Output, _Options) ->
    io:format("File has no extension: ~ts~n", [Input]),
    error;
compile_file(Ext, Input, Output, Options) ->
    case compiler(Ext) of
	no ->
	    io:format("Unknown extension: '~ts'\n", [Ext]),
	    error;
	{M, F} ->
	    case catch M:F(Input, Output, Options) of
		ok -> ok;
		error -> error;
		{'EXIT',Reason} ->
		    io:format("Compiler function ~w:~w/3 failed:\n~p~n",
			      [M,F,Reason]),
		    error;
		Other ->
		    io:format("Compiler function ~w:~w/3 returned:\n~p~n",
			      [M,F,Other]),
		    error
	    end
    end.

%% Guesses if a give name refers to a file or a directory.

file_or_directory(Name) ->
    case file:read_file_info(Name) of
	{ok, #file_info{type=regular}} ->
	    file;
	{ok, _} ->
	    directory;
	{error, _} ->
	    case filename:extension(Name) of
		[] -> directory;
		_Other -> file
	    end
    end.

%% Makes an Erlang term given a string.

make_term(Str) -> 
    case erl_scan:string(Str) of
	{ok, Tokens, _} ->		  
	    case erl_parse:parse_term(Tokens ++ [{dot, 1}]) of
		{ok, Term} -> Term;
		{error, {_,_,Reason}} ->
		    io:format("~ts: ~ts~n", [Reason, Str]),
		    throw(error)
	    end;
	{error, {_,_,Reason}, _} ->
	    io:format("~ts: ~ts~n", [Reason, Str]),
	    throw(error)
    end.
