package tim.prune.threedee;

import java.awt.event.InputEvent;

/*
 * Copyright (c) 2007 Sun Microsystems, Inc. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * - Redistribution of source code must retain the above copyright
 *   notice, this list of conditions and the following disclaimer.
 *
 * - Redistribution in binary form must reproduce the above copyright
 *   notice, this list of conditions and the following disclaimer in
 *   the documentation and/or other materials provided with the
 *   distribution.
 *
 * Neither the name of Sun Microsystems, Inc. or the names of
 * contributors may be used to endorse or promote products derived
 * from this software without specific prior written permission.
 *
 * This software is provided "AS IS," without a warranty of any
 * kind. ALL EXPRESS OR IMPLIED CONDITIONS, REPRESENTATIONS AND
 * WARRANTIES, INCLUDING ANY IMPLIED WARRANTY OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE OR NON-INFRINGEMENT, ARE HEREBY
 * EXCLUDED. SUN MICROSYSTEMS, INC. ("SUN") AND ITS LICENSORS SHALL
 * NOT BE LIABLE FOR ANY DAMAGES SUFFERED BY LICENSEE AS A RESULT OF
 * USING, MODIFYING OR DISTRIBUTING THIS SOFTWARE OR ITS
 * DERIVATIVES. IN NO EVENT WILL SUN OR ITS LICENSORS BE LIABLE FOR
 * ANY LOST REVENUE, PROFIT OR DATA, OR FOR DIRECT, INDIRECT, SPECIAL,
 * CONSEQUENTIAL, INCIDENTAL OR PUNITIVE DAMAGES, HOWEVER CAUSED AND
 * REGARDLESS OF THE THEORY OF LIABILITY, ARISING OUT OF THE USE OF OR
 * INABILITY TO USE THIS SOFTWARE, EVEN IF SUN HAS BEEN ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGES.
 *
 * You acknowledge that this software is not designed, licensed or
 * intended for use in the design, construction, operation or
 * maintenance of any nuclear facility.
 * 
 * Copyright (c) 2021 ActivityWorkshop simplifications and renamings,
 * and restriction to upright orientations.
 */

import java.awt.event.MouseEvent;
import java.awt.AWTEvent;

import javax.media.j3d.Transform3D;
import javax.media.j3d.Canvas3D;

import javax.vecmath.Vector3d;
import javax.vecmath.Point3d;
import javax.vecmath.Matrix3d;

import com.sun.j3d.utils.behaviors.vp.ViewPlatformAWTBehavior;
import com.sun.j3d.utils.universe.ViewingPlatform;


/**
 * Moves the View around a point of interest when the mouse is dragged with
 * a mouse button pressed.  Includes rotation, zoom, and translation
 * actions. Zooming can also be obtained by using mouse wheel.
 * <p>
 * The rotate action rotates the ViewPlatform around the point of interest
 * when the mouse is moved with the main mouse button pressed.  The
 * rotation is in the direction of the mouse movement, with a default
 * rotation of 0.01 radians for each pixel of mouse movement.
 * <p>
 * The zoom action moves the ViewPlatform closer to or further from the
 * point of interest when the mouse is moved with the middle mouse button
 * pressed (or Alt-main mouse button on systems without a middle mouse button).
 * The default zoom action is to translate the ViewPlatform 0.01 units for each
 * pixel of mouse movement.  Moving the mouse up moves the ViewPlatform closer,
 * moving the mouse down moves the ViewPlatform further away.
 * <p>
 * The translate action translates the ViewPlatform when the mouse is moved
 * with the right mouse button pressed.  The translation is in the direction
 * of the mouse movement, with a default translation of 0.01 units for each
 * pixel of mouse movement.
 * <p>
 * The actions can be reversed using the <code>REVERSE_</code><i>ACTION</i>
 * constructor flags.  The default action moves the ViewPlatform around the
 * objects in the scene.  The <code>REVERSE_</code><i>ACTION</i> flags can
 * make the objects in the scene appear to be moving in the direction
 * of the mouse movement.
 */
public class UprightOrbiter extends ViewPlatformAWTBehavior
{
	private final Transform3D _longitudeTransform = new Transform3D();
	private final Transform3D _latitudeTransform = new Transform3D();
	private final Transform3D _rotateTransform = new Transform3D();

	// needed for integrateTransforms but don't want to new every time
	private final Transform3D _temp1 = new Transform3D();
	private final Transform3D _temp2 = new Transform3D();
	private final Transform3D _translation = new Transform3D();
	private final Vector3d _transVector = new Vector3d();
	private final Vector3d _distanceVector = new Vector3d();
	private final Vector3d _centerVector = new Vector3d();
	private final Vector3d _invertCenterVector = new Vector3d();

	private double _deltaYaw = 0.0, _deltaPitch = 0.0;
	private double _startDistanceFromCenter = 20.0;
	private double _distanceFromCenter = 20.0;
	private final Point3d _rotationCenter = new Point3d();
	private final Matrix3d _rotMatrix = new Matrix3d();

	private int _mouseX = 0, _mouseY = 0;

	private double _xtrans = 0.0, _ytrans = 0.0, _ztrans = 0.0;

	private static final double MIN_RADIUS = 0.0;

	// the factor to be applied to wheel zooming so that it does not
	// look much different with mouse movement zooming.
	private static final float wheelZoomFactor = 50.0f;

	private static final double NOMINAL_ZOOM_FACTOR = .01;
	private static final double NOMINAL_ROT_FACTOR = .008;
	private static final double NOMINAL_TRANS_FACTOR = .003;

	private double _pitchAngle;


	/**
	 * Creates a new OrbitBehaviour
	 * @param inCanvas The Canvas3D to add the behaviour to
	 * @param inInitialPitch pitch angle in degrees
	 */
	public UprightOrbiter(Canvas3D inCanvas, double inInitialPitch)
	{
		super(inCanvas, MOUSE_LISTENER | MOUSE_MOTION_LISTENER | MOUSE_WHEEL_LISTENER );
		_pitchAngle = Math.toRadians(inInitialPitch);
	}

	protected synchronized void processAWTEvents( final AWTEvent[] events )
	{
		motion = false;
		for (AWTEvent event : events) {
			if (event instanceof MouseEvent) {
				processMouseEvent((MouseEvent) event);
			}
		}
	}

	protected void processMouseEvent( final MouseEvent evt )
	{
		if (evt.getID() == MouseEvent.MOUSE_PRESSED) {
			_mouseX = evt.getX();
			_mouseY = evt.getY();
			motion = true;
		}
		else if (evt.getID() == MouseEvent.MOUSE_DRAGGED)
		{
			int xchange = evt.getX() - _mouseX;
			int ychange = evt.getY() - _mouseY;
			// rotate
			if (isRotateEvent(evt))
			{
				_deltaYaw -= xchange * NOMINAL_ROT_FACTOR;
				_deltaPitch -= ychange * NOMINAL_ROT_FACTOR;
			}
			// translate
			else if (isTranslateEvent(evt))
			{
				_xtrans -= xchange * NOMINAL_TRANS_FACTOR;
				_ytrans += ychange * NOMINAL_TRANS_FACTOR;
			}
			// zoom
			else if (isZoomEvent(evt)) {
				doZoomOperations( ychange );
			}
			_mouseX = evt.getX();
			_mouseY = evt.getY();
			motion = true;
		}
		else if (evt.getID() == MouseEvent.MOUSE_WHEEL )
		{
			if (isZoomEvent(evt))
			{
				// if zooming is done through mouse wheel, the number of wheel increments
				// is multiplied by the wheelZoomFactor, to make zoom speed look natural
				if ( evt instanceof java.awt.event.MouseWheelEvent)
				{
					int zoom = ((int)(((java.awt.event.MouseWheelEvent)evt).getWheelRotation()
						* wheelZoomFactor));
					doZoomOperations( zoom );
					motion = true;
				}
			}
		}
	}

	/*
	 * zoom but stop at MIN_RADIUS
	 */
	private void doZoomOperations( int ychange )
	{
		if ((_distanceFromCenter - ychange * NOMINAL_ZOOM_FACTOR) > MIN_RADIUS) {
			_distanceFromCenter -= ychange * NOMINAL_ZOOM_FACTOR;
		}
		else {
			_distanceFromCenter = MIN_RADIUS;
		}
	}

	/**
	 * Sets the ViewingPlatform for this behaviour.  This method is
	 * called by the ViewingPlatform.
	 * If a sub-calls overrides this method, it must call
	 * super.setViewingPlatform(vp).
	 * NOTE: Applications should <i>not</i> call this method.
	 */
	@Override
	public void setViewingPlatform(ViewingPlatform vp)
	{
		super.setViewingPlatform( vp );

		if (vp != null) {
			resetView();
			integrateTransforms();
		}
	}

	/**
	 * Reset the orientation and distance of this behaviour to the current
	 * values in the ViewPlatform Transform Group
	 */
	private synchronized void resetView()
	{
		Vector3d centerToView = new Vector3d();

		targetTG.getTransform( targetTransform );

		targetTransform.get( _rotMatrix, _transVector );
		centerToView.sub( _transVector, _rotationCenter );
		_distanceFromCenter = centerToView.length();
		_startDistanceFromCenter = _distanceFromCenter;

		targetTransform.get( _rotMatrix );
		_rotateTransform.set( _rotMatrix );

		// compute the initial x/y/z offset
		_temp1.set(centerToView);
		_rotateTransform.invert();
		_rotateTransform.mul(_temp1);
		_rotateTransform.get(centerToView);
		_xtrans = centerToView.x;
		_ytrans = centerToView.y;
		_ztrans = centerToView.z;

		// reset rotMatrix
		_rotateTransform.set( _rotMatrix );
	}

	protected synchronized void integrateTransforms()
	{
		// Check if the transform has been changed by another behaviour
		Transform3D currentXfm = new Transform3D();
		targetTG.getTransform(currentXfm);
		if (! targetTransform.equals(currentXfm))
			resetView();

		// Three-step rotation process, firstly undo the pitch and apply the delta yaw
		_latitudeTransform.rotX(_pitchAngle);
		_rotateTransform.mul(_rotateTransform, _latitudeTransform);
		_longitudeTransform.rotY( _deltaYaw );
		_rotateTransform.mul(_rotateTransform, _longitudeTransform);
		// Now update pitch angle according to delta and apply
		_pitchAngle = Math.min(Math.max(0.0, _pitchAngle - _deltaPitch), Math.PI/2.0);
		_latitudeTransform.rotX(-_pitchAngle);
		_rotateTransform.mul(_rotateTransform, _latitudeTransform);

		_distanceVector.z = _distanceFromCenter - _startDistanceFromCenter;

		_temp1.set(_distanceVector);
		_temp1.mul(_rotateTransform, _temp1);

		// want to look at rotationCenter
		_transVector.x = _rotationCenter.x + _xtrans;
		_transVector.y = _rotationCenter.y + _ytrans;
		_transVector.z = _rotationCenter.z + _ztrans;

		_translation.set(_transVector);
		targetTransform.mul(_temp1, _translation);

		// handle rotationCenter
		_temp1.set(_centerVector);
		_temp1.mul(targetTransform);

		_invertCenterVector.x = -_centerVector.x;
		_invertCenterVector.y = -_centerVector.y;
		_invertCenterVector.z = -_centerVector.z;

		_temp2.set(_invertCenterVector);
		targetTransform.mul(_temp1, _temp2);

		Vector3d finalTranslation = new Vector3d();
		targetTransform.get(finalTranslation);

		targetTG.setTransform(targetTransform);

		// reset yaw and pitch deltas
		_deltaYaw = 0.0;
		_deltaPitch = 0.0;
	}

	private boolean isRotateEvent(MouseEvent evt)
	{
		final boolean isRightDrag = (evt.getModifiersEx() & InputEvent.BUTTON3_DOWN_MASK) > 0;
		return !evt.isAltDown() && !isRightDrag;
	}

	private boolean isZoomEvent(MouseEvent evt)
	{
		if (evt instanceof java.awt.event.MouseWheelEvent) {
			return true;
		}
		return evt.isAltDown() && !evt.isMetaDown();
	}

	private boolean isTranslateEvent(MouseEvent evt)
	{
		final boolean isRightDrag = (evt.getModifiersEx() & InputEvent.BUTTON3_DOWN_MASK) > 0;
		return !evt.isAltDown() && isRightDrag;
	}
}
