%% @author Yariv Sadan <yarivsblog@gmail.com> [http://yarivsblog.com]
%% @copyright Yariv Sadan 2006-2007
%%
%% @doc  Smerl: Simple Metaprogramming for Erlang
%%
%%  Smerl is an Erlang library
%%  that simplifies the creation and manipulation of Erlang modules in
%%  runtime.
%%
%%  You don't need to know Smerl in order to use ErlyWeb; Smerl
%%  is included in ErlyWeb because ErlyWeb uses it internally.
%%
%%  Smerl uses Erlang's capabilities for hot code swapping and
%%  abstract syntax tree transformations to do its magic. Smerl is inspired by
%%  the rdbms_codegen.erl module in the RDBMS application written by
%%  Ulf Wiger. RDBMS is part of Jungerl ([http://jungerl.sf.net]).
%%
%%  Here's a quick example illustrating how to use Smerl:
%%  ```
%%  test_smerl() ->
%%    M1 = smerl:new(foo),
%%    {ok, M2} = smerl:add_func(M1, "bar() -> 1 + 1."),
%%    smerl:compile(M2),
%%    foo:bar(),   % returns 2``
%%    smerl:has_func(M2, bar, 0). % returns true
%%  '''
%%
%%  New functions can be expressed either as strings of Erlang code
%%  or as abstract forms. For more information, read the Abstract Format
%%  section in the ERTS User's guide
%%   ([http://erlang.org/doc/doc-5.5/erts-5.5/doc/html/absform.html#4]).
%%
%%  Using the abstract format, the 3rd line of the above example
%%   would be written as
%%   ```
%%     {ok,M2} = smerl:add_func(M1, {function,1,bar,0,
%%                              [{clause,1,[],[],
%%                               [{op,1,'+',{integer,1,1},{integer,1,1}}]}]).
%%   '''
%%
%%   <p>The abstact format may look more verbose in this example, but
%%   it's also easier to manipulate in code.</p>
%%

%% Copyright (c) Yariv Sadan 2006-2007
%%
%% Permission is hereby granted, free of charge, to any person
%% obtaining a copy of this software and associated documentation
%% files (the "Software"), to deal in the Software without restriction,
%% including without limitation the rights to use, copy, modify, merge,
%% publish, distribute, sublicense, and/or sell copies of the Software,
%% and to permit persons to whom the Software is furnished to do
%% so, subject to the following conditions:
%%
%% The above copyright notice and this permission notice shall be included
%% in all copies or substantial portions of the Software.
%%
%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
%% EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
%% MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
%% IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
%% CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
%% TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
%% SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.


-module(smerl).
-author("Yariv Sadan (yarivsblog@gmail.com, http://yarivsblog.com").
-export([new/1,
	 for_module/1,
	 for_module/2,
	 for_module/3,
	 for_file/1,
	 for_file/2, 
	 for_file/3,
         get_module/1,
	 set_module/2,
         get_forms/1,
	 set_forms/2,
         get_exports/1,
	 set_exports/2,
	 get_export_all/1,
	 set_export_all/2,
	 remove_export/3,
	 get_attribute/2,
	 add_func/2,
	 add_func/3,
	 remove_func/3,
	 has_func/3,
	 get_func/3,
	 replace_func/2,
%	 replace_func/3,
	 compile/1,
	 compile/2,
	 rename/2,
	 curry/2,
	 curry/4,
	 curry/5,
	 curry_add/3,
	 curry_add/4,
	 curry_add/5,
	 curry_add/6,
	 curry_replace/3,
	 curry_replace/4,
	 embed_params/2,
	 embed_params/4,
	 embed_params/5,
	 embed_all/2,
	 extend/2,
	 extend/3,
	 extend/4,
	 to_src/1,
	 to_src/2,
     to_src/3,
	 to_src_comments/2
	]).

-define(L(Obj), io:format("LOG ~s ~w ~p\n", [?FILE, ?LINE, Obj])).
-define(S(Obj), io:format("LOG ~s ~w ~s\n", [?FILE, ?LINE, Obj])).	 

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

%% @type meta_mod(). A data structure holding the abstract representation
%%  for a module.
%% @type func_form(). The abstract form for the function, as described
%%    in the ERTS Users' manual.

%% The record type holding the abstract representation for a module.
-record(meta_mod, {module, file, exports = [], forms = [],
		   export_all = false}).

%% @doc Create a new meta_mod for a module with the given name.
%%
%% @spec new(Module::atom()) -> meta_mod()
new(ModuleName) when is_atom(ModuleName) ->
    #meta_mod{module = ModuleName}.


%% @equiv for_module(ModuleName, [])
for_module(ModuleName) ->
    for_module(ModuleName, []).

%% @equiv for_module(ModuleName, IncludePaths, [])
for_module(ModuleName, IncludePaths) ->
    for_module(ModuleName, IncludePaths, []).

%% @doc Create a meta_mod tuple for an existing module. If ModuleName is a
%% string, it is interpreted as a file name (this is the same as calling
%% @{link smerl:for_file}). If ModuleName is an atom, Smerl attempts to
%% find its abstract represtation either from its source file or from
%% its .beam file directly (if it has been compiled with debug_info).
%% If the abstract representation can't be found, this function returns
%% an error.
%%
%% The IncludePaths parameter is used when 'ModuleName' is a file name.
%%
%% @spec for_module(ModuleName::atom() | string(), IncludePaths::[string()], 
%%    Macros::[{atom(), term()}]) ->
%%   {ok, meta_mod()} | {error, Error}
for_module(ModuleName, IncludePaths, Macros) when is_list(ModuleName) ->
    for_file(ModuleName, IncludePaths, Macros);
for_module(ModuleName, IncludePaths, Macros) when is_atom(ModuleName) ->
    [_Exports, _Imports, _Attributes,
     {compile, [_Options, _Version, _Time, {source, Path}]}] =
	ModuleName:module_info(),
    case for_file(Path, IncludePaths, Macros) of
	{ok, _Mod} = Res->
	    Res;
	_Err ->
	    case code:which(ModuleName) of
		Path1 when is_list(Path1) ->
		    case get_forms(ModuleName, Path1) of
			{ok, Forms} ->
			    mod_for_forms(Forms);
			_Other ->
			    {error, {invalid_module, ModuleName}}
		    end;
		_Err ->
		    {error, {invalid_module, ModuleName}}
	    end
    end.

%% @equiv for_file(SrcFilePath, [])
for_file(SrcFilePath) ->
    for_file(SrcFilePath, []).

%% @equiv for_file(SrcFilePath, IncludePaths, [])
for_file(SrcFilePath, IncludePaths) ->
    for_file(SrcFilePath, IncludePaths, []).

%% @doc Create a meta_mod for a module from its source file.
%%
%% @spec for_file(SrcFilePath::string(), IncludePaths::[string()],
%%   Macros::[{atom(), term()}]) ->
%%  {ok, meta_mod()} | {error, invalid_module}
for_file(SrcFilePath, IncludePaths, Macros) ->
    case epp:parse_file(SrcFilePath, [filename:dirname(SrcFilePath) |
				      IncludePaths], Macros) of
	{ok, Forms} ->
	    mod_for_forms(Forms);
	_err ->
	    {error, {invalid_module, SrcFilePath}}
    end.

mod_for_forms([{attribute,_,file,{FileName,_FileNum}},
	       {attribute, _, module, ModuleName}|Forms]) ->
    {Exports, OtherForms, ExportAll} =
	lists:foldl(
	  fun({attribute, _, export, ExportList},
	      {ExportsAcc, FormsAcc, ExportAll}) ->
		  {ExportList ++ ExportsAcc, FormsAcc, ExportAll};
	     ({attribute, _, compile, export_all},
	      {ExportsAcc, FormsAcc, _ExportAll}) ->
		     {ExportsAcc, FormsAcc, true};
	     ({eof, _}, Acc) ->
		  Acc;
	     (Form, {ExportsAcc, FormsAcc, ExportAll}) ->
		  {ExportsAcc, [Form | FormsAcc], ExportAll}
	  end, {[], [], false}, Forms),
    {ok, #meta_mod{module = ModuleName,
		   file = FileName,
		   exports = Exports,
		   forms = OtherForms,
		   export_all = ExportAll
		  }};
mod_for_forms(Mod) ->
    {error, {invalid_module, Mod}}.

%% @doc Return the module name for the meta_mod.
%%
%% @spec(MetaMod::meta_mod()) -> atom()
get_module(MetaMod) -> 
    MetaMod#meta_mod.module.

%% @doc Set the meta_mod's module name.
%%
%% @spec set_module(MetaMod::meta_mod(), NewName::atom()) ->
%%   NewMod::meta_mod()
set_module(MetaMod, NewName) ->
    MetaMod#meta_mod{module = NewName}.

%% @doc Return the list of function forms in the meta_mod.
%%
%% @spec get_forms(MetaMod::meta_mod()) -> [Form]
get_forms(MetaMod) ->
    MetaMod#meta_mod.forms.

set_forms(MetaMod, Forms) ->
    MetaMod#meta_mod{forms = Forms}.

%% @doc Return the list of exports in the meta_mod.
%%
%% @spec get_exports(MetaMod::meta_mod()) ->
%%   [{FuncName::atom(), Arity::integer()}]
get_exports(MetaMod) ->
    case MetaMod#meta_mod.export_all of
	false ->
	    MetaMod#meta_mod.exports;
	true ->
	    lists:foldl(
	      fun({function, _L, Name, Arity, _Clauses}, Exports) ->
		      [{Name, Arity} | Exports];	      
		 (_Form, Exports) ->
		      Exports
	      end, [], MetaMod#meta_mod.forms)
    end.

%% @doc Set the meta_mod's export list to the new list.
%%
%% @spec set_exports(MetaMod::meta_mod(),
%%   Exports::[{FuncName::atom(), Arity::integer()}]) -> NewMod::meta_mod()
set_exports(MetaMod, Exports) ->
    MetaMod#meta_mod{exports = Exports}.

%% @doc Get the export_all value for the module.
%%
%% @spec get_export_all(MetaMod::meta_mod) -> true | false
get_export_all(MetaMod) ->
    MetaMod#meta_mod.export_all.

%% @doc Set the export_all value for the module.
%%
%% @spec set_export_all(MetaMod::meta_mod(), Val::true | false) ->
%%   NewMetaMod::meta_mod()
set_export_all(MetaMod, Val) ->
    MetaMod#meta_mod{export_all = Val}.

%% @doc Remove the export from the list of exports in the meta_mod.
%%
%% @spec remove_export(MetaMod::meta_mod(), FuncName::atom(),
%%   Arity::integer()) -> NewMod::meta_mod()
remove_export(MetaMod, FuncName, Arity) ->
    MetaMod#meta_mod{exports =
		     lists:delete({FuncName, Arity},
			     MetaMod#meta_mod.exports)}.

%% @doc Get the value a the module's attribute.
%%
%% @spec get_attribute(MetaMod::meta_mod(), AttName::atom()) ->
%%   {ok, Val} | error
get_attribute(MetaMod, AttName) ->
    case lists:keysearch(AttName, 3, get_forms(MetaMod)) of
	{value, {attribute,_,_,Val}} ->
	    {ok, Val};
	_ -> error
    end.


%% Get the abstract representation, if available, for the module.
%%
%% Strategy:
%% 1) Try to get the abstract code from the module if it's compiled
%%    with debug_info.
%% 2) Look for the source file in the beam file's directory.
%% 3) If the file's directory ends with 'ebin', then search in
%%    [beamdir]/../src
get_forms(Module, Path) ->
    case beam_lib:chunks(Path, [abstract_code]) of
	{ok, {_, [{abstract_code, {raw_abstract_v1, Forms}}]}} ->
	    {ok, Forms};
	_Err ->
	    case filename:find_src(Module, [{"ebin", "src"}]) of
		{error, _} = Err ->
		    get_forms_from_binary(Module, Err);
		{SrcPath, _} ->
		    Filename = SrcPath ++ ".erl",
		    epp:parse_file(Filename, [filename:dirname(Filename)], [])
	    end
    end.

get_dirs_in_dir(Dir) ->
    case file:list_dir(Dir) of 
	{error, _} ->
	    undefined;
	{ok, Listing} ->
	    lists:foldl(
	      fun (Name, Acc) ->
		      Path = Dir ++ "/" ++ Name,
		      case file:read_file_info(Path) of
			  {ok, #file_info{type=directory}} -> [Path | Acc];
			  _ -> Acc
		      end
	      end, [], Listing)
    end.

%% @doc Try to infer module source files from the beam code path.
get_forms_from_binary(Module, OrigErr) ->
    Ret = 
	case code:where_is_file(atom_to_list(Module) ++ ".beam") of
	    non_existing ->
		OrigErr;
	    Filename ->
		%% We could automatically obtain a list of all dirs under this dir, but we just do
		%% a hack for now.
		Basedir = filename:dirname(Filename),
		Lastdir = filename:basename(Basedir),
		if Lastdir == "ebin" ->
			Rootdir = filename:dirname(Basedir),
			DirList = [Rootdir ++ "/src"],
			get_forms_from_file_list(Module, Rootdir,
					DirList ++ get_dirs_in_dir(Rootdir ++ "/src"));
		   true ->
			DirList = [Basedir],
			get_forms_from_file_list(Module, Basedir, DirList)
		end
	end,
    if Ret == [] -> OrigErr;
       true -> Ret
    end.
get_forms_from_file_list(_Module, _Basedir, []) ->
    [];
get_forms_from_file_list(Module, Basedir, [H|T]) ->
    Filename = H ++ "/" ++ atom_to_list(Module) ++ ".erl",
    case file:read_file_info(Filename) of
	{ok, #file_info{type=regular}} ->
	    epp:parse_file(Filename, [filename:dirname(Filename)], []);
	_ ->
	    get_forms_from_file_list(Module, Basedir, T)
    end.

%% @doc Add a new function to the meta_mod and return the resulting meta_mod.
%% This function calls add_func(MetaMod, Form, true).
%%
%% @spec add_func(MetaMod::meta_mod(), Form::func_form() | string()) ->
%%   {ok, NewMod::meta_mod()} | {error, parse_error}
add_func(MetaMod, Form) ->
    add_func(MetaMod, Form, true).

%% @doc Add a new function to the meta_mod and return the new MetaMod
%% record. Export is a boolean variable indicating if the function should
%% be added to the module's exports.
%%
%% @spec add_func(MetaMod::meta_mod(), Func::func_form() | string(),
%%   Export::boolean()) ->
%%   {ok, NewMod::meta_mod()} | {error, parse_error}
add_func(MetaMod, Func, Export) when is_list(Func) ->
    case parse_func_string(Func) of
	{ok, Form} ->
	    add_func(MetaMod, Form, Export);
	Err ->
	    Err
    end;
add_func(MetaMod, {function, _Line, FuncName, Arity, _Clauses} = Form,
	 true) ->
    Foo = {ok, MetaMod#meta_mod{
      exports = [{FuncName, Arity} | MetaMod#meta_mod.exports],
      forms = [Form | MetaMod#meta_mod.forms]
     }},
    Foo;
add_func(MetaMod, {function, _Line, _FuncName, _Arity, _Clauses} = Form,
	 false) ->
   {ok, MetaMod#meta_mod{forms = [Form | MetaMod#meta_mod.forms]}};

%%add_func(MetaMod, Name, Fun) when is_function(Fun) ->
%%    add_func(MetaMod, Name, Fun, true);

add_func(_, _, _) ->
    {error, parse_error}.

%% add_func(MetaMod, Name, Fun, Export) when is_function(Fun) ->
%%     case form_for_fun(Name, Fun) of
%% 	{ok, Form} ->
%% 	    add_func(MetaMod, Form, Export);
%% 	Err ->
%% 	    Err
%%     end.

%% form_for_fun(Name, Fun) ->
%%     Line = 999,
%%     Info = erlang:fun_info(Fun),
%%     case Info of
%% 	[{module, _ModName}, _FuncName, _Arity, _Env, {type, external}] ->
%% 	    {error, cant_add_external_funcs};
%% 	[_Pid, _Module, _NewIdx, _NewUniq, _Index, _Uniq, _Name,
%% 	 {arity, Arity},
%% 	 {env, [Vars, _Unknown1, _Unknown2, Clauses]},
%% 	 {type, local}] ->
%% 	    EnvVars = lists:map(
%% 			fun({VarName, Val}) ->
%% 				{match,Line,{var,Line,VarName},
%% 				 erl_parse:abstract(Val)}
%% 			end, Vars),
%% 	    NewClauses = lists:map(
%% 			   fun({clause, Line1, Params, Guards, Exprs}) ->
%% 				   {clause, Line1, Params, Guards,
%% 				    EnvVars ++ Exprs}
%% 			   end, Clauses),
%% 	    {ok, {function, Line, Name, Arity, NewClauses}};
%% 	_Other ->
%% 	    {error, bad_fun}
%%     end.


parse_func_string(Func) ->
    case erl_scan:string(Func) of
	{ok, Toks, _} ->
	    case erl_parse:parse_form(Toks) of
		{ok, _Form} = Res ->
		    Res;
		_Err ->
		    {error, parse_error}
	    end;
	_Err ->
	    {error, parse_error}
    end.

%% @doc Try to remove the function from the meta_mod.
%% If the function exists, the new meta_mod is returned. Otherwise,
%% original meta_mod is returned.
%%
%% @spec remove_func(MetaMod::meta_mod(), FuncName::string(), Arity::integer())
%%   -> NewMod::meta_mod()
%%
remove_func(MetaMod, FuncName, Arity) ->
    MetaMod#meta_mod{forms =
		     lists:filter(
		       fun({function, _Line, FuncName1, Arity1, _Clauses})
			  when FuncName1 =:= FuncName, Arity =:= Arity1->
			       false;
			  (_) ->
			       true
		       end, MetaMod#meta_mod.forms),
		     exports =
		     lists:filter(
		       fun({FuncName1, Arity1})
			  when FuncName1 =:= FuncName,
			       Arity1 =:= Arity ->
			       false;
			  (_) ->
			       true
		       end, MetaMod#meta_mod.exports)
		      }.

%% @doc Check whether the meta_mod has a function with the given name
%%   and arity.
%% @spec has_func(MetaMod::meta_mod(), FuncName::atom(), Arity::integer()) ->
%%   bool()
has_func(MetaMod, FuncName, Arity) ->
    lists:any(fun({function, _Line, FuncName1, Arity1, _Clauses})
		 when FuncName1 == FuncName, Arity1 == Arity ->
		      true;
		 (_) ->
		      false
	      end, MetaMod#meta_mod.forms).


%% @doc Get the form for the function with the specified arity in the
%%   meta_mod.
%% 
%% @spec get_func(MetaMod::meta_mod() | atom(),
%%   FuncName::atom(), Arity::integer()) ->
%%     {ok, func_form()} | {error, Err}
get_func(Module, FuncName, Arity) when is_atom(Module) ->
    case smerl:for_module(Module) of
	{ok, C1} ->
	    get_func(C1, FuncName, Arity);
	Err ->
	    Err
    end;
get_func(MetaMod, FuncName, Arity) ->
    get_func2(MetaMod#meta_mod.forms, FuncName, Arity).

get_func2([], FuncName, Arity) ->
    {error, {function_not_found, {FuncName, Arity}}};
get_func2([{function, _Line, FuncName, Arity, _Clauses} = Form | _Rest],
	  FuncName, Arity) ->
    {ok, Form};
get_func2([_Form|Rest], FuncName, Arity) ->		      
    get_func2(Rest, FuncName, Arity).



%% @doc
%% Replace an existing function with the new one. If the function doesn't exist
%% the new function is added to the meta_mod.
%% This is tantamount to calling smerl:remove_func followed by smerl:add_func.
%%
%% @spec replace_func(MetaMod::meta_mod(), Function::string() | func_form()) ->
%%   {ok, NewMod::meta_mod()} | {error, Error}
replace_func(MetaMod, Function) when is_list(Function) ->
    case parse_func_string(Function) of
	{ok, Form} ->
	    replace_func(MetaMod, Form);
	Err ->
	    Err
    end;
replace_func(MetaMod, {function, _Line, FuncName, Arity, _Clauses} = Form) ->
    Mod1 = remove_func(MetaMod, FuncName, Arity),
    add_func(Mod1, Form);
replace_func(_MetaMod, _) ->
    {error, parse_error}.

%% %% @doc Simliar to replace_func/2, but accepts a function
%% %%   name + fun expression.
%% %%
%% %% @spec replace_func(MetaMod::meta_mod(), Name::atom(), Fun::function()) ->
%% %%   {ok, NewMod::meta_mod()} | {error, Error}
%% replace_func(MetaMod, Name, Fun) when is_function(Fun) ->
%%     case form_for_fun(Name, Fun) of
%% 	{ok, Form} ->
%% 	    replace_func(MetaMod, Form);
%% 	Err ->
%% 	    Err
%%     end.
	    
%% @doc Compile the module represented by the meta_mod and load the
%% resulting BEAM into the emulator. This function calls
%% compile(MetaMod, [report_errors, report_warnings]).
%%
%% @spec compile(MetaMod::meta_mod()) -> ok | {error, Error}
compile(MetaMod) ->
    compile(MetaMod, undefined).

%% @doc Compile the module represented by the meta_mod and load the
%% resulting BEAM into the emulator. 'Options' is a list of options as
%% described in the 'compile' module in the Erlang documentation.
%%
%% If the 'outdir' option is provided,
%% the .beam file is written to the destination directory.
%%
%% @spec compile(MetaMod::meta_mod(), Options::[term()]) -> ok | {error, Error}
compile(MetaMod, undefined) ->
    compile(MetaMod, [report_errors, report_warnings,
		      return_errors]);

compile(MetaMod, Options) ->
    Forms = [{attribute, 2, module, MetaMod#meta_mod.module},
	     {attribute, 3, export, get_exports(MetaMod)}],
    FileName =
	case MetaMod#meta_mod.file of
	    undefined -> atom_to_list(get_module(MetaMod));
	    Val -> Val
	end,

    Forms1 = [{attribute, 1, file, {FileName, 1}} | Forms],
    Forms2 = Forms1 ++ lists:reverse(MetaMod#meta_mod.forms),
    
    case compile:forms(Forms2, Options) of
	{ok, Module, Bin} ->
	    Res = 
		case lists:keysearch(outdir, 1, Options) of
		    {value, {outdir, OutDir}} ->
			file:write_file(
			  OutDir ++
			  ['/' | atom_to_list(MetaMod#meta_mod.module)] ++
			  ".beam", Bin);
		    false -> ok
		end,
	    case Res of
		ok ->
		    code:purge(Module),
		    case code:load_binary(
			   Module,
			   atom_to_list(Module) ++ ".erl", Bin) of
			{module, _Module} ->
			    ok;
			Err ->
			    Err
		    end;
		Err ->
		    Err
	    end;
	Err ->
	    Err
    end.

%% @doc Change the name of the function represented by the form.
%%
%% @spec rename(Form::func_form(), NewName::atom()) ->
%%   {ok, NewForm::func_form()} | {error, Err}
rename({function, Line, _Name, Arity, Clauses}, NewName) ->
    {function, Line, NewName, Arity, Clauses}.


%% @doc Get the curried form for the function and parameter(s). Currying
%% involves replacing one or more of the function's leading parameters
%% with predefined values.
%%
%% @spec curry(Form::func_form(), Param::term() | [term()]) ->
%%   {ok, NewForm::func_form()} | {error, Err}
curry(Form, Param) when not is_list(Param) ->
    curry(Form, [Param]);
curry({function, _Line, _Name, Arity, _Clauses}, Params)
  when length(Params) > Arity ->
    {error, too_many_params};
curry({function, Line, Name, Arity, Clauses}, NewParams) ->
    NewClauses =
	lists:foldl(
	  fun(Clause, Clauses1) ->
		  [curry_clause(Clause, NewParams) | Clauses1]
	  end, [], Clauses),
    {ok, {function, Line, Name, Arity-length(NewParams), NewClauses}}.

curry_clause({clause, L1, ExistingParams, Guards, _Exprs} = Clause,
	     NewParams) ->
    {FirstParams, LastParams} =
	lists:split(length(NewParams), ExistingParams),
%%     Matches =
%% 	lists:foldl(
%% 	  fun({Var, NewVal}, Acc) ->
%% 		  [{match, 1, Var, erl_parse:abstract(NewVal)} | Acc]
%% 	  end, [], lists:zip(FirstParams, NewParams)),
%%    {clause, L1, LastParams, Guards, Matches ++ Exprs}.
    
    Vals = 
	lists:foldl(
	  fun({{var,_,Name}, NewVal}, Acc) ->
		  [{Name, erl_parse:abstract(NewVal)} | Acc];
	     (_, Acc) ->
		  Acc
	  end, [], lists:zip(FirstParams, NewParams)),
    
    NewExprs = replace_vars(Clause, Vals),

    {clause, L1, LastParams, Guards, NewExprs}.

replace_vars(Clause, Vals) ->
    Tree =
	erl_syntax_lib:map(
	  fun({var,_L2,Name} = Expr) ->
		  case proplists:lookup(Name, Vals) of
		      none ->
			  Expr;
		      {_, Val} ->
			  Val
		  end;
	     (Expr) ->
		  Expr
	  end, Clause),
    {clause, _, _, _, NewExprs} = erl_syntax:revert(Tree),
    NewExprs.
    

%% @doc Curry the function from the module with the given param(s)
%%
%% @spec curry(ModName::atom(), Name::atom(), Arity::integer(),
%%   Params::term() | [term()]) ->
%%    {ok, NewForm} | {error, Err}
curry(ModName, Name, Arity, Params) when is_atom(ModName) ->
    case for_module(ModName) of
	{ok, MetaMod} ->
	    curry(MetaMod, Name, Arity, Params);
	Err ->
	    Err
    end;

%% @doc Curry the function from the meta_mod with
%%  the given param(s)
%%
%% @spec curry(MetaMod::meta_mod(), Name::atom(), arity::integer(),
%%   Params::term() | [terms()]) ->
%%    {ok, NewForm} | {error, Err}
curry(MetaMod, Name, Arity, Params) ->
    case get_func(MetaMod, Name, Arity) of
	{ok, Form} ->
	    curry(Form, Params);
	Err ->
	    Err
    end.



%% @doc Curry the function from the module or meta_mod
%%  with the param(s), and return its renamed form.
%%
%% @spec curry(Module::atom() | meta_mod(), Name::atom(), Arity::integer(),
%%   Params::term() | [terms()], NewName::atom()) ->
%%    {ok, NewForm} | {error, Err}
curry(Module, Name, Arity, Params, NewName) ->
    case curry(Module, Name, Arity, Params) of
	{ok, NewForm} ->
	    {ok, rename(NewForm, NewName)};
	Err ->
	    Err
    end.
		    

%% @doc Add the curried form of the function in the meta_mod
%%  with its curried form.
%%
%% @spec curry_add(MetaMod::meta_mod(), Form::func_form(),
%%   Params::term() | [term()]) ->
%%    {ok, NewMetaMod::meta_mod()} | {error, Err}
curry_add(MetaMod, {function, _Line, Name, Arity, _Clauses}, Params) ->
    curry_add(MetaMod, Name, Arity, Params).

%% @doc Add the curried form of the function
%%   in the meta_mod with its curried form.
%%
%% @spec curry_add(MetaMod::meta_mod(), Name::atom(), Arity::integer(),
%%   Params::term() | [term()]) ->
%%    {ok, NewMetaMod::meta_mod()} | {error, Err}
curry_add(MetaMod, Name, Arity, Params) ->
    curry_change(MetaMod, Name, Arity, Params, false).

%% @doc Curry the function form from the meta_mod, then add it
%%   to the other meta_mod with a new name.
%%
%% @spec curry_add(MetaMod::meta_mod(), Name::atom(), Arity::integer(),
%%   Params::[term()], NewName::atom()) -> {ok, NewMod::meta_mod()} |
%%     {error, Err}
curry_add(MetaMod, Name, Arity, Params, NewName) ->
    curry_add(MetaMod, MetaMod, Name, Arity, Params, NewName).
    
%% @doc Curry the function in the module, rename the curried form, and
%%   add it to the meta_mod.
%%
%% @spec curry_add(MetaMod::meta_mod(), Module::atom() | meta_mod(),
%%   Name::atom(), Arity::integer(), Params::term() | [term()],
%%   NewName::atom()) ->
%%     {ok, NewMod::meta_mod()} | {error, Error}
curry_add(MetaMod, Module, Name, Arity, Params, NewName) ->
    case curry(Module, Name, Arity, Params, NewName) of
	{ok, Form} ->
	    add_func(MetaMod, Form);
	Err ->
	    Err
    end.

curry_change(MetaMod, Name, Arity, Params, Remove) ->
    case get_func(MetaMod, Name, Arity) of
        {ok, OldForm} ->
            case curry(OldForm, Params) of
                {ok, NewForm} ->
		    MetaMod1 =
			case Remove of
			    true ->
				remove_func(MetaMod, Name, Arity);
			    false ->
				MetaMod
			end,
		    add_func(MetaMod1, NewForm);
		Err ->
		    Err
            end;
        Err ->
            Err
    end.

%% @doc Replace the function in the meta_mod with
%%   its curried form.
%%
%% @spec curry_replace(MetaMod::meta_mod(), Form::func_form(),
%%   Params::term() | [term()]) ->
%%    {ok, NewMetaMod::meta_mod()} | {error, Err}
curry_replace(MetaMod, {function, _Line, Name, Arity, _Clauses}, Params) ->
    curry_replace(MetaMod, Name, Arity, Params).


%% @doc Replace the function in the meta_mod with
%%   its curried form.
%%
%% @spec curry_replace(MetaMod::meta_mod(), Name::atom(),
%%   Arity::integer(), Params::term() | list()) ->
%%    {ok, NewMetaMod::meta_mod()} | {error, Err}
curry_replace(MetaMod, Name, Arity, Params) ->
    curry_change(MetaMod, Name, Arity, Params, true).


%% @doc This function takes a function form and list of name/value pairs,
%% and replaces all the function's parameters that whose names match an
%% element from the list with the predefined value.
%%
%% @spec embed_params(Func::func_form(),
%%   Vals::[{Name::atom(), Value::term()}]) -> NewForm::func_form()
embed_params({function, L, Name, Arity, Clauses}, Vals) ->
    NewClauses =
	lists:map(
	  fun({clause, L1, Params, Guards, _Exprs} = Clause) ->
		  {EmbeddedVals, OtherParams} =
		      lists:foldr(
			fun({var,_, VarName} = Param, {Embedded, Rest}) ->
				case proplists:lookup(VarName, Vals) of
				    none ->
					{Embedded, [Param | Rest]};
				    {_, Val} ->
					{[{VarName, erl_parse:abstract(Val)} |
					  Embedded], Rest}
				end;
			   (Param, {Embedded, Rest}) ->
				{Embedded, [Param | Rest]}
			end, {[], []}, Params),
		  NewExprs = replace_vars(Clause, EmbeddedVals),
		  {clause, L1, OtherParams, Guards, NewExprs}
		  
				    
%% 		  {Params1, Matches1, _RemainingVals} =
%% 		      lists:foldl(
%% 			fun({var, _L2, ParamName} = Param,
%% 			    {Params2, Matches2, Vals1}) ->
%% 				case lists:keysearch(ParamName, 1, Vals1) of 
%% 				    {value, {_Name, Val} = Elem} ->
%% 					Match = {match, L1, Param,
%% 						 erl_parse:abstract(Val)},
%% 					{Params2, [Match | Matches2],
%% 					 lists:delete(Elem, Vals1)};
%% 				    false ->
%% 					{[Param | Params2], Matches2, Vals1}
%% 				end;
%% 			   (Param, {Params2, Matches2, Vals1}) ->
%% 				{[Param | Params2], Matches2, Vals1}
%% 			end, {[], [], Vals}, Params),
%% 		  [{clause, L1, lists:reverse(Params1), Guards,
%% 				lists:reverse(Matches1) ++ Exprs} | Clauses1]
	  end, Clauses),
    NewArity =
	case NewClauses of
	    [{clause, _L2, Params, _Guards, _Exprs}|_] ->
		length(Params);
	    _ ->
		Arity
	end,
    {function, L, Name, NewArity, NewClauses}.

%% @doc Apply {@link embed_params/2} to a function from the meta_mod and
%%   add the resulting function to the meta_mod, and return the resulting
%%   meta_mod.
%%
%% @spec embed_params(MetaMod::meta_mod(), Name::atom(), Arity::integer(),
%%   Values::proplist()) -> {ok, NewMetaMod::meta_mod()} | {error, Err}
embed_params(MetaMod, Name, Arity, Values) ->
    embed_params(MetaMod, Name, Arity, Values, Name).

%% @doc Apply embed_params/2 to the function from the meta_mod and
%%   add the resulting function to the meta_mod after renaming the function.
%%
%% @spec embed_params(MetaMod::meta_mod(), Name::atom(), Arity::integer(),
%%   Values::proplist(), NewName::atom()) ->
%%    {ok, NewMetaMod::meta_mod()} | {error, Err}
embed_params(MetaMod, Name, Arity, Values, NewName) ->
    case get_func(MetaMod, Name, Arity) of
	{ok, Form} ->
	    NewForm = embed_params(Form, Values),
	    add_func(MetaMod, rename(NewForm, NewName));
	Err ->
	    Err
    end.
	
    


%% @doc Apply the embed_params function with the list of {Name, Value}
%% pairs to all forms in the meta_mod. Exports
%% for functions whose arities change due to the embedding are preserved.
%%
%% @spec embed_all(MetaMod::meta_mod(), Vals::[{Name::atom(),
%%   Value::term()}]) -> NewMetaMod::meta_mod()
embed_all(MetaMod, Vals) ->
    Forms = get_forms(MetaMod),
    Exports = get_exports(MetaMod),
    {NewForms, Exports3, NewExports} =
	lists:foldl(
	  fun({function, _L, Name, Arity, _Clauses} = Form,
	      {Forms1, Exports1, NewExports1}) ->
		  {function, _, _, NewArity, _} = NewForm =
		      embed_params(Form, Vals),
		  Exports2 = lists:delete({Name, Arity}, Exports1),
		  NewExports2 =
		      case length(Exports2) == length(Exports1) of
			  true ->
			      NewExports1;
			  false ->
			      [{Name, NewArity} | NewExports1]
		      end,
		  {[NewForm | Forms1], Exports2, NewExports2};
	     (Form, {Forms1, Exports1, NewExports1}) ->
		  {[Form | Forms1], Exports1, NewExports1}
	  end, {[], Exports, []}, Forms),
    #meta_mod{module = get_module(MetaMod),
	      exports = Exports3 ++ NewExports,
	      forms = lists:reverse(NewForms),
	      export_all = get_export_all(MetaMod)}.

%% @doc extend/2 
%% Add all the parent module's functions that are missing from the child
%% module to the child module. The new functions in the child module are
%% shallow: they have the name and arity as their corresponding functions in
%% the parent meta_mod, but instead of implementing their logic they call
%% the parent module's functions.
%%
%% @spec extend(Parent::atom() | meta_mod(), Child::atom() | meta_mod()) ->
%%      NewChildMod::meta_mod()
extend(Parent, Child) ->
    extend(Parent, Child, 0).

%% @doc Similar to extend/2, with the addition of the 'ArityDiff' parameter,
%% which indicates the difference
%% in arities Smerl should use when figuring out which functions to
%% generate based on the modules' exports. This is sometimes 
%% useful when calling extend() followed by embed_all().
%%
%% @spec extend(Parent::atom() | meta_mod(), Child::atom() | meta_mod(),
%%    ArityDiff::integer()) ->
%%      NewChildMod::meta_mod()
extend(Parent, Child, ArityDiff) ->
    extend(Parent, Child, ArityDiff, []).

extend(Parent, Child, ArityDiff, Options) ->
    {{ParentName, ParentExports, ParentMod}, ChildMod} = 
	get_extend_data(Parent, Child),
    ChildExports = get_exports(ChildMod),
    ChildExports1 = [{ExportName, ExportArity + ArityDiff} ||
			{ExportName, ExportArity} <-
			    ChildExports],
    ExportsDiff = ParentExports -- ChildExports1,
    NewChild =
	lists:foldl(
	  fun({FuncName, Arity}, ChildMod1) ->
		  Func =
		      case lists:member(copy, Options) of
			  true ->
			      {ok, ParentFunc} =
				  smerl:get_func(ParentMod, FuncName, Arity),
			      ParentFunc;
			  _ ->
			      Params = get_params(
					 ParentMod, FuncName, Arity),
			      Clause1 =
				  {clause,1,Params,[],
				   [{call,1,
				     {remote,1,{atom,1,ParentName},
				      {atom,1,FuncName}},
				     Params}]},
			      {function,1,FuncName,Arity, [Clause1]}
		      end,
		  {ok, ChildMod2} = add_func(ChildMod1, Func),
		  ChildMod2
	  end, ChildMod, ExportsDiff),
    NewChild.

get_extend_data(Parent, Child) when is_atom(Parent) ->
    [{exports, Exports} |_] = Parent:module_info(),
    Exports1 = Exports -- [{module_info, 0}],
    Exports2 = Exports1 -- [{module_info, 1}],
    ParentMod = case smerl:for_module(Parent) of
		    {ok, M} -> M;
		    {error, _} -> undefined
		end,
    get_extend_data({Parent, Exports2, ParentMod}, Child);
get_extend_data(Parent, Child) when is_record(Parent, meta_mod) ->
    get_extend_data({get_module(Parent),
		     get_exports(Parent),
		     Parent}, Child);
get_extend_data(Parent, Child) when is_list(Parent) ->
    case for_file(Parent) of
	{ok, M1} ->
	    get_extend_data(M1, Child);
	Err ->
	    Err
    end;
get_extend_data({_,_,_} = ParentData, Child) when is_atom(Child);
						  is_list(Child) ->
    case for_module(Child) of
	{ok, MetaMod} ->
	    {ParentData, MetaMod};
	Err ->
	    Err
    end;
get_extend_data(ParentData, Child) when is_record(Child, meta_mod) ->
    {ParentData, Child}.

get_params(_, _, 0) -> [];
get_params(undefined, _FuncName, Arity) ->
    [{var,1,list_to_atom("P" ++ integer_to_list(Num))}
     || Num <- lists:seq(1, Arity)];
get_params(ParentMod, FuncName, Arity) ->
    {ok, {function, _L, _Name, _Arity,
	  [{clause,_,Params,_Guards,_Exprs} | _]}} =
	get_func(ParentMod, FuncName, Arity),
    Params.


%% @doc Return the pretty-printed source code for the module.
%% 
%% @

-type(t_meta_mod()::record()).

-spec(to_src(MetaMod::t_meta_mod()) -> string()).
to_src(MetaMod) ->
    ExportsForm =
	{attribute,1,export,get_exports(MetaMod)},
    AllForms = [{attribute,1,module,get_module(MetaMod)}, ExportsForm |
      get_forms(MetaMod)],
    erl_prettypr:format(erl_syntax:form_list(AllForms)).



to_src_comments(MetaMod,Comments) ->
    ExportsForm =
	{attribute,1,export,get_exports(MetaMod)},
    AllForms = [{attribute,1,module,get_module(MetaMod)}, ExportsForm |
      get_forms(MetaMod)],
	Ast=erl_syntax:form_list(AllForms),
    erl_prettypr:format(erl_recomment:recomment_forms(Ast,Comments)).
		  
%% @doc Write the pretty printed source code for the module
%%   to the file with the given file name.
%%
%% @spec to_src(MetaMod::meta_mod(), FileName::string()) ->
%%   ok | {error, Error}
to_src(MetaMod, FileName) ->
    Src = to_src(MetaMod),
    file:write_file(FileName, list_to_binary(Src)).

to_src(MetaMod, FileName,Comments) ->
    Src = to_src_comments(MetaMod,Comments),
    file:write_file(FileName, list_to_binary(Src)).
