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

/** RTCylinder represents an cylinder, tapered cylinder, or cone to be raytraced.  It is 
    defined by specifying a Cylinder object, and the transformations to and from local 
    coordinates. */

public class RTCylinder extends RTObject
{
  Cylinder theCylinder;
  Vec3 v, v2, dir, topNormal, bottomNormal, trueNorm;
  double rx, rz, height, halfh, rx2, rz2, toprx2, cx, cy, cz, sy, sz, t, t2, time, param[];
  int hit, intersections;
  boolean bumpMapped, cone, transform, uniform, normValid;
  Mat4 toLocal, fromLocal;
  
  public static final double TOL = 1e-12;
  public static final int TOP = 0;
  public static final int BOTTOM = 1;
  public static final int SIDE = 2;

  public RTCylinder(Cylinder cylinder, Mat4 fromLocal, Mat4 toLocal, double time, double param[])
  {
    double ratio = cylinder.getRatio();
    Vec3 vx = toLocal.timesDirection(Vec3.vx()), vy = toLocal.timesDirection(Vec3.vy());
    Vec3 size;
    
    theCylinder = cylinder;
    this.time = time;
    this.param = param;
    size = cylinder.getBounds().getSize();
    v = new Vec3();
    v2 = new Vec3();
    trueNorm = new Vec3();
    uniform = cylinder.getTextureMapping() instanceof UniformMapping;
    cone = false;
    transform = true;
    if (vy.y == 1.0)
      {
	if (vx.x == 1.0 || vx.x == -1.0)
	  {
	    rx = size.x/2.0;
	    rz = size.z/2.0;
	    transform = false;
	  }
	else if (vx.z == 1.0 || vx.z == -1.0)
	  {
	    rx = size.z/2.0;
	    rz = size.x/2.0;
	    transform = false;
	  }
	if (transform == false && ratio == 0.0)
	  cone = true;
      }
    else if (vy.y == -1.0 && ratio != 0.0)
      {
	if (vx.x == 1.0 || vx.x == -1.0)
	  {
	    rx = size.x/2.0;
	    rz = size.z/2.0;
	    transform = false;
	  }
	else if (vx.z == 1.0 || vx.z == -1.0)
	  {
	    rx = size.y/2.0;
	    rz = size.z/2.0;
	    transform = false;
	  }
	if (transform == false)
	  {
	    rx *= ratio;
	    rz *= ratio;
	    ratio = 1.0/ratio;
	  }
      }
    height = size.y;
    halfh = height/2.0;
    if (transform)
      {
	rx = size.x/2.0;
	rz = size.z/2.0;
	if (ratio == 0.0)
	  cone = true;
	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;
    rz2 = rz*rz;
    toprx2 = rx2*ratio*ratio;
    sz = rx2/rz2;
    sy = rx*(ratio-1.0)/height;
    bottomNormal = fromLocal.timesDirection(Vec3.vy()).times(-1.0);
    if (!cone)
      topNormal = bottomNormal.times(-1.0);
    bumpMapped = cylinder.getTexture().hasComponent(Texture.BUMP_COMPONENT);
    this.toLocal = toLocal;
    lastRay = -1;
  }

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

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

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

  protected boolean checkIntersection(Ray r)
  {
    Vec3 orig = r.getOrigin(), rdir = r.getDirection();
    double a, b, c, d, e, temp1, temp2, mint;

    lastRay = r.getID();
    normValid = false;
    if (transform)
      {
	v.set(cx-orig.x, cy-orig.y, cz-orig.z);
	toLocal.transformDirection(v);
	v.y -= halfh;
	dir.set(rdir);
	toLocal.transformDirection(dir);
      }
    else
      {
	v.set(cx-orig.x, cy-orig.y-halfh, cz-orig.z);
	if (uniform)
	  dir = rdir;
	else
	  dir.set(rdir);
      }
    mint = Double.MAX_VALUE;
    if (dir.y != 0.0)
      {
	// See if the ray hits the top or bottom face of the cylinder.
	
	temp1 = v.y/dir.y;
	if (temp1 > TOL)
	  {
	    a = temp1*dir.x - v.x;
	    b = temp1*dir.z - v.z;
	    if (a*a+sz*b*b < rx2)
	      {
		hit = BOTTOM;
		mint = temp1;
	      }
	  }
	if (!cone)
	  {
	    temp1 = (v.y+height)/dir.y;
	    if (temp1 > TOL)
	      {
		a = temp1*dir.x - v.x;
		b = temp1*dir.z - v.z;
		if (a*a+sz*b*b < toprx2)
		  {
		    if (mint < Double.MAX_VALUE)
		      {
			// The ray hit both the top and bottom faces, so we know it
			// didn't hit the sides.

			intersections = 2;
			if (temp1 < mint)
			  {
			    hit = TOP;
			    t = temp1;
			    t2 = mint;
			  }
			else
			  {
			    t = mint;
			    t2 = temp1;
			  }
			v.set(orig.x+t*rdir.x, orig.y+t*rdir.y, orig.z+t*rdir.z);
			v2.set(orig.x+t2*rdir.x, orig.y+t2*rdir.y, orig.z+t2*rdir.z);
			return (lastRayResult = true);
		      }
		    else
		      {
			hit = TOP;
			mint = temp1;
		      }
		  }
	      }
	  }
      }
    
    // Now see if it hits the sides of the cylinder.
    
    temp1 = sz*dir.z;
    temp2 = sy*dir.y;
    d = rx - sy*v.y;
    b = dir.x*v.x + d*sy*dir.y + temp1*v.z;
    c = v.x*v.x + sz*v.z*v.z - d*d;
    t = Double.MAX_VALUE;
    t2 = mint;
    if (c > TOL)  // Ray origin is outside cylinder.
      {
	if (b > 0.0)  // Ray points toward cylinder.
	  {
	    a = dir.x*dir.x + temp1*dir.z - temp2*temp2;
	    e = b*b - a*c;
	    if (e >= 0.0)
	      {
	        temp1 = Math.sqrt(e);
	        t = (b - temp1)/a;
	        if (t2 == Double.MAX_VALUE)
	          t2 = (b + temp1)/a;
	      }
	  }
      }
    else if (c < -TOL)  // Ray origin is inside cylinder.
      {
	a = dir.x*dir.x + temp1*dir.z - temp2*temp2;
	e = b*b - a*c;
	if (e >= 0.0)
	  t = (b + Math.sqrt(e))/a;
      }
    else  // Ray origin is on the surface of the cylinder.
      {
	if (b > 0.0)  // Ray points into cylinder.
	  {
	    a = dir.x*dir.x + temp1*dir.z - temp2*temp2;
	    e = b*b - a*c;
	    if (e >= 0.0)
	      t = (b + Math.sqrt(e))/a;
	  }
      }
    if (t < mint)
      {
	a = t*dir.y-v.y;
	if (a > 0.0 && a < height)
	  {
	    hit = SIDE;
	    mint = t;
	  }
      }
    if (mint == Double.MAX_VALUE)
      return (lastRayResult = false);
    if (t2 < mint)
      {
	temp1 = t2;
	t2 = mint;
	mint = temp1;
      }
    t = mint;
    v.set(orig.x+t*rdir.x, orig.y+t*rdir.y, orig.z+t*rdir.z);
    if (hit == SIDE)
      projectPoint(v);
    if (t2 == Double.MAX_VALUE)
      intersections = 1;
    else
      {
	intersections = 2;
	v2.set(orig.x+t2*rdir.x, orig.y+t2*rdir.y, orig.z+t2*rdir.z);
      }
    return (lastRayResult = true);
  }
  
  /** Given a point, project it onto the surface of the cylinder.  This is necessary to
      prevent roundoff error. */
  
  private void projectPoint(Vec3 pos)
  {
    if (transform)
    {
      toLocal.transform(pos);
      double r = rx + sy*(pos.y+halfh);
      double scale = r/Math.sqrt(pos.x*pos.x+sz*pos.z*pos.z);
      pos.set(pos.x*scale, pos.y, pos.z*scale);
      fromLocal.transform(pos);
    }
    else
    {
      double dx = pos.x-cx, dz = pos.z-cz;
      double r = rx + sy*(pos.y-cy+halfh);
      double scale = r/Math.sqrt(dx*dx+sz*dz*dz);
      pos.set(cx+dx*scale, pos.y, 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 cylinder. */
  
  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 (hit == TOP)
      trueNorm.set(topNormal);
    else if (hit == BOTTOM)
      trueNorm.set(bottomNormal);
    else
      {
	if (transform)
	  {
	    trueNorm.set(v.x-cx, v.y-cy, v.z-cz);
	    toLocal.transformDirection(trueNorm);
	    trueNorm.set(trueNorm.x, -(rx+sy*(trueNorm.y+halfh))*sy, trueNorm.z*sz);
	    fromLocal.transformDirection(trueNorm);
	  }
	else
	  trueNorm.set(v.x-cx, -(rx+sy*(v.y-cy+halfh))*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 = theCylinder.getTextureMapping();
    if (uniform)
      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 = theCylinder.getTextureMapping();
    if (uniform)
      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 cylinder. */
  
  public BoundingBox getBounds()
  {
    if (transform)
      return (new BoundingBox(-rx, rx, -halfh, halfh, -rz, rz)).transformAndOutset(fromLocal);
    else if (toprx2 > rx2)
      {
	double xrad = Math.sqrt(toprx2), zrad = Math.sqrt(rz2*toprx2/rx2);
	return new BoundingBox(cx-xrad, cx+xrad, cy-halfh, cy+halfh, cz-zrad, cz+zrad);
      }
    else
      return new BoundingBox(cx-rx, cx+rx, cy-halfh, cy+halfh, cz-rz, cz+rz);
  }

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

  public boolean intersectsBox(BoundingBox bb)
  {
    double x, z;
    
    if (transform)
      {
        bb = bb.transformAndOutset(toLocal);
	if (bb.miny > halfh || bb.maxy < -halfh)
	  return false;
	x = 0.0;
	z = 0.0;
	if (bb.minx > 0.0)
	  x = bb.minx;
	else if (bb.maxx < 0.0)
	  x = bb.maxx;
	if (bb.minz > 0.0)
	  z = bb.minz;
	else if (bb.maxz < 0.0)
	  z = bb.maxz;
	if (x*x + sz*z*z > rx2)
	  return false;
	return true;
      }
    if (bb.miny > cy+halfh || bb.maxy < cy-halfh)
      return false;
    x = cx;
    z = cz;
    if (cx < bb.minx)
      x = bb.minx;
    else if (cx > bb.maxx)
      x = bb.maxx;
    if (cz < bb.minz)
      z = bb.minz;
    else if (cz > bb.maxz)
      z = bb.maxz;
    double maxrad2 = (rx2 > toprx2 ? rx2 : toprx2);
    if ((x-cx)*(x-cx) + sz*(z-cz)*(z-cz) > maxrad2)
      return false;
    return true;
  }
  
  /** Get the transformation from world coordinates to the object's local coordinates. */
  
  public Mat4 toLocal()
  {
    return toLocal;
  }
}