/* Copyright (C) 1999-2004 by Peter Eastman

   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 2 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. */

package artofillusion.raytracer;

import artofillusion.*;
import artofillusion.material.*;
import artofillusion.math.*;
import artofillusion.object.*;
import artofillusion.texture.*;

/** RTSphere represents a sphere to be raytraced.  It is defined by specifying a Sphere 
    object, and the transformations to and from local coordinates.  It must be a true 
    sphere, not an ellipsoid.  That is, all of its radii must be equal. */

public class RTSphere extends RTObject
{
  Sphere theSphere;
  Vec3 v, v2, localv, trueNorm;
  double r, r2, cx, cy, cz, t, t2, time, param[];
  int intersections;
  boolean bumpMapped, normValid;
  Mat4 toLocal, fromLocal;
  
  public static final double TOL = 1e-12;

  public RTSphere(Sphere sphere, Mat4 fromLocal, Mat4 toLocal, double time, double param[])
  {
    theSphere = sphere;
    this.time = time;
    this.param = param;
    v = new Vec3();
    v2 = new Vec3();
    trueNorm = new Vec3();
    if (!(sphere.getTextureMapping() instanceof UniformMapping))
      localv = new Vec3();
    cx = fromLocal.m14/fromLocal.m44;
    cy = fromLocal.m24/fromLocal.m44;
    cz = fromLocal.m34/fromLocal.m44;
    r = sphere.getRadii().x;
    r2 = r*r;
    bumpMapped = sphere.getTexture().hasComponent(Texture.BUMP_COMPONENT);
    if (bumpMapped)
      this.fromLocal = fromLocal;
    this.toLocal = toLocal;
    lastRay = -1;
  }

  /** Get the TextureMapping for this object. */
  
  public final TextureMapping getTextureMapping()
  {
    return theSphere.getTextureMapping();
  }

  /** Get the MaterialMapping for this object. */
  
  public final MaterialMapping getMaterialMapping()
  {
    return theSphere.getMaterialMapping();
  }  

  /** Determine whether the given ray intersects this sphere. */

  protected boolean checkIntersection(Ray r)
  {
    Vec3 orig = r.getOrigin(), dir = r.getDirection();
    double b, c, d, root;

    lastRay = r.getID();
    normValid = false;
    v.set(cx-orig.x, cy-orig.y, cz-orig.z);
    b = dir.x*v.x + dir.y*v.y + dir.z*v.z;
    c = v.x*v.x + v.y*v.y + v.z*v.z - r2;
    if (c > TOL)
      {
	// Ray origin is outside sphere.
	
	if (b <= 0.0)
	  return (lastRayResult = false);  // Ray points away from center of sphere.
	d = b*b - c;
	if (d < 0.0)
	  return (lastRayResult = false);
	intersections = 2;
	root = Math.sqrt(d);
	t = b - root;
	t2 = b + root;
	v2.set(orig.x+t2*dir.x, orig.y+t2*dir.y, orig.z+t2*dir.z);
        projectPoint(v2);
      }
    else if (c < -TOL)
      {
	// Ray origin is inside sphere.
	
	d = b*b - c;
	if (d < 0.0)
	  return (lastRayResult = false);
	intersections = 1;
	t = b + Math.sqrt(d);
      }
    else
      {
	// Ray origin is on the surface of the sphere.
	
	if (b <= 0.0)
	  return (lastRayResult = false);  // Ray points away from center of sphere.
	d = b*b - c;
	if (d < 0.0)
	  return (lastRayResult = false);
	intersections = 1;
	t = b + Math.sqrt(d);
      }
    v.set(orig.x+t*dir.x, orig.y+t*dir.y, orig.z+t*dir.z);
    projectPoint(v);
    return (lastRayResult = true);
  }
  
  /** Given a point, project it onto the surface of the sphere.  This is necessary to
      prevent roundoff error. */
  
  private void projectPoint(Vec3 pos)
  {
    double dx = pos.x-cx, dy = pos.y-cy, dz = pos.z-cz;
    double scale = r/Math.sqrt(dx*dx+dy*dy+dz*dz);
    pos.set(cx+dx*scale, cy+dy*scale, cz+dz*scale);
  }

  /** Get the number of intersections. */

  public int numIntersections()
  {
    return intersections;
  }

  /** Get the nth point of intersection. */
  
  public final void intersectionPoint(int n, Vec3 p)
  {
    if (n == 0)
      p.set(v);
    else
      p.set(v2);
  }

  /** Get the distance from the ray origin to the nth point of intersection. */

  public final double intersectionDist(int n)
  {
    if (n == 0)
      return t;
    else
      return t2;
  }

  /** Get the true normal of the sphere. */
  
  public void trueNormal(Vec3 n)
  {
    calcTrueNorm();
    n.set(trueNorm);
  }
  
  /** Calculate the true normal of the point of intersection. */
  
  private final void calcTrueNorm()
  {
    if (normValid)
      return;
    normValid = true;
    trueNorm.set(v.x-cx, v.y-cy, v.z-cz);
    trueNorm.normalize();
  }

  /** Get the surface properties at the point of intersection.
      @param spec        the texture properties will be stored in this
      @param n           the surface normal will be stored in this
      @param viewDir     the direction from which the surface is being viewed
      @param size        the width of the region over which the texture should be averaged
  */
  
  public void intersectionProperties(TextureSpec spec, Vec3 n, Vec3 viewDir, double size)
  {
    calcTrueNorm();
    n.set(trueNorm);
    TextureMapping map = theSphere.getTextureMapping();
    if (map instanceof UniformMapping)
      map.getTextureSpec(v, spec, -n.dot(viewDir), size, time, param);
    else
      {
	localv.set(v);
	toLocal.transform(localv);
	map.getTextureSpec(localv, spec, -n.dot(viewDir), size, time, param);
      }
    if (bumpMapped)
      {
	fromLocal.transformDirection(spec.bumpGrad);
	n.scale(spec.bumpGrad.dot(n)+1.0);
	n.subtract(spec.bumpGrad);
	n.normalize();
      }
  }

  /** Get the surface transparency at the nth point of intersection. */
  
  public void intersectionTransparency(int n, RGBColor trans, double angle, double size)
  {
    TextureMapping map = theSphere.getTextureMapping();
    if (map instanceof UniformMapping)
      map.getTransparency(v, trans, angle, size, time, param);
    else
      {
	if (n == 0)
	  localv.set(v);
	else
	  localv.set(v2);
	toLocal.transform(localv);
	map.getTransparency(localv, trans, angle, size, time, param);
      }
  }

  /** Get a bounding box for this sphere. */
  
  public BoundingBox getBounds()
  {
    return new BoundingBox(cx-r, cx+r, cy-r, cy+r, cz-r, cz+r);
  }

  /** Determine whether any part of the surface of the sphere lies within a bounding box. */

  public boolean intersectsBox(BoundingBox bb)
  {
    Vec3 c = new Vec3(cx, cy, cz);

    // Find the nearest point of the box to the sphere.

    if (cx < bb.minx)
      c.x = bb.minx;
    else if (cx > bb.maxx)
      c.x = bb.maxx;
    if (cy < bb.miny)
      c.y = bb.miny;
    else if (cy > bb.maxy)
      c.y = bb.maxy;
    if (cz < bb.minz)
      c.z = bb.minz;
    else if (cz > bb.maxz)
      c.z = bb.maxz;
    
    // If the sphere lies entirely outside the box, return false.
    
    c.set(c.x-cx, c.y-cy, c.z-cz);
    if (c.length2() > r2)
      return false;

    // If the box is completely inside the sphere, return false.  Otherwise, return true.

    c.set(bb.minx-cx, bb.miny-cy, bb.minz-cz);
    if (c.length2() > r2)
      return true;
    c.set(bb.minx-cx, bb.miny-cy, bb.maxz-cz);
    if (c.length2() > r2)
      return true;
    c.set(bb.minx-cx, bb.maxy-cy, bb.minz-cz);
    if (c.length2() > r2)
      return true;
    c.set(bb.minx-cx, bb.maxy-cy, bb.maxz-cz);
    if (c.length2() > r2)
      return true;
    c.set(bb.maxx-cx, bb.miny-cy, bb.minz-cz);
    if (c.length2() > r2)
      return true;
    c.set(bb.maxx-cx, bb.miny-cy, bb.maxz-cz);
    if (c.length2() > r2)
      return true;
    c.set(bb.maxx-cx, bb.maxy-cy, bb.minz-cz);
    if (c.length2() > r2)
      return true;
    c.set(bb.maxx-cx, bb.maxy-cy, bb.maxz-cz);
    if (c.length2() > r2)
      return true;
    return false;
  }
  
  /** Get the transformation from world coordinates to the object's local coordinates. */
  
  public Mat4 toLocal()
  {
    return toLocal;
  }
}