%%--------------------------------------------------------------------
%%
%% %CopyrightBegin%
%%
%% Copyright Ericsson AB 2009-2023. All Rights Reserved.
%%
%% 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.
%%
%% %CopyrightEnd%
%%
%%-----------------------------------------------------------------
%% File: erl_html_tools.erl
%% 
%% Description:
%%    This file generates the top index of the documentation.
%%
%%-----------------------------------------------------------------
-module(erl_html_tools). 

-export([top_index/0,top_index/1,top_index/4,top_index_silent/3]).

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

group_order() ->
    [
     {basic, "Basic"},
     {dat, "Database"},
     {oam, "Operation & Maintenance"},
     {comm, "Interface and Communication"},
     {tools, "Tools"},
     {test, "Test"},
     {doc, "Documentation"},
     {orb, "Object Request Broker & IDL"},
     {misc, "Miscellaneous"},
     {eric, "Ericsson Internal"}
    ].

top_index() ->
    case os:getenv("ERL_TOP") of
	false ->
	    io:format("Variable ERL_TOP is required\n",[]);
	Value ->
	    {_,RelName} = init:script_id(),	    
	    top_index(src, Value, filename:join(Value, "doc"), RelName)
    end.

top_index([src, RootDir, DestDir, OtpBaseVsn])
  when is_atom(RootDir), is_atom(DestDir), is_atom(OtpBaseVsn) ->
    top_index(src, atom_to_list(RootDir), atom_to_list(DestDir), atom_to_list(OtpBaseVsn));
top_index([rel, RootDir, DestDir, OtpBaseVsn])
  when is_atom(RootDir), is_atom(DestDir), is_atom(OtpBaseVsn) ->
    top_index(rel, atom_to_list(RootDir), atom_to_list(DestDir), atom_to_list(OtpBaseVsn));
top_index(RootDir)  when is_atom(RootDir) -> 
    {_,RelName} = init:script_id(),
    top_index(rel, RootDir, filename:join(RootDir, "doc"), RelName).



top_index(Source, RootDir, DestDir, OtpBaseVsn) ->
    report("****\nRootDir: ~tp", [RootDir]),
    report("****\nDestDir: ~tp", [DestDir]),
    report("****\nOtpBaseVsn: ~tp", [OtpBaseVsn]),

    put(otp_base_vsn, OtpBaseVsn),

    Templates = find_templates(["","templates",DestDir]),
    report("****\nTemplates: ~tp", [Templates]),
    Bases = [{"../lib/", filename:join(RootDir,"lib")},
	     {"../",     RootDir}],
    Groups = find_information(Source, Bases),
    report("****\nGroups: ~tp", [Groups]),
    process_templates(Templates, DestDir, Groups).

top_index_silent(RootDir, DestDir, OtpBaseVsn) ->
    put(silent,true),
    Result = top_index(rel, RootDir, DestDir, OtpBaseVsn),
    erase(silent),
    Result.




%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% Main loop - process templates
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

process_templates([], _DestDir, _Groups) ->
    report("\n", []);
process_templates([Template | Templates], DestDir, Groups) ->
    report("****\nIN-FILE: ~ts", [Template]),
    BaseName = filename:basename(Template, ".src"),
    case lists:reverse(filename:rootname(BaseName)) of
	"_"++_ ->
	    %% One template expands to several output files.
	    process_multi_template(BaseName, Template, DestDir, Groups);
	_ ->
	    %% Standard one-to-one template.
	    OutFile = filename:join(DestDir, BaseName),
	    subst_file("", OutFile, Template, Groups)
    end,
    process_templates(Templates, DestDir, Groups).


process_multi_template(BaseName0, Template, DestDir, Info) ->
    Ext = filename:extension(BaseName0),
    BaseName1 = filename:basename(BaseName0, Ext),
    [_|BaseName2] = lists:reverse(BaseName1),
    BaseName = lists:reverse(BaseName2),
    Groups0 = [{[$_|atom_to_list(G)],G} || {G, _} <- group_order()],
    Groups = [{"",basic}|Groups0],
    process_multi_template_1(Groups, BaseName, Ext, Template, DestDir, Info).

process_multi_template_1([{Suffix,Group}|Gs], BaseName, Ext, Template, DestDir, Info) ->
    OutFile = filename:join(DestDir, BaseName++Suffix++Ext),
    subst_file(Group, OutFile, Template, Info),
    process_multi_template_1(Gs, BaseName, Ext, Template, DestDir, Info);
process_multi_template_1([], _, _, _, _, _) -> ok.

subst_file(Group, OutFile, Template, Info) ->
    report("\nOUTFILE: ~ts", [OutFile]),
    case subst_template(Group, Template, Info) of
	{ok,Text,_NewInfo} ->
	    case file:open(OutFile, [write]) of
		{ok, Stream} ->
		    file:write(Stream, Text),
		    file:close(Stream);
		Error ->
		    local_error("Can't write to file ~ts: ~w", [OutFile,Error])
	    end;
	Error ->
	    local_error("Can't write to file ~ts: ~w", [OutFile,Error])
    end.


%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% Find the templates
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

find_templates(SearchPaths) ->
    find_templates(SearchPaths, SearchPaths).

find_templates([SearchPath | SearchPaths], AllSearchPaths) ->
    case filelib:wildcard(filename:join(SearchPath, "*.html.src")) of
	[] ->
	    find_templates(SearchPaths, AllSearchPaths);
	Result ->
	    Result
    end;
find_templates([], AllSearchPaths) ->
    local_error("No templates found in ~p",[AllSearchPaths]).


%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% This function read all application names and if present all "info" files.
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

find_information(Source, Bases) ->
    Paths = find_application_paths(Source, Bases),
%    report("****\nPaths: ~p", [Paths]),
    Apps = find_application_infos(Paths),
%    report("****\nApps: ~p", [Apps]),
    form_groups(Apps).

% The input is a list of tuples of the form
%
%   IN: [{BaseURL,SearchDir}, ...]
%
% and the output is a list
%
%  OUT: [{Appname,AppVersion,AppPath,IndexUTL}, ...]
%
% We know URL ends in a slash.

find_application_paths(_, []) ->
    [];
find_application_paths(Source, [{URL, Dir} | Paths]) ->

    AppDirs = get_app_dirs(Dir),   
    AppPaths = get_app_paths(Source, AppDirs, URL),
    AppPaths ++ find_application_paths(Source, Paths).


get_app_paths(src, AppDirs, URL) ->
    Sub1 = "doc/html/index.html",
%%     Sub2 = "doc/index.html",
    lists:map(
      fun({App, AppPath}) -> 
	      VsnFile = filename:join(AppPath, "vsn.mk"),
	      VsnStr = 
		  case file:read_file(VsnFile) of
		      {ok, Bin} ->
			  case re:run(Bin, ".*VSN\s*=\s*([0-9\.]+).*",[{capture,[1],list}]) of
			      {match, [V]} ->
				  V;
			      nomatch ->
				  exit(io_lib:format("No VSN variable found in ~ts\n",
						     [VsnFile]))
			  end;
		      {error, Reason} ->
			  exit(io_lib:format("~p : ~ts\n", [Reason, VsnFile]))
		  end,
	      AppURL = URL ++ App ++ "-" ++ VsnStr,
	      {App, VsnStr, AppPath, AppURL ++ "/" ++ Sub1}
      end, AppDirs);
get_app_paths(rel, AppDirs, URL) ->
    Sub1 = "doc/html/index.html",
%%     Sub2 = "doc/index.html",
    lists:map(
      fun({App, AppPath}) -> 
	      [AppName, VsnStr] = string:tokens(App, "-"),
	      AppURL = URL ++ App,
	      {AppName, VsnStr, AppPath, AppURL ++ "/" ++ Sub1}
      end, AppDirs).


get_app_dirs(Dir) ->
    {ok, Files} = file:list_dir(Dir),
    AFiles = 
	lists:map(fun(File) -> {File, filename:join([Dir, File])} end, Files),
    lists:zf(fun is_app_with_doc/1, AFiles).

is_app_with_doc({"." ++ _ADir, _APath}) ->
    false;
is_app_with_doc({ADir, APath}) ->
    case file:read_file_info(filename:join([APath, "info"])) of
	{ok, _FileInfo} ->
	    {true, {ADir, APath}};
	_ ->
	    false
    end.

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% Find info for one application.
% Read the "info" file for each application. Look at "group" and "short".
% key words.
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

%   IN: [{Appname,AppVersion,AppPath,IndexUTL}, ...]
%  OUT: [{Group,Heading,[{AppName,[{AppVersion,Path,URL,Text} | ...]}
%                        | ...]}, ...]

find_application_infos([]) ->
    [];
find_application_infos([{App, Vsn, AppPath, IndexURL} | Paths]) ->
    case read_info(filename:join(AppPath,"info")) of
	{error,_Reason} ->
	    warning("No info for app ~p", [AppPath]),
	    find_application_infos(Paths);
	Db ->
	    {Group,_Heading} =
		case lists:keysearch("group", 1, Db) of
		    {value, {_, G0}} ->
			% This value may be in two parts, 
		        % tag and description
			case string:str(G0, " ") of
			    0 ->
				{list_to_atom(G0), ""};
			    N ->
				{list_to_atom(string:substr(G0,1,N-1)),
				 string:substr(G0,N+1)}
			end;
		    false ->
			local_error("No group given",[])
		end,
	    Text =
		case lists:keysearch("short", 1, Db) of
		    {value, {_, G1}} ->
			G1;
		    false ->
			""
		end,
%%	    [{Group, Heading, {App, {Vsn, AppPath, IndexURL, Text}}}
	    [{Group, "", {App, {Vsn, AppPath, IndexURL, Text}}}
	     | find_application_infos(Paths)]
	end.

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% Group into one list element for each group name.
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

% IN : {Group,Heading,{AppName,{AppVersion,Path,URL,Text}}}
% OUT: {Group,Heading,[{AppName,[{AppVersion,Path,URL,Text} | ...]} | ...]}

form_groups(Apps) ->
    group_apps(lists:sort(Apps)).

group_apps([{Group,Heading,AppInfo} | Info]) ->
    group_apps(Info, Group, Heading, [AppInfo]);
group_apps([]) ->
    [].

% First description
group_apps([{Group,"",AppInfo} | Info], Group, Heading, AppInfos) ->
    group_apps(Info, Group, Heading, [AppInfo | AppInfos]);
group_apps([{Group,Heading,AppInfo} | Info], Group, "", AppInfos) ->
    group_apps(Info, Group, Heading, [AppInfo | AppInfos]);
% Exact match
group_apps([{Group,Heading,AppInfo} | Info], Group, Heading, AppInfos) ->
    group_apps(Info, Group, Heading, [AppInfo | AppInfos]);
% Different descriptions
group_apps([{Group,_OtherHeading,AppInfo} | Info], Group, Heading, AppInfos) ->
    warning("Group ~w descriptions differ",[Group]),
    group_apps(Info, Group, Heading, [AppInfo | AppInfos]);
group_apps(Info, Group, Heading, AppInfos) ->
    [{Group,Heading,combine_apps(AppInfos)} | group_apps(Info)].

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% Group into one list element for each application name.
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

% IN : {AppName,{AppVersion,Path,URL,Text}}
% OUT: {AppName,[{AppVersion,Path,URL,Text} | ...]}

combine_apps(Apps) ->
    combine_apps(Apps,[],[]).

combine_apps([{AppName,{Vsn1,Path1,URL1,Text1}}, 
	      {AppName,{Vsn2,Path2,URL2,Text2}} | Apps], AppAcc, Acc) ->
    combine_apps([{AppName,{Vsn2,Path2,URL2,Text2}} | Apps],
		 [{Vsn1,Path1,URL1,Text1} | AppAcc],
		 Acc);
combine_apps([{AppName,{Vsn1,Path1,URL1,Text1}}, 
	      {NewAppName,{Vsn2,Path2,URL2,Text2}} | Apps], AppAcc, Acc) ->
    App = lists:sort(fun vsncmp/2,[{Vsn1,Path1,URL1,Text1}|AppAcc]),
    combine_apps([{NewAppName,{Vsn2,Path2,URL2,Text2}} | Apps],
		 [],
		 [{AppName,App}|Acc]);
combine_apps([{AppName,{Vsn,Path,URL,Text}}], AppAcc, Acc) ->
    App = lists:sort(fun vsncmp/2,[{Vsn,Path,URL,Text}|AppAcc]),
    [{AppName,App}|Acc].


%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% Open a template and fill in the missing parts
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

% IN : {Group,Heading,[{AppName,[{AppVersion,Path,URL,Text} | ...]} | ...]}
% OUT: String that is the HTML code

subst_template(Group, File, Info) ->
    case file:open(File, read) of
	{ok,Stream} ->
	    Res = subst_template_1(Group, Stream, Info),
	    file:close(Stream),
	    Res;
	{error,Reason} ->
	    {error, Reason}
    end.

subst_template_1(Group, Stream, Info) ->
    case file:read(Stream, 100000) of
	{ok, Template} ->
	    Fun = fun(Match, _) -> {subst(Match, Info, Group),Info} end,
	    gsub(Template, "#[A-Za-z_0-9]+#", Fun, Info);
	{error, Reason} ->
	    {error, Reason}
    end.

get_version(Info) ->
    case lists:keysearch('runtime', 1, Info) of
	{value, {_,_,Apps}} ->
	    case lists:keysearch("erts", 1, Apps) of
		{value, {_,[{Vers,_,_,_} | _]}} ->
		    Vers;
		_ ->
		    ""
	    end;
	_ ->
	    ""
    end.
		    
subst("#otp_base_vsn#", _Info, _Group) ->
    get(otp_base_vsn);
subst("#version#", Info, _Group) ->
    get_version(Info);
subst("#copyrightyear#", _Info, _Group) ->
    {Year,_,_} = erlang:date(),
    integer_to_list(Year);
subst("#copyright#", _Info, _Group) ->
    "copyright  Copyright &copy; 1991-2004";
subst("#groups#", Info, _Group) ->
    [
     subst_groups(Info)
    ];
subst("#applinks#", Info, Group) ->
    subst_applinks(Info, Group);
subst(KeyWord, Info, _Group) ->
    case search_appname(KeyWord -- "##", Info) of
	{ok,URL} ->
	    URL;
	_ ->
	    warning("Can't substitute keyword ~s~n",[KeyWord]),
	    ""
    end.

search_appname(App, [{_Group,_,Apps} | Groups]) ->
    case lists:keysearch(App, 1, Apps) of
	{value, {_,[{_Vers,_Path,URL,_Text} | _]}} ->
	    {ok,lists:sublist(URL, length(URL) - length("/index.html"))}; 
	_ ->
	    search_appname(App, Groups)
    end;
search_appname(_App, []) ->
    {error, noapp}.

subst_applinks(Info, Group) ->
    subst_applinks_1(group_order(), Info, Group).

subst_applinks_1([{G, Heading}|Gs], Info0, Group) ->
    case lists:keysearch(G, 1, Info0) of
	{value,{G,_Heading,Apps}} ->
	    Info = lists:keydelete(G, 1, Info0),
	    ["\n<li>",Heading, "\n<ul>\n",
	     html_applinks(Apps), "\n</ul></li>\n"|
	     subst_applinks_1(Gs, Info, Group)];
	false ->
	    warning("No applications in group ~w\n", [G]),
	    subst_applinks_1(Gs, Info0, Group)
    end;
subst_applinks_1([], [], _) -> [];
subst_applinks_1([], Info, _) ->
    local_error("Info left: ~p\n", [Info]),
    [].

html_applinks([{Name,[{_,_,URL,_}|_]}|AppNames]) ->
    ["<li><a href=\"",URL,"\">",Name,
     "</a></li>\n"|html_applinks(AppNames)];
html_applinks([]) -> [].


% Info: [{Group,Heading,[{AppName,[{AppVersion,Path,URL,Text} | ..]} | ..]} ..]

subst_groups(Info0) ->
    {Html1,Info1} = subst_known_groups(group_order(), Info0, ""),
    {Html2,Info}  = subst_unknown_groups(Info1, Html1, []),
    Fun = fun({_Group,_GText,Applist}, Acc) -> Applist ++ Acc end,
    case lists:foldl(Fun, [], Info) of
	[] ->
	    Html2;
	Apps ->
	    [Html2,group_table("Misc Applications",Apps)]
    end.
    

subst_known_groups([], Info, Text) ->
    {Text,Info};
subst_known_groups([{Group, Heading} | Groups], Info0, Text0) ->    
    case lists:keysearch(Group, 1, Info0) of
	{value,{_,_Heading,Apps}} ->
	    Text = group_table(Heading,Apps),
	    Info = lists:keydelete(Group, 1, Info0),
	    subst_known_groups(Groups, Info, Text0 ++ Text);
	false ->
	    warning("No applications in group ~w~n",[Group]),
	    subst_known_groups(Groups, Info0, Text0)
    end.


subst_unknown_groups([], Text0, Left) ->
    {Text0,Left};
subst_unknown_groups([{Group,"",Apps} | Groups], Text0, Left) ->
    warning("No text describes ~w",[Group]),
    subst_unknown_groups(Groups, Text0, [{Group,"",Apps} | Left]);
subst_unknown_groups([{_Group,Heading,Apps} | Groups], Text0, Left) ->    
    Text = group_table(Heading,Apps),
    subst_unknown_groups(Groups, Text0 ++ Text, Left).


group_table(Heading,Apps) ->
    ["<h2>",Heading,"</h2>",
     "<table class=\"group-table\">\n",
     subst_apps(Apps),
     "</table>\n"
    ].

% Count and split the applications in half to get the right sort
% order in the table.

subst_apps([{App,VersionInfo} | Apps]) ->
    [subst_app(App, VersionInfo) | subst_apps(Apps)];
subst_apps([]) ->
    [].


subst_app(App, [{VSN,_Path,Link,Text}]) ->
    [
     "  <tr class=app>\n",
     "    <td>\n",
     "            <a href=\"",Link,"\" target=\"_top\">",uc(App),"</a>\n",
     "            <a href=\"",Link,"\" target=\"_top\">",VSN,"</a>\n",
     "    </td>\n",
     "    <td>\n",
     Text,"\n",
     "    </td>\n",
     "  </tr>\n"
    ];
subst_app(App, [{VSN,_Path,Link,Text} | VerInfos]) ->
    [
     "  <tr class=app>\n",
     "    <td>\n",
     "            <a href=\"",Link,"\" target=\"_top\">",uc(App),
     "</a>\n",
     "            <a href=\"",Link,"\" target=\"_top\">",VSN,"</a>\n",
     "                <br/>\n",
     subst_vsn(VerInfos),
     "    </td>\n",
     "    <td>\n",
     Text,"\n",
     "    </td>\n",
     "  </tr>\n"
    ].


subst_vsn([{VSN,_Path,Link,_Text} | VSNs]) ->
    [
     "      <font size=\"2\"><a class=anum href=\"",Link,"\" target=\"_top\">",
     VSN,
     "</a></font><br/>\n",
     subst_vsn(VSNs)
    ];
subst_vsn([]) ->
    "".
    

% Yes, this is very inefficient an is done for every comarision
% in the sort but it doesn't matter in this case.

vsncmp({Vsn1,_,_,_}, {Vsn2,_,_,_}) ->
    L1 = [list_to_integer(N1) || N1 <- string:tokens(Vsn1, ".")],
    L2 = [list_to_integer(N2) || N2 <- string:tokens(Vsn2, ".")],
    L1 > L2.


%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%
%  GENERIC FUNCTIONS, NOT SPECIFIC FOR GENERATING INDEX.HTML
%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% Read the "info" file into a list of Key/Value pairs
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

read_info(File) ->
    case file:open(File, read) of
	{ok,Stream} ->
	    Res =
		case file:read(Stream,10000) of
		    {ok, Text} ->
			Lines = string:tokens(Text, "\n\r"),
			KeyValues0 = lines_to_key_value(Lines),
			combine_key_value(KeyValues0);
		    {error, Reason} ->
			{error, Reason}
		end,
	    file:close(Stream),
	    Res;
	{error,Reason} ->
	    {error,Reason}
    end.

combine_key_value([{Key,Value1},{Key,Value2} | KeyValues]) ->
    combine_key_value([{Key,Value1 ++ "\n" ++ Value2} | KeyValues]);
combine_key_value([KeyValue | KeyValues]) ->
    [KeyValue | combine_key_value(KeyValues)];
combine_key_value([]) ->
    [].

lines_to_key_value([]) ->
    [];
lines_to_key_value([Line | Lines]) ->
    case re:run(Line, "^[a-zA-Z_\\-]+:") of
	nomatch ->
	    case re:run(Line, "[\041-\377]") of
		nomatch ->
		    lines_to_key_value(Lines);
		_ ->
		    warning("skipping line \"~s\"",[Line]),
		    lines_to_key_value(Lines)
	    end;
	{match, [{0, Length} |_]} ->
	    Value0 = lists:sublist(Line, Length+1, length(Line) - Length),
	    Value1 = re:replace(Value0, "^[ \t]*", "", 
					 [{return, list}]),
	    Value = re:replace(Value1, "[ \t]*$", "", 
                                         [{return, list}]),
            Key = lists:sublist(Line, Length-1),
	    [{Key, Value} | lines_to_key_value(Lines)]
    end.

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% Regular expression helpers.
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

%% -type gsub(String, RegExp, Fun, Acc) -> subres().
%%  Substitute every match of the regular expression RegExp with the
%%  string returned from the function Fun(Match, Acc). Accept pre-parsed
%%  regular expressions. Acc is an argument to the Fun. The Fun should return
%%  a tuple {Replacement, NewAcc}.
 
gsub(String, RegExp, Fun, Acc) when is_list(RegExp) ->
    case re:compile(RegExp) of
        {ok, RE} -> 
	    gsub(String, RE, Fun, Acc);
	{error, E} -> 
	    {error, E}
    end;
gsub(String, RE, Fun, Acc) ->
    {match, Ss} = re:run(String, RE, [global]),
    {NewString, NewAcc} = sub_repl(Ss, Fun, Acc, String, 0),
    {ok, NewString, NewAcc}.


% New code that uses fun for finding the replacement. Also uses accumulator
% to pass argument between the calls to the fun.
sub_repl([[{St, L}] |Ss], Fun, Acc0, S, Pos) ->
        Match = string:substr(S, St+1, L),
        {Rep, Acc} = Fun(Match, Acc0),
        {Rs, NewAcc} = sub_repl(Ss, Fun, Acc, S, St+L),
        {string:substr(S, Pos+1, St-Pos) ++ Rep ++ Rs, NewAcc};
sub_repl([], _Fun, Acc, S, Pos) -> {string:substr(S, Pos+1), Acc}.



%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% Error and warnings
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

local_error(Format, Args) ->
    io:format("ERROR: " ++ Format ++ "\n", Args),
    exit(1).

warning(Format, Args) ->
    case get(silent) of
	true -> ok;
	_ -> io:format("WARNING: " ++ Format ++ "\n", Args)
    end.

report(Format, Args) ->
    case get(silent) of
	true -> ok;
	_ -> io:format(Format ++ "\n", Args)
    end.


%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
% Extensions to the 'string' module.
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

uc(String) ->
    lists:reverse(uc(String, [])).

uc([], Acc) ->
    Acc;
uc([H | T], Acc) when is_integer(H), [97] =< H, H =< $z ->
    uc(T, [H - 32 | Acc]);
uc([H | T], Acc) ->
    uc(T, [H | Acc]).

