%%
%%  wings_undo.erl --
%%
%%     This module handles the undo stack.
%%
%%  Copyright (c) 2001-2004 Bjorn Gustavsson
%%
%%  See the file "license.terms" for information on usage and redistribution
%%  of this file, and for a DISCLAIMER OF ALL WARRANTIES.
%%
%%     $Id: wings_undo.erl,v 1.11 2004/01/01 14:17:21 bjorng Exp $
%%

-module(wings_undo).
-export([init/1,save/2,undo_toggle/1,undo/1,redo/1,info/1]).

-include("wings.hrl").

-import(lists, [reverse/1,reverse/2]).

%% The essential part of the state record.
-record(est,
	{shapes,
	 selmode,
	 sel,
	 onext,
	 mat
	}).

init(St) ->
    St#st{top=[],bottom=[],undone=[],next_is_undo=true}.
    
save(OldState, St0) ->
    St1 = discard_old_states(St0),
    St = push(St1, OldState),
    St#st{undone=[],next_is_undo=true}.

undo_toggle(#st{next_is_undo=true}=St) -> undo(St);
undo_toggle(St) -> redo(St).

undo(#st{undone=Undone}=St0) ->
    case pop(St0) of
	empty -> St0;
	St -> St#st{undone=[St0|Undone],next_is_undo=false}
    end.

redo(#st{undone=[StOld|Undone]}=St0) ->
    St1 = push(St0, St0),
    #st{shapes=Sh,selmode=Mode,sel=Sel,onext=Onext,mat=Mat} = StOld,
    St = St1#st{shapes=Sh,selmode=Mode,sel=Sel,onext=Onext,mat=Mat},
    St#st{undone=Undone,next_is_undo=true};
redo(St) -> St.

info(#st{top=Top,bottom=Bot,undone=Undone}) ->
    {length(Top)+length(Bot),length(Undone)}.

%%
%% Low-level queue operations.
%%

push(St, OldState) ->
    Est = save_essential(OldState),
    push_1(St, Est).

push_1(#st{top=[],bottom=[_|_]=Bottom}=St, #est{}=Est) ->
    push_1(St#st{top=reverse(Bottom),bottom=[]}, Est);
push_1(#st{top=[],bottom=[]}=St, #est{}=Est0) ->
    Est = compress(#est{shapes=[]}, Est0),
    St#st{top=[Est]};
push_1(#st{top=[PrevEst|PrevTop]=Top}=St, #est{sel=Sel}=Est0) ->
    Est = compress(PrevEst, Est0),
    case compare_states(PrevEst, Est) of
	new ->
	    St#st{top=[Est|Top]};
	new_sel ->
	    St#st{top=[PrevEst#est{sel=Sel}|PrevTop]}
    end.

save_essential(#st{shapes=Sh,selmode=Mode,sel=Sel,onext=Onext,mat=Mat}) ->
    #est{shapes=Sh,selmode=Mode,sel=Sel,onext=Onext,mat=Mat}.
    
compare_states(Old, New) ->
    #est{shapes=Osh,selmode=Omode,sel=Osel,onext=Oonext,mat=Omat} = Old,
    #est{shapes=Nsh,selmode=Nmode,sel=Nsel,onext=Nonext,mat=Nmat} = New,
    if
	Omode =/= Nmode -> new;
	Oonext =/= Nonext -> new;
	Omat =/= Nmat -> new;
	Osel =/= Nsel ->
	    if
		Osh =:= Nsh -> new_sel;
		true -> new
	    end;
	true -> new
    end.

compress(#est{shapes=OldShapes}, #est{shapes=NewShapes}=Est) ->
    Shapes = compress_1(OldShapes, gb_trees:values(NewShapes), []),
    Est#est{shapes=Shapes}.

compress_1([#we{id=OldId}|OldWes], [#we{id=NewId}|_]=NewWes, Acc) when OldId < NewId ->
    compress_1(OldWes, NewWes, Acc);
compress_1([#we{id=OldId}|OldWes], [#we{id=NewId}=NewWe|NewWes], Acc) when OldId > NewId ->
    We = NewWe#we{fs=undefined,vc=undefined},
    compress_1(OldWes, NewWes, [We|Acc]);
compress_1([#we{}=OldWe|OldWes], [#we{}=NewWe|NewWes], Acc) ->
    case NewWe#we{fs=undefined,vc=undefined} of
	OldWe -> compress_1(OldWes, NewWes, [OldWe|Acc]);
	We -> compress_1(OldWes, NewWes, [We|Acc])
    end;
compress_1(_, NewWes, Acc) ->
    reverse(Acc, NewWes).

pop(#st{top=[Est|Top]}=St0) ->
    #est{shapes=Sh0,selmode=Mode,sel=Sel,onext=Onext,mat=Mat} = Est,
    Sh = uncompress(Sh0, []),
    St = St0#st{shapes=Sh,selmode=Mode,sel=Sel,onext=Onext,mat=Mat},
    St#st{top=Top};
pop(#st{top=[],bottom=[_|_]=Bottom}=St) ->
    pop(St#st{top=reverse(Bottom),bottom=[]});
pop(_) -> empty.

discard_old_states(#st{top=Top,bottom=Bot}=St)
  when length(Top) + length(Bot) > ?UNDO_LEVELS ->
    discard_old_state(St);
discard_old_states(St) -> St.
    
discard_old_state(#st{bottom=[_|Bottom]}=St) ->
    St#st{bottom=Bottom};
discard_old_state(#st{bottom=[],top=[_|_]=Top}=St) ->
    discard_old_state(St#st{bottom=reverse(Top),top=[]}).

uncompress([#we{id=Id,next_id=Next}=We0|Wes], Acc) ->
    We = wings_we:rebuild(We0),
    uncompress(Wes, [{Id,We#we{next_id=Next}}|Acc]);
uncompress([], Acc) -> gb_trees:from_orddict(reverse(Acc)).
