%% ``The contents of this file are subject to the Erlang Public License,
%% Version 1.1, (the "License"); you may not use this file except in
%% compliance with the License. You should have received a copy of the
%% Erlang Public License along with this software. If not, it can be
%% retrieved via the world wide web at http://www.erlang.org/.
%% 
%% Software distributed under the License is distributed on an "AS IS"
%% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See
%% the License for the specific language governing rights and limitations
%% under the License.
%% 
%% The Initial Developer of the Original Code is Ericsson Utvecklings AB.
%% Portions created by Ericsson are Copyright 1999, Ericsson Utvecklings
%% AB. All Rights Reserved.''
%% 
%%     $Id$
%%
-module(bup).
-export([
         change_node_name/5,
         view/2,
         test/0,
         test/1
        ]).

-export([
	 count/1,
	 display/1
        ]).

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% Management of backups, a few demos

%0
change_node_name(Mod, From, To, Source, Target) ->
    Switch =
        fun(Node) when Node == From -> To;
           (Node) when Node == To -> throw({error, already_exists});
           (Node) -> Node
        end,
    Convert =
        fun({schema, db_nodes, Nodes}, Acc) ->
                {[{schema, db_nodes, lists:map(Switch,Nodes)}], Acc};
           ({schema, version, Version}, Acc) ->
                {[{schema, version, Version}], Acc};
           ({schema, cookie, Cookie}, Acc) ->
                {[{schema, cookie, Cookie}], Acc};
           ({schema, Tab, CreateList}, Acc) ->
                Keys = [ram_copies, disc_copies, disc_only_copies],
                OptSwitch =
                    fun({Key, Val}) ->
                            case lists:member(Key, Keys) of
                                true -> {Key, lists:map(Switch, Val)};
                                false-> {Key, Val}
                            end
                    end,
                {[{schema, Tab, lists:map(OptSwitch, CreateList)}], Acc};
           (Other, Acc) ->
                {[Other], Acc}
        end,
    mnesia:traverse_backup(Source, Mod, Target, Mod, Convert, switched).

view(Source, Mod) ->
    View = fun(Item, Acc) ->
                   io:format("~p.~n",[Item]),
                   {[Item], Acc + 1}
           end,
    mnesia:traverse_backup(Source, Mod, dummy, read_only, View, 0).
%0

-record(bup_rec, {key, val}).

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% Test change of node name
%%
%% Assume db_nodes to be current node and on all other nodes but one
%% Create new schema,  start Mnesia on all db_nodes
%% Create a table of disc_copies type which is replicated to all db_nodes
%% Perform a backup and change current node to unused node in backup
%% Start Mnesia on all nodes according to the new set of db_nodes
test() ->
    test(nodes()).

test(Nodes)->
    AllNodes = (Nodes -- [node()]) ++ [node()],
    case length(AllNodes)  of
        Length when Length > 1 ->
	    OldBup = "old.BUP",
	    NewBup = "new.BUP",
            Res = (catch test2(AllNodes, OldBup, NewBup)),
	    case Res of
                {'EXIT', Reason} ->
		    file:delete(OldBup),
		    file:delete(NewBup),
		    {error, Reason};
                ok ->
		    ok = count(NewBup),
		    file:delete(OldBup),
		    file:delete(NewBup),
		    ok
            end;
        _ ->
            {error,{"Must run on at least one other node",AllNodes}}
    end.
            
test2(AllNodes, OldBup, NewBup) ->
    ThisNode = node(),
    OtherNode = hd(AllNodes -- [ThisNode]),
    OldNodes = AllNodes -- [OtherNode],
    NewNodes = AllNodes -- [ThisNode],
    Mod = mnesia_backup, % Assume local files
    file:delete(OldBup),
    file:delete(NewBup),

    %% Create old backup
    rpc:multicall(AllNodes, mnesia, lkill, []),
    ok = mnesia:delete_schema(AllNodes),
    ok = mnesia:create_schema(OldNodes),
    rpc:multicall(OldNodes, mnesia, start, []),
    rpc:multicall(OldNodes, mnesia, wait_for_tables, [[schema], infinity]),
    
    CreateList = [{disc_copies, OldNodes},
                  {attributes, record_info(fields, bup_rec)}],
    {atomic, ok} = mnesia:create_table(bup_rec, CreateList),
    rpc:multicall(OldNodes, mnesia, wait_for_tables, [[bup_rec], infinity]),
    OldRecs = [#bup_rec{key = I, val = I * I} || I <- lists:seq(1, 10)],
    lists:foreach(fun(R) -> ok = mnesia:dirty_write(R) end,OldRecs),
    ok = mnesia:backup(OldBup, Mod),
    ok = mnesia:dirty_write(#bup_rec{key = 4711, val = 4711}),
    rpc:multicall(OldNodes, mnesia, stop, []),
    {ok,_} = view(OldBup, Mod),

    %% Change node name
    {ok,_} = change_node_name(Mod, ThisNode, OtherNode, OldBup, NewBup),
    ok = rpc:call(OtherNode, mnesia, install_fallback, [NewBup, Mod]),
    {_NewStartRes,[]} = rpc:multicall(NewNodes, mnesia, start, []),
    rpc:call(OtherNode, mnesia, wait_for_tables, [[bup_rec], infinity]),
    Wild = rpc:call(OtherNode, mnesia, table_info, [bup_rec, wild_pattern]),
    NewRecs = rpc:call(OtherNode, mnesia, dirty_match_object, [Wild]),
    rpc:multicall(NewNodes, mnesia, stop, []),
    {ok,_} = view(NewBup, Mod),

    %% Sanity test
    case {lists:sort(OldRecs), lists:sort(NewRecs)} of
        {Same, Same} -> ok
    end.

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

-record(state, {counter_tab, size_tab, acc_size = 0, n_records = 0}).


%% Iterates over a backup file and shows some statistics
%% The identity of ets table containing the counters is not removed
count(BupFile) ->
    CounterTab = ets:new(?MODULE, [set, public]),
    SizeTab = ets:new(?MODULE, [set, public]),
    Mod = mnesia:system_info(backup_module),
    State = #state{counter_tab = CounterTab, size_tab = SizeTab},
    case mnesia:traverse_backup(BupFile, Mod, dummy, read_only, fun incr/2, State) of
	{ok, State2} ->
	    Res = display(State2),
	    ets:delete(CounterTab),
	    ets:delete(SizeTab),
	    Res;
	{error, Reason} ->	
	    ets:delete(CounterTab),
	    ets:delete(SizeTab),
	    {error, Reason}
    end.

incr(Rec, State) ->
    Tab = element(1, Rec),
    Key = element(2, Rec),
    Oid = {Tab, Key},
    incr_counter(State#state.counter_tab, Oid),
    Size = size(term_to_binary(Rec)),
    max_size(State#state.size_tab, Tab, Key, Size),
    AccSize = State#state.acc_size,
    N = State#state.n_records,
    State2 = State#state{acc_size = AccSize + Size, n_records = N + 1},
    {[Rec], State2}.

incr_counter(T, Counter) ->
    case catch ets:update_counter(T, Counter, 1) of
	{'EXIT', _} ->
	    ets:insert(T, {Counter, 1});
	_ ->
	    ignore
    end.

max_size(T, Tab, Key, Size) ->
    case catch ets:lookup_element(T, Tab, 2) of
	{'EXIT', _} ->
	    ets:insert(T, {Tab, Size, Key});
	OldSize when OldSize < Size ->
	    ets:insert(T, {Tab, Size, Key});
	_ ->
	    ignore
    end.

%% Displays the statistics found in the ets table
display(State) ->
    CounterTab = State#state.counter_tab,
    Tabs = [T || {{_, T}, _} <- match_tab(CounterTab, schema)],
    io:format("~w tables with totally: ~w records, ~w keys, ~w bytes~n",
	      [length(Tabs),
	       State#state.n_records,
	       ets:info(CounterTab, size),
	       State#state.acc_size]),	       
    display(State, lists:sort(Tabs)).

display(State, [Tab | Tabs]) ->
    Counters = match_tab(State#state.counter_tab, Tab),
    io:format("~-10w     records in table ~w~n", [length(Counters), Tab]),
    Fun = fun({_Oid, Val}) when Val < 5 ->
		  ignore;
	     ({Oid, Val}) ->
		  io:format("~-10w *** records with key ~w~n", [Val, Oid])
	  end,
    lists:foreach(Fun, Counters),
    display_size(State#state.size_tab, Tab),
    display(State, Tabs);
display(_CounterTab, []) ->
    ok.
    
match_tab(T, Tab) ->
    ets:match_object(T, {{Tab, '_'}, '_'}).

display_size(T, Tab) ->
    case catch ets:lookup(T, Tab) of
	[] ->
	    ignore;
	[{_, Size, Key}] when Size > 1000 ->
	    io:format("~-10w --- bytes occupied by largest record ~w~n",
		      [Size, {Tab, Key}]);
	[{_, _, _}] ->
	    ignore
    end.
