%%<copyright>
%% <year>1999-2007</year>
%% <holder>Ericsson AB, All Rights Reserved</holder>
%%</copyright>
%%<legalnotice>
%% 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 online 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 AB.
%%</legalnotice>
%%
-module(snmp_community_mib).

-export([configure/1, reconfigure/1,
	 snmpCommunityTable/1, snmpCommunityTable/3,
	 snmpTargetAddrExtTable/3,
	 community2vacm/2, vacm2community/2,
	 get_target_addr_ext_mms/2]).
-export([add_community/5, delete_community/1]).
-export([check_community/1]).

-include("SNMP-COMMUNITY-MIB.hrl").
-include("SNMP-TARGET-MIB.hrl").
-include("SNMPv2-TC.hrl").
-include("snmp_types.hrl").

-define(VMODULE,"COMMUNITY-MIB").
-include("snmp_verbosity.hrl").

-ifndef(default_verbosity).
-define(default_verbosity,silence).
-endif.


%%%-----------------------------------------------------------------
%%% Implements the instrumentation functions and additional 
%%% functions for the SNMP-COMMUNITY-MIB.
%%% This MIB contains objects that makes it possible to use VACM
%%% with SNMPv1 and SNMPv2c.
%%%-----------------------------------------------------------------
%%-----------------------------------------------------------------
%% Func: configure/1
%% Args: Dir is the directory where the configuration files are found.
%% Purpose: If the tables doesn't exist, this function reads
%%          the config-files for the community mib tables, and
%%          inserts the data.  This means that the data in the tables
%%          survive a reboot.  However, the StorageType column is
%%          checked for each row.  If volatile, the row is deleted.
%% Returns: ok
%% Fails: exit(configuration_error)
%%-----------------------------------------------------------------
configure(Dir) ->
    set_sname(),
    case db(snmpCommunityTable) of
        {_, mnesia} ->
            ?vlog("community table in mnesia: cleanup",[]),
            gc_tabs();
	TabDb ->
	    case snmpa_local_db:table_exists(TabDb) of
		true ->
		    ?vlog("community table exist: cleanup",[]),
		    gc_tabs();
		false ->
		    ?vlog("community table does not exist: reconfigure",[]),
		    reconfigure(Dir)
	    end
    end.

%%-----------------------------------------------------------------
%% Func: reconfigure/1
%% Args: Dir is the directory where the configuration files are found.
%% Purpose: Reads the config-files for the community mib tables, and
%%          inserts the data.  Makes sure that all old data in
%%          the tables are deleted, and the new data inserted.
%%          This function makes sure that all (and only) 
%%          config-file-data are in the tables. 
%% Returns: ok
%% Fails: exit(configuration_error)
%%-----------------------------------------------------------------
reconfigure(Dir) ->
    set_sname(),
    case (catch do_reconfigure(Dir)) of
	ok ->
	    ok;
	{error, Reason} ->
	    ?vinfo("reconfigure error: ~p", [Reason]),
	    config_err("reconfigure failed: ~p", [Reason]),
	    exit(configuration_error);
	Error ->
	    ?vinfo("reconfigure failed: ~p", [Error]),
	    config_err("reconfigure failed: ~p", [Error]),
	    exit(configuration_error)
    end.

do_reconfigure(Dir) ->
    ?vdebug("read community config files",[]),
    Comms = read_community_config_files(Dir),
    ?vdebug("initiate tables",[]),
    init_tabs(Comms),
    ok.

init_tabs(Comms) ->
    ?vdebug("create community table",[]),
    snmpa_local_db:table_delete(db(snmpCommunityTable)),
    snmpa_local_db:table_create(db(snmpCommunityTable)),
    ?vdebug("invalidate cache",[]),
    invalidate_cache(),
    ?vdebug("initiate community table",[]),
    init_comm_table(Comms).
    

read_community_config_files(Dir) ->
    ?vdebug("read community config file",[]),
    Gen    = fun(_) -> ok end,
    Filter = fun(Comms) -> Comms end,
    Check  = fun(Entry) -> check_community(Entry) end,
    [Comms] = 
	snmp_conf:read_files(Dir, [{Gen, Filter, Check, "community.conf"}]),
    Comms.

check_community({Index, CommunityName, SecName, CtxName, TransportTag}) ->
    snmp_conf:check_string(Index,{gt,0}),
    snmp_conf:check_string(CommunityName),
    snmp_conf:check_string(SecName),
    snmp_conf:check_string(CtxName),
    snmp_conf:check_string(TransportTag),
    EngineID = get_engine_id(),
    Comm = {Index, CommunityName, SecName, EngineID, CtxName, TransportTag,
	    ?'StorageType_nonVolatile', ?'RowStatus_active'},
    {ok, Comm};
check_community(X) ->
    error({invalid_community, X}).

%% This is for the case when check_community is called from the 
%% snmp_config module (to generate community config file) and
%% the agent is not started. The actual return value is not 
%% checked, as long as it is '{ok, _}'.
get_engine_id() ->
    case (catch snmp_framework_mib:get_engine_id()) of
	{'EXIT', _} ->
	    "agentEngine";
	EngineID ->
	    EngineID
    end.

init_comm_table([Row | T]) ->
    ?vtrace("init_comm_table -> entry with"
	    "~n   Row: ~p", [Row]),
    Key = element(1, Row),
    snmpa_local_db:table_create_row(db(snmpCommunityTable), Key, Row),
    update_cache(Key),
    init_comm_table(T);
init_comm_table([]) -> 
    ?vtrace("init_comm_table -> entry when done", []),
    true.

table_cre_row(Tab, Key, Row) ->
    snmpa_mib_lib:table_cre_row(db(Tab), Key, Row).

table_del_row(Tab, Key) ->
    snmpa_mib_lib:table_del_row(db(Tab), Key).


%% FIXME: does not work with mnesia
add_community(Idx, CommName, SecName, CtxName, TransportTag) ->
    Community = {Idx, CommName, SecName, CtxName, TransportTag},
    case (catch check_community(Community)) of
	{ok, Row} ->
	    Key = element(1, Row),
	    case table_cre_row(snmpCommunityTable, Key, Row) of
		true ->
		    update_cache(Key),
		    {ok, Key};
		false ->
		    {error, create_failed}
	    end;
	{error, Reason} ->
	    {error, Reason};
	Error ->
	    {error, Error}
    end.

%% FIXME: does not work with mnesia
delete_community(Key) ->
    invalidate_cache(Key),
    case table_del_row(snmpCommunityTable, Key) of
	true ->
	    ok;
	false ->
	    {error, delete_failed}
    end.


gc_tabs() ->
    DB  = db(snmpCommunityTable),
    STC = stc(snmpCommunityTable),
    FOI = foi(snmpCommunityTable),
    IR  = fun(RowIndex) -> invalidate_cache(RowIndex) end,
    UR  = fun(RowIndex) -> update_cache(RowIndex) end,
    snmpa_mib_lib:gc_tab(DB, STC, FOI, IR, UR),
    ok.


%%-----------------------------------------------------------------
%% API functions
%%-----------------------------------------------------------------
%%-----------------------------------------------------------------
%% We keep two caches for mapping;
%% one that maps CommunityName to CommunityIndex, and
%% one that maps 
%%     {SecName, ContextEngineId, ContextName} to {CommunityName, Tag}
%% Name -> Index instead of Name -> Vacm allows us to save a 
%% few bytes of memory, although it introduces an extra level of
%% indirection.
%%-----------------------------------------------------------------
community2vacm(Community, Addr) ->
    Idxs = ets:lookup(snmp_community_cache, Community),
    loop_c2v_rows(lists:keysort(2, Idxs), Addr).

loop_c2v_rows([{_, CommunityIndex} | T], Addr) ->
    ?vtrace("loop_c2v_rows -> entry with"
	    "~n   CommunityIndex: ~p", [CommunityIndex]),
    case get_row(CommunityIndex) of
	{_Community, VacmParams, Tag} ->
	    {TDomain, TAddr} = Addr,
	    case snmp_target_mib:is_valid_tag(Tag, TDomain, TAddr) of
		true ->
		    ?vdebug("loop_c2v_rows -> "
			    "~p valid tag for community index ~p", 
			    [Tag, CommunityIndex]),
		    VacmParams;
		false ->
		    ?vtrace("loop_c2v_rows -> "
			    "~p not valid tag community index ~p", 
			    [Tag, CommunityIndex]),
		    loop_c2v_rows(T, Addr)
	    end;
	undefined ->
	    loop_c2v_rows(T, Addr)
    end;
loop_c2v_rows([], _Addr) ->
    undefined.


%%-----------------------------------------------------------------
%% Func: vacm2community(Vacm, {TDomain, TAddr}) -> 
%%         {ok, Community} | undefined
%% Types: Vacm = {SecName, ContextEngineId, ContextName}
%% Purpose: Follows the steps in RFC 2576 section
%%          5.2.3 in order to find a community string to be used
%%          in a notification.
%%-----------------------------------------------------------------
vacm2community(Vacm, Addr) ->
    Names = ets:lookup(snmp_community_cache, Vacm),
    loop_v2c_rows(lists:keysort(2, Names), Addr).
    
loop_v2c_rows([{_, {CommunityName, Tag}} | T], Addr) ->
    ?vtrace("loop_v2c_rows -> entry with"
	    "~n   CommunityName: ~p"
	    "~n   Tag:           ~p", [CommunityName, Tag]),
    {TDomain, TAddr} = Addr,
    case snmp_target_mib:is_valid_tag(Tag, TDomain, TAddr) of
	true ->
	    ?vdebug("loop_v2c_rows -> "
		    "~p valid tag for community name ~p", 
		    [Tag, CommunityName]),
	    {ok, CommunityName};
	false ->
	    loop_v2c_rows(T, Addr)
    end;
loop_v2c_rows([], _Addr) ->
    undefined.



get_row(RowIndex) ->
    case snmp_generic:table_get_row(db(snmpCommunityTable), RowIndex,
				    foi(snmpCommunityTable)) of
	{_, CommunityName, SecName, ContextEngineId, ContextName,
	 TransportTag, _StorageType, ?'RowStatus_active'} ->
	    {CommunityName, {SecName, ContextEngineId, ContextName}, 
	     TransportTag};
	_ ->
	    undefined
    end.

invalidate_cache(RowIndex) ->
    case get_row(RowIndex) of
	{CommunityName, VacmParams, TransportTag} ->
	    ets:match_delete(snmp_community_cache,
			     {CommunityName, RowIndex}),
	    ets:match_delete(snmp_community_cache,
			     {VacmParams, {CommunityName, TransportTag}});
	undefined ->
	    ok
    end.

update_cache(RowIndex) ->
    case get_row(RowIndex) of
	{CommunityName, VacmParams, TransportTag} ->
	    ets:insert(snmp_community_cache, 
		       {CommunityName, RowIndex}), % CommunityIndex
	    ets:insert(snmp_community_cache, 
		       {VacmParams, {CommunityName, TransportTag}});
	undefined ->
	    ok
    end.

invalidate_cache() ->
    ets:match_delete(snmp_community_cache, {'_', '_'}).


get_target_addr_ext_mms(TDomain, TAddress) ->
    get_target_addr_ext_mms(TDomain, TAddress, []).
get_target_addr_ext_mms(TDomain, TAddress, Key) ->
    case snmp_target_mib:table_next(snmpTargetAddrTable, Key) of
	endOfTable -> 
	    false;
	NextKey -> 
	    case snmp_target_mib:get(
		   snmpTargetAddrTable, NextKey, [?snmpTargetAddrTDomain,
						  ?snmpTargetAddrTAddress,
						  12]) of
		[{value, TDomain}, {value, TAddress}, {value, MMS}] ->
		    {ok, MMS};
		_ ->
		    get_target_addr_ext_mms(TDomain, TAddress, NextKey)
	    end
    end.
%%-----------------------------------------------------------------
%% Instrumentation Functions
%%-----------------------------------------------------------------
%% Op == new | delete
snmpCommunityTable(Op) ->
    snmp_generic:table_func(Op, db(snmpCommunityTable)).

%% Op == get | is_set_ok | set | get_next
snmpCommunityTable(get, RowIndex, Cols) ->
    get(snmpCommunityTable, RowIndex, Cols);
snmpCommunityTable(get_next, RowIndex, Cols) ->
    next(snmpCommunityTable, RowIndex, Cols);
snmpCommunityTable(is_set_ok, RowIndex, Cols0) ->
    case (catch verify_snmpCommunityTable_is_set_ok(Cols0)) of
	{ok, Cols1} ->
	    case (catch verify_snmpCommunityTable_cols(Cols1, [])) of
		{ok, Cols} ->
		    Db = db(snmpCommunityTable),
		    snmp_generic:table_func(is_set_ok, RowIndex, Cols, Db);
		Error ->
		    Error
	    end;
	Error ->
	    Error
    end;
snmpCommunityTable(set, RowIndex, Cols0) ->
    case (catch verify_snmpCommunityTable_cols(Cols0, [])) of
	{ok, Cols} ->
	    invalidate_cache(RowIndex),
	    Db  = db(snmpCommunityTable),
	    Res = snmp_generic:table_func(set, RowIndex, Cols, Db),
            snmpa_agent:invalidate_ca_cache(),
	    update_cache(RowIndex),
	    Res;
	Error ->
	    Error
    end;
snmpCommunityTable(Op, Arg1, Arg2) ->
    snmp_generic:table_func(Op, Arg1, Arg2, db(snmpCommunityTable)).


verify_snmpCommunityTable_is_set_ok(Cols) ->
    LocalEngineID = snmp_framework_mib:get_engine_id(),
    case lists:keysearch(?snmpCommunityContextEngineID, 1, Cols) of
	{value, {_, LocalEngineID}} -> 
	    {ok, Cols};
	{value, _} -> 
	    {inconsistentValue, ?snmpCommunityContextEngineID};
	false -> 
	    {ok, kinsert(Cols, LocalEngineID)}
    end.

verify_snmpCommunityTable_cols([], Cols) ->
    {ok, lists:reverse(Cols)};
verify_snmpCommunityTable_cols([{Col, Val0}|Cols], Acc) ->
    Val = verify_snmpCommunityTable_col(Col, Val0),
    verify_snmpCommunityTable_cols(Cols, [{Col, Val}|Acc]).

verify_snmpCommunityTable_col(?snmpCommunityIndex, Index) ->
    case (catch snmp_conf:check_string(Index,{gt,0})) of
	ok ->
	    Index;
	_ ->
	    wrongValue(?snmpCommunityIndex)
    end;
verify_snmpCommunityTable_col(?snmpCommunityName, Name) ->
    case (catch snmp_conf:check_string(Name)) of
	ok ->
	    Name;
	_ ->
	    wrongValue(?snmpCommunityName)
    end;
verify_snmpCommunityTable_col(?snmpCommunitySecurityName, Name) ->
    case (catch snmp_conf:check_string(Name)) of
	ok ->
	    Name;
	_ ->
	    wrongValue(?snmpCommunitySecurityName)
    end;
verify_snmpCommunityTable_col(?snmpCommunityContextName, Name) ->
    case (catch snmp_conf:check_string(Name)) of
	ok ->
	    Name;
	_ ->
	    wrongValue(?snmpCommunityContextName)
    end;
verify_snmpCommunityTable_col(?snmpCommunityTransportTag, Tag) ->
    case (catch snmp_conf:check_string(Tag)) of
	ok ->
	    Tag;
	_ ->
	    wrongValue(?snmpCommunityTransportTag)
    end;
verify_snmpCommunityTable_col(_, Val) ->
    Val.



%% Op == get | is_set_ok | set | get_next
snmpTargetAddrExtTable(get, RowIndex, Cols) ->
    NCols = conv1(Cols),
    get(snmpTargetAddrExtTable, RowIndex, NCols);
snmpTargetAddrExtTable(get_next, RowIndex, Cols) ->
    NCols = conv1(Cols),
    conv2(next(snmpTargetAddrExtTable, RowIndex, NCols));
snmpTargetAddrExtTable(set, RowIndex, Cols0) ->
    case (catch verify_snmpTargetAddrExtTable_cols(Cols0, [])) of
	{ok, Cols} ->
	    NCols = conv3(Cols),
	    snmp_generic:table_func(set, RowIndex, NCols, 
				    db(snmpTargetAddrExtTable));
	Error ->
	    Error
    end;
snmpTargetAddrExtTable(is_set_ok, RowIndex, Cols0) ->
    case (catch verify_snmpTargetAddrExtTable_cols(Cols0, [])) of
	{ok, Cols} ->
	    NCols = conv3(Cols),
	    snmp_generic:table_func(is_set_ok, RowIndex, NCols, 
				    db(snmpTargetAddrExtTable));
	Error ->
	    Error
    end.


verify_snmpTargetAddrExtTable_cols([], Cols) ->
    {ok, lists:reverse(Cols)};
verify_snmpTargetAddrExtTable_cols([{Col, Val0}|Cols], Acc) ->
    Val = verify_snmpTargetAddrExtTable_col(Col, Val0),
    verify_snmpTargetAddrExtTable_cols(Cols, [{Col, Val}|Acc]).

verify_snmpTargetAddrExtTable_col(?snmpTargetAddrTMask, []) ->
    [];
verify_snmpTargetAddrExtTable_col(?snmpTargetAddrTMask, TMask) ->
    case (catch snmp_conf:check_taddress(TMask)) of
	ok ->
	    TMask; 
	_ ->
	    wrongValue(?snmpTargetAddrTMask)
    end;
verify_snmpTargetAddrExtTable_col(?snmpTargetAddrMMS, MMS) ->
    case (catch snmp_conf:check_packet_size(MMS)) of
	ok ->
	    MMS;
	_ ->
	    wrongValue(?snmpTargetAddrMMS)
    end;
verify_snmpTargetAddrExtTable_col(_, Val) ->
    Val.

db(snmpTargetAddrExtTable) -> db(snmpTargetAddrTable);
db(X) -> snmpa_agent:db(X).

fa(snmpCommunityTable) -> ?snmpCommunityName;
fa(snmpTargetAddrExtTable) -> 11.
 
foi(snmpCommunityTable) -> ?snmpCommunityIndex;
foi(snmpTargetAddrExtTable) -> 11.
 
noc(snmpCommunityTable) -> 8;
noc(snmpTargetAddrExtTable) -> 12.

stc(snmpCommunityTable) -> ?snmpCommunityStorageType.
 
next(Name, RowIndex, Cols) ->
    snmp_generic:handle_table_next(db(Name), RowIndex, Cols,
                                   fa(Name), foi(Name), noc(Name)).

conv1([Col | T]) -> [Col + 10 | conv1(T)];
conv1([]) -> [].
     

conv2([{[Col | Oid], Val} | T]) ->
    [{[Col - 10 | Oid], Val} | conv2(T)];
conv2([X | T]) ->
    [X | conv2(T)];
conv2(X) -> X.


conv3([{Idx, Val}|T]) -> [{Idx+10, Val} | conv3(T)];
conv3([]) -> [].


get(Name, RowIndex, Cols) ->
    snmp_generic:handle_table_get(db(Name), RowIndex, Cols, foi(Name)).

kinsert([H | T], EngineID) when element(1, H) < ?snmpCommunityContextEngineID ->
    [H | kinsert(T, EngineID)];
kinsert(Cols, EngineID) ->
    [{?snmpCommunityContextEngineID, EngineID} | Cols].


wrongValue(V) -> throw({wrongValue, V}).


%% -----

set_sname() ->
    set_sname(get(sname)).

set_sname(undefined) ->
    put(sname,conf);
set_sname(_) -> %% Keep it, if already set.
    ok.


error(Reason) ->
    throw({error, Reason}).

config_err(F, A) ->
    snmpa_error:config_err("[COMMUNITY-MIB]: " ++ F, A).
