/*
 * Open Source Physics software is free software as described near the bottom of this code file.
 *
 * For additional information and documentation on Open Source Physics please see:
 * <http://www.opensourcephysics.org/>
 */

package org.opensourcephysics.display3d.simple3d;
import org.opensourcephysics.controls.XML;
import org.opensourcephysics.controls.XMLControl;
import org.opensourcephysics.numerics.Quaternion;
import org.opensourcephysics.numerics.Transformation;
import org.opensourcephysics.numerics.VectorMath;

public class Camera implements org.opensourcephysics.display3d.core.Camera {
  static private final double ratioToScreen = 2.5, ratioToFocus = 2.0;
  static private final double[] vertical = {0, 0, 1};
  static final int CHANGE_ANY = 0;
  static final int CHANGE_MODE = 1;
  static final int CHANGE_POSITION = 2;
  static final int CHANGE_FOCUS = 3;
  static final int CHANGE_ROTATION = 4;
  static final int CHANGE_SCREEN = 5;
  static final int CHANGE_ANGLES = 6;
  // Configuration variables
  private int projectionMode = org.opensourcephysics.display3d.core.Camera.MODE_PERSPECTIVE;
  private double posX, posY, posZ;
  private double focusX, focusY, focusZ;
  private double distanceToScreen, rotationAngle = 0;
  private double alpha = 0.0, beta = 0.0;
  // Implementation variables
  private double distanceToFocus, panelMaxSizeConstant;
  double cosAlpha = 1, sinAlpha = 0, cosBeta = 1, sinBeta = 0;
  private double cosRot = 1, sinRot = 0;
  private double[] e1, e2, e3;
  private Projection projection = new Projection();
  private Quaternion rotation = new Quaternion(1, 0, 0, 0);

  /**
   * The DrawingPanel3D to which it belongs.
   * This is needed to report to it any change that implies a call to update()
   */
  private DrawingPanel3D panel;

  Camera(DrawingPanel3D aPanel) {
    this.panel = aPanel;
  }

  /**
   * Only for the use of the XMLLoader for DrawingPanel3D!
   * Sets the panel of this camera
   * @param aPanel DrawingPanel3D
   */
  void setPanel(DrawingPanel3D aPanel) {
    this.panel = aPanel;
  }

  // -----------------------------
  // Implementation of Camera
  // ----------------------------
  public void setProjectionMode(int mode) {
    projectionMode = mode;
    if(panel!=null) {
      panelMaxSizeConstant = panel.getMaximum3DSize()*0.01;
      panel.cameraChanged(CHANGE_MODE);
    }
  }

  final public int getProjectionMode() {
    return projectionMode;
  }

  public void reset() {
    double[] center = panel.getCenter();
    focusX = center[0];
    focusY = center[1];
    focusZ = center[2];
    panelMaxSizeConstant = panel.getMaximum3DSize();
    rotationAngle = 0;
    cosRot = 1;
    sinRot = 0;
    distanceToScreen = ratioToScreen*panelMaxSizeConstant;
    distanceToFocus = ratioToFocus*panelMaxSizeConstant;
    posX = center[0]+distanceToFocus;
    posY = center[1];
    posZ = center[2];
    alpha = 0;
    cosAlpha = 1;
    sinAlpha = 0;
    beta = 0;
    cosBeta = 1;
    sinBeta = 0;
    e1 = new double[] {-1, 0, 0};
    e2 = new double[] {0, 1, 0};
    e3 = new double[] {0, 0, 1};
    panelMaxSizeConstant *= 0.01;
    panel.cameraChanged(CHANGE_ANY);
  }

  public void setXYZ(double x, double y, double z) {
    posX = x;
    posY = y;
    posZ = z;
    updateCamera(CHANGE_POSITION);
  }

  public void setXYZ(double[] point) {
    setXYZ(point[0], point[1], point[2]);
  }

  final public double getX() {
    return posX;
  }

  final public double getY() {
    return posY;
  }

  final public double getZ() {
    return posZ;
  }

  public void setFocusXYZ(double x, double y, double z) {
    focusX = x;
    focusY = y;
    focusZ = z;
    updateCamera(CHANGE_FOCUS);
  }

  public void setFocusXYZ(double[] point) {
    setFocusXYZ(point[0], point[1], point[2]);
  }

  final public double getFocusX() {
    return focusX;
  }

  final public double getFocusY() {
    return focusY;
  }

  final public double getFocusZ() {
    return focusZ;
  }

  public void setRotation(double angle) {
    rotationAngle = angle;
    cosRot = Math.cos(rotationAngle/2);
    sinRot = Math.sin(rotationAngle/2);
    updateCamera(CHANGE_ROTATION);
  }

  final public double getRotation() {
    return rotationAngle;
  }

  public void setDistanceToScreen(double distance) {
    distanceToScreen = distance;
    if(panel!=null) {
      panel.cameraChanged(CHANGE_SCREEN);
    }
  }

  final public double getDistanceToScreen() {
    return distanceToScreen;
  }

  public void setAzimuth(double angle) {
    alpha = angle;
    cosAlpha = Math.cos(alpha);
    sinAlpha = Math.sin(alpha);
    updateCamera(CHANGE_ANGLES);
  }

  final public double getAzimuth() {
    return alpha;
  }

  public void setAltitude(double angle) {
    beta = angle;
    if(beta<-Math.PI/2) {
      beta = -Math.PI/2;
    } else if(beta>Math.PI/2) {
      beta = Math.PI/2;
    }
    cosBeta = Math.cos(beta);
    sinBeta = Math.sin(beta);
    updateCamera(CHANGE_ANGLES);
  }

  final public double getAltitude() {
    return beta;
  }

  public void setAzimuthAndAltitude(double azimuth, double altitude) {
    alpha = azimuth;
    beta = altitude;
    if(beta<-Math.PI/2) {
      beta = -Math.PI/2;
    } else if(beta>Math.PI/2) {
      beta = Math.PI/2;
    }
    cosAlpha = Math.cos(alpha);
    sinAlpha = Math.sin(alpha);
    cosBeta = Math.cos(beta);
    sinBeta = Math.sin(beta);
    updateCamera(CHANGE_ANGLES);
  }

  final public Transformation getTransformation() {
    return projection;
  }

  public void copyFrom(org.opensourcephysics.display3d.core.Camera camera) {
    projectionMode = camera.getProjectionMode();
    if(panel!=null) {
      panelMaxSizeConstant = panel.getMaximum3DSize()*0.01;
    }
    posX = camera.getX();
    posY = camera.getY();
    posZ = camera.getZ();
    focusX = camera.getFocusX();
    focusY = camera.getFocusY();
    focusZ = camera.getFocusZ();
    rotationAngle = camera.getRotation();
    cosRot = Math.cos(rotationAngle/2);
    sinRot = Math.sin(rotationAngle/2);
    distanceToScreen = camera.getDistanceToScreen();
    updateCamera(CHANGE_ANY);
  }

  // -------------------------------------
  // Private methods
  // -------------------------------------
  private void updateCamera(int change) {
    switch(change) {
       case CHANGE_POSITION :
       case CHANGE_FOCUS :
         distanceToFocus = computeCameraVectors();
         alpha = Math.atan2(-e1[1], -e1[0]);
         beta = Math.atan2(-e1[2], Math.abs(e1[0]));
         cosAlpha = Math.cos(alpha);
         sinAlpha = Math.sin(alpha);
         cosBeta = Math.cos(beta);
         sinBeta = Math.sin(beta);
         break;
       case CHANGE_ROTATION :
         computeCameraVectors(); // e2 and e3 are different (rotated)
         break;
       case CHANGE_ANGLES :
         posX = focusX+distanceToFocus*cosBeta*cosAlpha;
         posY = focusY+distanceToFocus*cosBeta*sinAlpha;
         posZ = focusZ+distanceToFocus*sinBeta;
         computeCameraVectors();
         break;
       case CHANGE_ANY :
         distanceToFocus = computeCameraVectors();
         alpha = Math.atan2(-e1[1], -e1[0]);
         beta = Math.atan2(-e1[2], Math.abs(e1[0]));
         cosAlpha = Math.cos(alpha);
         sinAlpha = Math.sin(alpha);
         cosBeta = Math.cos(beta);
         sinBeta = Math.sin(beta);
         computeCameraVectors(); // e2 and e3 are different (rotated)
         break;
    }
    if(panel!=null) {
      panel.cameraChanged(change);
    }
  }

  private double computeCameraVectors() {
    e1 = new double[] {focusX-posX, focusY-posY, focusZ-posZ};
    double magnitudeE1 = VectorMath.magnitude(e1);
    for(int i = 0; i<e1.length; i++) {
      e1[i] /= magnitudeE1;
    }
    e2 = VectorMath.cross3D(e1, vertical);
    double magnitude = VectorMath.magnitude(e2);
    for(int i = 0; i<e2.length; i++) {
      e2[i] /= magnitude;
    }
    e3 = VectorMath.cross3D(e2, e1);
    magnitude = VectorMath.magnitude(e3);
    for(int i = 0; i<e3.length; i++) {
      e3[i] /= magnitude;
    }
    // Finally apply the rotation
    rotation.setCoordinates(cosRot, e1[0]*sinRot, e1[1]*sinRot, e1[2]*sinRot);
    rotation.direct(e2);
    rotation.direct(e3);
    return magnitudeE1;
  }

  // -------------------------------------
  // Projection methods
  // -------------------------------------

  /**
   * Whether the projection mode is three-dimensional
   * @return boolean
   */
  boolean is3dMode() {
    switch(projectionMode) {
       case MODE_PLANAR_XY :
       case MODE_PLANAR_XZ :
       case MODE_PLANAR_YZ :
         return false;
       default :
         return true;
    }
  }

  /**
   * Computes the projection of a size at a given point.
   * For internal use of DrawingPanel3D only
   */
  double[] projectSize(double[] p, double[] size, double[] pixelSize) {
    switch(projectionMode) {
       case MODE_PLANAR_XY :
         pixelSize[0] = size[0];
         pixelSize[1] = size[1];
         return pixelSize;
       case MODE_PLANAR_XZ :
         pixelSize[0] = size[0];
         pixelSize[1] = size[2];
         return pixelSize;
       case MODE_PLANAR_YZ :
         pixelSize[0] = size[1];
         pixelSize[1] = size[2];
         return pixelSize;
       case MODE_NO_PERSPECTIVE :
       case MODE_PERSPECTIVE_OFF :
         pixelSize[0] = Math.max(size[0], size[1]);
         pixelSize[1] = size[2];
         return pixelSize;
       default :
       case MODE_PERSPECTIVE :
       case MODE_PERSPECTIVE_ON :
         double factor = (p[0]-posX)*e1[0]+(p[1]-posY)*e1[1]+(p[2]-posZ)*e1[2];
         if(Math.abs(factor)<panelMaxSizeConstant) {
           factor = panelMaxSizeConstant; // Avoid division by zero
         }
         factor = distanceToScreen/factor;
         pixelSize[0] = Math.max(size[0], size[1])*factor;
         pixelSize[1] = size[2]*factor;
         return pixelSize;
    }
  }

  private class Projection implements org.opensourcephysics.numerics.Transformation {
    public Object clone() {
      try {
        return super.clone();
      } catch(CloneNotSupportedException exc) {
        exc.printStackTrace();
        return null;
      }
    }

    public double[] direct(double[] p) {
      switch(projectionMode) {
         case MODE_PLANAR_XY :
           p[0] = p[0]-focusX;
           p[1] = p[1]-focusY;
           p[2] = 1.0-(p[2]-focusZ)/distanceToFocus;
           return p;
         case MODE_PLANAR_XZ : {
           double aux = p[1];
           p[0] = p[0]-focusX;
           p[1] = p[2]-focusZ;
           p[2] = 1.0-(aux-focusY)/distanceToFocus;
           return p;
         }
         case MODE_PLANAR_YZ : {
           double aux = p[0];
           p[0] = p[1]-focusY;
           p[1] = p[2]-focusZ;
           p[2] = 1.0-(aux-focusX)/distanceToFocus;
           return p;
         }
         case MODE_NO_PERSPECTIVE :
         case MODE_PERSPECTIVE_OFF : {
           p[0] -= posX;
           p[1] -= posY;
           p[2] -= posZ;
           double aux1 = VectorMath.dot(p, e1);
           double aux2 = VectorMath.dot(p, e2);
           p[1] = VectorMath.dot(p, e3);
           p[0] = aux2;
           p[2] = aux1/distanceToFocus;
           return p;
         }
         default :
         case MODE_PERSPECTIVE :
         case MODE_PERSPECTIVE_ON : {
           p[0] -= posX;
           p[1] -= posY;
           p[2] -= posZ;
           double factor = VectorMath.dot(p, e1), aux1 = factor;
           if(Math.abs(factor)<panelMaxSizeConstant) {
             factor = panelMaxSizeConstant; // Avoid division by zero
           }
           // if (aux1<0) aux1 = Double.NaN;
           factor = distanceToScreen/factor;
           double aux2 = VectorMath.dot(p, e2)*factor;
           p[1] = VectorMath.dot(p, e3)*factor;
           p[0] = aux2;
           p[2] = aux1/distanceToFocus;
           return p;
         }
      }
    }

    public double[] inverse(double[] point) throws UnsupportedOperationException {
      throw new UnsupportedOperationException();
    }

  }

  // ----------------------------------------------------
  // XML loader
  // ----------------------------------------------------
  public static XML.ObjectLoader getLoader() {
    return new CameraLoader();
  }

  protected static class CameraLoader extends org.opensourcephysics.display3d.core.Camera.Loader {
    public Object createObject(XMLControl control) {
      return new Camera((DrawingPanel3D) null);
    }

  }

}

/*
 * Open Source Physics software is free software; you can redistribute
 * it and/or modify it under the terms of the GNU General Public License (GPL) as
 * published by the Free Software Foundation; either version 2 of the License,
 * or(at your option) any later version.

 * Code that uses any portion of the code in the org.opensourcephysics package
 * or any subpackage (subdirectory) of this package must must also be be released
 * under the GNU GPL license.
 *
 * This software 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; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston MA 02111-1307 USA
 * or view the license online at http://www.gnu.org/copyleft/gpl.html
 *
 * Copyright (c) 2007  The Open Source Physics project
 *                     http://www.opensourcephysics.org
 */
