%%% -*- comment-column: 34 -*-
%%%----------------------------------------------------------------------
%%% File    : site_code.erl
%%% Author  : Luke Gorrie <luke@bluetail.com>
%%% Purpose : Code server for loading site-local code.
%%%           Based on a require(PackageName) interface, like Emacs Lisp.
%%% Created : 27 Jan 2003 by Luke Gorrie <luke@bluetail.com>
%%%----------------------------------------------------------------------

%% This is a server for finding "site local" Erlang packages and
%% adding them to the code path. Here's how it works:
%%
%% If you have a directory called ~/site-erlang/myapp/, then calling
%% site_code:require(myapp) will add the ebin/ subdirectory to the
%% code server's load path, and also call myapp:site_init() if that
%% function exists. This site_init() function can also make calls to
%% require other packages that it needs, and so on. Nothing happens if
%% you require() a package that is already initialised.
%%
%% The default search path is ~/site-erlang/ and
%% /usr/local/share/erlang/site-erlang/, but more can be included with
%% add_dir/1.

-module(site_code).
-author('luke@bluetail.com').

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

-behaviour(gen_server).

%% External exports
-export([start_link/0, require/1, add_dir/1, loaded/0]).

%% gen_server callbacks
-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]).

%%%----------------------------------------------------------------------
%%% API
%%%----------------------------------------------------------------------

start_link() ->
    gen_server:start_link({local, site_code}, site_code, [], []).

require(Name) ->
    Res = case am_site_code_server() of
	      true  -> do_require(Name);
	      false -> gen_server:call(?MODULE, {require, Name})
	  end,
    case Res of
	ok -> ok;
	{error, Reason} -> exit({require, Name, Reason})
    end.

add_dir(Dir) -> gen_server:call(?MODULE, {add_dir, Dir}).
loaded()     -> gen_server:call(?MODULE, loaded).

%%%----------------------------------------------------------------------
%%% Callback functions from gen_server
%%%----------------------------------------------------------------------

init([]) ->
    setup_default_paths(),
    {ok, []}.

handle_call({require, Name}, From, []) ->
    {reply, do_require(Name), []};

handle_call({add_dir, Dir}, From, []) ->
    add_path(Dir),
    {reply, ok, []};

handle_call(loaded, From, []) ->
    {reply,
     [{Package, Path} || {{loaded, Package}, Path} <- get()],
     []}.

handle_cast(Msg, State)           -> {noreply, State}.
handle_info(Info, State)          -> {noreply, State}.
terminate(Reason, State)          -> ok.
code_change(OldVsn, State, Extra) -> {ok, State}.

%%%----------------------------------------------------------------------
%%% Internal functions
%%%----------------------------------------------------------------------

%% Ensure that a package is loaded.
%% Pkg = atom()
%% Returns: ok | {error, Reason}
do_require(Pkg) ->
    case is_loaded(Pkg) of
	false -> load(Pkg, get_paths());
	true  -> ok
    end.

load(Pkg, Paths) ->
    case find_package(Pkg, Paths) of
	{ok, Dir} ->
	    code:add_path(filename:join(Dir, "ebin")),
	    have_loaded(Pkg, Dir),
	    case init_package(Pkg) of
		ok ->
		    ok;
		{error, Reason} ->
		    was_corrupt(Pkg),
		    {error, Reason}
	    end;
	not_found ->
	    {error, "Can't find directory"}
    end.

%% Initiaize a package by calling the site_init() function of the
%% module with the same name, if it exists.
init_package(Mod) ->
    case has_init_function(Mod) of
	false -> ok;
	true  ->
	    case catch Mod:site_init() of
		ok ->
		    ok;
		{error, Rsn}  ->
		    {error, Rsn};
		{'EXIT', Rsn} ->
		    {error, fmt("Bad site_init result: ~p", [Rsn])}
	    end
    end.

%% true iff Mod is loadable and has a site_init/0 function
has_init_function(Mod) ->
    case catch Mod:module_info(exports) of
	Exports when list(Exports) ->
	    lists:member({site_init, 0}, Exports);
	{'EXIT', _} ->
	    false
    end.

%% find_package(Pkg, Paths) -> {ok, PkgDir} | not_found
find_package(Pkg, []) ->
    not_found;
find_package(Pkg, [Path|Paths]) ->
    Dir = filename:join(Path, atom_to_list(Pkg)),
    case dir_exists(Dir) of
	true  -> {ok, Dir};
	false -> find_package(Pkg, Paths)
    end.

dir_exists(Path) ->
    case file:read_file_info(Path) of
	{ok, #file_info{type=directory}} ->
	    true;
	_ ->
	    false
    end.

%% ----------------------------------------------------------------------
%% Setup the default search path

setup_default_paths() ->
    init_paths(),
    case os:getenv("HOME") of
	false -> ok;
	Home  -> maybe_add_path(filename:join(Home, "site-erlang"))
    end,
    maybe_add_path("/usr/local/share/erlang/site-erlang").

%% Add directory P to the search path, if it exists.
maybe_add_path(P) ->
    case dir_exists(P) of
	true  -> add_path(P);
	false -> ok
    end.

fmt(F, A) -> io_lib:format(F, A).

am_site_code_server() ->
    self() == whereis(?MODULE).

%% ----------------------------------------------------------------------
%% Here be dragons..

is_loaded(Pkg)        -> get({loaded, Pkg}) /= undefined.
have_loaded(Pkg, Dir) -> put({loaded, Pkg}, Dir).
was_corrupt(Pkg)      -> erase({loaded, Pkg}).

init_paths() -> put(paths, []).
get_paths()  -> get(paths).
add_path(P)  -> put(paths, get_paths() ++ [P]).

