%%
%%  wings_move.erl --
%%
%%     This module implements the Move command.
%%
%%  Copyright (c) 2001-2003 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_move.erl,v 1.48 2003/10/30 12:48:11 bjorng Exp $
%%
-module(wings_move).
-export([setup/2,setup_we/4,plus_minus/3,magnet_move_fun/3]).

-include("wings.hrl").
-import(lists, [map/2,foldr/3,foldl/3,sort/1]).

setup({'ASK',Ask}, St) ->
    wings:ask(Ask, St, fun setup/2);
setup({Vec,Magnet}, St) ->
    setup(Vec, Magnet, St);
setup(Vec, St) ->
    setup(Vec, none, St).

setup(Type, _Magnet, #st{selmode=body,sel=Sel}=St) ->
    Vec = wings_util:make_vector(Type),
    Fun = translate_fun(Vec),
    Ids = [{Id,Fun} || {Id,_} <- Sel],
    wings_drag:setup({matrix,Ids}, unit(Type), flags(Type), St);
setup(Vec0, Magnet, #st{selmode=Mode}=St) ->
    Vec = wings_util:make_vector(Vec0),
    Tvs = wings_sel:fold(
	    fun(Items, We, Acc) ->
		    Tv = setup_we(Mode, Vec, Items, We),
		    magnet_move(Tv, Vec, Magnet, We, Acc)
	    end, [], St),
    Flags = wings_magnet:flags(Magnet, flags(Vec0)),
    wings_drag:setup(Tvs, unit(Vec0, magnet_unit(Magnet)), Flags, St).

magnet_unit(none) -> [];
magnet_unit(_) -> [falloff].

plus_minus({'ASK',Ask}, Tvs, St0) ->
    wings:ask(Ask, St0, fun(Type, St) -> plus_minus(Type, Tvs, St) end);
plus_minus(Type, Tvs0, #st{selmode=Mode}=St) ->
    Vec = wings_util:make_vector(Type),
    Tvs = plus_minus_2(Mode, Vec, Tvs0, []),
    wings_drag:setup(Tvs, unit(Type, [falloff]), flags(Type), St).

plus_minus_2(Mode, Vec, [{Items,NewVs,Forbidden,We}|T], Acc0) ->
    Tv = setup_we(Mode, Vec, Items, We),
    Acc = plus_minus_3(Tv, NewVs, Forbidden, We, Acc0),
    plus_minus_2(Mode, Vec, T, Acc);
plus_minus_2(_Mode, _Vec, [], Acc) -> Acc.

plus_minus_3(Tv0, NewVs, Forbidden, #we{id=Id}=We, Acc) ->
    Affected0 = affected(Tv0),
    Vecs = move_vectors(NewVs, Forbidden, gb_sets:from_list(Affected0), We, []),
    Affected = [V || {V,_,_} <- Vecs],
    VsPos = move_away(1.0, Vecs),
    MoveAway = {Affected,move_away_fun(Vecs, VsPos)},
    [{Id,Tv0},{Id,MoveAway}|Acc].

setup_we(Mode, Vec, _, We) when ?IS_LIGHT(We) ->
    setup_we_1(Mode, Vec, wings_sel:get_all_items(Mode, We), We);
setup_we(Mode, Vec, Items, We) ->
    setup_we_1(Mode, Vec, Items, We).

setup_we_1(vertex, Vec, Items, We) ->
    vertices_to_vertices(gb_sets:to_list(Items), We, Vec);
setup_we_1(edge, Vec, Items, We) ->
    edges_to_vertices(Items, We, Vec);
setup_we_1(face, Vec, Items, We) ->
    faces_to_vertices(Items, We, Vec).

unit(Type) ->
    unit(Type, []).

unit(free, T) -> [dx,dy,dz|T];
unit(intrude, T) -> [{distance,{0.0,9.9E307}}|T];
unit(_, T) -> [distance|T].

flags(free) -> [screen_relative];
flags(_) -> [].

move_away_fun(Tv, VsPos0) ->
    fun(view_changed, _Acc) ->
	    move_away_fun(Tv, VsPos0);
       (new_falloff, Falloff) ->
	    VsPos = move_away(Falloff, Tv),
	    move_away_fun(Tv, VsPos);
       (_, Acc) -> VsPos0 ++ Acc
    end.

move_away(R0, Tv) ->
    R = R0-1.0,
    foldl(fun({V,Vec,{X,Y,Z}}, A) -> 
		  {Xt,Yt,Zt} = e3d_vec:mul(Vec, R),
		  Pos = wings_util:share(X+Xt, Y+Yt, Z+Zt),
		  [{V,Pos}|A]
	  end, [], Tv).
    
move_vectors([V|Vs], Forbidden, VsSet, #we{vp=Vtab}=We, Acc0) ->
    Acc = wings_vertex:fold(
	    fun(Edge, _, Rec, A) ->
		    OtherV = wings_vertex:other(V, Rec),
		    case gb_sets:is_member(OtherV, VsSet) andalso
			not gb_sets:is_member(Edge, Forbidden) of
			false -> A;
			true ->
			    Pa = gb_trees:get(OtherV, Vtab),
			    Pb = gb_trees:get(V, Vtab),
			    Vec = e3d_vec:sub(Pb, Pa),
			    [{V,Vec,Pb}|A]
		    end
	    end, Acc0, V, We),
    move_vectors(Vs, Forbidden, VsSet, We, Acc);
move_vectors([], _, _, _, Acc) -> Acc.

%%
%% Conversion of vertice selections to vertices. :-)
%% Not entirely pointless, as we'll need to add vectors for
%% the points (vertices).
%%

vertices_to_vertices(Vs, We, normal) -> vertex_normals(We, Vs);
vertices_to_vertices(Vs, We, Vec) -> make_tvs(Vs, Vec, We).

vertex_normals(We, Vs) ->
    foldl(fun(V, Acc) ->
		  Vec = wings_vertex:normal(V, We),
		  [{Vec,[V]}|Acc]
	  end, [], Vs).

%%
%% Conversion of edge selections to vertices.
%%

edges_to_vertices(Es, We, normal) ->
    #we{es=Etab,vp=Vtab} = We,
    Vs = foldl(fun(Edge, D0) ->
		       #edge{vs=Va,ve=Vb,lf=FaceL,rf=FaceR} =
			   gb_trees:get(Edge, Etab),
		       VaPos = gb_trees:get(Va, Vtab),
		       VbPos = gb_trees:get(Vb, Vtab),
		       EdgeDir = e3d_vec:norm(e3d_vec:sub(VbPos, VaPos)),
 		       NL = wings_face:normal(FaceL, We),
 		       NR = wings_face:normal(FaceR, We),
		       Normal = e3d_vec:norm(e3d_vec:add(NL, NR)),
		       [{Va,{Normal,VaPos,EdgeDir}},
			{Vb,{Normal,VbPos,e3d_vec:neg(EdgeDir)}}|D0]
	       end, [], gb_sets:to_list(Es)),
    average(Vs);
edges_to_vertices(Es, We, Vec) ->
    make_tvs(wings_edge:to_vertices(Es, We), Vec, We).

average(Vs) ->
    R = sofs:relation(Vs),
    F = sofs:relation_to_family(R),
    foldl(fun average/2, [], sofs:to_external(F)).

average({V,Info}, Acc) ->
    Normal = average_normals(Info),
    [{Normal,[V]}|Acc].

average_normals([{Normal,_,_}]) -> Normal;
average_normals([{Na,Orig,Da}|[{Nb,_,Db}|_]=T]) ->
    %% This code is probably obvious. :-)
    Oa = e3d_vec:add(Orig, Na),
    Ob = e3d_vec:add(Orig, Nb),
    Diff = e3d_vec:sub(Oa, Ob),
    A = e3d_vec:dot(Da, Da),
    B = -e3d_vec:dot(Da, Db),
    C = e3d_vec:dot(Db, Db),
    D = e3d_vec:dot(Da, Diff),
    Det = A*C-B*B,
    if
	Det*Det >= 1.0E-9*abs(A*B) ->
	    E = -e3d_vec:dot(Db, Diff),
	    S = (B*E-C*D)/Det,
	    NewPos = e3d_vec:add(Oa, e3d_vec:mul(Da, S)),
	    e3d_vec:sub(NewPos, Orig);
	true ->					%Parallel edges
	    average_normals(T)
    end.

%%
%% Conversion of face selections to vertices.
%%

faces_to_vertices(Faces, #we{vp=Vtab}=We, normal) ->
    Vs = foldl(fun(Face, Acc0) ->
		       Vs = wings_face:vertices_cw(Face, We),
		       face_normal(Vs, Vtab, Acc0)
	       end, [], gb_sets:to_list(Faces)),
    face_average(Vs, Vtab);
faces_to_vertices(Faces, We, Vec) ->
    make_tvs(wings_face:to_vertices(Faces, We), Vec, We).

face_normal(Vs, Vtab, Acc) ->
    Normal = wings_face:face_normal_cw(Vs, Vtab),
    foldl(fun(V, A) -> [{V,Normal}|A] end, Acc, Vs).

face_average(Vs, Vtab) ->
    R = sofs:relation(Vs),
    F = sofs:relation_to_family(R),
    foldl(fun({V,Ns0}, Acc) ->
		  Ns = filter_normals(Ns0),
		  N = face_average_normals(V, Ns, Vtab),
		  [{N,[V]}|Acc]
	  end, [], sofs:to_external(F)).

face_average_normals(_V, [Na], _Vtab) -> Na;
face_average_normals(_V, [Na,Nb], _Vtab) ->
    N = e3d_vec:norm(e3d_vec:add(Na, Nb)),
    case e3d_vec:dot(N, Na) of
	Dot when abs(Dot) < 1.0E-6 ->
	    e3d_vec:add(Na, Nb);
	Dot ->
	    e3d_vec:divide(N, Dot)
    end;
face_average_normals(V, [Na,Nb,Nc], Vtab) ->
    %% The caller assures that the normals are not co-linear.
    Vpos = wings_vertex:pos(V, Vtab),
    Nao = e3d_vec:add(Vpos, Na),
    Nbo = e3d_vec:add(Vpos, Nb),
    Nco = e3d_vec:add(Vpos, Nc),
    {A,D,G} = Na,
    {B,E,H} = Nb,
    {C,F,I} = Nc,
    J = e3d_vec:dot(Nao, Na),
    K = e3d_vec:dot(Nbo, Nb),
    L = e3d_vec:dot(Nco, Nc),

    %% Calculate intersection of three planes using Cramer's rule.
    if
	is_float(A), is_float(B), is_float(C),
	is_float(D), is_float(E), is_float(F),
	is_float(G), is_float(H), is_float(I),
	is_float(J), is_float(K), is_float(L) ->
	    EiMinusHf = E*I - H*F,
	    GFMinusDI = G*F - D*I,
	    DHMinusEG = D*H - E*G,
	    JCMinusAL = J*C - A*L,
	    AKMinusJB = A*K - J*B,
	    BLMinusKC = B*L - K*C,
	    case A*EiMinusHf + B*GFMinusDI + C*DHMinusEG of
		M when abs(M) < 0.0001 ->
		    %% Should not happen, but just in case...
		    face_average_normals(V, [Na,Nb], Vtab);
		M ->
		    X = (J*EiMinusHf + K*GFMinusDI + L*DHMinusEG)/M,
		    Y = (I*AKMinusJB + H*JCMinusAL + G*BLMinusKC)/M,
		    Z = -(F*AKMinusJB + E*JCMinusAL + D*BLMinusKC)/M,
		    e3d_vec:sub({X,Y,Z}, Vpos)
	    end
    end.

%% Filter out normals that are too close to each other.
filter_normals([_]=Ns) -> Ns;
filter_normals([_,_]=Ns) -> Ns;
filter_normals([Na|[_|_]=Ns]) ->
    %% Three or more normals. We want three normals that point
    %% in as different directions as possible, or just two normals
    %% if we can't find three different enough.
    Nb = largest_angle(Ns, Na),
    N1 = e3d_vec:cross(Na, Nb),
    case smallest_angle(Ns, N1) of
	{_Nc,Dot} when Dot < 0.01 ->
	    %% The third normal is not usable. It is too close to
	    %% the plane of the other two.
	    [Na,Nb];
	{Nc,_Dot} ->
	    %% The third normal is OK.
	    [Na,Nb,Nc]
    end.

%% Find the normal with the greatest angle from Na.
largest_angle([N|Ns], Na) ->
    Dot = abs(e3d_vec:dot(Na, N)),
    largest_angle(Ns, Na, Dot, N).
    
largest_angle([N|Ns], Na, OldDot, OldN) ->
    case abs(e3d_vec:dot(Na, N)) of
	Dot when Dot < OldDot ->
	    largest_angle(Ns, Na, Dot, N);
	_Dot ->
	    largest_angle(Ns, Na, OldDot, OldN)
    end;
largest_angle([], _Na, _Dot, N) -> N.

%% Find the normal with the smallest angle from Na.
smallest_angle([N|Ns], Na) ->
    Dot = abs(e3d_vec:dot(Na, N)),
    smallest_angle(Ns, Na, Dot, N).
    
smallest_angle([N|Ns], Na, OldDot, OldN) ->
    case abs(e3d_vec:dot(Na, N)) of
	Dot when Dot > OldDot ->
	    smallest_angle(Ns, Na, Dot, N);
	_Dot ->
	    smallest_angle(Ns, Na, OldDot, OldN)
    end;
smallest_angle([], _Na, Dot, N) -> {N,Dot}.

%%
%% Conversion of body selections (entire objects) to vertices.
%%

translate_fun(free) ->
    fun(Matrix0, [Dx,Dy,Dz]) ->
	    #view{azimuth=Az,elevation=El} = wings_view:current(),
	    M0 = e3d_mat:mul(Matrix0, e3d_mat:rotate(-Az, {0.0,1.0,0.0})),
	    M1 = e3d_mat:mul(M0, e3d_mat:rotate(-El, {1.0,0.0,0.0})),
	    {Xt,Yt,Zt} = e3d_mat:mul_point(M1, {Dx,Dy,-Dz}),
	    e3d_mat:translate(Xt, Yt, Zt)
    end;
translate_fun({Xt0,Yt0,Zt0}) ->
    fun(_Matrix0, [Dx]) when is_float(Dx) ->
	    Xt = Xt0*Dx,
	    Yt = Yt0*Dx,
	    Zt = Zt0*Dx,
	    e3d_mat:translate(Xt, Yt, Zt)
    end.

%%%
%%% Magnet move.
%%%

magnet_move(Tv, _Vec, none, #we{id=Id}, Acc) -> [{Id,Tv}|Acc];
magnet_move(Tv, Vec0, Magnet0, #we{id=Id}=We, Acc) ->
    Vs = affected(Tv),
    {VsInf,Magnet,Affected} = wings_magnet:setup(Magnet0, Vs, We),
    Vec = magnet_vec(Vec0, Affected, We),
    [{Id,{Affected,magnet_move_fun(Vec, VsInf, Magnet)}}|Acc].

magnet_vec(normal, Vs, We) ->
    VsVec = [{V,Vec} || {Vec,[V]} <- vertex_normals(We, Vs)],
    gb_trees:from_orddict(sort(VsVec));
magnet_vec(Vec, _, _) -> Vec.

magnet_move_fun({Xt0,Yt0,Zt0}=Vec, VsInf0, {_,R}=Magnet0) ->
    fun(new_falloff, Falloff) ->
	    VsInf = wings_magnet:recalc(Falloff, VsInf0, Magnet0),
	    magnet_move_fun(Vec, VsInf, Magnet0);
       (new_type, {Type,Falloff}) ->
	    Magnet = {Type,R},
	    VsInf = wings_magnet:recalc(Falloff, VsInf0, Magnet),
	    magnet_move_fun(Vec, VsInf, Magnet);
       ([Dx0|_], A0) ->
	    foldl(fun({V,{Px,Py,Pz},_,Inf}, A) ->
			  Dx = Dx0*Inf,
			  Xt = Xt0*Dx, Yt = Yt0*Dx, Zt = Zt0*Dx,
			  Pos = wings_util:share(Px+Xt, Py+Yt, Pz+Zt),
			  [{V,Pos}|A]
		  end, A0, VsInf0)
    end;
magnet_move_fun(free, VsInf0, {_,R}=Magnet0) ->
    fun(view_changed, We) ->
	    VsInf = wings_util:update_vpos(VsInf0, We),
	    magnet_move_fun(free, VsInf, Magnet0);
       (new_falloff, Falloff) ->
	    VsInf = wings_magnet:recalc(Falloff, VsInf0, Magnet0),
	    magnet_move_fun(free, VsInf, Magnet0);
       (new_type, {Type,Falloff}) ->
	    Magnet = {Type,R},
	    VsInf = wings_magnet:recalc(Falloff, VsInf0, Magnet),
	    magnet_move_fun(free, VsInf, Magnet);
       ([Dx,Dy,Dz|_], Acc) ->
	    M = view_matrix(),
	    foldl(
	      fun({V,{Px,Py,Pz},_,Inf}, A) ->
		      {Xt,Yt,Zt} = e3d_mat:mul_point(M, {Dx*Inf,Dy*Inf,-Dz*Inf}),
		      Pos = wings_util:share(Px+Xt, Py+Yt, Pz+Zt),
		      [{V,Pos}|A]
	      end, Acc, VsInf0)
    end;
magnet_move_fun(VsVec, VsInf0, {_,R}=Magnet0) ->
    %% Move each element along its own normal.
    fun(new_falloff, Falloff) ->
	    VsInf = wings_magnet:recalc(Falloff, VsInf0, Magnet0),
	    magnet_move_fun(VsVec, VsInf, Magnet0);
       (new_type, {Type,Falloff}) ->
	    Magnet = {Type,R},
	    VsInf = wings_magnet:recalc(Falloff, VsInf0, Magnet),
	    magnet_move_fun(VsVec, VsInf, Magnet);
       ([Dx0|_], A0) ->
	    foldl(fun({V,{Px,Py,Pz},_,Inf}, A) ->
			  {Xt0,Yt0,Zt0} = gb_trees:get(V, VsVec),
			  Dx = Dx0*Inf,
			  Xt = Xt0*Dx, Yt = Yt0*Dx, Zt = Zt0*Dx,
			  Pos = wings_util:share(Px+Xt, Py+Yt, Pz+Zt),
			  [{V,Pos}|A]
		  end, A0, VsInf0)
    end.

%%%
%%% Utilities.
%%%

affected([_|_]=Tv) -> lists:append([Vs || {_,Vs} <- Tv]);
affected({Vs,Fun}) when is_function(Fun) -> Vs.
    
make_tvs(Vs, free, We) ->
    VsPos = wings_util:add_vpos(Vs, We),
    {Vs,move_fun(VsPos, view_matrix())};
make_tvs(Vs, Vec, _We) -> [{Vec,Vs}].

move_fun(VsPos, ViewMatrix) ->
    fun(new_falloff, _) ->
	    move_fun(VsPos, ViewMatrix);
       (view_changed, NewWe) ->
	    move_fun(wings_util:update_vpos(VsPos, NewWe), view_matrix());
       ([Dx,Dy,Dz|_], Acc) ->
	    {Xt,Yt,Zt} = e3d_mat:mul_point(ViewMatrix, {Dx,Dy,-Dz}),
	    foldl(fun({V,{X,Y,Z}}, A) -> 
			  Pos = wings_util:share(X+Xt, Y+Yt, Z+Zt),
			  [{V,Pos}|A]
		  end, Acc, VsPos)
    end.

view_matrix() ->
    #view{azimuth=Az,elevation=El} = wings_view:current(),
    M = e3d_mat:rotate(-Az, {0.0,1.0,0.0}),
    e3d_mat:mul(M, e3d_mat:rotate(-El, {1.0,0.0,0.0})).
