#!/usr/bin/env escript
-mode(compile).

% TODO: Don't concurrently print warnings and errors
% TODO: Some tests

main(_) ->
    ok = io:setopts([binary, {encoding, utf8}]),
    ok = configure_logging(),
    compile_package_loop().

compile_package_loop() ->
    case io:get_line("") of
        eof -> ok;
        Line ->
            Chars = unicode:characters_to_list(Line),
            {ok, Tokens, _} = erl_scan:string(Chars),
            {ok, {Lib, Out, Modules}} = erl_parse:parse_term(Tokens),
            case compile_package(Lib, Out, Modules) of
                {ok, ModuleNames} ->
                    PrintModuleName = fun(ModuleName) ->
                        io:put_chars("gleam-compile-module:" ++ atom_to_list(ModuleName) ++ "\n")
                    end,
                    lists:map(PrintModuleName, ModuleNames),
                    io:put_chars("gleam-compile-result-ok\n");
                err ->
                    io:put_chars("gleam-compile-result-error\n")
            end,
            compile_package_loop()
    end.

compile_package(Lib, Out, Modules) ->
    IsElixirModule = fun(Module) ->
        filename:extension(Module) =:= ".ex"
    end,
    {ElixirModules, ErlangModules} = lists:partition(IsElixirModule, Modules),
    ok = filelib:ensure_dir([Out, $/]),
    ok = add_lib_to_erlang_path(Lib),
    {ErlangOk, ErlangBeams} = compile_erlang(ErlangModules, Out),
    {ElixirOk, ElixirBeams} = case ErlangOk of
        true -> compile_elixir(ElixirModules, Out);
        false -> {false, []}
    end,
    ok = del_lib_from_erlang_path(Lib),
    case ErlangOk andalso ElixirOk of
        true ->
            ModuleNames = proplists:get_keys(ErlangBeams ++ ElixirBeams),
            {ok, ModuleNames};
        false ->
            err
    end.

compile_erlang(Modules, Out) ->
    Workers = start_compiler_workers(Out),
    ok = producer_loop(Modules, Workers),
    collect_results({true, []}).

collect_results(Acc = {Result, Beams}) ->
    receive
        {compiled, ModuleName, Beam} -> collect_results({Result, [{ModuleName, Beam} | Beams]});
        failed -> collect_results({false, Beams})
        after 0 -> Acc
    end.

producer_loop([], 0) ->
    ok;
producer_loop([], Workers) ->
    receive
        {work_please, _} -> producer_loop([], Workers - 1)
    end;
producer_loop([Module | Modules], Workers) ->
    receive
        {work_please, Worker} ->
            erlang:send(Worker, {module, Module}),
            producer_loop(Modules, Workers)
    end.

start_compiler_workers(Out) ->
    Parent = self(),
    NumSchedulers = erlang:system_info(schedulers),
    SpawnWorker = fun(_) ->
        erlang:spawn_link(fun() -> worker_loop(Parent, Out) end)
    end,
    lists:foreach(SpawnWorker, lists:seq(1, NumSchedulers)),
    NumSchedulers.

worker_loop(Parent, Out) ->
    Options = [report_errors, report_warnings, debug_info, {outdir, Out}],
    erlang:send(Parent, {work_please, self()}),
    receive
        {module, Module} ->
            log({compiling, Module}),
            case compile:file(Module, Options) of
                {ok, ModuleName} ->
                    Beam = filename:join(Out, ModuleName) ++ ".beam",
                    Message = {compiled, ModuleName, Beam},
                    log(Message),
                    erlang:send(Parent, Message);
                error ->
                    log({failed, Module}),
                    erlang:send(Parent, failed)
            end,
            worker_loop(Parent, Out)
    end.

compile_elixir(Modules, Out) ->
    Error = [
        "The program elixir was not found. Is it installed?",
        $\n,
        "Documentation for installing Elixir can be viewed here:",
        $\n,
        "https://elixir-lang.org/install.html"
    ],
    case Modules of
        [] -> {true, []};
        _ ->
            log({starting, "compiler.app"}),
            ok = application:start(compiler),
            log({starting, "elixir.app"}),
            case application:start(elixir) of
                ok -> do_compile_elixir(Modules, Out);
                _ ->
                    io:put_chars(standard_error, [Error, $\n]),
                    {false, []}
            end
    end.

do_compile_elixir(Modules, Out) ->
    ModuleBins = lists:map(fun(Module) ->
        log({compiling, Module}),
        list_to_binary(Module)
    end, Modules),
    OutBin = list_to_binary(Out),
    Options = [{dest, OutBin}],
    % Silence "redefining module" warnings.
    % Compiled modules in the build directory are added to the code path.
    % These warnings result from recompiling loaded modules.
    % TODO: This line can likely be removed if/when the build directory is cleaned before every compilation.
    'Elixir.Code':compiler_options([{ignore_module_conflict, true}]),
    case 'Elixir.Kernel.ParallelCompiler':compile_to_path(ModuleBins, OutBin, Options) of
        {ok, ModuleAtoms, _} ->
            ToBeam = fun(ModuleAtom) ->
                Beam = filename:join(Out, atom_to_list(ModuleAtom)) ++ ".beam",
                log({compiled, Beam}),
                {ModuleAtom, Beam}
            end,
            {true, lists:map(ToBeam, ModuleAtoms)};
        {error, Errors, _} ->
            % Log all filenames associated with modules that failed to compile.
            % Note: The compiler prints compilation errors upon encountering them.
            ErrorFiles = lists:usort([File || {File, _, _} <- Errors]),
            Log = fun(File) ->
                log({failed, binary_to_list(File)})
            end,
            lists:foreach(Log, ErrorFiles),
            {false, []};
        _ -> {false, []}
    end.

add_lib_to_erlang_path(Lib) ->
    code:add_paths(expand_lib_paths(Lib)).

-if(?OTP_RELEASE >= 26).
del_lib_from_erlang_path(Lib) ->
    code:del_paths(expand_lib_paths(Lib)).
-else.
del_lib_from_erlang_path(Lib) ->
    lists:foreach(fun code:del_path/1, expand_lib_paths(Lib)).
-endif.

expand_lib_paths(Lib) ->
    filelib:wildcard([Lib, "/*/ebin"]).

configure_logging() ->
    Enabled = os:getenv("GLEAM_LOG") /= false,
    persistent_term:put(gleam_logging_enabled, Enabled).

log(Term) ->
    case persistent_term:get(gleam_logging_enabled) of
        true -> io:fwrite("~p~n", [Term]), ok;
        false -> ok
    end.
