%% 
%% %CopyrightBegin%
%% 
%% Copyright Ericsson AB 1996-2022. 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%
%% 
-module(snmp_shadow_table).

-export([table_func/2, table_func/4]).

-include("snmpa_internal.hrl").

-record(time_stamp, {key, data}).

-define(verify(Expr, Error), verify(catch Expr, Error, ?FILE, ?LINE)).

verify(Res, Error, File, Line) ->
    case Res of
	{atomic, _} ->
	    Res;
	ok ->
	    Res;
	_ ->
	    error_msg("~s(~w): crashed ~p -> ~p ~p~n",
		      [File, Line, Error, Res, process_info(self())]),
	    Res
    end.


%%%-----------------------------------------------------------------
%%% This module contains generic functions for implementing an SNMP
%%% table as a 'shadow'-table in Mnesia.  This means that for the
%%% SNMP table, there exists one Mnesia table with all information
%%% of the table.
%%% The Mnesia table is updated whenever an SNMP request is issued
%%% for the table and a specified amount of time has past since the
%%% last update.
%%% This is implemented as instrumentation functions to be used
%%% for the table.
%%%-----------------------------------------------------------------

create_time_stamp_table() ->
    Props = [{type, set},
	     {attributes, record_info(fields, time_stamp)}],
    create_table(time_stamp, Props, ram_copies, false),
    NRef = 
	case mnesia:dirty_read({time_stamp, ref_count}) of
	    [] -> 1;
	    [#time_stamp{data = Ref}] -> Ref + 1
	end,
    ok = mnesia:dirty_write(#time_stamp{key = ref_count, data = NRef}).

delete_time_stamp_table() ->
    Tab = time_stamp,
    case catch mnesia:dirty_read({Tab, ref_count}) of
	{'EXIT', _Reason} ->
	    delete_table(Tab);
	[] ->
	    delete_table(Tab);
	[#time_stamp{data = 1}] ->
	    delete_table(Tab);
	[#time_stamp{data = Ref}] ->
	    ok = mnesia:dirty_write(#time_stamp{key = ref_count, data = Ref - 1})
    end.

update(Name, UpdateFunc, Interval) ->
    CurrentTime = snmp_misc:now(ms),
    case mnesia:dirty_read({time_stamp, Name}) of
	[#time_stamp{data = Expire}] when CurrentTime =< Expire -> ok;
	_ ->
	    UpdateFunc(),
	    ok = mnesia:dirty_write(#time_stamp{key = Name,
						data = CurrentTime + Interval})
    end.
	  

%%-----------------------------------------------------------------
%% Func: table_func(Op, Extra)
%%       table_func(Op, RowIndex, Cols, Extra)
%% Args: Extra = {Name, SnmpKey, Attributes, Interval, UpdateFunc}
%% Purpose: Instrumentation function for the table.
%%          Name is the name of the table
%%          SnmpKey is the snmpkey as it should be specified in order
%%            to create the Mnesia table as an SNMP table
%%          Attributes is the attributes as it should be specified in order
%%            to create the Mnesia table as an SNMP table
%%          Interval is the minimum time in milliseconds between two
%%            updates of the table
%%          UpdateFunc is a function with no arguments that is called
%%            whenever the table must be updated
%% Returns: As specified for an SNMP table instrumentation function.
%%-----------------------------------------------------------------
table_func(new, {Name, SnmpKey, Attribs, _Interval, _UpdateFunc}) ->
    create_time_stamp_table(),
    Props = [{type, set},
	     {snmp, [{key, SnmpKey}]},
	     {attributes, Attribs}],
    create_table(Name, Props, ram_copies, true);
table_func(delete, {Name, _SnmpKey, _Attribs, _Interval, _UpdateFunc}) ->
    delete_time_stamp_table(),
    delete_table(Name).

table_func(Op, RowIndex, Cols, 
	   {Name, _SnmpKey, _Attribs, Interval, UpdateFunc}) ->
    update(Name, UpdateFunc, Interval),
    snmp_generic:table_func(Op, RowIndex, Cols, {Name, mnesia}).


%%-----------------------------------------------------------------
%% Urrk.
%% We want named tables, without schema info; the tables should
%% be locally named, but if the node crashes, info about the
%% table shouldn't be kept.  We could use ets tables for this.
%% BUT, we also want the snmp functionality, therefore we must
%% use mnesia.
%% The problem arises when the node that implements these tables
%% crashes, and another node takes over the MIB-implementations.
%% That node cannot create the shadow tables again, because they
%% already exist (according to mnesia...).  Therefore, we must
%% check if we maybe must delete the table first, and then create
%% it again.
%%-----------------------------------------------------------------
create_table(Tab, Props, Storage, DeleteAll) ->
    case lists:member(Tab, mnesia:system_info(tables)) of
	true ->
	    case mnesia:table_info(Tab, storage_type) of
		unknown ->
		    ?verify(mnesia:add_table_copy(Tab, node(), Storage),
			    [add_table_copy, Tab, node(), Storage]);
		Storage when DeleteAll == true ->
		    delete_all(Tab);
		_ ->
		    ignore
	    end;
	false ->
	    Nodes = [node()],
	    Props2 = [{local_content, true}, {Storage, Nodes}] ++ Props,
	    ?verify(mnesia:create_table(Tab, Props2),
		    [create_table, Tab, Props2])
    end.

delete_all(Tab) ->
    delete_all(mnesia:dirty_first(Tab), Tab).

delete_all('$end_of_table', _Tab) ->
    ok;
delete_all(Key, Tab) ->
    ok = mnesia:dirty_delete({Tab, Key}),
    delete_all(mnesia:dirty_next(Tab, Key), Tab).

delete_table(Tab) ->
    case lists:member(Tab, mnesia:system_info(tables)) of
	true ->
	    case ?verify(mnesia:del_table_copy(Tab, node()),
			 [del_table_copy, Tab, node()]) of
		{atomic, ok} ->
		    ok;
		{aborted, _Reason} ->
		    catch delete_all(Tab),
		    ok
	    end;
	false ->
	    ok
    end.


%%-----------------------------------------------------------------

error_msg(F, A) ->
    ?snmpa_error(F, A).
