/*
 * Camera.java
 *
 * Created on September 25, 2000, 1:36 PM
 */

package ovt.object;

import ovt.*;
import ovt.gui.*;
import ovt.mag.*;
import ovt.util.*;
import ovt.event.*;
import ovt.beans.*;
import ovt.datatype.*;
import ovt.interfaces.*;
import ovt.object.editor.*;

import vtk.*;

import java.io.*;
import java.util.*;
import java.lang.*;
import java.beans.*;

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.event.*;

/**
 *
 * @author  ko
 * @version
 */
public class Camera extends BasicObject implements CameraChangeListener, TimeChangeListener, CoordinateSystemChangeListener {
    
    public static final int VIEW_CUSTOM = 0;
    public static final int VIEW_FROM_X = 1;
    public static final int VIEW_FROM_Y = 2;
    public static final int VIEW_FROM_Z = 3;
    public static final int VIEW_FROM_MINUS_X = 4;
    public static final int VIEW_FROM_MINUS_Y = 5;
    public static final int VIEW_FROM_MINUS_Z = 6;
    public static final int VIEW_PERPENDICULAR_TO_ORBIT = 7;
    protected CameraCustomizer customizer;
    public vtkCamera cam;
    
    protected ViewToObjects viewToObjects;
    
    /** Holds value of property customizerVisible. */
    private boolean customizerVisible;
    /** Holds value of property view. */
    protected int view;
    protected String[] viewNames           = {"Custom", "X", "Y", "Z", "-X", "-Y", "-Z"};
    protected String[] viewWithSatNames    = {"Custom", "X", "Y", "Z", "-X", "-Y", "-Z", "p. to Orbit"};
    
    static final double[][] views = {{0, 0, 0},
    { 1, 0, 0}, {0,  1, 0}, {0, 0,  0.99},
    {-1, 0, 0}, {0, -1, 0}, {0, 0, -0.99} };
    
    /** Holds value of property viewTo. */
    protected String viewTo = "Earth";
    
    protected ComboBoxPropertyEditor viewToEditor;
    protected ComboBoxPropertyEditor viewFromEditor;
    
    /** Creates new Camera */
    public Camera(OVTCore core) {
        super(core, "Camera");
        showInTree(false);
        Log.log("Camera :: init ...", 3);
        cam = getRenderer().GetActiveCamera();
        
        viewToObjects = new ViewToObjects(this);
        
        // set initial position
        try {
            setPosition(new double[]{10, 10, 10});
        } catch (PropertyVetoException ignore) {}
        
        Descriptors propertyDescriptors = new Descriptors();
        try {
        /* r property descriptor */
            BasicPropertyDescriptor pd = new BasicPropertyDescriptor("r", this);
            pd.setDisplayName("R");
            pd.setTextOnlyAccessible();
            TextFieldEditor editor = new DoubleEditor(pd, 2);
            // Render each time user changes time by means of gui
            editor.addGUIPropertyEditorListener(new GUIPropertyEditorListener() {
                public void editingFinished(GUIPropertyEditorEvent evt) {
                    Render();
                }
            });
            editor.setEditCompleteOnKey(true);
            pd.setPropertyEditor(editor);
            propertyDescriptors.put(pd);
            addPropertyChangeListener("position", editor);
            
        /* delta property descriptor */
            pd = new BasicPropertyDescriptor("delta", this);
            pd.setDisplayName("delta");
            pd.setTextOnlyAccessible();
            editor = new DoubleEditor(pd, 2);
            // Render each time user changes time by means of gui
            editor.addGUIPropertyEditorListener(new GUIPropertyEditorListener() {
                public void editingFinished(GUIPropertyEditorEvent evt) {
                    Render();
                }
            });
            editor.setEditCompleteOnKey(true);
            pd.setPropertyEditor(editor);
            propertyDescriptors.put(pd);
            addPropertyChangeListener("position", editor);
            
        /* phi property descriptor */
            pd = new BasicPropertyDescriptor("phi", this);
            pd.setDisplayName("Phi");
            pd.setTextOnlyAccessible();
            editor = new DoubleEditor(pd, 2);
            // Render each time user changes time by means of gui
            editor.addGUIPropertyEditorListener(new GUIPropertyEditorListener() {
                public void editingFinished(GUIPropertyEditorEvent evt) {
                    Render();
                }
            });
            editor.setEditCompleteOnKey(true);
            pd.setPropertyEditor(editor);
            propertyDescriptors.put(pd);
            addPropertyChangeListener("position", editor);
            
        /* viewFrom property descriptor */
            pd = new BasicPropertyDescriptor("viewFrom", this);
            pd.setDisplayName("View From:");
            viewFromEditor = new ComboBoxPropertyEditor(pd,  Utils.getIndexes(viewNames), viewNames);
            // Render each time user changes time by means of gui
            viewFromEditor.addGUIPropertyEditorListener(new GUIPropertyEditorListener() {
                public void editingFinished(GUIPropertyEditorEvent evt) {
                    Render();
                }
            });
            pd.setPropertyEditor(viewFromEditor);
            propertyDescriptors.put(pd);
            addPropertyChangeListener("viewFrom", viewFromEditor);
            
        /* viewTo property descriptor */
            pd = new BasicPropertyDescriptor("viewTo", this);
            pd.setDisplayName("View To:");
            String[] viewToObjectList = viewToObjects.getList();
            viewToEditor = new ComboBoxPropertyEditor(pd, viewToObjectList, viewToObjectList);
            // Render each time user changes time by means of gui
            viewToEditor.addGUIPropertyEditorListener(new GUIPropertyEditorListener() {
                public void editingFinished(GUIPropertyEditorEvent evt) {
                    Render();
                }
            });
            pd.setPropertyEditor(viewToEditor);
            propertyDescriptors.put(pd);
            addPropertyChangeListener("viewTo", viewToEditor);
            
            if (!OVTCore.isServer()) {
            /* customizerVisible property descriptor */
                pd = new BasicPropertyDescriptor("customizerVisible", this);
                pd.setDisplayName("View Controls");
                GUIPropertyEditor ed = new VisibilityEditor(pd);
                addPropertyChangeListener("customizerVisible", ed);
                pd.setPropertyEditor(ed);
                propertyDescriptors.put(pd);
            }
            
        } catch (IntrospectionException e2) {
            System.out.println(getClass().getName() + " -> " + e2.toString());
            System.exit(0);
        }
        setDescriptors(propertyDescriptors);
        
        if (!OVTCore.isServer()) {
            customizer = new CameraCustomizer(this, getCore().getXYZWin());
            // customizer will listen to changes of visibility, etc.
            addPropertyChangeListener(customizer);
            
            // listen to VisualizationPanel camera change
            VisualizationPanel vp =  getCore().getXYZWin().getVisualizationPanel();
            vp.addCameraChangeListener(this);
        }
    }
    
    /** Getter for property position.
     * @return Value of property position.
     */
    public double[] getPosition() {
        return cam.GetPosition();
    }
    
    /** Setter for property position.
     * @param position New value of property position.
     *
     * @throws PropertyVetoException
     */
    public void setPosition(double[] position) throws java.beans.PropertyVetoException {
        double[] oldPosition = getPosition();
        
        vetoableChangeSupport.fireVetoableChange("position", oldPosition, position);
        
        cam.SetPosition(position[0], position[1], position[2]);
        cam.ComputeViewPlaneNormal();
        resetClippingRange();
        
        // no more need to avoid the case of parallel view up and camera view direction vectors
        // it is avoided in setDelta()
        cam.SetViewUp(0, 0, 1);
        
        //cam.OrthogonalizeViewUp();
        //Render();
        firePropertyChange ("position", oldPosition, position);
    }
    
    /** Set the position related to focal point */
    public void setRelativePosition(double[] position) throws java.beans.PropertyVetoException {
        setPosition(Vect.add(getFocalPoint(), position));
    }
    
    /** Set the position related to focal point */
    public double[] getRelativePosition() {
        return Vect.sub(getPosition(), getFocalPoint());
    }
    
    /** Getter for camera focalpoint position.
     * @return Value of property position.
     */
    public double[] getFocalPoint() {
        return cam.GetFocalPoint();
    }
    
    /** Setter for camera focalpoint position.
     * @param position New value of property position.
     *
     * @throws PropertyVetoException
     */
    public void setFocalPoint(double[] position) throws java.beans.PropertyVetoException {
        double[] oldPosition = getFocalPoint();
        
        vetoableChangeSupport.fireVetoableChange("focalpoint", oldPosition, position);
        
        cam.SetFocalPoint(position[0], position[1], position[2]);
        cam.ComputeViewPlaneNormal();
        resetClippingRange();
        
        firePropertyChange ("focalpoint", oldPosition, position);
    }
    
    // its called by time-, CS-cahnge-listeners
    public void update() {
        double radius = Vect.absv(Vect.sub(getPosition(), getFocalPoint()));
        
        try {
            // update the Focal Point
            setFocalPoint(viewToObjects.getPosition(getViewTo()));
            
            // update camera position
            
            switch(getViewFrom()) {
                case VIEW_CUSTOM: 
                    break;
                case VIEW_FROM_X:
                case VIEW_FROM_Y:
                case VIEW_FROM_MINUS_X:
                case VIEW_FROM_MINUS_Y:
                    setRelativePosition(Vect.multiply(views[getViewFrom()], radius));
                    break;
                case VIEW_FROM_Z:
                    setDelta(90);
                    break;
                case VIEW_FROM_MINUS_Z:
                    setDelta(-90);
                    break;
                case VIEW_PERPENDICULAR_TO_ORBIT:
                    // we are in the sattelite following mode
                    // and look always perpendicular to orbit
                    Sat sat = (Sat)viewToObjects.getSat(getViewTo());
                    double[] n = normalToOrbit(sat);
                    
                    Trajectory tr = sat.getTrajectory();
                    double[] r = tr.get(getMjd()).get(getCS());
                    
                    // we can view at the orbit plane from 2 sides.
                    // determine from which side we are looking now and choose it.
                    // r_cam - vector starting at sat to current position
                    double[] r_cam = Vect.sub(getPosition(), r);
                    
                    // get Cos(alpha). - the angle between n and r_cam
                    // 0 < cos < 1  - camera is in n direction side (+)
                    // -1 < cos < 0 -  (-)
                    
                    double cos_alpha = Vect.cosAngle(r_cam, n);
                    int sign;
                    if (cos_alpha >= -1  &&  cos_alpha < 0) sign = -1;
                    else sign = 1;
                    
                    double[] cam_pos = Vect.add(r, Vect.multiply(n, radius * (double)sign));
                    
                    // camera position
                    setPosition(cam_pos);
                    break;
            }
        } catch (PropertyVetoException e) { System.out.println("WASSUP?? Unexpected "+e);}
    }

    /** computes normal vector to orbit in specyfied point r */
public double[] normalToOrbit(Sat sat) {
    // workaround extream conditions
    Trajectory tr = sat.getTrajectory();
    double[] r = tr.get(getMjd()).get(getCS());
    TimeSet timeSet = getCore().getTimeSettings().getTimeSet();
    
    int pos = timeSet.indexOf(getMjd());
    double[] timeMap = timeSet.getValues();
    
    double mjd_a, mjd_b;
    if (pos == 0) {
        mjd_a = timeSet.get(2);
        mjd_b = timeSet.get(1);
    } else if (pos == timeSet.getNumberOfValues() - 1) {
        mjd_a = timeSet.get(timeSet.getNumberOfValues() - 2);
        mjd_b = timeSet.get(timeSet.getNumberOfValues() - 3);
    } else {
        mjd_a = timeSet.get(pos - 1);
        mjd_b = timeSet.get(pos + 1);
    }
    
    double[] r_a = tr.get(mjd_a).get(getCS());;
    double[] r_b = tr.get(mjd_b).get(getCS());;
    
    double[] a = Vect.sub(r_a, r);
    double[] b = Vect.sub(r_b, r);
    
    // compute the normalized vector, orthogonal to a and b
    double[] n = Vect.crossn(a, b);
    
    return n;
}

public void reset() {
    getRenderer().ResetCamera();
    try {
        setViewFrom(VIEW_CUSTOM);
        setViewTo("Custom");
    } catch (PropertyVetoException ignore) {}
    firePropertyChange("position", null, null);
}

    /** Setter for property r.
     * @param r New value of property r.
     *
     * @throws PropertyVetoException
     */
public void setR(double r) throws java.beans.PropertyVetoException {
    double oldR = getR();
    if (r <= 1 && getViewTo().equals("Earth")) throw new PropertyVetoException("R <= 1", null);
    vetoableChangeSupport.fireVetoableChange("r", new Double (oldR), new Double (r));
    double[] r_delta_phi = Utils.rec2sph(getRelativePosition());
    r_delta_phi[0] = r;
    setRelativePosition(Utils.sph2rec(r_delta_phi));
    propertyChangeSupport.firePropertyChange ("r", new Double (oldR), new Double (r));
}
    /** Getter for property r.
     * @return Value of property r.
     */
public double getR() {
    return Utils.rec2sph(getRelativePosition())[0];
}

    /** Setter for property delta.
     * @param delta New value of property delta.
     *
     * @throws PropertyVetoException
     */
public void setDelta(double delta) throws java.beans.PropertyVetoException {
    if ((delta - 90) % 180 == 0) delta -= 0.001;    // avoid delta = 90
    double olddelta = getDelta();
    vetoableChangeSupport.fireVetoableChange("delta", new Double (olddelta), new Double (delta));
    double[] r_delta_phi = Utils.rec2sph(getRelativePosition());
    r_delta_phi[1] = delta;
    setRelativePosition(Utils.sph2rec(r_delta_phi));
    propertyChangeSupport.firePropertyChange ("delta", new Double (olddelta), new Double (delta));
}

    /** Getter for property delta.
     * @return Value of property delta.
     */
public double getDelta() {
    return Utils.rec2sph(getRelativePosition())[1];
}

    /** Setter for property phi.
     * @param phi New value of property phi.
     *
     * @throws PropertyVetoException
     */
public void setPhi(double phi) throws java.beans.PropertyVetoException {
    double oldPhi = getPhi();
    vetoableChangeSupport.fireVetoableChange("phi", new Double (oldPhi), new Double (phi));
    double[] r_delta_phi = Utils.rec2sph(getRelativePosition());
    r_delta_phi[2] = phi;
    setRelativePosition(Utils.sph2rec(r_delta_phi));
    propertyChangeSupport.firePropertyChange ("phi", new Double (oldPhi), new Double (phi));
}

    /** Getter for property phi.
     * @return Value of property phi.
     */
public double getPhi() {
    return Utils.rec2sph(getRelativePosition())[2];
}

public void coordinateSystemChanged(CoordinateSystemEvent evt) {
    update();
}

public void timeChanged(TimeEvent evt) {
    update();
}

    /** Setter for property viewUp.
     * @param viewUp New value of property viewUp.
     *
     * @throws PropertyVetoException
     */
public void setViewUp(double[] viewUp) throws java.beans.PropertyVetoException {
    double[] oldViewUp = getViewUp();
    vetoableChangeSupport.fireVetoableChange("viewUp", oldViewUp, viewUp);
    cam.SetViewUp(viewUp[0], viewUp[1], viewUp[2]);
    propertyChangeSupport.firePropertyChange ("viewUp", oldViewUp, viewUp);
}

    /** Getter for property viewUp.
     * @return Value of property viewUp.
     */
public double[] getViewUp() {
    return cam.GetViewUp();
}

    /** Setter for property customizerVisible.
     * @param customizerVisible New value of property customizerVisible.
     */
public void setCustomizerVisible(boolean customizerVisible) {
    boolean oldCustomizerVisible = this.customizerVisible;
    if (!OVTCore.isServer()) {
        customizer.setVisible(customizerVisible);
        this.customizerVisible = customizerVisible;
        propertyChangeSupport.firePropertyChange ("customizerVisible", new Boolean (oldCustomizerVisible), new Boolean (customizerVisible));
    }
}
    /** Getter for property customizerVisible.
     * @return Value of property customizerVisible.
     */
public boolean isCustomizerVisible() {
    return customizerVisible;
}

    /** Setter for property view.
     * @param view New value of property view.
     */
public void setViewFrom(int view) throws PropertyVetoException {
    // view == 0  means no special view - "Custom"
    int oldView = this.view;
    this.view = view;
    if (view != VIEW_CUSTOM) update();
    propertyChangeSupport.firePropertyChange ("viewFrom", new Integer (oldView), new Integer (view));
}
    /** Getter for property view.
     * @return Value of property view.
     */
public int getViewFrom() {
    return view;
}

    /** Set the view to <CODE>Earth</CODE>, <CODE>SatName</CODE>, or <CODE>Custom</CODE>
     * @param viewTo New value of property viewTo.
     *
     * @throws PropertyVetoException
     */
public void setViewTo(String viewTo) throws java.beans.PropertyVetoException {
    String oldViewTo = this.viewTo;
    //System.out.println("viewTo : " + viewTo);
    this.viewTo = viewTo;
    // change items in viewFromEditor if
    if (viewToObjects.isSat(oldViewTo)) {
        if (!viewToObjects.isSat(viewTo)) {
            // Sat -> Not Sat  =  remove "Orbit item"
            setViewFrom(VIEW_CUSTOM);
            viewFromEditor.setValues(Utils.getIndexes(viewNames));
            viewFromEditor.setTags(viewNames);
        }
    } else {
        if (viewToObjects.isSat(viewTo)) {
            // Not Sat -> Sat  =  add "Orbit item"
            viewFromEditor.setValues(Utils.getIndexes(viewWithSatNames));
            viewFromEditor.setTags(viewWithSatNames);
        }
    }
    
    if (viewToObjects.isGroundObject(viewTo)) {
        // if a ground object, view to this from up [  ).<---  ]
        PositionSource obj = (PositionSource) viewToObjects.visibleObjects.get(viewTo);
        double[] obj_pos = obj.getPosition();
        double cam_radius = Vect.absv(getPosition());
        if (cam_radius < 2) cam_radius = 2;
        double[] pos = Vect.multiply( obj_pos,  cam_radius / Vect.absv(obj_pos) );
        setPosition(pos);
    }
    
    update();
    propertyChangeSupport.firePropertyChange ("viewTo", oldViewTo, viewTo);
}
    /** Getter for property viewTo.
     * @return Value of property viewTo.
     */
public String getViewTo()
{ return viewTo; }

public void resetClippingRange() {
    //getRenderer().ResetCameraClippingRange(-1000,1000,-1000,1000,-1000,1000);
    //getRenderer().ResetCameraClippingRange();
    cam.SetClippingRange(0.01, 1000.01);
    //System.out.println("Reset camera clipping");
}

public void cameraChanged(CameraEvent evt) {
    resetClippingRange();
    
    firePropertyChange("position", null, null);
    
    if ((getViewFrom() >= VIEW_FROM_X) && (getViewFrom() <= VIEW_FROM_MINUS_Z)) {
        double[] view = views[getViewFrom()];
        double[] pos = getRelativePosition();
        for (int i=0; i<3; i++) {
            if (view[i] == 0) {
                try {
                    if (pos[i] != 0) setViewFrom(VIEW_CUSTOM);
                } catch(PropertyVetoException e2) {}
                break;
            }
        }
    }
    
    if ((getViewFrom() == VIEW_PERPENDICULAR_TO_ORBIT)) {
        Sat sat = (Sat)viewToObjects.getSat(getViewTo());
        double eps = 0.002;
        double[] n1 = normalToOrbit(sat);
        double[] n2 = Vect.multiply(n1, -1);
        double[] n = Vect.norm(Vect.sub(getPosition(), getFocalPoint()));
        if (!Vect.equal(n, n1, eps) && !Vect.equal(n, n2, eps)) {
            try {
                setViewFrom(VIEW_CUSTOM);
            } catch(PropertyVetoException e2) {}
        }
    }
    
    if (getViewTo() != "Custom") {
        double[] foc = getFocalPoint();
        double[] obj_pos = viewToObjects.getPosition(getViewTo());
        for (int i=0; i<3; i++) {
            if (obj_pos[i] != foc[i]) {
                try {
                    setViewTo("Custom");
                } catch(PropertyVetoException e2) {}
                break;
            }
        }
    }
}

public PropertyChangeListener getViewToListener() {
    return viewToObjects;
}
}


/* *******************************************************************/
/* *******************************************************************/
/* *******************************************************************/
class ViewToObjects implements PropertyChangeListener {
    protected Camera cam;
    protected Hashtable visibleObjects = new Hashtable();
    
    ViewToObjects(Camera cam) {
        this.cam = cam;
        
        // register myself as a listener to "visible" property of each
        // visualobject which implements PositionSource interface
        
        //System.out.println("Camera> Getting visual leafs");
        Enumeration e = cam.getCore().getVisualChildren().elements();
        while (e.hasMoreElements()) {
            VisualObject obj = (VisualObject) e.nextElement();
            if (obj instanceof PositionSource) {
                //System.out.println("Camera> listen to object: " + obj.getName());
                obj.addPropertyChangeListener(this);
                if (obj.isVisible()) visibleObjects.put(obj.getName(), obj);
            }
        }
        //System.out.println("Camera> Finished getting visual leafs");
        
    }
    
    public Sat getSat(String name) {
        return (Sat)visibleObjects.get(name);
    }
    
    public String[] getList() {
        int n = visibleObjects.size();
        String[] list = new String[n + 2];
        list[0] = "Custom";
        list[1] = "Earth";
        Enumeration e = visibleObjects.elements();
        int i=2;
        while(e.hasMoreElements())
            list[i++] = ((OVTObject)e.nextElement()).getName();
        return list;
    }
    
    public double[] getPosition(String object) throws IllegalArgumentException {
        if (object.equals("Earth")) return new double[]{0, 0, 0};
        else if (object.equals("Custom")) return cam.getFocalPoint();
        else {
            // an object is a Sat
            if (visibleObjects.containsKey(object)) {
                return ((PositionSource)visibleObjects.get(object)).getPosition();
            } else throw new IllegalArgumentException(object + " not found in watched objects list");
        }
    }
    
    public void propertyChange(PropertyChangeEvent evt) {
        //System.out.println("RECIEVED: " + evt.getPropertyName());
        if (evt.getPropertyName().equals("visible")) {
            
            VisualObject obj = (VisualObject)evt.getSource();
            boolean visible = ((Boolean)evt.getNewValue()).booleanValue();
            
            // if some sat comes visible - add it.
            if (visible) {
                //cb.removeActionListener(this);
                //System.out.println("Adding item -" + obj.getName());
                visibleObjects.put(obj.getName(), obj);
            }
            else {
                // if we remove sat from list we set watched object to earth
                if (cam.getViewTo().equals(obj.getName())) {
                    try {
                        cam.setViewTo("Earth");
                    } catch (PropertyVetoException e2) {}
                }
                visibleObjects.remove(obj.getName());
                
                // if no sats left, and user was watching the sat
                if (visibleObjects.size() == 0  &&  cam.getViewFrom() == cam.VIEW_PERPENDICULAR_TO_ORBIT) {
                    try {
                        cam.setViewFrom(cam.VIEW_CUSTOM);
                    } catch (PropertyVetoException ignore) {}
                }
            }
        }
        
        String[] list = getList();
        cam.viewToEditor.setValues(list);
        cam.viewToEditor.setTags(list);
    }
    
    public boolean isSat(String name) {
        if (visibleObjects.containsKey(name)) {
            if (visibleObjects.get(name) instanceof Sat) return true;
        }
        return false;
    }
    
    /* may be better chech if the object on the ground ... */
    public boolean isGroundObject(String name) {
        if (visibleObjects.containsKey(name)) {
            if (visibleObjects.get(name) instanceof GroundStation) return true;
        }
        return false;
    }
}

