%% =====================================================================
%% This library is free software; you can redistribute it and/or modify
%% it under the terms of the GNU Lesser General Public License as
%% published by the Free Software Foundation; either version 2 of the
%% License, or (at your option) any later version.
%%
%% This library is distributed in the hope that it will be useful, but
%% WITHOUT ANY WARRANTY; without even the implied warranty of
%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
%% Lesser General Public License for more details.
%%
%% You should have received a copy of the GNU Lesser General Public
%% License along with this library; if not, write to the Free Software
%% Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
%% USA
%%
%% @copyright 2003-2006 Richard Carlsson
%% @author Richard Carlsson <carlsson.richard@gmail.com>
%% @see edoc
%% @end
%% =====================================================================

%% @doc Standard doclet module for EDoc.

%% Note that this is written so that it is *not* depending on edoc.hrl!

%% TODO: copy "doc-files" subdirectories, recursively.
%% TODO: generate summary page of TODO-notes
%% TODO: generate summary page of deprecated things
%% TODO: generate decent indexes over modules, methods, records, etc.

-module(edoc_doclet).

-export([run/2]).

-import(edoc_report, [report/2, warning/2]).

%% @headerfile "edoc_doclet.hrl"
-include("../include/edoc_doclet.hrl").

-define(EDOC_APP, edoc).
-define(DEFAULT_FILE_SUFFIX, ".html").
-define(INDEX_FILE, "index.html").
-define(OVERVIEW_FILE, "overview.edoc").
-define(PACKAGE_SUMMARY, "package-summary.html").
-define(OVERVIEW_SUMMARY, "overview-summary.html").
-define(PACKAGES_FRAME, "packages-frame.html").
-define(MODULES_FRAME, "modules-frame.html").
-define(STYLESHEET, "stylesheet.css").
-define(IMAGE, "erlang.png").
-define(NL, "\n").

-include_lib("xmerl/include/xmerl.hrl").

%% Sources is the list of inputs in the order they were found.  Packages
%% and Modules are sorted lists of atoms without duplicates. (They
%% usually include the data from the edoc-info file in the target
%% directory, if it exists.) Note that the "empty package" is never
%% included in Packages!

%% @spec (Command::doclet_gen() | doclet_toc(), edoc_context()) -> ok
%% @doc Main doclet entry point. See the file <a
%% href="../include/edoc_doclet.hrl">`edoc_doclet.hrl'</a> for the data
%% structures used for passing parameters.
%%
%% Also see {@link edoc:layout/2} for layout-related options, and
%% {@link edoc:get_doc/2} for options related to reading source
%% files.
%%
%% Options:
%% <dl>
%%  <dt>{@type {file_suffix, string()@}}
%%  </dt>
%%  <dd>Specifies the suffix used for output files. The default value is
%%      `".html"'.
%%  </dd>
%%  <dt>{@type {hidden, boolean()@}}
%%  </dt>
%%  <dd>If the value is `true', documentation of hidden modules and
%%      functions will also be included. The default value is `false'.
%%  </dd>
%%  <dt>{@type {overview, edoc:filename()@}}
%%  </dt>
%%  <dd>Specifies the name of the overview-file. By default, this doclet
%%      looks for a file `"overview.edoc"' in the target directory.
%%  </dd>
%%  <dt>{@type {private, boolean()@}}
%%  </dt>
%%  <dd>If the value is `true', documentation of private modules and
%%      functions will also be included. The default value is `false'.
%%  </dd>
%%  <dt>{@type {stylesheet, string()@}}
%%  </dt>
%%  <dd>Specifies the URI used for referencing the stylesheet. The
%%      default value is `"stylesheet.css"'. If an empty string is
%%      specified, no stylesheet reference will be generated.
%%  </dd>
%%  <dt>{@type {stylesheet_file, edoc:filename()@}}
%%  </dt>
%%  <dd>Specifies the name of the stylesheet file. By default, this
%%      doclet uses the file `"stylesheet.css"' in the `priv'
%%      subdirectory of the EDoc installation directory. The named file
%%      will be copied to the target directory.
%%  </dd>
%%  <dt>{@type {title, string()@}}
%%  </dt>
%%  <dd>Specifies the title of the overview-page.
%%  </dd>
%% </dl>

%% INHERIT-OPTIONS: title/2
%% INHERIT-OPTIONS: sources/5
%% INHERIT-OPTIONS: overview/4
%% INHERIT-OPTIONS: copy_stylesheet/2
%% INHERIT-OPTIONS: stylesheet/1

run(#doclet_gen{}=Cmd, Ctxt) ->
    gen(Cmd#doclet_gen.sources,
	Cmd#doclet_gen.app,
	Cmd#doclet_gen.packages,
	Cmd#doclet_gen.modules,
	Cmd#doclet_gen.filemap,
	Ctxt);
run(#doclet_toc{}=Cmd, Ctxt) ->
    toc(Cmd#doclet_toc.paths, Ctxt).

gen(Sources, App, Packages, Modules, FileMap, Ctxt) ->
    Dir = Ctxt#context.dir,
    Env = Ctxt#context.env,
    Options = Ctxt#context.opts,
    Title = title(App, Options),
    CSS = stylesheet(Options),
    {Modules1, Error} = sources(Sources, Dir, Modules, Env, Options),
    modules_frame(Dir, Modules1, Title, CSS),
    packages(Packages, Dir, FileMap, Env, Options),
    packages_frame(Dir, Packages, Title, CSS),
    overview(Dir, Title, Env, Options),
    index_file(Dir, length(Packages) > 1, Title),
    edoc_lib:write_info_file(App, Packages, Modules1, Dir),
    copy_stylesheet(Dir, Options),
    copy_image(Dir),
    %% handle postponed error during processing of source files
    case Error of
	true -> exit(error);
	false -> ok
    end.


%% NEW-OPTIONS: title
%% DEFER-OPTIONS: run/2

title(App, Options) ->
    proplists:get_value(title, Options,
			if App == ?NO_APP ->
				"Overview";
			   true ->
				io_lib:fwrite("Application: ~s", [App])
			end).


%% Processing the individual source files.

%% NEW-OPTIONS: file_suffix, private, hidden
%% INHERIT-OPTIONS: edoc:layout/2
%% INHERIT-OPTIONS: edoc:get_doc/3
%% DEFER-OPTIONS: run/2

sources(Sources, Dir, Modules, Env, Options) ->
    Suffix = proplists:get_value(file_suffix, Options,
				 ?DEFAULT_FILE_SUFFIX),
    Private = proplists:get_bool(private, Options),
    Hidden = proplists:get_bool(hidden, Options),
    {Ms, E} = lists:foldl(fun (Src, {Set, Error}) ->
				  source(Src, Dir, Suffix, Env, Set,
					 Private, Hidden, Error, Options)
			  end,
			  {sets:new(), false}, Sources),
    {[M || M <- Modules, sets:is_element(M, Ms)], E}.


%% Generating documentation for a source file, adding its name to the
%% set if it was successful. Errors are just flagged at this stage,
%% allowing all source files to be processed even if some of them fail.

source({M, P, Name, Path}, Dir, Suffix, Env, Set, Private, Hidden,
       Error, Options) ->
    File = filename:join(Path, Name),
    case catch {ok, edoc:get_doc(File, Env, Options)} of
	{ok, {Module, Doc}} ->
	    check_name(Module, M, P, File),
	    case ((not is_private(Doc)) orelse Private)
		andalso ((not is_hidden(Doc)) orelse Hidden) of
		true ->
		    Text = edoc:layout(Doc, Options),
		    Name1 = atom_to_list(M) ++ Suffix,
                    Encoding = [{encoding,encoding(Doc)}],
		    edoc_lib:write_file(Text, Dir, Name1, P, Encoding),
		    {sets:add_element(Module, Set), Error};
		false ->
		    {Set, Error}
	    end;
	R ->
	    report("skipping source file '~ts': ~W.", [File, R, 15]),
	    {Set, true}
    end.

check_name(M, M0, P0, File) ->
    P = '',
    N = M,
    N0 = M0,
    case N of
	[$? | _] ->
	    %% A module name of the form '?...' is assumed to be caused
	    %% by the epp_dodger parser when the module declaration has
	    %% the form '-module(?MACRO).'; skip the filename check.
	    ok;
	_ ->
	    if N =/= N0 ->
		    warning("file '~ts' actually contains module '~s'.",
			    [File, M]);
	       true ->
		    ok
	    end
    end,
    if P =/= P0 ->
	    warning("file '~ts' belongs to package '~s', not '~s'.",
		    [File, P, P0]);
       true ->
	    ok
    end.


%% Generating the summary files for packages.

%% INHERIT-OPTIONS: read_file/4
%% INHERIT-OPTIONS: edoc_lib:run_layout/2

packages(Packages, Dir, FileMap, Env, Options) ->
    lists:foreach(fun (P) ->
			  package(P, Dir, FileMap, Env, Options)
		  end,
		  Packages).

package(P, Dir, FileMap, Env, Opts) ->
    Tags = case FileMap(P) of
	       "" ->
		   [];
	       File ->
		   read_file(File, package, Env, Opts)
	   end,
    Data = edoc_data:package(P, Tags, Env, Opts),
    F = fun (M) ->
		M:package(Data, Opts)
	end,
    Text = edoc_lib:run_layout(F, Opts),
    edoc_lib:write_file(Text, Dir, ?PACKAGE_SUMMARY, P).


%% Creating an index file, with some frames optional.
%% TODO: get rid of frames, or change doctype to Frameset

index_file(Dir, Packages, Title) ->
    Frame1 = {frame, [{src,?PACKAGES_FRAME},
		      {name,"packagesFrame"},{title,""}],
	      []},
    Frame2 = {frame, [{src,?MODULES_FRAME},
		      {name,"modulesFrame"},{title,""}],
	      []},
    Frame3 = {frame, [{src,?OVERVIEW_SUMMARY},
		      {name,"overviewFrame"},{title,""}],
	      []},
    Frameset = {frameset, [{cols,"20%,80%"}],
		case Packages of
		    true ->
			[?NL,
			 {frameset, [{rows,"30%,70%"}],
			  [?NL, Frame1, ?NL, Frame2, ?NL]}
			];
		    false ->
 			[?NL, Frame2, ?NL]
		end
		++ [?NL, Frame3, ?NL,
		    {noframes,
		     [?NL,
		      {h2, ["This page uses frames"]},
		      ?NL,
		      {p, ["Your browser does not accept frames.",
			   ?NL, br,
			   "You should go to the ",
			   {a, [{href, ?OVERVIEW_SUMMARY}],
			    ["non-frame version"]},
			   " instead.", ?NL]},
		      ?NL]},
		    ?NL]},
    XML = xhtml_1(Title, [], Frameset),
    Text = xmerl:export_simple([XML], xmerl_html, []),
    edoc_lib:write_file(Text, Dir, ?INDEX_FILE).

packages_frame(Dir, Ps, Title, CSS) ->
    Body = [?NL,
	    {h2, [{class, "indextitle"}], ["Packages"]},
	    ?NL,
	    {table, [{width, "100%"}, {border, 0},
		     {summary, "list of packages"}],
	     lists:concat(
	       [[?NL,
		 {tr, [{td, [], [{a, [{href, package_ref(P)},
				      {target,"overviewFrame"},
				      {class, "package"}],
				  [atom_to_list(P)]}]}]}]
		|| P <- Ps])},
	    ?NL],
    XML = xhtml(Title, CSS, Body),
    Text = xmerl:export_simple([XML], xmerl_html, []),
    edoc_lib:write_file(Text, Dir, ?PACKAGES_FRAME).

modules_frame(Dir, Ms, Title, CSS) ->
    Body = [?NL,
	    {h2, [{class, "indextitle"}], ["Modules"]},
	    ?NL,
	    {table, [{width, "100%"}, {border, 0},
		     {summary, "list of modules"}],
	     lists:concat(
	       [[?NL,
		 {tr, [{td, [],
			[{a, [{href, module_ref(M)},
			      {target, "overviewFrame"},
			      {class, "module"}],
			  [atom_to_list(M)]}]}]}]
		 || M <- Ms])},
	    ?NL],
    XML = xhtml(Title, CSS, Body),
    Text = xmerl:export_simple([XML], xmerl_html, []),
    edoc_lib:write_file(Text, Dir, ?MODULES_FRAME).

module_ref(M) ->
    edoc_refs:relative_package_path(M, '') ++ ?DEFAULT_FILE_SUFFIX.

package_ref(P) ->
    edoc_lib:join_uri(edoc_refs:relative_package_path(P, ''),
		      ?PACKAGE_SUMMARY).

xhtml(Title, CSS, Content) ->
    xhtml_1(Title, CSS, {body, [{bgcolor, "white"}], Content}).

xhtml_1(Title, CSS, Body) ->
    {html, [?NL,
	    {head, [?NL, {title, [Title]}, ?NL] ++ CSS},
	    ?NL,
	    Body,
	    ?NL]
    }.

%% NEW-OPTIONS: overview
%% INHERIT-OPTIONS: read_file/4
%% INHERIT-OPTIONS: edoc_lib:run_layout/2
%% INHERIT-OPTIONS: edoc_extract:file/4
%% DEFER-OPTIONS: run/2

overview(Dir, Title, Env, Opts) ->
    File = proplists:get_value(overview, Opts,
			       filename:join(Dir, ?OVERVIEW_FILE)),
    Encoding = edoc_lib:read_encoding(File, [{in_comment_only, false}]),
    Tags = read_file(File, overview, Env, Opts),
    Data0 = edoc_data:overview(Title, Tags, Env, Opts),
    EncodingAttribute = #xmlAttribute{name = encoding,
                                      value = atom_to_list(Encoding)},
    #xmlElement{attributes = As} = Data0,
    Data = Data0#xmlElement{attributes = [EncodingAttribute | As]},
    F = fun (M) ->
		M:overview(Data, Opts)
	end,
    Text = edoc_lib:run_layout(F, Opts),
    EncOpts = [{encoding,Encoding}],
    edoc_lib:write_file(Text, Dir, ?OVERVIEW_SUMMARY, '', EncOpts).

copy_image(Dir) ->
    case code:priv_dir(?EDOC_APP) of
	PrivDir when is_list(PrivDir) ->
	    From = filename:join(PrivDir, ?IMAGE),
	    edoc_lib:copy_file(From, filename:join(Dir, ?IMAGE));
	_ ->
	    report("cannot find default image file.", []),
	    exit(error)
    end.

%% NEW-OPTIONS: stylesheet_file
%% DEFER-OPTIONS: run/2

copy_stylesheet(Dir, Options) ->
    case proplists:get_value(stylesheet, Options) of
	undefined ->
	    From = case proplists:get_value(stylesheet_file, Options) of
		       File when is_list(File) ->
			   File;
		       _ ->
			   case code:priv_dir(?EDOC_APP) of
			       PrivDir when is_list(PrivDir) ->
				   filename:join(PrivDir, ?STYLESHEET);
			       _ ->
				   report("cannot find default "
					  "stylesheet file.", []),
				   exit(error)
			   end
		   end,
	    edoc_lib:copy_file(From, filename:join(Dir, ?STYLESHEET));
	_ ->
	    ok
    end.

%% NEW-OPTIONS: stylesheet
%% DEFER-OPTIONS: run/2

stylesheet(Options) ->
    case proplists:get_value(stylesheet, Options) of
	"" ->
	    [];
	S ->
	    Ref = case S of
		      undefined ->
			  ?STYLESHEET;
		      "" ->
			  "";    % no stylesheet
		      S when is_list(S) ->
			  S;
		      _ ->
			  report("bad value for option 'stylesheet'.",
				 []),
			  exit(error)
		  end,
	    [{link, [{rel, "stylesheet"},
		     {type, "text/css"},
		     {href, Ref},
		     {title, "EDoc"}], []},
	     ?NL]
    end.

is_private(E) ->
    case get_attrval(private, E) of
 	"yes" -> true;
 	_ -> false
    end.

is_hidden(E) ->
    case get_attrval(hidden, E) of
 	"yes" -> true;
 	_ -> false
    end.

encoding(E) ->
    case get_attrval(encoding, E) of
        "latin1" -> latin1;
        _ -> utf8
    end.

get_attrval(Name, #xmlElement{attributes = As}) ->
    case get_attr(Name, As) of
	[#xmlAttribute{value = V}] ->
	    V;
	[] -> ""
    end.

get_attr(Name, [#xmlAttribute{name = Name} = A | As]) ->
    [A | get_attr(Name, As)];
get_attr(Name, [_ | As]) ->
    get_attr(Name, As);
get_attr(_, []) ->
    [].

%% Read external source file. Fails quietly, returning empty tag list.

%% INHERIT-OPTIONS: edoc_extract:file/4

read_file(File, Context, Env, Opts) ->
    case edoc_extract:file(File, Context, Env, Opts) of
	{ok, Tags} ->
	    Tags;
	{error, _} ->
	    []
    end.


%% TODO: FIXME: meta-level index generation

%% Creates a Table of Content from a list of Paths (ie paths to applications)
%% and an overview file.

-define(EDOC_DIR, "doc").
-define(INDEX_DIR, "doc/index").
-define(CURRENT_DIR, ".").

toc(Paths, Ctxt) ->
    Opts = Ctxt#context.opts,
    Dir = Ctxt#context.dir,
    Env = Ctxt#context.env,
    app_index_file(Paths, Dir, Env, Opts).

%% TODO: FIXME: it's unclear how much of this is working at all

%% NEW-OPTIONS: title
%% INHERIT-OPTIONS: overview/4

app_index_file(Paths, Dir, Env, Options) ->
    Title = proplists:get_value(title, Options,"Overview"),
%    Priv = proplists:get_bool(private, Options),
    CSS = stylesheet(Options),
    Apps1 = [{filename:dirname(A),filename:basename(A)} || A <- Paths],
    index_file(Dir, false, Title),
    application_frame(Dir, Apps1, Title, CSS),
    modules_frame(Dir, [], Title, CSS),
    overview(Dir, Title, Env, Options),
%    edoc_lib:write_info_file(Prod, [], Modules1, Dir),
    copy_stylesheet(Dir, Options).

application_frame(Dir, Apps, Title, CSS) ->
    Body = [?NL,
	    {h2, ["Applications"]},
	    ?NL,
	    {table, [{width, "100%"}, {border, 0}],
	     lists:concat(
	       [[{tr, [{td, [], [{a, [{href,app_ref(Path,App)},
				      {target,"_top"}],
				  [App]}]}]}]
		|| {Path,App} <- Apps])},
	    ?NL],
    XML = xhtml(Title, CSS, Body),
    Text = xmerl:export_simple([XML], xmerl_html, []),
    edoc_lib:write_file(Text, Dir, ?MODULES_FRAME).

app_ref(Path,M) ->
    filename:join([Path,M,?EDOC_DIR,?INDEX_FILE]).