/*
 * Copyright (C) 2013-2015 F(X)yz, 
 * Sean Phillips, Jason Pollastrini and Jose Pereda
 * All rights reserved.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package org.fxyz.extras;

import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.geometry.Bounds;
import javafx.geometry.Point3D;
import javafx.scene.Node;
import javafx.scene.shape.Shape3D;
import javafx.scene.transform.Affine;
import javafx.scene.transform.Transform;
import org.fxyz.geometry.Vector3D;

/**
 * Basic interface for Billboard Nodes. 
 * ie: Keeps this Node oriented towards specified Node.
 * 
 * @author jdub1581
 * @param <T> Type of node to be used for (this) "Billboard".
 */
public interface BillboardBehavior<T extends Node>{
    /**
     * Spherical means object will look at other on all axes
     * Cylindrical means object will rotate on Y axis only
     */
    public enum BillboardMode{
        SPHERICAL, 
        CYLINDRICAL;
    }
    
    
    static BillboardTimer timer = new BillboardTimer();
    /**
     * 
     * @return The node to be used for this behavior.
     */
    public T getBillboardNode();    
    /**
     * 
     * @return The node to look at.
     */
    public Node getOther();
      
    
    public Affine affine = new Affine();  
    /**
     *  Adds the Affine transform to Node and starts timer.
     */
    default void startBillboardBehavior(){
        if(timer.getUpdateList().isEmpty()){
            timer.addUpdate(() -> {
                updateMatrix();
                return null;
            });
        }
        getBillboardNode().getTransforms().addAll(affine);
        timer.start();
    }
    /**
     *  Stops timer and removes transform 
     */
    default void stopBillboardBehavior(){
        timer.stop();
        getBillboardNode().getTransforms().clear();
    }
    /**
     * Updates the transformation matrix.
     * can change the Translate for fixed distance  
     */
    default void updateMatrix(){
        Transform cam  = getOther().getLocalToSceneTransform(),
                  self = getBillboardNode().getLocalToSceneTransform();         
                
        Bounds b;
        double cX,
               cY,
               cZ;
        
        if(!(getBillboardNode() instanceof Shape3D)){
            b = getBillboardNode().getBoundsInLocal();
                cX = b.getWidth() / 2;
                cY = b.getHeight() / 2;
                cZ = b.getDepth() / 2;            
        }else{
            cX = self.getTx();
            cY = self.getTy();
            cZ = self.getTz();
        }       
        
        Point3D camPos = new Point3D(cam.getTx(), cam.getTy(), cam.getTz());
                Point3D selfPos = new Point3D(cX, cY, cZ);
                
        Vector3D up = Vector3D.UP,
        forward = new Vector3D(
                (selfPos.getX()) - camPos.getX(),
                (selfPos.getY()) - camPos.getY(),
                (selfPos.getZ()) - camPos.getZ()
        ).toNormal(),
        right = up.crossProduct(forward).toNormal();
        up = forward.crossProduct(right).toNormal();
                
        switch(getBillboardMode()){
            
            case SPHERICAL:                  
                affine.setMxx(right.x); affine.setMxy(up.x); affine.setMzx(forward.x); 
                affine.setMyx(right.y); affine.setMyy(up.y); affine.setMzy(forward.y); 
                affine.setMzx(right.z); affine.setMzy(up.z); affine.setMzz(forward.z);
        
                affine.setTx(cX * (1 - affine.getMxx()) - cY * affine.getMxy() - cZ * affine.getMxz());
                affine.setTy(cY * (1 - affine.getMyy()) - cX * affine.getMyx() - cZ * affine.getMyz());
                affine.setTz(cZ * (1 - affine.getMzz()) - cX * affine.getMzx() - cY * affine.getMzy());                
                break;
                
            case CYLINDRICAL:                
                affine.setMxx(right.x); affine.setMxy(0); affine.setMzx(forward.x); 
                affine.setMyx(0);       affine.setMyy(1); affine.setMzy(0); 
                affine.setMzx(right.z); affine.setMzy(0); affine.setMzz(forward.z);
                                
                affine.setTx(cX * (1 - affine.getMxx()) - cY * affine.getMxy() - cZ * affine.getMxz());
                affine.setTy(cY * (1 - affine.getMyy()) - cX * affine.getMyx() - cZ * affine.getMyz());
                affine.setTz(cZ * (1 - affine.getMzz()) - cX * affine.getMzx() - cY * affine.getMzy());
                break;
        }
        
    }
    
    ObjectProperty<BillboardMode> mode = new SimpleObjectProperty<>(BillboardMode.SPHERICAL);
    default BillboardMode getBillboardMode(){
        return mode.get();
    }
    default void setBillboardMode(BillboardMode m){
        mode.set(m);
    }
    default ObjectProperty<BillboardMode> getBillboardModeProperty(){
        return mode;
    }
}