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

/** RTEllipsoid represents an ellipsoid to be raytraced.  It is defined by specifying a Sphere 
    object, and the transformations to and from local coordinates. */

public class RTEllipsoid extends RTObject
{
  Sphere theSphere;
  Vec3 v, v2, dir, trueNorm;
  double rx, ry, rz, rx2, ry2, rz2, cx, cy, cz, sy, sz, t, t2, time, param[];
  int intersections;
  boolean bumpMapped, transform, uniform, normValid;
  Mat4 toLocal, fromLocal;
  
  public static final double TOL = 1e-12;

  public RTEllipsoid(Sphere sphere, Mat4 fromLocal, Mat4 toLocal, double time, double param[])
  {
    Vec3 radii = sphere.getRadii();
    Vec3 vx = toLocal.timesDirection(Vec3.vx()), vy = toLocal.timesDirection(Vec3.vy());
    theSphere = sphere;
    this.time = time;
    this.param = param;
    v = new Vec3();
    v2 = new Vec3();
    trueNorm = new Vec3();
    uniform = sphere.getTextureMapping() instanceof UniformMapping;
    transform = true;
    if (vx.x == 1.0 || vx.x == -1.0)
      {
	if (vy.y == 1.0 || vy.y == -1.0)
	  {
	    rx = radii.x;
	    ry = radii.y;
	    rz = radii.z;
	    transform = false;
	  }
	else if (vy.z == 1.0 || vy.z == -1.0)
	  {
	    rx = radii.x;
	    ry = radii.z;
	    rz = radii.y;
	    transform = false;
	  }
      }
    else if (vx.y == 1.0 || vx.y == -1.0)
      {
	if (vy.x == 1.0 || vy.x == -1.0)
	  {
	    rx = radii.y;
	    ry = radii.x;
	    rz = radii.z;
	    transform = false;
	  }
	else if (vy.z == 1.0 || vy.z == -1.0)
	  {
	    rx = radii.y;
	    ry = radii.z;
	    rz = radii.x;
	    transform = false;
	  }
      }
    else if (vx.z == 1.0 || vx.z == -1.0)
      {
	if (vy.x == 1.0 || vy.x == -1.0)
	  {
	    rx = radii.z;
	    ry = radii.x;
	    rz = radii.y;
	    transform = false;
	  }
	else if (vy.y == 1.0 || vy.y == -1.0)
	  {
	    rx = radii.z;
	    ry = radii.y;
	    rz = radii.x;
	    transform = false;
	  }
      }
    if (transform)
      {
	rx = radii.x;
	ry = radii.y;
	rz = radii.z;
        this.fromLocal = fromLocal;
      }
    if (transform || !uniform)
      dir = new Vec3();
    cx = fromLocal.m14/fromLocal.m44;
    cy = fromLocal.m24/fromLocal.m44;
    cz = fromLocal.m34/fromLocal.m44;
    rx2 = rx*rx;
    ry2 = ry*ry;
    rz2 = rz*rz;
    sy = rx2/ry2;
    sz = rx2/rz2;
    bumpMapped = sphere.getTexture().hasComponent(Texture.BUMP_COMPONENT);
    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(), rdir = r.getDirection();
    double a, b, c, d, temp1, temp2;

    lastRay = r.getID();
    normValid = false;
    v.set(cx-orig.x, cy-orig.y, cz-orig.z);
    if (transform)
      {
	toLocal.transformDirection(v);
	dir.set(rdir);
	toLocal.transformDirection(dir);
      }
    else if (uniform)
      dir = rdir;
    else
      dir.set(rdir);
    temp1 = sy*dir.y;
    temp2 = sz*dir.z;
    b = dir.x*v.x + temp1*v.y + temp2*v.z;
    c = v.x*v.x + sy*v.y*v.y + sz*v.z*v.z - rx2;
    if (c > TOL*b)
      {
	// Ray origin is outside ellipsoid.
	
	if (b <= 0.0)
	  return (lastRayResult = false);  // Ray points away from the ellipsoid.
	a = dir.x*dir.x + temp1*dir.y + temp2*dir.z;
	d = b*b - a*c;
	if (d < 0.0)
	  return (lastRayResult = false);
	intersections = 2;
	temp1 = Math.sqrt(d);
	t = (b - temp1)/a;
	t2 = (b + temp1)/a;
	v2.set(orig.x+t2*dir.x, orig.y+t2*dir.y, orig.z+t2*dir.z);
        projectPoint(v2);
      }
    else if (c < -TOL*b)
      {
	// Ray origin is inside ellipsoid.
	
	a = dir.x*dir.x + temp1*dir.y + temp2*dir.z;
	d = b*b - a*c;
	if (d < 0.0)
	  return (lastRayResult = false);
	intersections = 1;
	t = (b + Math.sqrt(d))/a;
      }
    else
      {
	// Ray origin is on the surface of the ellipsoid.
	
	if (b <= 0.0)
	  return (lastRayResult = false);  // Ray points away from the ellipsoid.
	a = dir.x*dir.x + temp1*dir.y + temp2*dir.z;
	d = b*b - a*c;
	if (d < 0.0)
	  return (lastRayResult = false);
	intersections = 1;
	t = (b + Math.sqrt(d))/a;
      }
    v.set(orig.x+t*rdir.x, orig.y+t*rdir.y, orig.z+t*rdir.z);
    projectPoint(v);
    return (lastRayResult = true);
  }
  
  /** Given a point, project it onto the surface of the ellipsoid.  This is necessary to
      prevent roundoff error. */
  
  private void projectPoint(Vec3 pos)
  {
    if (transform)
    {
      toLocal.transform(pos);
      double scale = rx/Math.sqrt(pos.x*pos.x+sy*pos.y*pos.y+sz*pos.z*pos.z);
      pos.scale(scale);
      fromLocal.transform(pos);
    }
    else
    {
      double dx = pos.x-cx, dy = pos.y-cy, dz = pos.z-cz;
      double scale = rx/Math.sqrt(dx*dx+sy*dy*dy+sz*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 ellipsoid. */
  
  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;
    if (transform)
      {
	trueNorm.set(v.x-cx, v.y-cy, v.z-cz);
	toLocal.transformDirection(trueNorm);
	trueNorm.set(trueNorm.x, trueNorm.y*sy, trueNorm.z*sz);
	fromLocal.transformDirection(trueNorm);
      }
    else
      trueNorm.set(v.x-cx, (v.y-cy)*sy, (v.z-cz)*sz);
    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
      {
	dir.set(v);
	toLocal.transform(dir);
	map.getTextureSpec(dir, spec, -n.dot(viewDir), size, time, param);
      }
    if (bumpMapped)
      {
	if (transform)
	  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)
	  dir.set(v);
	else
	  dir.set(v2);
	toLocal.transform(dir);
	map.getTransparency(dir, trans, angle, size, time, param);
      }
  }

  /** Get a bounding box for this ellipsoid. */
  
  public BoundingBox getBounds()
  {
    if (transform)
      return (new BoundingBox(-rx, rx, -ry, ry, -rz, rz)).transformAndOutset(fromLocal);
    else
      return new BoundingBox(cx-rx, cx+rx, cy-ry, cy+ry, cz-rz, cz+rz);
  }

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

  public boolean intersectsBox(BoundingBox bb)
  {
    double dx, dy, dz, centerx, centery, centerz;

    if (transform)
      {
        bb = bb.transformAndOutset(toLocal);
        centerx = centery = centerz = 0.0;
      }
    else
      {
        centerx = cx;
        centery = cy;
        centerz = cz;
      }
    Vec3 c = new Vec3(centerx, centery, centerz);

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

    if (centerx < bb.minx)
      c.x = bb.minx;
    else if (centerx > bb.maxx)
      c.x = bb.maxx;
    if (centery < bb.miny)
      c.y = bb.miny;
    else if (centery > bb.maxy)
      c.y = bb.maxy;
    if (centerz < bb.minz)
      c.z = bb.minz;
    else if (centerz > bb.maxz)
      c.z = bb.maxz;
    
    // If the ellipsoid lies entirely outside the box, return false.
    
    if (!transform)
      c.set(c.x-centerx, c.y-centery, c.z-centerz);
    if (c.x*c.x + c.y*c.y*sy + c.z*c.z*sz > rx2)
      return false;

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

    dx = 1.0/rx;
    dy = 1.0/ry;
    dz = 1.0/rz;
    c.set((bb.minx-centerx)*dx, (bb.miny-centery)*dy, (bb.minz-centerz)*dz);
    if (c.length2() > 1.0)
      return true;
    c.set((bb.minx-centerx)*dx, (bb.miny-centery)*dy, (bb.maxz-centerz)*dz);
    if (c.length2() > 1.0)
      return true;
    c.set((bb.minx-centerx)*dx, (bb.maxy-centery)*dy, (bb.minz-centerz)*dz);
    if (c.length2() > 1.0)
      return true;
    c.set((bb.minx-centerx)*dx, (bb.maxy-centery)*dy, (bb.maxz-centerz)*dz);
    if (c.length2() > 1.0)
      return true;
    c.set((bb.maxx-centerx)*dx, (bb.miny-centery)*dy, (bb.minz-centerz)*dz);
    if (c.length2() > 1.0)
      return true;
    c.set((bb.maxx-centerx)*dx, (bb.miny-centery)*dy, (bb.maxz-centerz)*dz);
    if (c.length2() > 1.0)
      return true;
    c.set((bb.maxx-centerx)*dx, (bb.maxy-centery)*dy, (bb.minz-centerz)*dz);
    if (c.length2() > 1.0)
      return true;
    c.set((bb.maxx-centerx)*dx, (bb.maxy-centery)*dy, (bb.maxz-centerz)*dz);
    if (c.length2() > 1.0)
      return true;
    return false;
  }
  
  /** Get the transformation from world coordinates to the object's local coordinates. */
  
  public Mat4 toLocal()
  {
    return toLocal;
  }
}