%%
%%  wings_dl.erl --
%%
%%     Manage display lists for objects in Geometry and AutoUV windows
%%     (providing "garbage collection" of display lists).
%%
%%  Copyright (c) 2001-2011 Bjorn Gustavsson
%%
%%  See the file "license.terms" for information on usage and redistribution
%%  of this file, and for a DISCLAIMER OF ALL WARRANTIES.
%%
%%     $Id$
%%

-module(wings_dl).

-export([init/0,delete_dlists/0,
	 update/2,map/2,fold/2,changed_materials/1,
	 display_lists/0,
	 call/1,mirror_matrix/1]).

%%% This module manages display lists for all objects in a Geometry
%%% or AutoUV window.
%%%
%%% The major data structure of this module is a list of #dlo{}
%%% records. There is one #dlo{} record for each visible object
%%% (hidden objects have no #dlo{} record in the list). The #dlo{}
%%% records holds several different type of display lists for
%%% the corresponding object (e.g. a work display list for faces
%%% in smooth mode), and also other needed information (e.g. the
%%% #we{} record for the object).
%%%
%%% The update/2 and map/2 functions are used for updating #dlo{}
%%% records. The update/2 function supports deletion of records
%%% (for objects that has been deleted or hidden) and addition of
%%% new records (for newly added objects), while the map/2 function
%%% only allows modification of already existing #dlo{} records.
%%%
%%% Both functions automatically deletes any display lists that
%%% are no longer referenced by any #dlo{} record ("garbage collection").
%%%
%%% The fold/2 function can be used for folding over all #dlo{}
%%% records to collect information. The callback fun called by fold/2
%%% is not allowed to do any changes to any #dlo{} entries.

-define(NEED_OPENGL, 1).
-include("wings.hrl").
-import(lists, [reverse/1,foreach/2,foldl/3]).

-record(du,
	{dl=[],					%Display list records.
	 mat=gb_trees:empty(),			%Materials.
	 used=[]				%Display lists in use.
	 }).

init() ->
    Dl = case get_dl_data() of
	     undefined -> [];
	     #du{dl=Dl0,used=Used} ->
		 ?CHECK_ERROR(),
		 foreach(fun(DL) -> gl:deleteLists(DL, 1) end, Used),
		 gl:getError(),			%Clear error.
		 clear_old_dl(Dl0)
	 end,
    put_dl_data(#du{dl=Dl}).

display_lists() ->
    #du{dl=Dls} = get_dl_data(),
    Dls.

%% changed_materials(St)
%%  Get a list of all materials that were changed since the last time
%%  the display lists were updated. (The list does not include materials
%%  that were deleted or added.)

changed_materials(#st{mat=NewMat}) ->
    case get_dl_data() of
	#du{mat=NewMat} -> [];
	#du{mat=OldMat}=Du ->
	    put_dl_data(Du#du{mat=NewMat}),
	    changed_materials_1(gb_trees:to_list(OldMat), NewMat, [])
    end.

%% update(CallbackFun, Data0)
%%  Update existing #dlo{} records and/or delete or add new #dlo{}
%%  records. Data0 is any Erlang term. The callback Fun will be
%%  called like this
%%      CallbackFun(#dlo{}, DataN)
%%  for each #dlo{} entry in the list kept by this module.
%%
%%  Allowed return values are:
%%    #dlo{}          Updated #dlo{} record; Data not updated.
%%    {#dlo{},Data}   Update #dlo{} record and Data.
%%    deleted         The object has been deleted; Data not updated.
%%    {deleted,Data}  The object has been deleted; new value of Data.
%%
%%  When the callback fun has been called once for each #dlo{} record,
%%  it will be called at least once more like this:
%%
%%    CallbackFun(eol, DataN)
%%
%%  Allowed return values are:
%%  #dlo{}          Add a #dlo{} record for a new object
%%  {#dlo{},Data}   Add a #dlo{} record for a new object; updated Data.
%%  eol             Finished (the callback fun will not be called again).

update(Fun, Data) ->
    #du{dl=Dlists} = get_dl_data(),
    update_1(Fun, Dlists, Data, [], []).

%% map(CallbackFun, Data0)
%%  Map over existing #dlo{} records and update them.
%%  Data0 is any Erlang term. The callback Fun will be
%%  called like this
%%      CallbackFun(#dlo{}, DataN)
%%  for each #dlo{} entry in the list kept by this module.
%%
%%  Allowed return values are:
%%    #dlo{}          Updated #dlo{} record; Data not updated.
%%    {#dlo{},Data}   Update #dlo{} record and Data.

map(Fun, Data) ->
    case get_dl_data() of
	undefined -> ok;
	#du{dl=Dlists} ->
	    map_1(Fun, Dlists, Data, [], [])
    end.

%% fold(CallbackFun, Acc0)
%%  Fold over the list of #dlo{} records. The callback will
%%  be called like this
%%    CallbackFun(#dlo{}, AccN)
%%  for each #dlo{} record. The callback fun must return
%%  the updated Acc value.

fold(Fun, Acc) ->
    #du{dl=Dlists} = get_dl_data(),
    foldl(Fun, Acc, Dlists).

%% call(DisplayListTerm)
%%  Call the OpenGL display list using gl:callList/1 for
%%  the display lists embedded in the display list term.

call(none) -> none;
call({call,Dl,_}) -> call(Dl);
call({call_in_this_win,Win,Dl}) ->
    case wings_wm:this() of
	Win -> call(Dl);
	_ -> ok
    end;
call([H|T]) -> call(H), call(T);
call([]) -> ok;
call(Dl) when is_integer(Dl) -> gl:callList(Dl).

%% mirror_matrix(Id)
%%  Return the mirror matrix for the object having id Id.

mirror_matrix(Id) -> fold(fun mirror_matrix/2, Id).

mirror_matrix(#dlo{mirror=Matrix,src_we=#we{id=Id}}, Id) -> Matrix;
mirror_matrix(_, Acc) -> Acc.

%%%
%%% Local functions.
%%%

delete_dlists() ->
    case erase(wings_wm:get_prop(display_lists)) of
	#du{used=Used} ->
	    foreach(fun(DL) -> gl:deleteLists(DL, 1) end, Used),
	    gl:getError();			%Clear error.
	_ ->
	    ok
    end.

clear_old_dl([#dlo{src_we=We,proxy_data=Pd0,ns=Ns}|T]) ->
    Pd = wings_proxy:invalidate(Pd0, dl),
    [#dlo{src_we=We,mirror=none,proxy_data=Pd,ns=Ns}|clear_old_dl(T)];
clear_old_dl([]) -> [].

get_dl_data() ->
    case wings_wm:lookup_prop(display_lists) of
	none -> undefined;
	{value,DlName} -> get(DlName)
    end.

put_dl_data(Data) ->
    put(wings_wm:get_prop(display_lists), Data).

update_1(Fun, [D0|Dlists], Data0, Seen0, Acc) ->
    case Fun(D0, Data0) of
	#dlo{}=D ->
	    Seen = update_seen(D, Seen0),
	    update_1(Fun, Dlists, Data0, Seen, [D|Acc]);
	deleted ->
	    update_1(Fun, Dlists, Data0, Seen0, Acc);
	{deleted,Data} ->
	    update_1(Fun, Dlists, Data, Seen0, Acc);
	{D,Data} ->
	    Seen = update_seen(D, Seen0),
	    update_1(Fun, Dlists, Data, Seen, [D|Acc])
    end;
update_1(Fun, [], Data0, Seen0, Acc) ->
    case Fun(eol, Data0) of
	{D,Data} ->
	    Seen = update_seen(D, Seen0),
	    update_1(Fun, [], Data, Seen, [D|Acc]);
	eol ->
	    update_last(Data0, Seen0, Acc)
    end.


changed_materials_1([{Name,Val}|T], New, Acc) ->
    case gb_trees:lookup(Name, New) of
	none -> changed_materials_1(T, New, Acc);
	{value,Val} -> changed_materials_1(T, New, Acc);
	{value,_} -> changed_materials_1(T, New, [Name|Acc])
    end;
changed_materials_1([], _, Acc) -> Acc.


map_1(Fun, [D0|Dlists], Data0, Seen0, Acc) ->
    case Fun(D0, Data0) of
	#dlo{}=D ->
	    Seen = update_seen(D, Seen0),
	    map_1(Fun, Dlists, Data0, Seen, [D|Acc]);
	{D,Data} ->
	    Seen = update_seen(D, Seen0),
	    map_1(Fun, Dlists, Data, Seen, [D|Acc])
    end;
map_1(_Fun, [], Data, Seen, Acc) ->
    update_last(Data, Seen, Acc).

update_last(Data, Seen, Acc) ->
    #du{used=Used0} = Du = get_dl_data(),
    Used = ordsets:from_list(Seen),
    put_dl_data(Du#du{used=Used,dl=reverse(Acc)}),
    NotUsed = ordsets:subtract(Used0, Used),
    delete_lists(NotUsed),
    Data.

delete_lists([]) -> ok;
delete_lists([D1,D2|Dls]) when D1+1 =:= D2 ->
    gl:deleteLists(D1, 2),
    delete_lists(Dls);
delete_lists([Dl|Dls]) ->
    gl:deleteLists(Dl, 1),
    delete_lists(Dls).
    
update_seen(D, Seen) ->
    update_seen_0(tuple_size(D), D, Seen).

update_seen_0(0, _, Seen) -> Seen;
update_seen_0(I, D, Seen0) ->
    Seen = update_seen_1(element(I, D), Seen0),
    update_seen_0(I-1, D, Seen).
    
update_seen_1([H|T], Seen) ->
    update_seen_1(T, update_seen_1(H, Seen));
update_seen_1([], Seen) -> Seen;
update_seen_1(none, Seen) -> Seen;
update_seen_1({call,Dl1,Dl2}, Seen) ->
    update_seen_1(Dl1, update_seen_1(Dl2, Seen));
update_seen_1({matrix,_,Dl}, Seen) ->
    update_seen_1(Dl, Seen);
update_seen_1(Dl, Seen) when is_integer(Dl) ->
    [Dl|Seen];
update_seen_1(Dl, Seen) when is_tuple(Dl), element(1, Dl) =:= sp ->
    %% Proxy DL's
    update_seen(Dl, Seen);
update_seen_1(_, Seen) -> Seen.
