#!/usr/bin/env escript
%% -*- erlang -*-
%%! -pa ebin

main(Dirs) ->
    Txts =
	lists:foldl(
	  fun(Dir, Acc) ->
		  EbinDir = filename:join(Dir, "ebin"),
		  SrcDir = filename:join(Dir, "src"),
		  filelib:fold_files(
		    EbinDir, ".+\.beam\$", false,
		    fun(BeamFile, Res) ->
			    Mod = mod(BeamFile),
			    ErlFile = filename:join(SrcDir, Mod ++ ".erl"),
			    case get_forms(BeamFile, ErlFile) of
				{ok, BeamForms, ErlForms} ->
				    process_forms(BeamForms, Mod, application) ++
					process_forms(ErlForms, Mod, macro) ++ Res;
				_Err ->
				    Res
			    end
		    end, Acc)
	  end, [], Dirs),
    Dict = lists:foldl(
	     fun({B, Meta}, Acc) ->
		     dict:update(
		       binary_to_list(B),
		       fun(OldMeta) ->
			       lists:usort([Meta|OldMeta])
		       end,
		       [Meta], Acc)
	     end, dict:new(), Txts),
    generate_pot(Dict).

process_forms(Forms, Mod, Type) ->
    Tree = erl_syntax:form_list(Forms),
    erl_syntax_lib:fold_subtrees(
      fun(Form, Acc) ->
	      case erl_syntax:type(Form) of
		  function ->
		      case map(Form, Mod, Type) of
			  [] ->
			      Acc;
			  Vars ->
			      Vars ++ Acc
		      end;
		  _ ->
		      Acc
	      end
      end, [], Tree).

map(Tree, Mod, Type) ->
    Vars = erl_syntax_lib:fold(
	     fun(Form, Acc) ->
		     case erl_syntax:type(Form) of
			 Type when Type == application ->
			     analyze_app(Form, Mod) ++ Acc;
			 Type when Type == macro ->
			     analyze_macro(Form, Mod) ++ Acc;
			 _ ->
			     Acc
		     end
	     end, [], Tree),
    Bins = lists:flatmap(
	     fun({Var, Pos}) when is_atom(Var) ->
		     Res = erl_syntax_lib:fold(
			     fun(Form, Acc) ->
				     case process_match_expr(
					    Form, Var, Mod) of
					 {ok, Binary, NewPos} ->
					     [{Binary, NewPos}|Acc];
					 error ->
					     Acc
				     end
			     end, [], Tree),
		     case Res of
			 [] ->
			     log("~s:~p: unresolved variable: ~s~n",
				 [Mod, Pos, Var]);
			 _ ->
			     ok
		     end,
		     Res;
		({Var, Pos}) when is_binary(Var) ->
		     [{Var, Pos}]
	     end, lists:usort(Vars)),
    [{B, {Mod, Pos}} || {B, Pos} <- Bins, B /= <<"">>].

process_match_expr(Form, Var, Mod) ->
    case erl_syntax:type(Form) of
	match_expr ->
	    Pattern = erl_syntax:match_expr_pattern(Form),
	    Body = erl_syntax:match_expr_body(Form),
	    {V, Expr} =
		case {erl_syntax:type(Pattern), erl_syntax:type(Body)} of
		    {variable, _} ->
			{erl_syntax:variable_name(Pattern), Body};
		    {_, variable} ->
			{erl_syntax:variable_name(Body), Pattern};
		    _ ->
			{'', none}
		end,
	    Text = maybe_extract_tuple(Expr),
	    if V == Var ->
		    Pos = erl_syntax:get_pos(Text),
		    try {ok, erl_syntax:concrete(Text), Pos}
		    catch _:_ ->
			    case catch erl_syntax_lib:analyze_application(Text) of
				{_M, {Fn, 1}} when Fn == format_error;
						   Fn == io_format_error ->
				    error;
				_ ->
				    log("~s:~p: not a binary: ~s~n",
					[Mod, Pos, erl_prettypr:format(Text)]),
				    {ok, <<>>, Pos}
			    end
		    end;
	       true ->
		    error
	    end;
	_ ->
	    error
    end.

maybe_extract_tuple(none) ->
    none;
maybe_extract_tuple(Form) ->
    try
	tuple = erl_syntax:type(Form),
	[Text, _] = erl_syntax:tuple_elements(Form),
	Text
    catch _:{badmatch, _} ->
	    Form
    end.

analyze_app(Form, Mod) ->
    try
	{M, {F, A}} = erl_syntax_lib:analyze_application(Form),
	Args = erl_syntax:application_arguments(Form),
	Txt = case {M, atom_to_list(F), A, Args} of
		  {xmpp, "err_" ++ _, 2, [T|_]} -> T;
		  {xmpp, "serr_" ++ _, 2, [T|_]} -> T;
		  {xmpp, "mk_text", 2, [T|_]} -> T;
		  {xmpp_tr, "tr", 2, [_,T|_]} -> T;
		  {translate, "translate", 2, [_,T|_]} -> T
	      end,
	Pos = erl_syntax:get_pos(Txt),
	case erl_syntax:type(Txt) of
	    binary ->
		try [{erl_syntax:concrete(Txt), Pos}]
		catch _:_ ->
			Pos = erl_syntax:get_pos(Txt),
			log("~s:~p: not a binary: ~s~n",
			    [Mod, Pos, erl_prettypr:format(Txt)]),
			[]
		end;
	    variable ->
		[{erl_syntax:variable_name(Txt), Pos}];
	    application ->
		Vars = sets:to_list(erl_syntax_lib:variables(Txt)),
		case Vars of
		    [Var] ->
			[{Var, Pos}];
		    [_|_] ->
			log("Too many variables: ~p~n", [Vars]),
			[];
		    [] ->
			[]
		end;
	    _ ->
		[]
	end
    catch _:{badmatch, _} ->
	    [];
	  _:{case_clause, _} ->
	    []
    end.

analyze_macro(Form, Mod) ->
    try
	Name = erl_syntax:macro_name(Form),
	variable = erl_syntax:type(Name),
	'T' = erl_syntax:variable_name(Name),
	[Txt] = erl_syntax:macro_arguments(Form),
	string = erl_syntax:type(Txt),
	Pos = erl_syntax:get_pos(Txt),
	try [{list_to_binary(erl_syntax:string_value(Txt)), Pos}]
	catch _:_ ->
		log("~s:~p: not a binary: ~s~n",
		    [Mod, Pos, erl_prettypr:format(Txt)]),
		[]
	end
    catch _:{badmatch, _} ->
	    []
    end.

generate_pot(Dict) ->
    io:format("~s~n~n", [pot_header()]),
    lists:foreach(
      fun({Msg, Location}) ->
	      S1 = format_location(Location),
	      S2 = format_msg(Msg),
	      io:format("~smsgstr \"\"~n~n", [S1 ++ S2])
      end, lists:keysort(1, dict:to_list(Dict))).

format_location([A, B, C|T]) ->
    format_location_list([A,B,C]) ++ format_location(T);
format_location([A, B|T]) ->
    format_location_list([A,B]) ++ format_location(T);
format_location([A|T]) ->
    format_location_list([A]) ++ format_location(T);
format_location([]) ->
    "".

format_location_list(L) ->
    "#: " ++ string:join(
	       lists:map(
		 fun({File, Pos}) ->
			 io_lib:format("~s.erl:~B", [File, Pos])
		 end, L),
	       " ") ++ io_lib:nl().

format_msg(Bin) ->
    io_lib:format("msgid \"~s\"~n", [escape(Bin)]).

escape(Bin) ->
    lists:map(
      fun($") -> "\\\"";
	 (C) -> C
      end, binary_to_list(iolist_to_binary(Bin))).

pot_header() ->
    string:join(
      ["msgid \"\"",
       "msgstr \"\"",
       "\"Project-Id-Version: 15.11.127\\n\"",
       "\"X-Language: Language Name\\n\"",
       "\"Last-Translator: Translator name and contact method\\n\"",
       "\"MIME-Version: 1.0\\n\"",
       "\"Content-Type: text/plain; charset=UTF-8\\n\"",
       "\"Content-Transfer-Encoding: 8bit\\n\""],
      io_lib:nl()).

mod(Path) ->
    filename:rootname(filename:basename(Path)).

log(Format, Args) ->
    io:format(standard_error, Format, Args).

get_forms(BeamFile, ErlFile) ->
    try
	{ok, BeamForms} = get_beam_forms(BeamFile),
	{ok, ErlForms} = get_erl_forms(ErlFile),
	{ok, BeamForms, ErlForms}
    catch _:{badmatch, error} ->
	    error
    end.

get_beam_forms(File) ->
    case beam_lib:chunks(File, [abstract_code]) of
        {ok, {_, List}} ->
            case lists:keyfind(abstract_code, 1, List) of
                {abstract_code, {raw_abstract_v1, Abstr}} ->
                    {ok, Abstr};
                _Err ->
		    log("failed to get abstract code from ~s~n", [File]),
                    error
            end;
        Err ->
	    log("failed to read chunks from ~s: ~p~n", [File, Err]),
            error
    end.

get_erl_forms(Path) ->
    case file:open(Path, [read]) of
        {ok, Fd} ->
            parse(Path, Fd, 1, []);
        {error, Why} ->
	    log("failed to read ~s: ~s~n", [Path, file:format_error(Why)]),
            error
    end.

parse(Path, Fd, Line, Acc) ->
    {ok, Pos} = file:position(Fd, cur),
    case epp_dodger:parse_form(Fd, Line) of
        {ok, Form, NewLine} ->
	    {ok, NewPos} = file:position(Fd, cur),
	    {ok, RawForm} = file:pread(Fd, Pos, NewPos - Pos),
	    file:position(Fd, {bof, NewPos}),
	    AnnForm = erl_syntax:set_ann(Form, RawForm),
	    parse(Path, Fd, NewLine, [AnnForm|Acc]);
        {eof, _} ->
	    {ok, NewPos} = file:position(Fd, cur),
	    if NewPos > Pos ->
		    {ok, RawForm} = file:pread(Fd, Pos, NewPos - Pos),
		    Form = erl_syntax:text(""),
		    AnnForm = erl_syntax:set_ann(Form, RawForm),
		    {ok, lists:reverse([AnnForm|Acc])};
	       true ->
		    {ok, lists:reverse(Acc)}
	    end;
        Err ->
	    log("failed to parse ~s: ~p~n", [Path, Err]),
	    error
    end.
