/* Copyright (C) 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.*;

/** RTCube represents a cube (or more generally, any rectangular box) to be raytraced.  It is
    defined by specifying a Cube object, and the transformations to and from local coordinates. */

public class RTCube extends RTObject
{
  private Cube theCube;
  private Vec3 v, v2, trueNorm;
  private double minx, miny, minz, maxx, maxy, maxz;
  private double mint, maxt, time, param[];
  private int intersections;
  private boolean bumpMapped, transform, uniform;
  private Mat4 toLocal, fromLocal;
  
  public static final double TOL = 1e-12;

  private static final Vec3 temp1 = new Vec3();
  private static final Vec3 temp2 = new Vec3();

  public RTCube(Cube cube, Mat4 fromLocal, Mat4 toLocal, double time, double param[])
  {
    Vec3 size = cube.getBounds().getSize();
    Vec3 vx = toLocal.timesDirection(Vec3.vx()), vy = toLocal.timesDirection(Vec3.vy());
    theCube = cube;
    this.time = time;
    this.param = param;
    v = new Vec3();
    v2 = new Vec3();
    trueNorm = new Vec3();
    uniform = cube.getTextureMapping() instanceof UniformMapping;
    double xsize = size.x, ysize = size.y, zsize = size.z;
    transform = true;
    if (vx.x == 1.0 || vx.x == -1.0)
    {
      if (vy.y == 1.0 || vy.y == -1.0)
      {
        transform = false;
      }
      else if (vy.z == 1.0 || vy.z == -1.0)
      {
        ysize = size.z;
        zsize = size.y;
        transform = false;
      }
    }
    else if (vx.y == 1.0 || vx.y == -1.0)
    {
      if (vy.x == 1.0 || vy.x == -1.0)
      {
        xsize = size.y;
        ysize = size.x;
        transform = false;
      }
      else if (vy.z == 1.0 || vy.z == -1.0)
      {
        xsize = size.y;
        ysize = size.z;
        zsize = size.x;
        transform = false;
      }
    }
    else if (vx.z == 1.0 || vx.z == -1.0)
    {
      if (vy.x == 1.0 || vy.x == -1.0)
      {
        xsize = size.z;
        ysize = size.x;
        zsize = size.y;
        transform = false;
      }
      else if (vy.y == 1.0 || vy.y == -1.0)
      {
        xsize = size.z;
        zsize = size.x;
        transform = false;
      }
    }
    if (transform)
    {
      this.fromLocal = fromLocal;
      minx = -0.5*xsize;
      miny = -0.5*ysize;
      minz = -0.5*zsize;
      maxx = 0.5*xsize;
      maxy = 0.5*ysize;
      maxz = 0.5*zsize;
    }
    else
    {
      Vec3 center = fromLocal.times(new Vec3());
      minx = center.x-0.5*xsize;
      miny = center.y-0.5*ysize;
      minz = center.z-0.5*zsize;
      maxx = center.x+0.5*xsize;
      maxy = center.y+0.5*ysize;
      maxz = center.z+0.5*zsize;
    }
    bumpMapped = cube.getTexture().hasComponent(Texture.BUMP_COMPONENT);
    this.toLocal = toLocal;
    lastRay = -1;
  }

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

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

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

  protected boolean checkIntersection(Ray r)
  {
    Vec3 rorig = r.getOrigin(), rdir = r.getDirection();
    lastRay = r.getID();
    Vec3 origin, direction;
    if (transform)
    {
      origin = temp1;
      origin.set(rorig);
      toLocal.transform(origin);
      direction = temp2;
      direction.set(rdir);
      toLocal.transformDirection(direction);
    }
    else
    {
      origin = rorig;
      direction = rdir;
    }
    mint = -Double.MAX_VALUE;
    maxt = Double.MAX_VALUE;
    if (direction.x == 0.0)
    {
      if (origin.x < minx || origin.x > maxx)
        return (lastRayResult = false);
    }
    else
    {
      double t1 = (minx-origin.x)/direction.x;
      double t2 = (maxx-origin.x)/direction.x;
      if (t1 < t2)
      {
        if (t1 > mint)
          mint = t1;
        if (t2 < maxt)
          maxt = t2;
      }
      else
      {
        if (t2 > mint)
          mint = t2;
        if (t1 < maxt)
          maxt = t1;
      }
      if (mint > maxt || maxt < TOL)
        return (lastRayResult = false);
    }
    if (direction.y == 0.0)
    {
      if (origin.y < miny || origin.y > maxy)
        return (lastRayResult = false);
    }
    else
    {
      double t1 = (miny-origin.y)/direction.y;
      double t2 = (maxy-origin.y)/direction.y;
      if (t1 < t2)
      {
        if (t1 > mint)
          mint = t1;
        if (t2 < maxt)
          maxt = t2;
      }
      else
      {
        if (t2 > mint)
          mint = t2;
        if (t1 < maxt)
          maxt = t1;
      }
      if (mint > maxt || maxt < TOL)
        return (lastRayResult = false);
    }
    if (direction.z == 0.0)
    {
      if (origin.z < minz || origin.z > maxz)
        return (lastRayResult = false);
    }
    else
    {
      double t1 = (minz-origin.z)/direction.z;
      double t2 = (maxz-origin.z)/direction.z;
      if (t1 < t2)
      {
        if (t1 > mint)
          mint = t1;
        if (t2 < maxt)
          maxt = t2;
      }
      else
      {
        if (t2 > mint)
          mint = t2;
        if (t1 < maxt)
          maxt = t1;
      }
      if (mint > maxt || maxt < TOL)
        return (lastRayResult = false);
    }
    if (mint < TOL)
    {
      v.set(rorig.x+maxt*rdir.x, rorig.y+maxt*rdir.y, rorig.z+maxt*rdir.z);
      mint = maxt;
      intersections = 1;
    }
    else
    {
      v2.set(rorig.x+maxt*rdir.x, rorig.y+maxt*rdir.y, rorig.z+maxt*rdir.z);
      projectPoint(v2, false);
      v.set(rorig.x+mint*rdir.x, rorig.y+mint*rdir.y, rorig.z+mint*rdir.z);
      intersections = 2;
    }
    projectPoint(v, true);
    return (lastRayResult = true);
  }
  
  /** Given a point, project it onto the surface of the cube.  This is necessary to
      prevent roundoff error.  This routine can optionally set the true normal, since
      it is easy to do at the same time. */
  
  private void projectPoint(Vec3 pos, boolean setNormal)
  {
    if (transform)
      toLocal.transform(pos);
    
    // Determine which side it is closest to.
    
    int side = 0;
    double mindist = Math.abs(pos.x-minx), dist;
    dist = Math.abs(pos.x-maxx);
    if (dist < mindist)
    {
      mindist = dist;
      side = 1;
    }
    dist = Math.abs(pos.y-miny);
    if (dist < mindist)
    {
      mindist = dist;
      side = 2;
    }
    dist = Math.abs(pos.y-maxy);
    if (dist < mindist)
    {
      mindist = dist;
      side = 3;
    }
    dist = Math.abs(pos.z-minz);
    if (dist < mindist)
    {
      mindist = dist;
      side = 4;
    }
    dist = Math.abs(pos.z-maxz);
    if (dist < mindist)
    {
      mindist = dist;
      side = 5;
    }
    
    // Project it onto the appropriate side.
    
    if (side == 0)
      pos.x = minx;
    else if (side == 1)
      pos.x = maxx;
    else if (side == 2)
      pos.y = miny;
    else if (side == 3)
      pos.y = maxy;
    else if (side == 4)
      pos.z = minz;
    else if (side == 5)
      pos.z = maxz;
    if (transform)
      fromLocal.transform(pos);
    
    // If requested, also set the normal.
    
    if (setNormal)
    {
      if (side == 0)
        trueNorm.set(-1.0, 0.0, 0.0);
      if (side == 1)
        trueNorm.set(1.0, 0.0, 0.0);
      if (side == 2)
        trueNorm.set(0.0, -1.0, 0.0);
      if (side == 3)
        trueNorm.set(0.0, 1.0, 0.0);
      if (side == 4)
        trueNorm.set(0.0, 0.0, -1.0);
      if (side == 5)
        trueNorm.set(0.0, 0.0, 1.0);
      if (transform)
        fromLocal.transformDirection(trueNorm);
    }
  }

  /** 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 mint;
    else
      return maxt;
  }

  /** Get the true normal of the surface at the first intersection point. */
  
  public void trueNormal(Vec3 n)
  {
    n.set(trueNorm);
  }
  
  /** 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)
  {
    n.set(trueNorm);
    TextureMapping map = theCube.getTextureMapping();
    if (map instanceof UniformMapping)
      map.getTextureSpec(v, spec, -n.dot(viewDir), size, time, param);
    else
    {
      temp1.set(v);
      toLocal.transform(temp1);
      map.getTextureSpec(temp1, 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 = theCube.getTextureMapping();
    if (map instanceof UniformMapping)
      map.getTransparency(v, trans, angle, size, time, param);
    else
      {
	if (n == 0)
	  temp1.set(v);
	else
	  temp1.set(v2);
	toLocal.transform(temp1);
	map.getTransparency(temp1, trans, angle, size, time, param);
      }
  }

  /** Get a bounding box for this cube. */
  
  public BoundingBox getBounds()
  {
    BoundingBox bounds = new BoundingBox(minx, maxx, miny, maxy, minz, maxz);
    if (transform)
      bounds = bounds.transformAndOutset(fromLocal);
    return bounds;
  }

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

  public boolean intersectsBox(BoundingBox bb)
  {
    if (!bb.intersects(getBounds()))
      return false;
    
    // Check whether the box is entirely contained within this object.
    
    if (transform)
      bb = bb.transformAndOutset(toLocal);
    if (bb.minx > minx && bb.maxx < maxx && bb.miny > miny && bb.maxy < maxy && bb.minz > minz && bb.maxz < maxz)
      return false;
    return true;
  }
  
  /** Get the transformation from world coordinates to the object's local coordinates. */
  
  public Mat4 toLocal()
  {
    return toLocal;
  }
}
