%% 多进程编译,修改自otp/lib/tools/src/make.erl
%% 解析Emakefile,根据获取{mods, options}列表,
%% 按照次序编译每项(解决编译顺序的问题)
%% 其中mods也可以包含多个模块,当大于1个时,
%% 可以启动多个process进行编译,从而提高编译速度.

-module(utMMake).
-include_lib("kernel/include/file.hrl").

-export([
   all/1,
   all/2,
   files/2,
   files/3
]).

-define(MakeOpts, [noexec, load, netload, noload]).

all(Worker) when is_integer(Worker) ->
   all(Worker, []).

all(Worker, Options) when is_integer(Worker) ->
   {MakeOpts, CompileOpts} = sort_options(Options, [], []),
   case read_emakefile('Emakefile', CompileOpts) of
      Files when is_list(Files) ->
         do_make_files(Worker, Files, MakeOpts);
      error ->
         error
   end.

files(Worker, Fs) ->
   files(Worker, Fs, []).

files(Worker, Fs0, Options) ->
   Fs = [filename:rootname(F, ".erl") || F <- Fs0],
   {MakeOpts, CompileOpts} = sort_options(Options, [], []),
   case get_opts_from_emakefile(Fs, 'Emakefile', CompileOpts) of
      Files when is_list(Files) ->
         do_make_files(Worker, Files, MakeOpts);
      error -> error
   end.

do_make_files(Worker, Fs, Opts) ->
   %io:format("worker:~p~nfs:~p~nopts:~p~n", [Worker, Fs, Opts]),
   process(Fs, Worker, lists:member(noexec, Opts), load_opt(Opts)).

sort_options([H | T], Make, Comp) ->
   case lists:member(H, ?MakeOpts) of
      true ->
         sort_options(T, [H | Make], Comp);
      false ->
         sort_options(T, Make, [H | Comp])
   end;
sort_options([], Make, Comp) ->
   {Make, lists:reverse(Comp)}.

%%% Reads the given Emakefile and returns a list of tuples: {Mods,Opts}
%%% Mods is a list of module names (strings)
%%% Opts is a list of options to be used when compiling Mods
%%%
%%% Emakefile can contain elements like this:
%%% Mod.
%%% {Mod,Opts}.
%%% Mod is a module name which might include '*' as wildcard
%%% or a list of such module names
%%%
%%% These elements are converted to [{ModList,OptList},...]
%%% ModList is a list of modulenames (strings)
read_emakefile(Emakefile, Opts) ->
   case file:consult(Emakefile) of
      {ok, Emake} ->
         transform(Emake, Opts, [], []);
      {error, enoent} ->
         %% No Emakefile found - return all modules in current
         %% directory and the options given at command line
         Mods = [filename:rootname(F) || F <- filelib:wildcard("*.erl")],
         [{Mods, Opts}];
      {error, Other} ->
         io:format("make: Trouble reading 'Emakefile':~n~p~n", [Other]),
         error
   end.

transform([{Mod, ModOpts} | Emake], Opts, Files, Already) ->
   case expand(Mod, Already) of
      [] ->
         transform(Emake, Opts, Files, Already);
      Mods ->
         transform(Emake, Opts, [{Mods, ModOpts ++ Opts} | Files], Mods ++ Already)
   end;
transform([Mod | Emake], Opts, Files, Already) ->
   case expand(Mod, Already) of
      [] ->
         transform(Emake, Opts, Files, Already);
      Mods ->
         transform(Emake, Opts, [{Mods, Opts} | Files], Mods ++ Already)
   end;
transform([], _Opts, Files, _Already) ->
   lists:reverse(Files).

expand(Mod, Already) when is_atom(Mod) ->
   expand(atom_to_list(Mod), Already);
expand(Mods, Already) when is_list(Mods), not is_integer(hd(Mods)) ->
   lists:concat([expand(Mod, Already) || Mod <- Mods]);
expand(Mod, Already) ->
   case lists:member($*, Mod) of
      true ->
         Fun = fun(F, Acc) ->
            M = filename:rootname(F),
            case lists:member(M, Already) of
               true -> Acc;
               false -> [M | Acc]
            end
               end,
         lists:foldl(Fun, [], filelib:wildcard(Mod ++ ".erl"));
      false ->
         Mod2 = filename:rootname(Mod, ".erl"),
         case lists:member(Mod2, Already) of
            true -> [];
            false -> [Mod2]
         end
   end.

%%% Reads the given Emakefile to see if there are any specific compile
%%% options given for the modules.
get_opts_from_emakefile(Mods, Emakefile, Opts) ->
   case file:consult(Emakefile) of
      {ok, Emake} ->
         Modsandopts = transform(Emake, Opts, [], []),
         ModStrings = [coerce_2_list(M) || M <- Mods],
         get_opts_from_emakefile2(Modsandopts, ModStrings, Opts, []);
      {error, enoent} ->
         [{Mods, Opts}];
      {error, Other} ->
         io:format("make: Trouble reading 'Emakefile':~n~p~n", [Other]),
         error
   end.

get_opts_from_emakefile2([{MakefileMods, O} | Rest], Mods, Opts, Result) ->
   case members(Mods, MakefileMods, [], Mods) of
      {[], _} ->
         get_opts_from_emakefile2(Rest, Mods, Opts, Result);
      {I, RestOfMods} ->
         get_opts_from_emakefile2(Rest, RestOfMods, Opts, [{I, O} | Result])
   end;
get_opts_from_emakefile2([], [], _Opts, Result) ->
   Result;
get_opts_from_emakefile2([], RestOfMods, Opts, Result) ->
   [{RestOfMods, Opts} | Result].

members([H | T], MakefileMods, I, Rest) ->
   case lists:member(H, MakefileMods) of
      true ->
         members(T, MakefileMods, [H | I], lists:delete(H, Rest));
      false ->
         members(T, MakefileMods, I, Rest)
   end;
members([], _MakefileMods, I, Rest) ->
   {I, Rest}.


%% Any flags that are not recognixed as make flags are passed directly
%% to the compiler.
%% So for example make:all([load,debug_info]) will make everything
%% with the debug_info flag and load it.
load_opt(Opts) ->
   case lists:member(netload, Opts) of
      true ->
         netload;
      false ->
         case lists:member(load, Opts) of
            true ->
               load;
            _ ->
               noload
         end
   end.

%% 处理
process([{[], _Opts} | Rest], Worker, NoExec, Load) ->
   process(Rest, Worker, NoExec, Load);
process([{L, Opts} | Rest], Worker, NoExec, Load) ->
   Len = length(L),
   Worker2 = erlang:min(Len, Worker),
   case catch do_worker(L, Opts, NoExec, Load, Worker2) of
      error ->
         error;
      ok ->
         process(Rest, Worker, NoExec, Load)
   end;
process([], _Worker, _NoExec, _Load) ->
   up_to_date.

%% worker进行编译
%do_worker(L, Opts, NoExec, Load, Worker) ->
%    WorkerList = do_split_list(L, Worker),
%    io:format("worker:~p worker list(~p)~n,WorkerList:~p~n", [Worker, length(WorkerList)]),
%    % 启动进程
%    Ref = make_ref(),
%    Pids =
%    [begin
%        start_worker(E, Opts, NoExec, Load, self(), Ref)
%    end || E <- WorkerList],
%    do_wait_worker(length(Pids), Ref).
%
%%% 等待结果
%do_wait_worker(0, _Ref) ->
%    ok;
%do_wait_worker(N, Ref) ->
%    receive
%        {ack, Ref} ->
%            do_wait_worker(N - 1, Ref);
%        {error, Ref} ->
%            throw(error);
%        {'EXIT', _P, _Reason} ->
%            do_wait_worker(N, Ref);
%        _Other ->
%            io:format("receive unknown msg:~p~n", [_Other]),
%            do_wait_worker(N, Ref)
%    end.

do_worker(L, Opts, NoExec, Load, Worker) ->

   %% 先开启worker个编译进程
   SplitNum = min(length(L), Worker),
   {L1, L2} = lists:split(SplitNum, L),

   % 启动进程
   Ref = make_ref(),
   Pids =
      [begin
          start_worker([E], Opts, NoExec, Load, self(), Ref)
       end || E <- L1],
   do_wait_worker(length(Pids), L2, Opts, NoExec, Load, Ref).

%% @doc 一个文件编译完成后，立即进行下一个文件编译,不用等待
do_wait_worker(0, [], _Opts, _NoExec, _Load, _Ref) ->
   ok;
do_wait_worker(N, L, Opts, NoExec, Load, Ref) ->
   receive
      {ack, Ref} ->
         case L of
            [H | T] ->
               %% 未编译完成，编译后面的文件
               start_worker(H, Opts, NoExec, Load, self(), Ref),
               do_wait_worker(N, T, Opts, NoExec, Load, Ref);
            [] ->
               %% 等待所有结果返回
               do_wait_worker(N - 1, [], Opts, NoExec, Load, Ref)
         end;
      {error, Ref} ->
         throw(error);
      {'EXIT', _P, _Reason} ->
         do_wait_worker(N, L, Opts, NoExec, Load, Ref);
      _Other ->
         io:format("receive unknown msg:~p~n", [_Other]),
         do_wait_worker(N, L, Opts, NoExec, Load, Ref)
   end.

%% 将L分割成最多包含N个子列表的列表
%do_split_list(L, N) ->
%    Len = length(L),
%    % 每个列表的元素数
%    LLen = (Len + N - 1) div N,
%    do_split_list(L, LLen, []).
%
%do_split_list([], _N, Acc) ->
%    lists:reverse(Acc);
%do_split_list(L, N, Acc) ->
%    {L2, L3} = lists:split(erlang:min(length(L), N), L),
%    do_split_list(L3, N, [L2 | Acc]).
%
%%% 启动worker进程
%start_worker(L, Opts, NoExec, Load, Parent, Ref) ->
%    Fun =
%    fun() ->
%        [begin
%            case recompilep(coerce_2_list(F), NoExec, Load, Opts) of
%                error ->
%                    Parent ! {error, Ref},
%                    exit(error);
%                _ ->
%                    ok
%            end
%        end || F <- L],
%        Parent ! {ack, Ref}
%    end,
%    spawn_link(Fun).
%% 启动worker进程
start_worker(F, Opts, NoExec, Load, Parent, Ref) ->
   Fun =
      fun() ->
         case recompilep(coerce_2_list(F), NoExec, Load, Opts) of
            error ->
               Parent ! {error, Ref},
               exit(error);
            _ ->
               ok
         end,
         Parent ! {ack, Ref}
      end,
   spawn_link(Fun).

recompilep(File, NoExec, Load, Opts) ->
   ObjName = lists:append(filename:basename(File),
      code:objfile_extension()),
   ObjFile = case lists:keysearch(outdir, 1, Opts) of
                {value, {outdir, OutDir}} ->
                   filename:join(coerce_2_list(OutDir), ObjName);
                false ->
                   ObjName
             end,
   case exists(ObjFile) of
      true ->
         recompilep1(File, NoExec, Load, Opts, ObjFile);
      false ->
         recompile(File, NoExec, Load, Opts)
   end.

recompilep1(File, NoExec, Load, Opts, ObjFile) ->
   {ok, Erl} = file:read_file_info(lists:append(File, ".erl")),
   {ok, Obj} = file:read_file_info(ObjFile),
   recompilep1(Erl, Obj, File, NoExec, Load, Opts).

recompilep1(#file_info{mtime = Te},
   #file_info{mtime = To}, File, NoExec, Load, Opts) when Te > To ->
   recompile(File, NoExec, Load, Opts);
recompilep1(_Erl, #file_info{mtime = To}, File, NoExec, Load, Opts) ->
   recompile2(To, File, NoExec, Load, Opts).

%% recompile2(ObjMTime, File, NoExec, Load, Opts)
%% Check if file is of a later date than include files.
recompile2(ObjMTime, File, NoExec, Load, Opts) ->
   IncludePath = include_opt(Opts),
   case check_includes(lists:append(File, ".erl"), IncludePath, ObjMTime) of
      true ->
         recompile(File, NoExec, Load, Opts);
      false ->
         false
   end.

include_opt([{i, Path} | Rest]) ->
   [Path | include_opt(Rest)];
include_opt([_First | Rest]) ->
   include_opt(Rest);
include_opt([]) ->
   [].

%% recompile(File, NoExec, Load, Opts)
%% Actually recompile and load the file, depending on the flags.
%% Where load can be netload | load | noload

recompile(File, true, _Load, _Opts) ->
   io:format("Out of date: ~s\n", [File]);
recompile(File, false, noload, Opts) ->
   io:format("Recompile: ~s\n", [File]),
   compile:file(File, [report_errors, report_warnings, error_summary | Opts]);
recompile(File, false, load, Opts) ->
   io:format("Recompile: ~s\n", [File]),
   c:c(File, Opts);
recompile(File, false, netload, Opts) ->
   io:format("Recompile: ~s\n", [File]),
   c:nc(File, Opts).

exists(File) ->
   case file:read_file_info(File) of
      {ok, _} ->
         true;
      _ ->
         false
   end.

coerce_2_list(X) when is_atom(X) ->
   atom_to_list(X);
coerce_2_list(X) ->
   X.

%%% If you an include file is found with a modification
%%% time larger than the modification time of the object
%%% file, return true. Otherwise return false.
check_includes(File, IncludePath, ObjMTime) ->
   Path = [filename:dirname(File) | IncludePath],
   case epp:open(File, Path, []) of
      {ok, Epp} ->
         check_includes2(Epp, File, ObjMTime);
      _Error ->
         false
   end.

check_includes2(Epp, File, ObjMTime) ->
   case epp:parse_erl_form(Epp) of
      {ok, {attribute, 1, file, {File, 1}}} ->
         check_includes2(Epp, File, ObjMTime);
      {ok, {attribute, 1, file, {IncFile, 1}}} ->
         case file:read_file_info(IncFile) of
            {ok, #file_info{mtime = MTime}} when MTime > ObjMTime ->
               epp:close(Epp),
               true;
            _ ->
               check_includes2(Epp, File, ObjMTime)
         end;
      {ok, _} ->
         check_includes2(Epp, File, ObjMTime);
      {eof, _} ->
         epp:close(Epp),
         false;
      {error, _Error} ->
         check_includes2(Epp, File, ObjMTime)
   end.