/* render1.c
 *
 * By Byron C. Darrah
 * 4/28/94, Thursday
 *
 * render1 renders simple lists of polygons.  render1 keeps an aggregate
 * list of all polygons submitted to it, and also keeps an index to this
 * list keyed by object ID numbers, so that it knows which polygons
 * belong to which objects.
 */

/* Here is the implementation of the renderer called render1.  */

#include <stdlib.h>
#include <stdio.h>
#include <math.h>
#include <assert.h>

#include "render1.h"
#include "display_list.h"

typedef float rend_matrix_t[4][4];
rend_matrix_t camera_transformation;

float perspective,
      default_perspective = DEFAULT_PERSPECTIVE;

polygon_node_t ag_polygon_list;   /* A dummy node to be the head of list */

/* Here is the structure for the nodes of the object ID index to the
 * aggregate list.
 */
typedef struct obj_ki_struct
{
   struct obj_ki_struct  *next;
   int                    obj_id;
   polygon_node_t        *pnodes;    /* array of polygon nodes. */
   int                    npolygons; /* size of above array.    */
} obj_node_t;

obj_node_t obj_id_index;    /* This list also has a dummy node for a head */

/*---------------------------------------------------------------------------*/
/*---------------------- INTERNAL FUNCTION PROTOTYPES -----------------------*/
/*---------------------------------------------------------------------------*/

void        rend_agg_poly_insert(polygon_node_t *pnode);
obj_node_t *rend_obj_lookup(int obj_id);
void        mtx_mult_4x4(rend_matrix_t f1, rend_matrix_t f2,
                         rend_matrix_t product);
void        mtx_transform_pt(real_point_t *pos, rend_matrix_t m1,
                             real_point_t *new_pos);
void        mtx_transform_pg(polygon_node_t *polygon_node);

/*---------------------------------------------------------------------------*/
/*--------------------------- EXPORTED FUNCTIONS ----------------------------*/
/*---------------------------------------------------------------------------*/

/* This sets the camera matrix to a default value, initializes the
 * polygon lists, and the display.
 */

void rend_init(int *argc, char *argv[], float init_perspect)
{
   real_point_t c_position = {0.0,  0.0, -20.0}; /* Camera positon         */
   real_point_t c_focus    = {0.0,  0.0,   0.0}, /* Camera focus direction */
                c_up       = {0.0, 10.0,   0.0}; /* Camera up direction    */
   int i, color = 1;

   /* Scan for the -mono option in the argument list */
   for(i = 0; i < *argc; i++)
   {
      if(color)
      {
         if(!strcmp(argv[i], "-mono"))
         {
            color = 0;
            (*argc)--;
            if(i < *argc)
               argv[i] = argv[i+1];
         }
      }
      else
         argv[i] = argv[i+1];
   }
   

   /* Initialize the display */
   disp_init(argc, argv, color);

   ag_polygon_list.next = NULL;
   ag_polygon_list.prev = NULL;

   obj_id_index.next      = NULL;
   obj_id_index.pnodes    = NULL;
   obj_id_index.npolygons = 0;

   rend_set_camera(&c_position, &c_focus, &c_up);

   /* Initialize the perspective.  If no perspective value was given,
    * set it to the default perspective.
    */
   if (init_perspect != 0.0) default_perspective = init_perspect;

   /* Now work in a fudge-factor to cause points to be scaled according
    * to the display window size. */
   if (disp_x_scale < disp_y_scale)
      perspective = default_perspective * disp_x_scale;
   else
      perspective = default_perspective * disp_y_scale;

}

/*---------------------------------------------------------------------------*/

void rend_end(void)
{
   disp_end();
}

/*---------------------------------------------------------------------------*/

/*      Synchronize the scale of drawing operations with the size of the
 *      actual output window, and redraw the output frame.
 */

void rend_setsize(void)
{
   unsigned short width, height;

   disp_getsize(&width, &height);
   disp_setsize(width, height);

   /* Now work in a fudge-factor to cause points to be scaled according
    * to the display window size. */
   if (disp_x_scale < disp_y_scale)
      perspective = default_perspective * disp_x_scale;
   else
      perspective = default_perspective * disp_y_scale;

   /* Now that the drawing size has been adjusted, redraw the output */
   rend_transform_update_all();
   rend_frame();
}

/*---------------------------------------------------------------------------*/


/* This is used to set the camera matrix, which describes the position
 * and orientation of the POV.  Whenever the camera matrix is changed,
 * the entire list of polygons must be recomputed.
 *
 * The position and orientation of the camera determines what gets displayed
 * (obviously).  As a result, the camera's orientation is used to determine
 * a transformation which will be applied to all points to map them
 * to screen coordinates.  For more information, see "Faster 3-D Drawing"
 * by Brian Carmichael, in The AmigaWorld Tech Journal, pg 20, January/February
 * 1992, or any decent 3-D rendering text.
 *
 * Whenever the camera is moved, the aggregate polygon list must be
 * recomputed (because the mapping of sim-space to screen-space is
 * changed).  This can be done by calling rend_transform_update_all().
 * This is an time-expensive operation, so it is good if the calling
 * application can remove from the aggregate list any objects that are going
 * to resubmit their polygons before the next frame-render, so these objects
 * will not have to be redundantly transformed.
 */

void rend_set_camera(real_point_t *position,
                     real_point_t *focus,
                     real_point_t *up)
{
   rend_matrix_t m1, m2, m3;
   float cosAy, sinAy, cosAx, sinAx, cosAz, sinAz;
   float Dxz, Dyz, Dxy;
   real_point_t new_pos;


   /* let m1 be the transformation matrix for the translation, which moves
    * the camera's focus point to the origin.
    */

   m1[0][0] = 1; m1[0][1] =     m1[0][2] =    m1[0][3] = 0;
   m1[1][0] = 0; m1[1][1] =  1; m1[1][2] =    m1[1][3] = 0;
   m1[2][0] =    m1[2][1] =  0; m1[2][2] = 1; m1[2][3] = 0;
   m1[3][0] = -position->x;
   m1[3][1] = -position->y;
   m1[3][2] = -position->z;
   m1[3][3] = 1;

   /* Compute the camera focus relative to the position.
    */
   new_pos.x = focus->x - position->x;
   new_pos.y = focus->y - position->y;
   new_pos.z = focus->z - position->z;

   /* Now compute the cosine and sine of the y-rotation angle, Ay,
    * between the focus vector and the zy plane.
    */
   Dxz = sqrt((double)(new_pos.x * new_pos.x + new_pos.z * new_pos.z));
   if (Dxz != 0.0)
   {
      cosAy = new_pos.z / Dxz;
      sinAy = -new_pos.x / Dxz;
   }
   else  /* Uh-oh - handle indeterminate case */
   {
      cosAy = 1.0;
      sinAy = 0.0;
   }

   /* let m2 be the transformation matrix for rotating the focus point about
    * the y-axis onto the zy plane.
    */

   m2[0][0] = cosAy;  m2[0][1] = 0;  m2[0][2] = -sinAy;  m2[0][3] = 0;
   m2[1][0] = 0;      m2[1][1] = 1;  m2[1][2] = 0;       m2[1][3] = 0;
   m2[2][0] = sinAy;  m2[2][1] = 0;  m2[2][2] = cosAy;   m2[2][3] = 0;
   m2[3][0] = 0;      m2[3][1] = 0;  m2[3][2] = 0;       m2[3][3] = 1;

   /* let m3 be the product of m1 and m2 */
   mtx_mult_4x4(m1, m2, m3);

   /* Update the focus pos by multiplying it by m2. */
   new_pos.x = new_pos.x *  cosAy + new_pos.z * sinAy;
   new_pos.z = new_pos.x * -sinAy + new_pos.z * cosAy;

   /* Now compute the cosine and sine of the x-rotation angle, Ax */
   Dyz = sqrt((double)(new_pos.y * new_pos.y + new_pos.z * new_pos.z));
   cosAx = new_pos.z/Dyz;
   sinAx = new_pos.y/Dyz;

   /* let m2 be the transformation matrix for rotating the focus point about
    * the x-axis onto the +z-axis.
    */
   m2[0][0] = 1;  m2[0][1] = 0;       m2[0][2] = 0;      m2[0][3] = 0;
   m2[1][0] = 0;  m2[1][1] = cosAx;   m2[1][2] = sinAx;  m2[1][3] = 0;
   m2[2][0] = 0;  m2[2][1] = -sinAx;  m2[2][2] = cosAx;  m2[2][3] = 0;
   m2[3][0] = 0;  m2[3][1] = 0;       m2[3][2] = 0;      m2[3][3] = 1;

   /* let m1 be the product of m3 and m2 */
   mtx_mult_4x4(m3, m2, m1);

   /* Update the "up" point pos by multiplying it by m1 */
   mtx_transform_pt(up, m1, &new_pos);

   /* Now compute the cosine and sine of the z-axis rotation angle, Az */
   Dxy = sqrt((double)(new_pos.x * new_pos.x + new_pos.y * new_pos.y));
   cosAz = new_pos.y/Dxy;
   sinAz = new_pos.x/Dxy;

   /* let m2 be the transformation matrix for rotating the up point about
    * the z-axis onto the zy-plane.
    */

   m2[0][0] = cosAz;   m2[0][1] = sinAz;  m2[0][2] = 0;  m2[0][3] = 0;
   m2[1][0] = -sinAz;  m2[1][1] = cosAz;  m2[1][2] = 0;  m2[1][3] = 0;
   m2[2][0] = 0;       m2[2][1] = 0;      m2[2][2] = 1;  m2[2][3] = 0;
   m2[3][0] = 0;       m2[3][1] = 0;      m2[3][2] = 0;  m2[3][3] = 1;

   /* Now the final multiplication gives us the camera transformation: */
   mtx_mult_4x4(m1, m2, camera_transformation);


/* If you want to debug by printing out the camera matrix, uncomment this. */
/*   fprintf(stderr, "---Camera Matrix---\n");
   {
      int row, col;
      for(row = 0; row < 4; row++)
      {
         for(col = 0; col < 4; col++)
            fprintf(stderr, "%6.2f ", camera_transformation[row][col]);
         printf("\n");
      }
   }
   fprintf(stderr, "-------------------\n");
*/

}

/*---------------------------------------------------------------------------*/

/* rend_register_obj is called by objects that wish to use the display.
 * An object that calls this function is "registered", and may submit
 * lists of polygons for display.
 *
 */

void rend_register_obj(int obj_id, int npolygons)
{
   polygon_node_t *newlist;
   obj_node_t     *newobj;

   if ((newlist = (polygon_node_t *)malloc(sizeof(polygon_node_t) * npolygons))
       == NULL)
   {
      fprintf(stderr, "rend_register_obj: out of memory");
      assert(0);
   }

   if ((newobj = (obj_node_t *)malloc(sizeof(obj_node_t))) == NULL)
   {
      fprintf(stderr, "rend_register_obj: out of memory");
      assert(0);
   }

   /* Add the new object to the object ID index list */
   newobj->next = obj_id_index.next;
   obj_id_index.next = newobj;

   /* Now fill in the data for this object node */
   newobj->obj_id    = obj_id;
   newobj->pnodes    = newlist;
   newobj->npolygons = npolygons;
}

/*---------------------------------------------------------------------------*/

/* An object calls this function to submit an updatedlist of polygons
 * to the renderer.  Render1 knows how many polygons to expect, from
 * when the object was registered by rend_register_obj.  It transforms
 * the object's polygons to screen space, and adds them to the aggregate
 * list.  Note: An object must be sure that it has removed it's polygons
 * before resubmitting them, or else the aggregate list will get messed up.
 */

int rend_submit_plist(int obj_id, polygon_t *polygons)
{
   obj_node_t *obj_record;
   int i;
   polygon_node_t *pnode;

   if((obj_record = rend_obj_lookup(obj_id)) == NULL)
   {
      fprintf(stderr, "rend_submit_plist: unknown object %d.\n", obj_id);
      return (R1_OBJ_UNKNOWN);
   }

   /* Add the object's polygons */
   for(i = 0; i < obj_record->npolygons; i++)
   {
      pnode = obj_record->pnodes+i;
      pnode->polygon = polygons+i;
      /* transform the polygons and add them to the aggregate list */
      mtx_transform_pg(pnode);
      rend_agg_poly_insert(pnode);
   }

   return(R1_NORMAL);
}

/*---------------------------------------------------------------------------*/

/* remove an object's polygons from the aggregate display list */

int rend_rm_plist(int obj_id)
{
   obj_node_t *obj_node;
   polygon_node_t *poly_array;
   int i;

   if ((obj_node = rend_obj_lookup(obj_id)) == NULL)
      return(R1_OBJ_UNKNOWN);
   poly_array = obj_node->pnodes;

   /* Since the polygon list is a doubly-linked list, polygons can be
    * removed from it by simply removing the pointers to them in the
    * previous and next nodes.  The following loop does this.
    */
   for (i = 0; i < obj_node->npolygons; i++)
   {
      if (poly_array[i].next)  poly_array[i].next->prev = poly_array[i].prev;
      poly_array[i].prev->next = poly_array[i].next;
      poly_array[i].next = poly_array[i].prev = NULL;
   }
   return(R1_NORMAL);
}

/*---------------------------------------------------------------------------*/

/* Update the entire list of polygons --
 * perform transformations on all polygons and rebuild the aggregate
 * polygon display list in z-sorted order.
 */

void        rend_transform_update_all(void)
{
   polygon_node_t *templist;
   polygon_node_t *curnode;

   /* Make a copy of the aggregate polygon list, then clear it */
   templist = ag_polygon_list.next;
   ag_polygon_list.next = NULL;

   curnode = templist;
   while(curnode)
   {
      templist = curnode->next;
      mtx_transform_pg(curnode);     /* Transform polygon to screen space.   */
      rend_agg_poly_insert(curnode); /* Insert the node to the agg. list.    */
      curnode = templist;            /* Get the next node from the templist. */
   }
}

/*---------------------------------------------------------------------------*/

/* Render a display frame --
 * This outputs the aggregate polygon list to the display device.
 */

void rend_frame(void)
{
   disp_polygons(ag_polygon_list.next);
}

/*---------------------------------------------------------------------------*/

/* Ring the audio bell. */

void rend_bell(void)
{
   disp_bell();
}

/*---------------------------------------------------------------------------*/

/* Freeze the image as is.  Returns when either the specified interval has
 * expired, or a mouseclick has occurred in the image window.
 */

int  rend_freeze(unsigned long interval)
{
   int reason;
   while(1)
   {
      if ((reason = disp_freeze(interval)) == RESIZE)
         rend_setsize();
      else
         return(reason);
   }
         
}

/*---------------------------------------------------------------------------*/
/*--------------------------- INTERNAL FUNCTIONS ----------------------------*/
/*---------------------------------------------------------------------------*/

/* Given a pointer to a polygon_node_t, insert the node into the aggregate
 * polygon display list.
 */

void rend_agg_poly_insert(polygon_node_t *pnode)
{
   polygon_node_t *curnode = &ag_polygon_list;

   /* Find the node that pnode should be inserted after */
   while(curnode->next && (curnode->next->k > pnode->k))
      curnode = curnode->next;

   /* Insert the pnode into the aggregate list */
   if(curnode->next) curnode->next->prev = pnode;
   pnode->next = curnode->next;
   curnode->next = pnode;
   pnode->prev = curnode;
}

/*---------------------------------------------------------------------------*/

/* Given an object id, look up the object's entry in the object list.
 * NULL is returned if the end of the list is reached before the
 * object id is found.
 */

obj_node_t *rend_obj_lookup(int obj_id)
{
   obj_node_t *curnode = obj_id_index.next;

   for(;curnode && curnode->obj_id != obj_id; curnode = curnode->next)
      ;
   return(curnode);
}

/*---------------------------------------------------------------------------*/

/* mtx_mult_4x4 is a function which multiplies two 4x4 matrices and gives the
 * result.
 */

void mtx_mult_4x4(rend_matrix_t f1, rend_matrix_t f2, rend_matrix_t product)
{
   int i, row, col;
   float sum;

   for (row = 0; row < 4; row++)
      for (col = 0; col < 4; col++)
      {
         sum = f1[row][0] * f2[0][col];
         for(i = 1; i < 4; i++)
            sum += f1[row][i] * f2[i][col];
         product[row][col] = sum;
      }
}

/*---------------------------------------------------------------------------*/

/* mtx_transform_pt transforms a point by the specified matrix.  This is used
 *  by other functions to map a point in simulation space to screen space.
 *
 * This routine is optimized based on the assumption that a point is a 1x4
 * matrix with the first three elements equal to the x,y,z of the point, and
 * the last element always equal to one.
 */

void        mtx_transform_pt(real_point_t *pos, rend_matrix_t m1,
                             real_point_t *new_pos)
{
   new_pos->x =   pos->x * m1[0][0] + pos->y * m1[1][0] + pos->z * m1[2][0]
                + m1[3][0];
   new_pos->y =   pos->x * m1[0][1] + pos->y * m1[1][1] + pos->z * m1[2][1]
                + m1[3][1];
   new_pos->z =   pos->x * m1[0][2] + pos->y * m1[1][2] + pos->z * m1[2][2]
                + m1[3][2];
}

/*---------------------------------------------------------------------------*/

/* Given a polygon, transform it to screen space */

void mtx_transform_pg(polygon_node_t *polygon_node)
{
   int i;
   int clipped = 0;
   float scale;
   float d_sum = 0.0;
   polygon_t *polygon = polygon_node->polygon;

   for (i = polygon->npoints-1; i >= 0; i--)
   {
      real_point_t scrn_pt;
      int_point_t  *int_pt = polygon->int_points+i;

      /* Tanslate and rotate each point in the polygon into screen space */
      mtx_transform_pt(polygon->sim_points+i, camera_transformation,
                       &scrn_pt);

      /* Perform perspective scaling adjustment on each point */
      if (scrn_pt.z > 0.0)
      {
         scale = perspective / scrn_pt.z;
         int_pt->x = Gfx_X_Origin + (int)(scrn_pt.x * scale);
         int_pt->y = Gfx_Y_Origin - (int)(scrn_pt.y * scale);
      }

      /* Keep track of the z-sum */
      if (scrn_pt.z > 0.0)
      {
         d_sum += (float) sqrt((double)(scrn_pt.x*scrn_pt.x +
                              scrn_pt.y*scrn_pt.y +
                              scrn_pt.z*scrn_pt.z));
      }
      else  /* Uh-oh: this point is behind the focus -- clip it */
      {
         d_sum   = 0.0;
         clipped = 1;
         break;
      }
   }

   /* Store the z-center of the polygon, which is used to determine where
    * the polygon will fit into the aggregate list.
    */
   if (!clipped)
      polygon_node->k = d_sum/((float)polygon->npoints);
   else
      polygon_node->k = 0.0;
                                             
}

/*---------------------------------------------------------------------------*/
