/* camera.c */

#include <stdlib.h>
#include <stdio.h>
#include <math.h>
#include "camera.h"
#include "error.h"
#include "vector.h"
#include "Boolean.h"
#include "pools.h"

CAMERA Camera;	/* the one and only virtual camera */

#define NEWCAMERA()	(CAMERA *)Alloc(sizeof(CAMERA))
#define DISPOSECAMERA(ptr) Free((char *)ptr, sizeof(CAMERA))

CAMERA *CameraCreate(void)
{
  CAMERA *cam;
  cam = NEWCAMERA();
  *cam = Camera;

  return cam;
}

void CameraDestroy(CAMERA *cam)
{
  DISPOSECAMERA(cam);
}

/* a stack of virtual camera positions, used for temporary saving the camera and
 * later restoring */
static CAMERA CamStack[MAXCAMSTACK], *CamStackPtr=CamStack;

void CameraPrint(FILE *out, CAMERA *Camera)
{
  fprintf(out, "eyepoint: "); VectorPrint(out, Camera->eyep);
  fprintf(out, "\nlooking to: "); VectorPrint(out, Camera->lookp);
  fprintf(out, "\nupdir: "); VectorPrint(out, Camera->updir);
  fprintf(out, "\nfov = %f", (float)Camera->fov);
  fprintf(out, "\nhres = %d, vres = %d", Camera->hres, Camera->vres);
  fprintf(out, "\nbackground color: "); RGBPrint(out, Camera->background);
  fprintf(out, "\n");
}

/* sets virtual camera position, focus point, up-direction, field of view
 * (in degrees), horizontal and vertical window resolution and window
 * background. Returns (CAMERA *)NULL if eyepoint and focus point coincide or
 * viewing direction is equal to the up-direction. */
CAMERA *CameraSet(CAMERA *Camera, VECTOR *eyep, VECTOR *lookp, VECTOR *updir, 
		  float fov, int hres, int vres, RGB *background)
{
  float n;

  Camera->eyep = *eyep;
  Camera->lookp = *lookp;
  Camera->updir = *updir;
  Camera->fov = fov;
  Camera->hres = hres;
  Camera->vres = vres;
  Camera->background = *background;
  Camera->changed = TRUE;

  /* compute viewing direction ==> Z axis of eye coordinate system */
  VECTORSUBTRACT(*lookp, *eyep, Camera->Z);

  /* distance from virtual camera position to focus point */
  Camera->viewdist = VECTORNORM(Camera->Z);
  if (Camera->viewdist < EPSILON) {
    Error("SetCamera", "eyepoint and look-point coincide");
    return NULL;
  }
  VECTORSCALEINVERSE(Camera->viewdist, Camera->Z, Camera->Z);

  /* Camera->X is a direction pointing to the right in the window */
  VECTORCROSSPRODUCT(Camera->Z, *updir, Camera->X);
  n = VECTORNORM(Camera->X);
  if (n < EPSILON) {
    Error("SetCamera", "up-direction and viewing direction coincide");
    return NULL;
  }
  VECTORSCALEINVERSE(n, Camera->X, Camera->X);

  /* Camera->Y is a direction pointing down in the window */
  VECTORCROSSPRODUCT(Camera->Z, Camera->X, Camera->Y);
  VECTORNORMALIZE(Camera->Y);

  /* compute horizontal and vertical field of view angle from the specified one */
  if (hres < vres) {
    Camera->hfov = fov; 
    Camera->vfov = atan(tan(fov * M_PI/180.)*(float)vres/(float)hres) * 180./M_PI;
  } else {
    Camera->vfov = fov; 
    Camera->hfov = atan(tan(fov * M_PI/180.)*(float)hres/(float)vres) * 180./M_PI;
  }
  
  /* default near and far clipping plane distance, will be set to a more reasonable
  * value when setting the camera for rendering. */
  Camera->near = EPSILON;
  Camera->far = 2. * Camera->viewdist;

  return Camera;
}

/* only sets virtual camera position in 3D space */
CAMERA *CameraSetEyep(CAMERA *cam, float x, float y, float z)
{
  VECTOR neweyep;
  VECTORSET(neweyep, x, y, z);
  return CameraSet(cam, &neweyep, &cam->lookp, &cam->updir, 
		   cam->fov, cam->hres, cam->vres, &cam->background);	
}

CAMERA *CameraSetLookp(CAMERA *cam, float x, float y, float z)
{
  VECTOR newlookp;
  VECTORSET(newlookp, x, y, z);
  return CameraSet(cam, &cam->eyep, &newlookp, &cam->updir, 
		   cam->fov, cam->hres, cam->vres, &cam->background);	
}

CAMERA *CameraSetUpdir(CAMERA *cam, float x, float y, float z)
{
  VECTOR newupdir;
  VECTORSET(newupdir, x, y, z);
  return CameraSet(cam, &cam->eyep, &cam->lookp, &newupdir,
		   cam->fov, cam->hres, cam->vres, &cam->background);	
}

CAMERA *CameraSetFov(CAMERA *cam, float fov)
{
  return CameraSet(cam, &cam->eyep, &cam->lookp, &cam->updir,
		   fov, cam->hres, cam->vres, &cam->background);
}

CAMERA *CameraMoveForward(CAMERA *cam, float step)
{
  VECTOR neweyep, newlookp, dir;
  VECTORORTHOCOMP(cam->Z, cam->updir, dir);
  VECTORNORMALIZE(dir);

  VECTORSUMSCALED(cam->eyep, step, dir, neweyep);
  VECTORSUMSCALED(cam->lookp, step, dir, newlookp);
  return CameraSet(cam, &neweyep, &newlookp, &cam->updir, 
		   cam->fov, cam->hres, cam->vres, &cam->background);
}

CAMERA *CameraMoveRight(CAMERA *cam, float step)
{
  VECTOR neweyep, newlookp;
  
  VECTORSUMSCALED(cam->eyep, step, cam->X, neweyep);
  VECTORSUMSCALED(cam->lookp, step, cam->X, newlookp);
  return CameraSet(cam, &neweyep, &newlookp, &cam->updir, 
		   cam->fov, cam->hres, cam->vres, &cam->background);
}

CAMERA *CameraMoveUp(CAMERA *cam, float step)
{
  VECTOR neweyep, newlookp;
  
  VECTORSUMSCALED(cam->eyep, step, cam->Y, neweyep);
  VECTORSUMSCALED(cam->lookp, step, cam->Y, newlookp);
  return CameraSet(cam, &neweyep, &newlookp, &cam->updir, 
		   cam->fov, cam->hres, cam->vres, &cam->background);
}

CAMERA *CameraTurnRight(CAMERA *cam, float angle)
{
  VECTOR newlookp;
  float z=cam->viewdist*cos(angle), 
    x=cam->viewdist*sin(angle);
  
  VECTORCOMB3(cam->eyep, z, cam->Z, x, cam->X, newlookp);
  return CameraSet(cam, &cam->eyep, &newlookp, &cam->updir, 
		   cam->fov, cam->hres, cam->vres, &cam->background);
}

CAMERA *CameraTurnUp(CAMERA *cam, float angle)
{
  VECTOR newlookp;
  float z=cam->viewdist*cos(angle), 
    y=-cam->viewdist*sin(angle);
  
  VECTORCOMB3(cam->eyep, z, cam->Z, y, cam->Y, newlookp);
  return CameraSet(cam, &cam->eyep, &newlookp, &cam->updir, 
		   cam->fov, cam->hres, cam->vres, &cam->background);
}

CAMERA *CameraTilt(CAMERA *cam, float angle)
{
  VECTOR newupdir;
  float x, y, z, r;
  
  r=VECTORDOTPRODUCT(cam->updir, cam->Y);
  x=-r*sin(angle);
  y=r*cos(angle); 
  z=VECTORDOTPRODUCT(cam->updir, cam->Z);
  
  VECTORCOORD(x, cam->X, y, cam->Y, z, cam->Z, newupdir);
  return CameraSet(cam, &cam->eyep, &cam->lookp, &newupdir, 
		   cam->fov, cam->hres, cam->vres, &cam->background);
}

CAMERA *CameraZoom(CAMERA *cam, float amount)
{
  cam->fov /= amount;
  cam->hfov /= amount;
  cam->vfov /= amount;

  cam->changed = TRUE;
  return cam;
}

/* save camera position on the camera stack */
void CameraPush(CAMERA *cam)
{
  if (CamStackPtr - CamStack >= MAXCAMSTACK)
    Error("PushCamera", "Camera Stack depth exceeded");
  else
    *CamStackPtr++ = *cam;
}

/* restore camera position from the stack */
CAMERA *CameraPop(CAMERA *cam)
{
  CAMERA poppedcam;

  if (CamStackPtr - CamStack <= 0)
    Error("PopCamera", "Camera Stack empty");
  else {
    poppedcam = *--CamStackPtr;
    cam = CameraSet(cam, 
		    &poppedcam.eyep, 
		    &poppedcam.lookp, 
		    &poppedcam.updir, 
		    poppedcam.fov, 
		    cam->hres, cam->vres, 
		    &poppedcam.background);
  }
  return cam;
}

/* returns pointer to the next saved camera. If previous==NULL, the first saved
 * camera is returned. In subsequent calls, the previous camera returned
 * by this function should be passed as the parameter. If all saved cameras
 * have been iterated over, NULL is returned. */
CAMERA *NextSavedCamera(CAMERA *previous)
{
  CAMERA *cam = previous ? previous : CamStackPtr;
  cam--;
  return (cam < CamStack) ? (CAMERA *)NULL : cam;
}
