%% -*- mode: erlang; indent-tabs-mode: nil; -*-
%%=============================================================================
%% Copyright 2014 Ulf Wiger
%%
%% Licensed under the Apache License, Version 2.0 (the "License");
%% you may not use this file except in compliance with the License.
%% You may obtain a copy of the License at
%%
%% http://www.apache.org/licenses/LICENSE-2.0
%%
%% Unless required by applicable law or agreed to in writing, software
%% distributed under the License is distributed on an "AS IS" BASIS,
%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%% See the License for the specific language governing permissions and
%% limitations under the License.
%%=============================================================================
-module(setup_gen).
-export([main/1,   % escript-style
         run/1,    % when called from within erlang
         help/0]). % prints help text.

-import(setup_lib, [abort/2]).

-define(if_verbose(Expr),
        case get(verbose) of
            true -> Expr;
            _    -> ok
        end).


main([]) ->
    help(),
    halt(1);
main([H]) when H=="-h"; H=="-help" ->
    help(),
    halt(0);
main(["-" ++ _|_] = Args) ->
    Opts = try options(Args)
           catch
               error:E ->
                   abort(E, [])
           end,
    run(Opts);
main([Name, Config, Out| InArgs]) ->
    Opts = try options(InArgs)
           catch
               error:E ->
                   abort(E, [])
           end,
    run([{name, Name}, {conf, Config}, {outdir, Out} | Opts]).

help() ->
    setup_lib:help().

%% @spec run(Options) -> ok
%% @doc Generates .rel file(s) and boot scripts for a given configuration.
%%
%% This function reads a configuration specification and generates the
%% files needed to start a node from an OTP boot script. Optionally, it can
%% also generate a 'setup' script, which contains the same applications, but
%% only loaded (except the `setup' application, if present, which is started).
%% This way, a node can be started with all paths set, and all environment
%% variables defined, such that a database can be created, and other setup
%% tasks be performed.
%%
%% Mandatory options:
%% * `{name, Name}'  - Name of the release (and of the .rel and .script files)
%% * `{outdir, Dir}' - Where to put the generated files. Dir is created if not
%%                     already present.
%% * `{conf, Conf}'  - Config file listing apps and perhaps other options
%% * `{relconf, File}' - can be used instead of `conf', and identifies a
%%       reltool.config file (see {@link //reltool. `reltool'}) to be used as
%%       system description. If a `conf' option is present, it will be used;
%%       otherwise, a `relconf' option must be present.
%%
%% Additional options:
%%
%% * `{apps, [App]}' - List of applications to include in the release. Only the
%%                     first instance of this option is considered.
%% * `{add_apps, [App]}' - Adds applications to the ones given in the `apps'
%%                         option.
%% * `{remove_apps, Apps}' - Remove `Apps' from the list of applications.
%% * `{sort_app, App, Before}' - Change the sort order so that `App' comes
%%                       before `Before'. `Before' can be either an application
%%                       name or a list of names. In the latter case, `App'
%%                       is inserted before either of the applications in
%%                       the list, whichever comes first.
%% * `{include, ConfigFile}' - include options from the given file. The file
%%                             is processed using `file:script/2'.
%% * `{include, ConfigFile, Vars}' - as above, but passing along a list of
%%                        variable bindings to ConfigFile.
%% * `{include_lib, ConfigFile}' - As above, but ConfigFile is named as with
%%                 the `-include_lib(...)' directive in Erlang source code.
%% * `{include_lib, ConfigFile, Vars}' - as above, but passing along a list of
%%                        variable bindings to ConfigFile.
%% * `{sys, SysConfigFile}' - Read an existing sys.config file. The environment
%%        found in this file may be redefined by `env' and `set_env' entries
%%        (see below).
%% * `{env, [{App, [{K,V}]}]}' - Environment variables for the `sys.config'
%%        file. `setup_gen' will merge all `env' entries, where later entries
%%        replace earlier entries (based on the environment variable name).
%% * `{set_env, [{App, [{KeyPath, V}]}]}' - Modifies existing environment
%%        structures, where `KeyPath' is a list of names (top name must be
%%        an atom) describing a path in a tree structure, where each node
%%        is either a `{Key, SubTree}' or a `{Key, Any, SubTree}' tuple. The
%%        `set_env' function will continue into `SubTree' and either replace
%%        the value representing the full `KeyPath' or create the remaining
%%        subtree.
%% * `{target, Dir}' - Where to produce the generated files. The files will
%%        end up in `Dir/releases/Vsn/'. If a `reltool.config' file is used,
%%        the `{target_dir, D}' option will be translated into `{target,D}'.
%% * `{vsn, Vsn}' - System version, used to determine where to generate the
%%        files (see `target' above).
%% * `{root, RootDir}' - Where to look for applications. Normally, `RootDir'
%%        should represent either `RootDir/lib/*/ebin', or `RootDir/*/ebin',
%%        but if the option `{wild_roots,true}' is given, it can be either
%%        an "ebin" directory, or any parent directory to "ebin" directories.
%%        Multiple `root' options can be given. If `target' is not given
%%        "boot variables" will be generated for each root directory in turn,
%%        named `V1 ... Vn', then generating a relocatable boot script.
%% * `{pa, Path}' - Prepends `Path' to the code path. Multiple `pa' options
%%        can be given.
%% * `{pz, Path}' - Appends `Path' to the code path. Multiple `pz' options
%%        can be given.
%% * `{install, true|false}' - Tells setup whether to also build "install"
%%        scripts and config files. An install script contains the same
%%        applications as the normal script, but only loads them, starting
%%        only the `setup' application. This allows a system to be installed
%%        using "setup hooks", while having all the target system code
%%        and environment available. An "install.config" file is also created,
%%        which, if a `{nodes, Ns}' option is given, also configures Erlang
%%        to wait for all given nodes, and then start the `setup' application
%%        on the first node.
%% * `{start_setup, true|false}' - Tells whether setup should be started
%%        automatically. The default is `true' (as it should be). The best way
%%        to include setup, but not start it, would be to add `{setup, load}' to
%%        the `apps' list.
%% * `{verbose, true|false}' - (Default: `false') Turns on verbose printouts.
%%
%% == Application entries ==
%%
%% Applications can be represented in a number of different ways:
%% * `AppName::atom()' - `setup' will search for the latest version
%%       along the current code path.
%% * `{App::atom(), Vsn::latest|list()}' - where `Vsn' is an explicit version
%%       identifying the application. `latest' instructs `setup' to pick the
%%       latest version, if several versions can be found along the path.
%% * `{App::atom(), Type::atom()}' - where
%%     `Type::permanent|temporary|transient|load' is the application start
%%     type (or, in the case of 'load', no start at all).
%% * `{App, Vsn, Type}' - see `App', `Vsn' and `Type' above
%% * `{App, Vsn, Incl}' - where `Incl' is a list of included applications.
%% * `{App, Vsn, Type, Incl}'
%%
%% == Command-line options ==
%%
%% The following options can be given on the command line of `setup_gen':
%% * `-target Dir' - Equivalent to `{target, Dir}'
%% * `-name Name'  - Equivalent to `{name, Name}'
%% * `-root Dir'   - Equivalent to `{root, Dir}'
%% * `-out Dir'    - Equivalent to `{outdir, Dir}'
%% * `-relconf F'  - Equivalent to `{relconf, F}'
%% * `-conf F'     - Equivalent to `{conf, F}'
%% * `-install'    - Equivalent to `{install, true}'
%% * `-sys F'      - Equivalent to `{sys, F}'
%% * `-vsn V'      - Equivalent to `{vsn, V}'
%% * `-pa Dir'     - Equivalent to `{pa, Dir}'
%% * `-pz Dir'     - Equivalent to `{pa, Dir}'
%% * `-v'          - Equivalent to `{verbose, true}'
%%
%% == Config File evaluation ==
%%
%% `setup' uses a customized version of `file:script()'. The return value
%% from the script will be treated as a list of instructions to `setup'.
%% Currently, a pseudo-local function, `b()' allows the script to inspect
%% the current variable bindings, and using instructions like
%% `{include, ConfigFile, Vars}', variables can be passed along to helper
%% scripts. Using the pattern `{include, ConfigFile, [{Key, Value}|b()]}',
%% all current variables can be passed to the helper script. Note that
%% `setup' may override the values of variables `Name', `SCRIPT', `CWD'
%% and `OPTIONS'. Specifically, these variables are bound to:
%%
%% * `Name': the name of the system being installed
%% * `SCRIPT': the (absolute) name of the script currently being evaluated
%% * `CWD': the current working directory when setup_gen was invoked
%% * `OPTIONS': the options passed to the setup_gen script
%%
%% The following local functions are handled by the script evaluator:
%%
%% * `b() -> Bindings'
%% * `eval(File) -> {ok, Result} | {error, Reason}'
%% * `eval(File, Vars) -> {ok, Result} | {error, Reason}'
%% * `eval_lib(File) -> {ok, Result} | {error, Reason}'
%% * `eval_lib(File, Vars) -> {ok, Result} | {error, Reason}'
%%
%% The `eval/[1,2]' and `eval_lib/[1,2]' functions work like the
%% `include' and `include_lib' instructions above, except the result is
%% returned as a normal function return value rather than being embedded
%% into the `setup' data. Essentially, they work like `file:script()', but
%% with the variable bindings expected by a `setup' script and these local
%% functions supported.
%% @end
%%
run(Options) ->
    case lists:keyfind(verbose, 1, Options) of
        {_, true} -> put(verbose, true);
        _ -> ignore
    end,
    setup_lib:determine_if_escript(),
    ?if_verbose(io:fwrite("Options = ~p~n", [Options])),
    Config = read_config(Options),
    ?if_verbose(io:fwrite("Config = ~p~n", [Config])),
    FullOpts = insert_config(Config, Options),
    ?if_verbose(io:fwrite("FullOpts = ~p~n", [FullOpts])),
    {Name, OutDir, RelDir, RelVsn, GenTarget} = name_and_target(FullOpts),
    ensure_dir(RelDir),
    Roots = roots(FullOpts),
    ?if_verbose(io:fwrite("Roots = ~p~n", [Roots])),
    check_config(FullOpts),
    Env = env_vars(FullOpts),
    InstEnv = install_env(Env, FullOpts),
    add_paths(Roots, FullOpts),
    Apps0 = apps(FullOpts, Env),
    Rel = {release, {Name, RelVsn}, {erts, erts_vsn()}, [A || {A,_} <- Apps0]},
    ?if_verbose(io:fwrite("Rel: ~p~n", [Rel])),
    build_target_lib(GenTarget, OutDir, Apps0),
    copy_erts(GenTarget, OutDir),
    in_dir(RelDir,
           fun() ->
                   setup_lib:write_eterm("start.rel", Rel),
                   make_boot("start", GenTarget, Roots),
                   setup_lib:write_eterm("sys.config", Env),
                   if_install(FullOpts,
                              fun() ->
                                      InstRel = make_install_rel(Rel),
                                      setup_lib:write_eterm(
                                        "install.rel", InstRel),
                                      setup_lib:write_eterm(
                                        "install.config", InstEnv),
                                      make_boot("install", GenTarget, Roots)
                              end, ok),
                   setup_lib:write_eterm("setup_gen.eterm", FullOpts)
           end).

insert_config(Conf, Options) ->
    lists:flatmap(
      fun({conf, _} = C) ->
              [C|Conf];
         (Other) ->
              [Other]
      end, Options).

name_and_target(FullOpts) ->
    Name = option(name, FullOpts),
    case proplists:get_value(target, FullOpts, false) of
        false ->
            RelDir = option(outdir, FullOpts),
            RelVsn = rel_vsn(RelDir, FullOpts),
            {Name, RelDir, RelDir, RelVsn, false};
        TargetDir ->
            RelVsn = option(vsn, FullOpts),
            RelDir = filename:join([TargetDir, "releases", RelVsn]),
            case filelib:is_dir(TargetDir) of
                true  -> abort("Target directory exists: ~s~n", [TargetDir]);
                false -> ok
            end,
            {Name, TargetDir, RelDir, RelVsn, true}
    end.


build_target_lib(false, _, _) ->
    ok;
build_target_lib(true, Root, Apps) ->
    LibRoot = filename:join(Root, "lib"),
    file:make_dir(LibRoot),
    in_dir(LibRoot,
           fun() ->
                   lists:foreach(
                     fun({A, D}) ->
                             AppName = element(1, A),
                             AppVsn = element(2, A),
                             AppD = atom_to_list(AppName) ++ "-" ++ AppVsn,
                             Dir = filename:dirname(D),
                             copy_app(Dir, AppD)
                     end, Apps)
           end).

copy_erts(false, _) ->
    ok;
copy_erts(true, Root) ->
    {ok, [[ErlRoot]]} = init:get_argument(root),
    [Erts] = filelib:wildcard(filename:join(ErlRoot, "erts-*")),
    BaseName = filename:basename(Erts),
    file:make_dir(filename:join(Root, BaseName)),
    Cmd = "cp -r " ++ Erts ++ "/bin " ++ Root ++ "/" ++ BaseName ++ "/",
    os:cmd(Cmd).


copy_app(A0, To) ->
    file:make_dir(To),
    os:cmd("cp -r " ++ A0 ++ "/priv " ++ To),
    os:cmd("cp -r " ++ A0 ++ "/ebin " ++ To).


if_install(Options, F, Else) ->
    case proplists:get_value(install,Options,false) of
        true ->
            F();
        _ ->
            Else
    end.

options(["-target"       , D|T]) -> [{target,D}|options(T)];
options(["-name"         , N|T]) -> [{name, N}|options(T)];
options(["-root"         , D|T]) -> [{root, D}|options(T)];
options(["-out"          , D|T]) -> [{outdir, D}|options(T)];
options(["-relconf"      , F|T]) -> [{relconf, F}|options(T)];
options(["-conf"         , F|T]) -> [{conf, F}|options(T)];
%% options(["-target_subdir", D|T]) -> [{target_subdir, D}|options(T)];
options(["-install"])            -> [{install, true}];
options(["-install" | ["-" ++ _|_] = T]) -> [{install, true}|options(T)];
options(["-install"      , D|T]) -> [{install, mk_bool(D)}|options(T)];
options(["-start_setup"  , D|T]) -> [{start_setup, mk_bool(D)}|options(T)];
options(["-sys"          , D|T]) -> [{sys, D}|options(T)];
options(["-vsn"          , D|T]) -> [{vsn, D}|options(T)];
options(["-pa"           , D|T]) -> [{pa, D}|options(T)];
options(["-pz"           , D|T]) -> [{pz, D}|options(T)];
options(["-v"               |T]) -> [{verbose, true}|options(T)];
options(["-V" ++ VarName, ExprStr | T]) ->
    Var = list_to_atom(VarName),
    Term = parse_term(ExprStr),
    [{var, Var, Term}|options(T)];
options([_Other|_] = L) ->
    abort("Unknown_option: ~p~n", [L]);
options([]) ->
    [].

mk_bool(T) when T=="true" ; T=="1" -> true;
mk_bool(F) when F=="false"; F=="0" -> false;
mk_bool(Other) ->
    abort("Expected truth value (~p)~n", [Other]).

parse_term(Str) ->
    case erl_scan:string(Str) of
        {ok, Ts, _} ->
            case erl_parse:parse_term(ensure_dot(Ts)) of
                {ok, T} ->
                    T;
                {error,_} = EParse ->
                    abort(EParse, [])
            end;
        {error,_,_} = EScan ->
            abort(EScan, [])
    end.

ensure_dot(Ts) ->
    case lists:reverse(Ts) of
        [{dot,_}|_] ->
            Ts;
        Rev ->
            lists:reverse([{dot,1}|Rev])
    end.

%% target_dir(RelDir, Config) ->
%%     D = case proplists:get_value(target_subdir, Config) of
%%          undefined ->
%%              RelDir;
%%          Sub ->
%%              filename:join(RelDir, Sub)
%%      end,
%%     ensure_dir(D),
%%     D.

ensure_dir(D) ->
    case filelib:is_dir(D) of
        true ->
            ok;
        false ->
            case filelib:ensure_dir(D) of
                ok ->
                    case file:make_dir(D) of
                        ok ->
                            ok;
                        MakeErr ->
                            abort("Could not create ~s (~p)~n", [D, MakeErr])
                    end;
                EnsureErr ->
                    abort("Parent of ~s could not be created or is not "
                          "writeable (~p)~n", [D, EnsureErr])
            end
    end.

read_config(Opts) ->
    case lists:keyfind(conf, 1, Opts) of
        false ->
            read_rel_config(Opts);
        {_, F} ->
            Name = option(name, Opts),
            setup:read_config_script(F, Name, Opts)
    end.

read_rel_config(Opts) ->
    case lists:keyfind(relconf, 1, Opts) of
        {relconf, F} ->
            Name = option(name, Opts),
            case file:consult(F) of
                {ok, Conf} ->
                    ?if_verbose(io:fwrite("Relconf = ~p~n", [Conf])),
                    SysConf = option(sys, Conf),
                    LibDirs = option(lib_dirs, SysConf),
                    TargetOpt = rel_conf_target_dir(Conf, Opts),
                    case [{V,As} || {rel,N,V,As} <- SysConf,
                                N == Name] of
                        [] ->
                            abort("No matching 'rel' (~w) in ~s~n", [Name, F]);
                        [{V,Apps}] ->
                            TargetOpt ++
                                [{vsn, V},
                                 {apps, Apps} | [{root, D} || D <- LibDirs]]
                    end;
                Error ->
                    abort("Error reading relconf ~s:~n"
                          "~p~n", [F, Error])
            end;
        false ->
            abort("No usable config file~n", [])
    end.

rel_conf_target_dir(Conf, Opts) ->
    case lists:keyfind(target, 1, Opts) of
        {_, _} ->
            %% The 'target' option overrides whatever is in the relconf
            [];
        false ->
            case lists:keyfind(target_dir, 1, Conf) of
                false ->
                    [];
                {_, TargetDir} ->
                    [{target, TargetDir}]
            end
    end.

roots(Opts) ->
    [R || {root, R} <- Opts].

check_config(Conf) ->
    _ = [mandatory(K, Conf) || K <- [apps]],
    ok.

option(K, Opts) ->
    case lists:keyfind(K, 1, Opts) of
        {_, V} ->
            V;
        false ->
            abort("Mandatory: -~s~n", [atom_to_list(K)])
    end.

env_vars(Options) ->
    Env0 = case proplists:get_value(sys, Options) of
               undefined ->
                   [];
               Sys ->
                   case file:consult(Sys) of
                       {ok, [E]} ->
                           E;
                       {error, Reason} ->
                           abort("Error reading ~s:~n"
                                 "~p~n", [Sys, Reason])
                   end
           end,
    SetupEnv = if_install(Options, fun() -> [{env, [{setup,
                                                     [{conf,Options}]}]}]
                                   end, []),
    lists:foldl(fun merge_env/2,
                Env0, [{K,E} || {K, E} <- Options ++ SetupEnv,
                                K == env orelse K == set_env]).

install_env(Env, Options) ->
    Dist =
        case proplists:get_value(nodes, Options, []) of
            []  -> [];
            [_] -> [];
            [_,_|_] = Nodes ->
                [{sync_nodes_mandatory, Nodes},
                 {sync_nodes_timeout, infinity},
                 {distributed, [{setup, [hd(Nodes)]}]}]
        end,
    case lists:keyfind(kernel, 1, Env) of
        false ->
            [{kernel, Dist} | Env];
        {_, KEnv} ->
            Env1 = Dist ++
                [E || {K,_} = E <- KEnv,
                      not lists:member(K, [sync_nodes_optional,
                                           sync_nodes_mandatory,
                                           sync_nodes_timeout,
                                           distributed])],
            lists:keyreplace(kernel, 1, Env, {kernel, Env1})
    end.

merge_env({env, E}, Env) ->
    lists:foldl(
      fun({App, AEnv}, Acc) ->
              case lists:keyfind(App, 1, Env) of
                  false ->
                      Acc ++ [{App, AEnv}];
                  {_, AEnv1} ->
                      New = {App, lists:foldl(
                                    fun({K,V}, Acc1) ->
                                            lists:keystore(K,1,Acc1,{K,V});
                                       ({K,W,V}, Acc1) ->
                                            lists:keystore(K,1,Acc1,{K,W,V})
                                    end, AEnv1, AEnv)},
                      lists:keyreplace(App, 1, Acc, New)
              end
      end, Env, E);
merge_env({set_env, E}, Env) ->
    lists:foldl(
      fun({App, AEnv}, Acc) ->
              case lists:keyfind(App, 1, Env) of
                  false ->
                      Acc ++ [{App, merge_set_env(AEnv, [])}];
                  {_, AEnv1} ->
                      New = {App, merge_set_env(AEnv, AEnv1)},
                      lists:keyreplace(App, 1, Acc, New)
              end
      end, Env, E).

merge_set_env(E, E0) ->
    lists:foldl(
      fun({K, V}, Acc) when is_atom(K) ->
              lists:keystore(K, 1, Acc, {K, V});
         ({[H|T], V}, Acc) ->
              case lists:keyfind(H, 1, Acc) of
                  false ->
                      Acc ++ [{H, mktree(T, V)}];
                  {_, Tree} ->
                      lists:keyreplace(H, 1, Acc, {H, into_tree(T, Tree, V)});
                  {_, V1, Tree} ->
                      lists:keyreplace(
                        H, 1, Acc, {H, V1, into_tree(T, Tree, V)})
              end;
         ({[], _}, Acc) ->
              %% allow, but ignore
              Acc
      end, E0, E).

mktree([K], V) ->
    {K, V};
mktree([H|T], V) ->
    {H, [mktree(T, V)]}.

into_tree([K], T0, V) ->
    lists:keystore(K, 1, T0, {K, V});
into_tree([H|T] = L, T0, V) when is_list(T0) ->
    case lists:keyfind(H, 1, T0) of
        false ->
            T0 ++ [mktree(L, V)];
        {_, T1} ->
            lists:keystore(H, 1, T0, {H, into_tree(T, T1, V)});
        {_, V1, T1} ->
            lists:keystore(H, 1, T0, {H, V1, into_tree(T, T1, V)})
    end;
into_tree([_|_] = L, _, V) ->
    [mktree(L, V)].


mandatory(K, Conf) ->
    case lists:keymember(K, 1, Conf) of
        false ->
            abort("missing mandatory config item: ~p~n", [K]);
        true ->
            ok
    end.

in_dir(D, F) ->
    {ok, Old} = file:get_cwd(),
    try file:set_cwd(D) of
        ok ->
            ?if_verbose(io:fwrite("entering directory ~s~n", [D])),
            F();
        Error ->
            abort("Error entering rel dir (~p): ~p~n", [D,Error])
    after
        ok = file:set_cwd(Old)
    end.

-define(is_type(T), T==permanent;T==temporary;T==transient;T==load).

apps(Options, Env) ->
    %% Apps0 = proplists:get_value(apps, Options, [])
    %%     ++ lists:concat(proplists:get_all_values(add_apps, Options)),
    {AddApps, RemoveApps} = add_remove_apps(Options, Env),
    Apps0 = remove_apps(RemoveApps,
                        proplists:get_value(apps, Options, []) ++ AddApps),
    Apps1 = trim_duplicates(
              if_install(Options,
                         fun() ->
                                 ensure_setup(Apps0)
                         end, Apps0)),
    AppNames = app_names(Apps1),
    ?if_verbose(io:fwrite("Apps1 = ~p~n", [Apps1])),
    AppVsns = lists:flatmap(
                fun(A) when is_atom(A) ->
                        {V,D} = app_vsn(A),
                        [{{A, V}, D}];
                   ({A,V}) when is_list(V) ->
                        {V1,D} = app_vsn(A, V),
                        [{{A, V1}, D}];
                   ({A,Type}) when ?is_type(Type) ->
                        {V1,D} = app_vsn(A),
                        [{{A, V1, Type}, D}];
                   ({A,V,Type}) when ?is_type(Type) ->
                        {V1,D} = app_vsn(A, V),
                        [{{A, V1, Type}, D}];
                   ({A,V,Incl}) when is_list(Incl) ->
                        {V1, D} = app_vsn(A, V),
                        expand_included(Incl, AppNames)
                            ++ [{{A, V1, Incl}, D}];
                   ({A,V,Type,Incl}) when ?is_type(Type) ->
                        {V1, D} = app_vsn(A, V),
                        expand_included(Incl, AppNames)
                            ++ [{{A, V1, Type, Incl}, D}]
                end, sort_apps(Options, Apps1)),
    ?if_verbose(io:fwrite("AppVsns = ~p~n", [AppVsns])),
    %% setup_is_load_only(replace_versions(AppVsns, Apps1)).
    case proplists:get_value(start_setup, Options, true) of
        true -> AppVsns;
        false ->
            setup_is_load_only(AppVsns)
    end.

add_remove_apps(Options, _Env) ->
    lists:foldl(
      fun({add_apps, As}, {Incl, Excl}) ->
              {add_to_set(As, Incl), del_from_set(As, Excl)};
         ({remove_apps, As}, {Incl, Excl}) ->
              {del_from_set(As, Incl), add_to_set(app_names(As), Excl)};
         (_, Acc) ->
              Acc
      end, {[], []}, Options).

sort_apps(Options, Apps) ->
    lists:foldl(fun({sort_app, A, Before}, Acc) ->
                        case is_in_set(A, Acc) of
                            {true, App} ->
                                insert_before(Acc -- [App], App,
                                              mk_set(Before));
                            false ->
                                abort("Cannot re-sort ~p - not found~n", [A])
                        end;
                   (_, Acc) ->
                        Acc
                end, Apps, Options).

insert_before([H|T], App, Before) ->
    case is_in_set(H, Before) of
        {true, _} ->
            [App, H|T];
        false ->
            [H|insert_before(T, App, Before)]
    end.

add_to_set(As, Set) ->
    lists:foldl(fun(A, Acc) ->
                        case is_in_set(A, Acc) of
                            {true, Prev} ->
                                lists:delete(Prev, Acc) ++ [A];
                            false ->
                                Acc ++ [A]
                        end
                end, Set, As).

del_from_set(As, Set) ->
    lists:foldl(fun(A, Acc) ->
                        case is_in_set(A, Acc) of
                            {true, Prev} ->
                                lists:delete(Prev, Acc);
                            false ->
                                Acc
                        end
                end, Set, As).

%% ensure that this is a 'set' (well, at least a list)
mk_set(Set) when is_list(Set) ->
    Set;
mk_set(Entry) ->
    [Entry].

is_in_set(Entry, Set) ->
    A = if is_tuple(Entry) -> element(1, Entry);
           is_atom(Entry)  -> Entry
        end,
    name_in_set(A, Set).

app_name(A) when is_atom(A) -> A;
app_name(T) when is_tuple(T) -> element(1, T).

app_names(As) ->
    [app_name(A) || A <- As].

name_in_set(A, [A|_]) -> {true, A};
name_in_set(A, [H|_]) when element(1,H) == A -> {true, H};
name_in_set(A, [_|T]) ->
    name_in_set(A, T);
name_in_set(_, []) ->
    false.


remove_apps(Remove, Apps) ->
    lists:filter(
      fun(Entry) ->
              A = case Entry of
                      Am when is_atom(Am) -> Am;
                      T when is_tuple(T)  -> element(1,T)
                  end,
              not lists:member(A, Remove)
      end, Apps).



trim_duplicates([A|As0]) when is_atom(A) ->
    As1 = [Ax || Ax <- As0, Ax =/= A],
    case lists:keymember(A, 1, As0) of
        false ->
            [A|trim_duplicates(As1)];
        true ->
            %% a more well-defined entry exists; use that one.
            trim_duplicates(As1)
    end;
trim_duplicates([At|As0]) when is_tuple(At) ->
    %% Remove all exact duplicates
    As1 = [Ax || Ax <- As0, Ax =/= At],
    %% If other detailed entries (though not duplicates) exist, we should
    %% perhaps try to combine them. For now, let's just abort.
    case [Ay || Ay <- As1, element(1,Ay) == element(1,At)] of
        [] ->
            [At|trim_duplicates(As0)];
        [_|_] = Duplicates ->
            abort("Conflicting app entries: ~p~n", [[At|Duplicates]])
    end;
trim_duplicates([]) ->
    [].


expand_included(Incl, AppNames) ->
    R = case Incl -- AppNames of
            [] ->
                [];
            Implicit ->
                [{A, app_vsn(A), load} || A <- Implicit]
        end,
    ?if_verbose(io:fwrite("expand_included(~p, ~p) -> ~p~n",
                          [Incl, AppNames, R])),
    R.

ensure_setup([setup|_] = As) -> As;
ensure_setup([A|_] = As) when element(1,A) == setup -> As;
ensure_setup([H|T]) -> [H|ensure_setup(T)];
ensure_setup([]) ->
    [setup].

setup_is_load_only(Apps) ->
    lists:map(fun({{setup,V}, D}) ->
                      {{setup,V,load}, D};
                 (A) ->
                      A
              end, Apps).

add_paths(Roots, Opts) ->
    APaths = proplists:get_all_values(pa, Opts),
    _ = [ true = code:add_patha(P) || P <- APaths ],

    ZPaths = proplists:get_all_values(pz, Opts),
    _ = [ true = code:add_pathz(P) || P <- ZPaths ],

    Paths = case proplists:get_value(wild_roots, Opts, false) of
                true ->
                    lists:foldl(fun(R, Acc) ->
                                        expand_root(R, Acc)
                                end, [], Roots);
                false ->
                    lists:concat(
                      lists:map(fun(R) ->
                                        case filelib:wildcard(
                                               filename:join(R, "lib/*/ebin")) of
                                            [] ->
                                                filelib:wildcard(
                                                  filename:join(R, "*/ebin"));
                                            Ds ->
                                                Ds
                                        end
                                end, Roots))
            end,
    ?if_verbose(io:fwrite("Paths = ~p~n", [Paths])),
    Res = code:set_path(Paths ++ (code:get_path() -- Paths)),
    %% Res = code:add_pathsa(Paths -- code:get_path()),
    ?if_verbose(io:fwrite("add path Res = ~p~n", [Res])).

expand_root(R, Acc) ->
    case filename:basename(R) of
        "ebin" ->
            [R|Acc];
        _ ->
            case file:list_dir(R) of
                {ok, Fs} ->
                    lists:foldl(fun(F, Acc1) ->
                                        expand_root(filename:join(R, F), Acc1)
                                end, Acc, Fs);
                {error,enotdir} ->
                    Acc;
                {error,_} = E ->
                    ?if_verbose(io:fwrite("warning: ~p (~s)~n", [E, R])),
                    Acc
            end
    end.

rel_vsn(RelDir, Options) ->
    case proplists:get_value(vsn, Options) of
        undefined ->
            Dir =
                case RelDir of
                    "." ->
                        {ok,Cwd} = file:get_cwd(),
                        Cwd;
                    D ->
                        D
                end,
            filename:basename(Dir);
        V when is_list(V) ->
            V;
        Other ->
            abort("Invalid release version ~p~n", [Other])
    end.

erts_vsn() ->
    erlang:system_info(version).

app_vsn(A) ->
    app_vsn(A, latest).
    %% AName = if is_atom(A) -> A;
    %%            true -> element(1, A)
    %%         end,
    %% D = code:lib_dir(AName),
    %% AppFile = filename:join(D, "ebin/" ++ atom_to_list(AName) ++ ".app"),
    %% case file:consult(AppFile) of
    %%     {ok, [{application, _, Opts}]} ->
    %%         V = proplists:get_value(vsn, Opts),
    %%         ?if_verbose(io:fwrite("app_vsn(~p) -> ~p~n", [A,V])),
    %%         V;
    %%     Other ->
    %%         abort("Oops reading .app file (~p): ~p~n", [AppFile, Other])
    %% end.

app_vsn(A, V) ->
    AppStr = atom_to_list(A),
    Path = code:get_path(),
    Found = [D || D <- Path, is_app(AppStr, D)],
    Sorted = setup_lib:sort_vsns(lists:usort(Found), AppStr),
    ?if_verbose(io:fwrite("Sorted = ~p~n", [Sorted])),
    match_app_vsn(Sorted, V, AppStr).

match_app_vsn(Vsns, latest, App) ->
    %% element(1, lists:last(Vsns));
    case Vsns of
        [] ->
            abort("No version of ~s found~n", [App]);
        [_|_] ->
            lists:last(Vsns)
    end;
match_app_vsn(Vsns, V, App) when is_list(V) ->
    case [Pair || {V1, _} = Pair <- Vsns,
                V == V1] of
        [FoundV] ->
            FoundV;
        [] ->
            abort("Cannot find version ~s of ~s~n", [V, App])
    end.


is_app(A, D) ->
    case re:run(D, A ++ "[^/]*/ebin\$") of
        {match, _} ->
            true;
        nomatch ->
            false
    end.

%% replace_versions([App|Apps], [H|T]) ->
%%     A = element(1, App),
%%     V = element(2, App),
%%     Res =
%%         if is_atom(H) ->
%%                 A = H,  % assertion
%%                 {A, V};
%%            true ->
%%                 A = element(1, H), % assertion
%%                 setelement(2, H, V)
%%         end,
%%     [Res | replace_versions(Apps, T)];
%% replace_versions([], []) ->
%%     [].

make_boot(Rel, GenTarget, Roots) ->
    Path = path(Roots),
    Vars =
        if GenTarget -> [];
           true ->
                {Vs, _} = lists:mapfoldl(
                            fun(R, N) ->
                                    V = var_name(N),
                                    {{V, R}, N+1}
                            end, 1, Roots),
                Vs
        end,
    ?if_verbose(io:fwrite("Path = ~p~n", [Path])),
    Opts = if GenTarget -> [no_module_tests];
              true      -> [no_module_tests, local, {variables, Vars}]
           end,
    Res = systools:make_script(Rel, [{path, path(Roots)}|Opts]),
    ?if_verbose(io:fwrite("make_script() -> ~p~n", [Res])).


make_install_rel({release, R, Erts, Apps}) ->
    Apps1 =
        lists:map(
          fun({setup,V,load}) ->
                  {setup, V};
             (A) ->
                  case lists:member(element(1,A), [stdlib,kernel,sasl]) of
                      true ->
                          A;
                      false ->
                          case A of
                              {Nm,Vsn} ->
                                  {Nm,Vsn,load};
                              {Nm,Vsn,Inc} when is_list(Inc) ->
                                  {Nm,Vsn,load,Inc};
                              _ ->
                                  A
                          end
                  end
          end, Apps),
    %% Apps2 = case app_vsn(setup) of
    %%          undefined ->
    %%              Apps1;
    %%          V ->
    %%              Apps1 ++ [{setup, V}]
    %%      end,
    {release, R, Erts, Apps1}.


path(Roots) ->
    [filename:join(R, "lib/*/ebin") || R <- Roots].


var_name(N) ->
    "V" ++ integer_to_list(N).
