%% =====================================================================
%% 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 2001-2003 Richard Carlsson
%% @author Richard Carlsson <carlsson.richard@gmail.com>
%% @see edoc
%% @end
%% =====================================================================

%% @doc Utility functions for EDoc.

-module(edoc_lib).

-export([count/2, lines/1, split_at/2, split_at_stop/1,
	 split_at_space/1, filename/1, transpose/1, segment/2,
	 get_first_sentence/1, is_space/1, strip_space/1, parse_expr/2,
	 parse_contact/2, escape_uri/1, join_uri/2, is_relative_uri/1,
	 is_name/1, to_label/1, find_doc_dirs/0, find_sources/2,
	 find_sources/3, find_file/3, try_subdir/2, unique/1,
	 write_file/3, write_file/4, write_file/5, write_info_file/4,
	 read_info_file/1, get_doc_env/1, get_doc_env/4, copy_file/2,
	 uri_get/1, run_doclet/2, run_layout/2,
	 simplify_path/1, timestr/1, datestr/1, read_encoding/2]).

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

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

-define(FILE_BASE, "/").


%% ---------------------------------------------------------------------
%% List and string utilities

%% @private
timestr({H,M,Sec}) ->
    lists:flatten(io_lib:fwrite("~2.2.0w:~2.2.0w:~2.2.0w",[H,M,Sec])).

%% @private
datestr({Y,M,D}) ->
    Ms = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep",
	  "Oct", "Nov", "Dec"],
    lists:flatten(io_lib:fwrite("~s ~w ~w",[lists:nth(M, Ms),D,Y])).

%% @private
read_encoding(File, Options) ->
    case epp:read_encoding(File, Options) of
        none -> epp:default_encoding();
        Encoding -> Encoding
    end.

%% @private
count(X, Xs) ->
    count(X, Xs, 0).

count(X, [X | Xs], N) ->
    count(X, Xs, N + 1);
count(X, [_ | Xs], N) ->
    count(X, Xs, N);
count(_X, [], N) ->
    N.

%% @private
lines(Cs) ->
    lines(Cs, [], []).

lines([$\n | Cs], As, Ls) ->
    lines(Cs, [], [lists:reverse(As) | Ls]);
lines([C | Cs], As, Ls) ->
    lines(Cs, [C | As], Ls);
lines([], As, Ls) ->
    lists:reverse([lists:reverse(As) | Ls]).

%% @private
split_at(Cs, K) ->
    split_at(Cs, K, []).

split_at([K | Cs], K, As) ->
    {lists:reverse(As), Cs};
split_at([C | Cs], K, As) ->
    split_at(Cs, K, [C | As]);
split_at([], _K, As) ->
    {lists:reverse(As), []}.

%% @private
split_at_stop(Cs) ->
    split_at_stop(Cs, []).

split_at_stop([$., $\s | Cs], As) ->
    {lists:reverse(As), Cs};
split_at_stop([$., $\t | Cs], As) ->
    {lists:reverse(As), Cs};
split_at_stop([$., $\n | Cs], As) ->
    {lists:reverse(As), Cs};
split_at_stop([$.], As) ->
    {lists:reverse(As), []};
split_at_stop([C | Cs], As) ->
    split_at_stop(Cs, [C | As]);
split_at_stop([], As) ->
    {lists:reverse(As), []}.

%% @private
split_at_space(Cs) ->
    split_at_space(Cs, []).

split_at_space([$\s | Cs], As) ->
    {lists:reverse(As), Cs};
split_at_space([$\t | Cs], As) ->
    {lists:reverse(As), Cs};
split_at_space([$\n | Cs], As) ->
    {lists:reverse(As), Cs};
split_at_space([C | Cs], As) ->
    split_at_space(Cs, [C | As]);
split_at_space([], As) ->
    {lists:reverse(As), []}.

%% @private
is_space([$\s | Cs]) -> is_space(Cs);
is_space([$\t | Cs]) -> is_space(Cs);
is_space([$\n | Cs]) -> is_space(Cs);
is_space([_C | _Cs]) -> false;
is_space([]) -> true.

%% @private
strip_space([$\s | Cs]) -> strip_space(Cs);
strip_space([$\t | Cs]) -> strip_space(Cs);
strip_space([$\n | Cs]) -> strip_space(Cs);
strip_space(Cs) -> Cs.

%% @private
segment(Es, N) ->
    segment(Es, [], [], 0, N).

segment([E | Es], As, Cs, N, M) when N < M ->
    segment(Es, [E | As], Cs, N + 1, M);
segment([_ | _] = Es, As, Cs, _N, M) ->
    segment(Es, [], [lists:reverse(As) | Cs], 0, M);
segment([], [], Cs, _N, _M) ->
    lists:reverse(Cs);
segment([], As, Cs, _N, _M) ->
    lists:reverse([lists:reverse(As) | Cs]).

%% @private
transpose([]) -> [];
transpose([[] | Xss]) -> transpose(Xss);
transpose([[X | Xs] | Xss]) ->
    [[X | [H || [H | _T] <- Xss]]
     | transpose([Xs | [T || [_H | T] <- Xss]])].

%% Note that the parser will not produce two adjacent text segments;
%% thus, if a text segment ends with a period character, it marks the
%% end of the summary sentence only if it is also the last segment in
%% the list, or is followed by a 'p' or 'br' ("whitespace") element.

%% @private
get_first_sentence([#xmlElement{name = p, content = Es} | _]) ->
    %% Descend into initial paragraph.
    get_first_sentence_1(Es);
get_first_sentence(Es) ->
    get_first_sentence_1(Es).

get_first_sentence_1([E = #xmlText{value = Txt} | Es]) ->
    Last = case Es of
	       [#xmlElement{name = p} | _] -> true;
	       [#xmlElement{name = br} | _] -> true;
	       [] -> true;
	       _ -> false
	   end,
    case end_of_sentence(Txt, Last) of
	{value, Txt1} ->
	    [E#xmlText{value = Txt1}];
	none ->
	    [E | get_first_sentence_1(Es)]
    end;
get_first_sentence_1([E | Es]) ->
    % Skip non-text segments - don't descend further
    [E | get_first_sentence_1(Es)];
get_first_sentence_1([]) ->
    [].

end_of_sentence(Cs, Last) ->
    end_of_sentence(Cs, Last, []).

%% We detect '.' and '!' as end-of-sentence markers.

end_of_sentence([C=$., $\s | _], _, As) ->
    end_of_sentence_1(C, true, As);
end_of_sentence([C=$., $\t | _], _, As) ->
    end_of_sentence_1(C, true, As);
end_of_sentence([C=$., $\n | _], _, As) ->
    end_of_sentence_1(C, true, As);
end_of_sentence([C=$.], Last, As) ->
    end_of_sentence_1(C, Last, As);
end_of_sentence([C=$!, $\s | _], _, As) ->
    end_of_sentence_1(C, true, As);
end_of_sentence([C=$!, $\t | _], _, As) ->
    end_of_sentence_1(C, true, As);
end_of_sentence([C=$!, $\n | _], _, As) ->
    end_of_sentence_1(C, true, As);
end_of_sentence([C=$!], Last, As) ->
    end_of_sentence_1(C, Last, As);
end_of_sentence([C | Cs], Last, As) ->
    end_of_sentence(Cs, Last, [C | As]);
end_of_sentence([], Last, As) ->
    end_of_sentence_1($., Last, strip_space(As)).  % add a '.'

end_of_sentence_1(C, true, As) ->
    {value, lists:reverse([C | As])};
end_of_sentence_1(_, false, _) ->
    none.

%% For handling ISO 8859-1 (Latin-1) we use the following information:
%%
%% 000 - 037	NUL - US	control
%% 040 - 057	SPC - /		punctuation
%% 060 - 071	0 - 9		digit
%% 072 - 100	: - @		punctuation
%% 101 - 132	A - Z		uppercase
%% 133 - 140	[ - `		punctuation
%% 141 - 172	a - z		lowercase
%% 173 - 176	{ - ~		punctuation
%% 177		DEL		control
%% 200 - 237			control
%% 240 - 277	NBSP - ¿	punctuation
%% 300 - 326	À - Ö		uppercase
%% 327		×		punctuation
%% 330 - 336	Ø - Þ		uppercase
%% 337 - 366	ß - ö		lowercase
%% 367		÷		punctuation
%% 370 - 377	ø - ÿ		lowercase

%% Names must begin with a lowercase letter and contain only
%% alphanumerics and underscores.

%% @private
is_name([C | Cs]) when C >= $a, C =< $z ->
    is_name_1(Cs);
is_name([C | Cs]) when C >= $\337, C =< $\377, C =/= $\367 ->
    is_name_1(Cs);
is_name(_) -> false.

is_name_1([C | Cs]) when C >= $a, C =< $z ->
    is_name_1(Cs);
is_name_1([C | Cs]) when C >= $A, C =< $Z ->
    is_name_1(Cs);
is_name_1([C | Cs]) when C >= $0, C =< $9 ->
    is_name_1(Cs);
is_name_1([C | Cs]) when C >= $\300, C =< $\377, C =/= $\327, C =/= $\367 ->
    is_name_1(Cs);
is_name_1([$_ | Cs]) ->
    is_name_1(Cs);
is_name_1([]) -> true;
is_name_1(_) -> false.

to_atom(A) when is_atom(A) -> A;
to_atom(S) when is_list(S) -> list_to_atom(S).

to_list(A) when is_atom(A) -> atom_to_list(A);
to_list(S) when is_list(S) -> S.

    
%% @private
unique([X | Xs]) -> [X | unique(Xs, X)];
unique([]) -> [].

unique([X | Xs], X) -> unique(Xs, X);
unique([X | Xs], _) -> [X | unique(Xs, X)];
unique([], _) -> [].


%% ---------------------------------------------------------------------
%% Parsing utilities

%% @doc EDoc Erlang expression parsing. For parsing things like the
%% content of <a href="overview-summary.html#ftag-equiv">`@equiv'</a>
%% tags, and strings denoting file names, e.g. in @headerfile. Also used
%% by {@link edoc_run}.
%% @private

parse_expr(S, L) ->
    case erl_scan:string(S ++ ".", L) of
	{ok, Ts, _} ->
	    case erl_parse:parse_exprs(Ts) of
		{ok, [Expr]} ->
		    Expr;
 		{error, {999999, erl_parse, _}} ->
 		    throw_error(eof, L);
 		{error, E} ->
 		    throw_error(E, L)
	    end;
	{error, E, _} ->
	    throw_error(E, L)
    end.


%% @doc EDoc "contact information" parsing. This is the type of the
%% content in e.g.
%% <a href="overview-summary.html#mtag-author">`@author'</a> tags.
%% @private

%% % @type info() = #info{name  = string(),
%% %                      email = string(),
%% %                      uri   = string()}

-record(info, {name = ""  :: string(),
	       email = "" :: string(),
	       uri = ""   :: string()}).

parse_contact(S, L) ->
    I = scan_name(S, L, #info{}, []),
    {I#info.name, I#info.email, I#info.uri}.

%% The name is taken as the first non-whitespace-only string before,
%% between, or following the e-mail/URI sections. Subsequent text that
%% is not e/mail or URI is ignored.

scan_name([$< | Cs], L, I, As) ->
    case I#info.email of
	"" ->
	    {Cs1, I1} = scan_email(Cs, L, set_name(I, As), []),
	    scan_name(Cs1, L, I1, []);
	_ ->
	    throw_error("multiple '<...>' sections.", L)
    end;
scan_name([$[ | Cs], L, I, As) ->
    case I#info.uri of
	"" ->
	    {Cs1, I1} = scan_uri(Cs, L, set_name(I, As), []),
	    scan_name(Cs1, L, I1, []);
	_ ->
	    throw_error("multiple '[...]' sections.", L)
    end;
scan_name([$\n | Cs], L, I, As) ->
    scan_name(Cs, L + 1, I, [$\n | As]);
scan_name([C | Cs], L, I, As) ->
    scan_name(Cs, L, I, [C | As]);
scan_name([], _L, I, As) ->
    set_name(I, As).

scan_uri([$] | Cs], _L, I, As) ->
    {Cs, I#info{uri = strip_and_reverse(As)}};
scan_uri([$\n | Cs], L, I, As) ->
    scan_uri(Cs, L + 1, I, [$\n | As]);
scan_uri([C | Cs], L, I, As) ->
    scan_uri(Cs, L, I, [C | As]);
scan_uri([], L, _I, _As) ->
    throw_error({missing, $]}, L).

scan_email([$> | Cs], _L, I, As) ->
    {Cs, I#info{email = strip_and_reverse(As)}};
scan_email([$\n | Cs], L, I, As) ->
    scan_email(Cs, L + 1, I, [$\n | As]);
scan_email([C | Cs], L, I, As) ->
    scan_email(Cs, L, I, [C | As]);
scan_email([], L, _I, _As) ->
    throw_error({missing, $>}, L).

set_name(I, As) ->
    case I#info.name of
	"" -> I#info{name = strip_and_reverse(As)};
	_ -> I
    end.

strip_and_reverse(As) ->
    edoc_lib:strip_space(lists:reverse(edoc_lib:strip_space(As))).


%% ---------------------------------------------------------------------
%% URI and Internet

%% This is a conservative URI escaping, which escapes anything that may
%% not appear in an NMTOKEN ([a-zA-Z0-9]|'.'|'-'|'_'), including ':'.
%% Characters are first encoded in UTF-8.
%%
%% Note that this should *not* be applied to complete URI, but only to
%% segments that may need escaping, when forming a complete URI.
%%
%% TODO: general utf-8 encoding for all of Unicode (0-16#10ffff)

%% @private
escape_uri([C | Cs]) when C >= $a, C =< $z ->
    [C | escape_uri(Cs)];
escape_uri([C | Cs]) when C >= $A, C =< $Z ->
    [C | escape_uri(Cs)];
escape_uri([C | Cs]) when C >= $0, C =< $9 ->
    [C | escape_uri(Cs)];
escape_uri([C = $. | Cs]) ->
    [C | escape_uri(Cs)];
escape_uri([C = $- | Cs]) ->
    [C | escape_uri(Cs)];
escape_uri([C = $_ | Cs]) ->
    [C | escape_uri(Cs)];
escape_uri([C | Cs]) when C > 16#7f ->
    %% This assumes that characters are at most 16 bits wide.
    escape_byte(((C band 16#c0) bsr 6) + 16#c0)
	++ escape_byte(C band 16#3f + 16#80)
	++ escape_uri(Cs);
escape_uri([C | Cs]) ->
    escape_byte(C) ++ escape_uri(Cs);
escape_uri([]) ->
    [].

escape_byte(C) when C >= 0, C =< 255 ->
    [$%, hex_digit(C bsr 4), hex_digit(C band 15)].

hex_digit(N) when N >= 0, N =< 9 ->
    N + $0;
hex_digit(N) when N > 9, N =< 15 ->
    N + $a - 10.

% utf8([C | Cs]) when C > 16#7f ->
%     [((C band 16#c0) bsr 6) + 16#c0, C band 16#3f ++ 16#80 | utf8(Cs)];
% utf8([C | Cs]) ->
%     [C | utf8(Cs)];
% utf8([]) ->
%     [].

%% Please note that URI are *not* file names. Don't use the stdlib
%% 'filename' module for operations on (any parts of) URI.

%% @private
join_uri(Base, "") ->
    Base;
join_uri("", Path) ->
    Path;
join_uri(Base, Path) ->
    Base ++ "/" ++ Path.

%% Check for relative URI; "network paths" ("//...") not included!

%% @private
is_relative_uri([$: | _]) ->
    false;
is_relative_uri([$/, $/ | _]) ->
    false;
is_relative_uri([$/ | _]) ->
    true;
is_relative_uri([$? | _]) ->
    true;
is_relative_uri([$# | _]) ->
    true;
is_relative_uri([_ | Cs]) ->
    is_relative_uri(Cs);
is_relative_uri([]) ->
    true.

%% @private
uri_get("file:///" ++ Path) ->
    uri_get_file(Path);
uri_get("file://localhost/" ++ Path) ->
    uri_get_file(Path);
uri_get("file://" ++ Path) ->
    Msg = io_lib:format("cannot handle 'file:' scheme with "
			"nonlocal network-path: 'file://~ts'.",
			[Path]),
    {error, Msg};
uri_get("file:/" ++ Path) ->
    uri_get_file(Path);
uri_get("file:" ++ Path) ->
    Msg = io_lib:format("ignoring malformed URI: 'file:~ts'.", [Path]),
    {error, Msg};
uri_get("http:" ++ Path) ->
    uri_get_http("http:" ++ Path);
uri_get("ftp:" ++ Path) ->
    uri_get_ftp("ftp:" ++ Path);
uri_get("//" ++ Path) ->
    Msg = io_lib:format("cannot access network-path: '//~ts'.", [Path]),
    {error, Msg};
uri_get([C, $:, $/ | _]=Path) when C >= $A, C =< $Z; C >= $a, C =< $z ->
    uri_get_file(Path);  % special case for Windows
uri_get([C, $:, $\ | _]=Path) when C >= $A, C =< $Z; C >= $a, C =< $z ->
    uri_get_file(Path);  % special case for Windows
uri_get(URI) ->
    case is_relative_uri(URI) of
	true ->
	    uri_get_file(URI);
	false ->
	    Msg = io_lib:format("cannot handle URI: '~ts'.", [URI]),
	    {error, Msg}
    end.

uri_get_file(File0) ->
    File = filename:join(?FILE_BASE, File0),
    case read_file(File) of
	{ok, Text} ->
	    {ok, Text};
	{error, R} ->
	    {error, file:format_error(R)}
    end.

uri_get_http(URI) ->
    %% Try using option full_result=false
    case catch {ok, httpc:request(get, {URI,[]}, [],
				  [{full_result, false}])} of
	{'EXIT', _} ->
	    uri_get_http_r10(URI);
	Result ->
	    uri_get_http_1(Result, URI)
    end.

uri_get_http_r10(URI) ->
    %% Try most general form of request
    Result = (catch {ok, httpc:request(get, {URI,[]}, [], [])}),
    uri_get_http_1(Result, URI).

uri_get_http_1(Result, URI) ->
    case Result of
	{ok, {ok, {200, Text}}} when is_list(Text) ->
	    %% new short result format
	    {ok, Text};
	{ok, {ok, {Status, Text}}} when is_integer(Status), is_list(Text) ->
	    %% new short result format when status /= 200
	    Phrase = httpd_util:reason_phrase(Status),
	    {error, http_errmsg(Phrase, URI)};
	{ok, {ok, {{_Vsn, 200, _Phrase}, _Hdrs, Text}}} when is_list(Text) ->
	    %% new long result format
	    {ok, Text};
	{ok, {ok, {{_Vsn, _Status, Phrase}, _Hdrs, Text}}} when is_list(Text) ->
	    %% new long result format when status /= 200
	    {error, http_errmsg(Phrase, URI)};
	{ok, {200,_Hdrs,Text}} when is_list(Text) ->
	    %% old result format
	    {ok, Text};
	{ok, {Status,_Hdrs,Text}} when is_list(Text) ->
	    %% old result format when status /= 200
	    Phrase = httpd_util:reason_phrase(Status),
	    {error, http_errmsg(Phrase, URI)};
	{ok, {error, R}} ->
	    Reason = inet:format_error(R),
	    {error, http_errmsg(Reason, URI)};
	{ok, R} ->
	    Reason = io_lib:format("bad return value ~P", [R, 5]),
	    {error, http_errmsg(Reason, URI)};
	{'EXIT', R} ->
	    Reason = io_lib:format("crashed with reason ~w", [R]),
	    {error, http_errmsg(Reason, URI)};
	R ->
	    Reason = io_lib:format("uncaught throw: ~w", [R]),
	    {error, http_errmsg(Reason, URI)}
    end.

http_errmsg(Reason, URI) ->
    io_lib:format("http error: ~ts: '~ts'", [Reason, URI]).

%% TODO: implement ftp access method

uri_get_ftp(URI) ->
    Msg = io_lib:format("cannot access ftp scheme yet: '~ts'.", [URI]),
    {error, Msg}.

%% @private
to_label([$\s | Cs]) ->
    to_label(Cs);
to_label([$\t | Cs]) ->
    to_label(Cs);
to_label([$\n | Cs]) ->
    to_label(Cs);
to_label([]) ->
    [];
to_label(Cs) ->
    to_label_1(Cs).

to_label_1([$\s | Cs]) ->
    to_label_2([$\s | Cs]);
to_label_1([$\t | Cs]) ->
    to_label_2([$\s | Cs]);
to_label_1([$\n | Cs]) ->
    to_label_2([$\s | Cs]);
to_label_1([C | Cs]) ->
    [C | to_label_1(Cs)];
to_label_1([]) ->
    [].

to_label_2(Cs) ->
    case to_label(Cs) of
	[] -> [];
	Cs1 -> [$_ | Cs1]
    end.


%% ---------------------------------------------------------------------
%% Files

%% @private
filename([C | T]) when is_integer(C), C > 0 ->
    [C | filename(T)];
filename([H|T]) ->
    filename(H) ++ filename(T);
filename([]) ->
    [];
filename(N) when is_atom(N) ->
    atom_to_list(N);
filename(N) ->
    report("bad filename: `~P'.", [N, 25]),
    exit(error).

%% @private
copy_file(From, To) ->
    case file:copy(From, To) of
	{ok, _} -> ok;
	{error, R} ->
	    R1 = file:format_error(R),
	    report("error copying '~ts' to '~ts': ~ts.", [From, To, R1]),
	    exit(error)
    end.

list_dir(Dir, Error) ->
    case file:list_dir(Dir) of
	{ok, Fs} ->
	    Fs;
	{error, R} ->
	    F = case Error of
		    %% true ->
		    %%	fun (S, As) -> report(S, As), exit(error) end;
		    false ->
			fun (S, As) -> warning(S, As), [] end
		end,
	    R1 = file:format_error(R),
	    F("could not read directory '~ts': ~ts.", [filename(Dir), R1])
    end.

%% @private
simplify_path(P) ->
    case filename:basename(P) of
	"." ->
	    simplify_path(filename:dirname(P));
	".." ->
	    simplify_path(filename:dirname(filename:dirname(P)));
	_ ->
	    P
    end.

%% The directories From and To are assumed to exist.

%% copy_dir(From, To) ->
%%     Es = list_dir(From, true),    % error if listing fails
%%     lists:foreach(fun (E) -> copy_dir(From, To, E) end, Es).

%% copy_dir(From, To, Entry) ->
%%     From1 = filename:join(From, Entry),
%%     To1 = filename:join(To, Entry),
%%     case filelib:is_dir(From1) of
%% 	true ->
%% 	    make_dir(To1),
%% 	    copy_dir(From1, To1);
%% 	false ->
%% 	    copy_file(From1, To1)
%%     end.

%% make_dir(Dir) ->
%%     case file:make_dir(Dir) of
%% 	ok -> ok;
%% 	{error, R} ->
%% 	    R1 = file:format_error(R),
%% 	    report("cannot create directory '~ts': ~ts.", [Dir, R1]),
%% 	    exit(error)
%%     end.

%% @private
try_subdir(Dir, Subdir) ->
    D = filename:join(Dir, Subdir),
    case filelib:is_dir(D) of
	true -> D; 
	false -> Dir
    end.

%% @spec (Text::deep_string(), Dir::edoc:filename(),
%%        Name::edoc:filename()) -> ok
%%
%% @doc Write the given `Text' to the file named by `Name' in directory
%% `Dir'. If the target directory does not exist, it will be created.
%% @private

write_file(Text, Dir, Name) ->
    write_file(Text, Dir, Name, '').

%% @spec (Text::deep_string(), Dir::edoc:filename(),
%%        Name::edoc:filename(), Package::atom()|string()) -> ok
%% @doc Like {@link write_file/3}, but adds path components to the target
%% directory corresponding to the specified package.
%% @private

write_file(Text, Dir, Name, Package) ->
    write_file(Text, Dir, Name, Package, [{encoding,latin1}]).

write_file(Text, Dir, Name, Package, Options) ->
    File = filename:join([Dir, to_list(Package), Name]),
    ok = filelib:ensure_dir(File),
    case file:open(File, [write] ++ Options) of
	{ok, FD} ->
	    io:put_chars(FD, Text),
	    ok = file:close(FD);
	{error, R} ->
	    R1 = file:format_error(R),
	    report("could not write file '~ts': ~ts.", [File, R1]),
	    exit(error)
    end.

%% @private
write_info_file(App, Packages, Modules, Dir) ->
    Ts = [{packages, Packages},
	  {modules, Modules}],
    Ts1 = if App =:= ?NO_APP -> Ts;
	     true -> [{application, App} | Ts]
	  end,
    S0 = [io_lib:fwrite("~p.\n", [T]) || T <- Ts1],
    S = ["%% encoding: UTF-8\n" | S0],
    write_file(S, Dir, ?INFO_FILE, '', [{encoding,unicode}]).

%% @spec (Name::edoc:filename()) -> {ok, string()} | {error, Reason}
%%
%% @doc Reads text from the file named by `Name'.

read_file(File) ->
    case file:read_file(File) of
	{ok, Bin} ->
            Enc = edoc_lib:read_encoding(File, []),
            case catch unicode:characters_to_list(Bin, Enc) of
                String when is_list(String) ->
                    {ok, String};
                _ ->
                    {error, invalid_unicode}
            end;
	{error, Reason} -> {error, Reason}
    end.


%% ---------------------------------------------------------------------
%% Info files

info_file_data(Ts) ->
    App = proplists:get_value(application, Ts, ?NO_APP),
    Ps = proplists:append_values(packages, Ts),
    Ms = proplists:append_values(modules, Ts),
    {App, Ps, Ms}.

%% Local file access - don't complain if file does not exist.

%% @private
read_info_file(Dir) ->
    File = filename:join(Dir, ?INFO_FILE),
    case filelib:is_file(File) of
	true ->
	    case read_file(File) of
		{ok, Text} ->
		    parse_info_file(Text, File);
		{error, R} ->
		    R1 = file:format_error(R),
		    warning("could not read '~ts': ~ts.", [File, R1]),
		    {?NO_APP, [], []}
	    end;	    
	false ->
	    {?NO_APP, [], []}
    end.

%% URI access

uri_get_info_file(Base) ->
    URI = join_uri(Base, ?INFO_FILE),
    case uri_get(URI) of
	{ok, Text} ->
	    parse_info_file(Text, URI);
	{error, Msg} ->
	    warning("could not read '~ts': ~ts.", [URI, Msg]),
	    {?NO_APP, [], []}
    end.

parse_info_file(Text, Name) ->
    case parse_terms(Text) of
	{ok, Vs} ->
	    info_file_data(Vs);
	{error, eof} ->
	    warning("unexpected end of file in '~ts'.", [Name]),
	    {?NO_APP, [], []};
	{error, {_Line,Module,R}} ->
	    warning("~ts: ~ts.", [Module:format_error(R), Name]),
	    {?NO_APP, [], []}
    end.

parse_terms(Text) ->
    case erl_scan:string(Text) of
	{ok, Ts, _Line} ->
	    parse_terms_1(Ts, [], []);
	{error, R, _Line} ->
	    {error, R}
    end.

parse_terms_1([T={dot, _L} | Ts], As, Vs) ->
    case erl_parse:parse_term(lists:reverse([T | As])) of
	{ok, V} ->
	    parse_terms_1(Ts, [], [V | Vs]);
	{error, R} ->
	    {error, R}
    end;
parse_terms_1([T | Ts], As, Vs) ->
    parse_terms_1(Ts, [T | As], Vs);
parse_terms_1([], [], Vs) ->
    {ok, lists:reverse(Vs)};
parse_terms_1([], _As, _Vs) ->
    {error, eof}.


%% ---------------------------------------------------------------------
%% Source files and packages

%% @private
find_sources(Path, Opts) ->
    find_sources(Path, "", Opts).

%% @doc See {@link edoc:run/3} for a description of the options
%% `subpackages', `source_suffix' and `exclude_packages'.
%% @private

%% NEW-OPTIONS: subpackages, source_suffix, exclude_packages
%% DEFER-OPTIONS: edoc:run/3

find_sources(Path, Pkg, Opts) ->
    Rec = proplists:get_bool(subpackages, Opts),
    Ext = proplists:get_value(source_suffix, Opts, ?DEFAULT_SOURCE_SUFFIX),
    find_sources(Path, Pkg, Rec, Ext, Opts).

find_sources(Path, Pkg, Rec, Ext, Opts) ->
    Skip = proplists:get_value(exclude_packages, Opts, []),
    lists:flatten(find_sources_1(Path, to_atom(Pkg), Rec, Ext, Skip)).

find_sources_1([P | Ps], Pkg, Rec, Ext, Skip) ->
    Dir = filename:join(P, atom_to_list(Pkg)),
    Fs1 = find_sources_1(Ps, Pkg, Rec, Ext, Skip),
    case filelib:is_dir(Dir) of
	true ->
	    [find_sources_2(Dir, Pkg, Rec, Ext, Skip) | Fs1];
	false ->
	    Fs1
    end;
find_sources_1([], _Pkg, _Rec, _Ext, _Skip) ->
    [].

find_sources_2(Dir, Pkg, Rec, Ext, Skip) ->
    case lists:member(Pkg, Skip) of
	false ->
	    Es = list_dir(Dir, false),    % just warn if listing fails
	    Es1 = [{Pkg, E, Dir} || E <- Es, is_source_file(E, Ext)],
	    case Rec of
		true ->
		    [find_sources_3(Es, Dir, Pkg, Rec, Ext, Skip) | Es1];
		false ->
		    Es1
	    end;
	true ->
	    []
    end.

find_sources_3(Es, Dir, Pkg, Rec, Ext, Skip) ->
    [find_sources_2(filename:join(Dir, E),
		    to_atom(join(Pkg, E)), Rec, Ext, Skip)
     || E <- Es, is_package_dir(E, Dir)].

join('', E) -> E;
join(Pkg, E) -> filename:join(Pkg, E).

is_source_file(Name, Ext) ->
    (filename:extension(Name) == Ext)
	andalso is_name(filename:rootname(Name, Ext)).

is_package_dir(Name, Dir) ->
    is_name(filename:rootname(filename:basename(Name)))
	andalso filelib:is_dir(filename:join(Dir, Name)).

%% @private
find_file([P | Ps], []=Pkg, Name) ->
    Pkg = [],
    File = filename:join(P, Name),
    case filelib:is_file(File) of
	true ->
	    File;    
	false ->
	    find_file(Ps, Pkg, Name)
    end;    
find_file([], [], _Name) ->
    "".

%% @private
find_doc_dirs() ->
    find_doc_dirs(code:get_path()).

find_doc_dirs([P0 | Ps]) ->
    P = filename:absname(P0),
    P1 = case filename:basename(P) of
	     ?EBIN_DIR ->
		 filename:dirname(P);
	     _ ->
		 P
	 end,
    Dir = try_subdir(P1, ?EDOC_DIR),
    File = filename:join(Dir, ?INFO_FILE),
    case filelib:is_file(File) of
	true ->
	    [Dir | find_doc_dirs(Ps)]; 
	false ->
	    find_doc_dirs(Ps)
    end;
find_doc_dirs([]) ->
    [].

%% All names with "internal linkage" are mapped to the empty string, so
%% that relative references will be created. For apps, the empty string
%% implies that we use the default app-path.

%% NEW-OPTIONS: doc_path
%% DEFER-OPTIONS: get_doc_env/4

get_doc_links(App, Packages, Modules, Opts) ->
    Path = proplists:append_values(doc_path, Opts) ++ find_doc_dirs(),
    Ds = [{P, uri_get_info_file(P)} || P <- Path],
    Ds1 = [{"", {App, Packages, Modules}} | Ds],
    D = dict:new(),
    make_links(Ds1, D, D, D).

make_links([{Dir, {App, Ps, Ms}} | Ds], A, P, M) ->
    A1 = if App == ?NO_APP -> A;
	    true -> add_new(App, Dir, A)
	 end,
    F = fun (K, D) -> add_new(K, Dir, D) end,
    P1 = lists:foldl(F, P, Ps),
    M1 = lists:foldl(F, M, Ms),
    make_links(Ds, A1, P1, M1);
make_links([], A, P, M) ->
    F = fun (D) ->
		fun (K) ->
			case dict:find(K, D) of
			    {ok, V} -> V;
			    error -> ""
			end
		end
	end,
    {F(A), F(P), F(M)}.

add_new(K, V, D) ->
    case dict:is_key(K, D) of
	true ->
	    D;
	false ->
	    dict:store(K, V, D)
    end.

%% @spec (Options::proplist()) -> edoc_env()
%% @equiv get_doc_env([], [], [], Opts)
%% @private

get_doc_env(Opts) ->
    get_doc_env([], [], [], Opts).

%% @spec (App, Packages, Modules, Options::proplist()) -> edoc_env()
%%     App = [] | atom()
%%     Packages = [atom()]
%%     Modules = [atom()]
%%     proplist() = [term()]
%%
%% @type proplist() = proplists:property().
%% @type edoc_env(). Environment information needed by EDoc for
%% generating references. The data representation is not documented.
%%
%% @doc Creates an environment data structure used by parts of EDoc for
%% generating references, etc. See {@link edoc:run/3} for a description
%% of the options `file_suffix', `app_default' and `doc_path'.
%%
%% @see edoc_extract:source/4
%% @see edoc:get_doc/3

%% NEW-OPTIONS: file_suffix, app_default
%% INHERIT-OPTIONS: get_doc_links/4
%% DEFER-OPTIONS: edoc:run/3

get_doc_env(App, Packages, Modules, Opts) ->
    Suffix = proplists:get_value(file_suffix, Opts,
				 ?DEFAULT_FILE_SUFFIX),
    AppDefault = proplists:get_value(app_default, Opts, ?APP_DEFAULT),
    Includes = proplists:append_values(includes, Opts),

    {A, P, M} = get_doc_links(App, Packages, Modules, Opts),
    #env{file_suffix = Suffix,
	 package_summary = ?PACKAGE_SUMMARY ++ Suffix,
	 apps = A,
	 packages = P,
	 modules = M,
	 app_default = AppDefault,
	 includes = Includes
	}.

%% ---------------------------------------------------------------------
%% Plug-in modules

%% @doc See {@link edoc:run/3} for a description of the `doclet' option.

%% NEW-OPTIONS: doclet
%% DEFER-OPTIONS: edoc:run/3

%% @private
run_doclet(Fun, Opts) ->
    run_plugin(doclet, ?DEFAULT_DOCLET, Fun, Opts).

%% @doc See {@link edoc:layout/2} for a description of the `layout'
%% option.

%% NEW-OPTIONS: layout
%% DEFER-OPTIONS: edoc:layout/2

%% @private
run_layout(Fun, Opts) ->
    run_plugin(layout, ?DEFAULT_LAYOUT, Fun, Opts).

run_plugin(Name, Default, Fun, Opts) ->
    run_plugin(Name, Name, Default, Fun, Opts).

run_plugin(Name, Key, Default, Fun, Opts) when is_atom(Name) ->
    Module = get_plugin(Key, Default, Opts),
    case catch {ok, Fun(Module)} of
	{ok, Value} ->
	    Value;
	R ->
	    report("error in ~ts '~w': ~P.", [Name, Module, R, 20]),
	    exit(error)
    end.

get_plugin(Key, Default, Opts) ->
    case proplists:get_value(Key, Opts, Default) of
	M when is_atom(M) ->
	    M;
	Other ->
	    report("bad value for option '~w': ~P.", [Key, Other, 10]),
	    exit(error)
    end.


%% ---------------------------------------------------------------------
%% Error handling

-type line() :: erl_scan:line().
-type err()  :: 'eof'
	      | {'missing', char()}
	      | {line(), atom(), string()}
	      | string().

-spec throw_error(err(), line()) -> no_return().

throw_error({missing, C}, L) ->
    throw_error({"missing '~c'.", [C]}, L);
throw_error(eof, L) ->
    throw({error,L,"unexpected end of expression."});
throw_error({L, M, D}, _L) ->
    throw({error,L,{format_error,M,D}});
throw_error(D, L) ->
    throw({error, L, D}).