#!/usr/bin/env escript
%%!
%-*-Mode:erlang;coding:utf-8;tab-width:4;c-basic-offset:4;indent-tabs-mode:()-*-
% ex: set ft=erlang fenc=utf-8 sts=4 ts=4 sw=4 et nomod:
%%%------------------------------------------------------------------------
%%% @doc
%%% ==Elixir-to-Erlang converter==
%%% @end
%%%
%%% MIT License
%%%
%%% Copyright (c) 2016-2021 Michael Truog <mjtruog at protonmail dot com>
%%%
%%% 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.
%%%
%%% @version 2.0.2 {@date} {@time}
%%%------------------------------------------------------------------------

-module(ex2erl).
-vsn("2.0.2").

-mode(compile).

-export([main/1]).

-record(state,
        {
            file_path_elixir = undefined :: undefined | string()
        }).

% erl_parse tree nodes represented as records
-record('var',
        {
            anno :: erl_anno:anno(),
            name :: atom()
        }).

-define(FILE_ENCODING, utf8).
-define(HEADER_COMMENT_LINE1, "-*-Mode:erlang;coding:utf-8;tab-width:4;c-basic-offset:4;indent-tabs-mode:()-*-").
-define(HEADER_COMMENT_LINE2, " ex: set ft=erlang fenc=utf-8 sts=4 ts=4 sw=4 et nomod:").
-define(HEADER_COMMENT_TITLE, "%% Automatically generated by ex2erl at ~s").
-define(HEADER_COMMENT_VERSIONS, "%% (Erlang/OTP ~s, Elixir ~s)").
-define(HEADER_COMMENT_BLANK, "%%").

-spec main(Arguments :: list(string())) ->
    no_return().

main(Arguments) ->
    #state{file_path_elixir = FilePathElixir} = main_arguments(Arguments),
    ElixirRoot = filename:join(code:root_dir(), "../elixir"),
    true = code:add_path(filename:join(ElixirRoot,
                                       "lib/elixir/ebin")),
    {ok, _} = application:ensure_all_started(elixir),

    Header = header(),
    FileDirectoryElixir = filename:dirname(FilePathElixir),
    Modules = 'Elixir.Kernel.ParallelCompiler':
              files_to_path([erlang:list_to_binary(FilePathElixir)],
                            erlang:list_to_binary(FileDirectoryElixir),
                            []),
    DirectoryIn = FileDirectoryElixir,
    DirectoryOut = FileDirectoryElixir,
    FilePaths = [convert(Module, DirectoryIn, DirectoryOut, Header)
                 || Module <- Modules],
    io:format("~p~n", [FilePaths]),
    exit_code(0).

%%%------------------------------------------------------------------------
%%% Private functions
%%%------------------------------------------------------------------------

main_arguments(Arguments) ->
    main_arguments(main_arguments_split(Arguments), #state{}).

main_arguments([], State) ->
    State;
main_arguments(["-h" | _], _) ->
    io:format(help(), [filename:basename(?FILE)]),
    exit_code(0);
main_arguments(["-" ++ InvalidParameter | _], _) ->
    erlang:error({invalid_parameter, InvalidParameter});
main_arguments([FilePathElixir | Arguments], State) ->
    main_arguments(Arguments, State#state{file_path_elixir = FilePathElixir}).

main_arguments_split([] = Arguments) ->
    Arguments;
main_arguments_split([[$-, _] = Argument | Arguments]) ->
    [Argument | main_arguments_split(Arguments)];
main_arguments_split([[$-, C | L] | Arguments]) ->
    [[$-, C] | main_arguments_split([[$- | L] | Arguments])];
main_arguments_split([Argument | Arguments]) ->
    [Argument | main_arguments_split(Arguments)].

variable_erlang(#'var'{name = Name} = Variable) ->
    NewName = case erlang:atom_to_list(Name) of
        [$_ | Suffix] ->
            erlang:list_to_atom([$_, $E | Suffix]);
        Suffix ->
            erlang:list_to_atom([$E | Suffix])
    end,
    Variable#'var'{name = NewName}.

convert(Module, DirectoryIn, DirectoryOut, Header) ->
    ModuleName = erlang:atom_to_list(Module),
    FilePathBeam = filename:join(DirectoryIn,
                                 ModuleName ++ ".beam"),
    FilePathErlang = filename:join(DirectoryOut,
                                   ModuleName ++ ".erl"),

    % as described at http://erlang.org/doc/man/beam_lib.html
    % with minor modifications to make it valid Erlang source code
    {ok,
     {_, [{abstract_code,
           {_, Forms}}]}} = beam_lib:chunks(FilePathBeam,
                                            [abstract_code]),
    SyntaxTree0 = erl_syntax_lib:map(fun(TreeNode) ->
        case TreeNode of
            #'var'{} = Variable ->
                variable_erlang(Variable);
            _ ->
                TreeNode
        end
    end, erl_syntax:form_list(Forms)),
    [[TreeNodeFile | TreeNodesBody]] = erl_syntax:subtrees(SyntaxTree0),
    TreeNodeFileNew = erl_syntax:add_precomments(Header, TreeNodeFile),
    SyntaxTreeN = erl_syntax:make_tree(form_list,
                                       [[TreeNodeFileNew | TreeNodesBody]]),
    ErlangSourceCode = erl_prettypr:format(SyntaxTreeN,
                                           [{encoding, ?FILE_ENCODING}]),
    ok = file:write_file(FilePathErlang,
                         unicode:characters_to_binary(ErlangSourceCode)),
    % cleanup
    ok = file:delete(FilePathBeam),
    FilePathErlang.

header() ->
    VersionErlang = version_erlang(),
    VersionElixir = version_elixir(),
    HeaderTime = calendar:system_time_to_rfc3339(erlang:system_time(second)),
    HeaderCommentTitle = format_to_string(?HEADER_COMMENT_TITLE,
                                          [HeaderTime]),
    HeaderCommentVersions = format_to_string(?HEADER_COMMENT_VERSIONS,
                                             [VersionErlang, VersionElixir]),
    [erl_syntax:comment([?HEADER_COMMENT_LINE1]),
     erl_syntax:comment([?HEADER_COMMENT_LINE2]),
     erl_syntax:comment([?HEADER_COMMENT_BLANK]),
     erl_syntax:comment([HeaderCommentTitle]),
     erl_syntax:comment([HeaderCommentVersions]),
     erl_syntax:comment([?HEADER_COMMENT_BLANK])].

version_erlang() ->
    Major = erlang:system_info(otp_release),
    VersionPath = filename:join([code:root_dir(),
                                 "releases", Major, "OTP_VERSION"]),
    case file:read_file(VersionPath) of
        {ok, FileVersion} ->
            [DetailedVersion |
             _] = binary:split(FileVersion, [<<"\r">>, <<"\n">>, <<" ">>]),
            DetailedVersion;
        {error, _} ->
            Major
    end.

version_elixir() ->
    'Elixir.System':version().

format_to_string(Format, Args) ->
    lists:flatten(io_lib:format(Format, Args)).

exit_code(ExitCode) when is_integer(ExitCode) ->
    erlang:halt(ExitCode, [{flush, true}]).

help() ->
"Usage ~s FILE.ex

  -h              List available command line flags
".

