-module(eets).


-export([seq/3]).

-export([increment/3]).  % example


%%% seq(Tab, Preconditions, Actions) -> boolean()
%%%
%%% Executes a sequence of preconditions and actions on an ets table,
%%% as one atomic operation. It is possible to bind the results of
%%% select operations to up to 10 numbered slots, and act on them in
%%% the Actions sequence.
%%%
%%% Preconditions = [{Expected, Operator, Data}]
%%%    Expected = term()
%%%    Operator = member | select_count | bind
%%%    Data     = <depends on Operator>
%%%
%%% Actions = [Action]
%%%    Action = {Op, Data} |
%%%             {Cond, Var, SubAction}
%%%        Op = insert | delete
%%%      Cond = foreach | if_nil
%%% SubAction = <depends on Cond>
%%%
%%% Behaviour for preconditions:
%%% {Bool, member, Key}      -> Bool == ets:member(Tab, Key)
%%% {Int, select_count, Pat} -> Int == ets:select_count(Tab, Pat)
%%% {Var, bind, Pat}         -> Var = ets:select(Tab, Pat)
%%%
%%% Behaviour for actions:
%%% {insert, Data}      -> ets:insert(Tab, Data)
%%% {delete, Key}       -> ets:delete(Tab, Key)
%%% {foreach, Var, Do}  -> [ets:Do(Tab, V) || V <- Var]
%%% {if_nil, Var, Do}   -> case Var of [] ->
%%%                          foreach(fun({insert,Data}) -> ets:insert(Tab,Data);
%%%                                     ({delete,Data}) -> ets:delete(Tab,Data)
%%%                                  end, Do);
%%%                          [_|_] -> true
%%%                        end.
%%%
seq(T, Precond, Actions) ->
    case check_preconds(T, Precond) of
	{true, Vars} ->
	    perform_actions(T, Actions, Vars);
	false ->
	    false
    end.

%%% Example: increment(Tab, Key, Incr)
%%%   Inserts {Key, Incr} if Key is not present in the table.
%%%   The actual increment operation is done in the select pattern.
%%%
increment(Tab, Key, Incr) ->
    Pat = [{ {Key, '$1'}, [], [{{ Key, {'+','$1',Incr} }}] }],
    seq(Tab, [{1, bind, Pat}], [{foreach, 1, insert},
				{if_nil,  1, [{insert, {Key, Incr}}]}]).


%%% Precondition checking
%%%
check_preconds(T, Preconds) ->
    try
	Vars = lists:foldl(
		 fun({Exp, Op, Obj}, Vs) ->
			 case Op of
			     member ->
				 case ets:member(T, Obj) of
				     Bool when Bool == Exp ->
					 Vs;
				     _ ->
					 throw(false)
				 end;
			     select_count ->
				 case {ets:select_count(T, Obj), Exp} of
				     {N, N} -> Vs;
				     {N, {lt,M}} when N < M -> Vs;
				     {N, {gt,M}} when N > M -> Vs;
				     _ ->
					 throw(false)
				 end;
			     bind ->
				 Pos = var_pos(Exp),
				 case element(Pos, Vs) of
				     undefined ->
					 Res = ets:select(T, Obj),
					 setelement(Pos, Vs, Res);
				     _ ->
					 erlang:error(badarg)
				 end;
			     _ ->
				 erlang:error(badarg)
			 end;
		    (_, _) ->
			 erlang:error(badarg)
		 end, erlang:make_tuple(10, undefined), Preconds),
	{true, Vars}
    catch
	throw:false ->
	    false
    end.

perform_actions(T, Actions, Vs) ->
    lists:foreach(
      fun({insert, Data}) ->
	      ets:insert(T, Data);
	 ({delete, K}) ->
	      ets:delete(T, K);
	 ({select_delete, Data}) ->
	      ets:select_delete(T, Data);
	 ({foreach, V, Op}) ->
	      Data = get_var(V, Vs),
	      case Op of
		  insert ->
		      ets:insert(T, Data);
		  delete ->
		      [ets:delete(T, K) || K <- Data]
	      end;
	 ({if_nil, V, Ops}) ->
	      case get_var(V, Vs) of
		  undefined ->
		      %% V hasn't been set to anything
		      erlang:error(badarg);
		  [] ->
		      if is_list(Ops) ->
			      lists:foreach(
				fun({insert, Data1}) ->
					ets:insert(T, Data1);
				   ({delete, Data1}) ->
					ets:delete(T, Data1)
				end, Ops);
			 true ->
			      erlang:error(badarg)
		      end;
		  [_|_] ->
		      %% V is not nil
		      true
	      end
      end, Actions),
    true.


var_pos(P) when is_integer(P), P >= 0, P =< 10 ->
    P;
var_pos(_) ->
    erlang:error(badarg).

get_var(P, Vars) ->
    case element(P, Vars) of
	undefined ->
	    erlang:error(badarg);
	L when is_list(L) ->
	    L
    end.

			      
				  
	    
	
