/* PhBRML.C: PhBRML extension to VRML'97 for physically
 * based materials. */

#ifndef NO_VRML

#include "readvrml.H"
#include "PhBRML.H"
#include "cie.h"
#include "texture_projection.H"
#include "hit.h"
#include "patch.h"


using namespace xrml;



// Special texture projection class for passing default RenderPark texture
// coordinates to a PhBRML shader.
// The 'point' argument of the project() member function (objecto coordinates
// of a point to be shaded) doesn't matter: it returns the texture coordinates 
// of the hit point in 'hit'. The returned shading frame must be in world 
// coordinates.
class hit_projection: public texture_projection {
  HITREC *hit;

public:
  hit_projection() {
    hit = NULL;
  }

  inline void set_hit(HITREC *newhit)
  {
    hit = newhit;
  }

  Vec3 project(const Vec3& point, Vec3* X, Vec3 *Y, Vec3 *Z);
};

Vec3 hit_projection::project(const Vec3& point, Vec3* X, Vec3 *Y, Vec3 *Z)
{
  VECTOR texCoord;
  if (!hit || !HitTexCoord(hit, &texCoord)) {
    xrml::Warning("hit_projection::project", "Couldn't get texture coordinates");
    return Vec3(0.,0.,0.);
  }

  // We cannot call HitShadingFrame() here because HitShadingFrame() on a
  // PhBRML shader indirectly calls this routine and infinite recursion would 
  // result. Neither can we re-use a previously computed shading frame 
  // in the hit record because that includes the potential effect of bump-mapping
  // or displacement mapping, which would taken into account twice if we would
  // return it here.
  if (Z) {
    DVEC2D uv;
    if (HitUV(hit, &uv) && (hit->flags & HIT_PATCH)) {
      VECTOR VX, VY, VZ;
      PatchInterpolatedFrameAtUV(hit->patch, uv.u, uv.v, &VX, &VY, &VZ);
      *Z = VECTORTOVEC3(VZ);
      *X = VECTORTOVEC3(VX);
      *Y = VECTORTOVEC3(VY);
#ifdef NEVER
      VECTOR N = PatchShadingNormalAtUV(hit->patch, uv.u, uv.v);
      *Z = Vec3(N.x, N.y, N.z);
      if (X && Y) {
	//#ifdef NEVER
	
	// X = direction in which u changes for constant v.
	VECTOR u1, u2;
	PatchPoint(hit->patch, 0., hit->uv.v, &u1);
	PatchPoint(hit->patch, 1., hit->uv.v, &u2);
	*X = Vec3(u2.x-u1.x, u2.y-u1.y, u2.z-u1.z).normalize();
	//#endif
	double zz = sqrt(1 - Z->z*Z->z);
	*X = (zz < EPSILON)	// horizontal brush direction: X in Z=0 plane
	  ? Vec3(1.,0.,0.)	// TODO: take X along direction in which u changes at constant v
	  : Vec3(Z->y/zz, -Z->x/zz, 0.);
	
	*Y = (*Z) ^ (*X);
      }
#endif
      } else {
	xrml::Warning("hit_projection::project", "Couldn't determine shading frame");
	if (X) *X = Vec3(1.,0.,0.);
	if (Y) *Y = Vec3(0.,1.,0.);
	if (Z) *Z = Vec3(0.,0.,1.);
      }
  }
  
  return Vec3(texCoord.x, texCoord.y, texCoord.z);
}




typedef struct VRML_APPEARANCE {
  PhBAppearance *appearance;
  // transforms world coordinates to object coordinates and vice versa
  Mat4 world2object, object2world; 
  int refcount;		// number of references: unreferenced if 0
} VRML_APPEARANCE;

// last query point, normal and other data + corresponding context for 
// PhBAppearance queries
struct context {
  VRML_APPEARANCE *app;
  HITREC hit;
  BSDF *inBsdf, *outBsdf;
  class phbcontext *ctx;
  class hit_projection projector;

  context() {
    inBsdf = outBsdf = NULL;
    ctx = NULL;
  }

  void reset(void) {
    inBsdf = outBsdf = NULL;
    if (ctx) app->appearance->endquery(ctx);
    ctx = NULL;
  }

  bool check(HITREC *newhit, BSDF *inb, BSDF *outb)
  {
    return (ctx &&
	    VECTOREQUAL(newhit->point, hit.point, EPSILON) &&
	    VECTOREQUAL(newhit->gnormal, hit.gnormal, EPSILON) &&
	    inBsdf == inb && outBsdf == outb);
  }

  void update(VRML_APPEARANCE *v_app, HITREC *newhit, BSDF *inb =NULL, BSDF *outb =NULL)
  {
    if (check(newhit, inb, outb))
      return;	// query context still adequate

    // copy new "state"
    app = v_app;

    hit = *newhit;

    inBsdf = inb;
    outBsdf = outb;

    Vec3 p(hit.point.x, hit.point.y, hit.point.z);
    Vec3 n(hit.normal.x, hit.normal.y, hit.normal.z);

    // determine index of refraction for medium outside the surface and
    // make sure normal always point outside the surface too.
    complex indexOut(1.,0.);	// default is vacuum 
    if (inBsdf && (void *)app == inBsdf->data) {
      // ray comes from inside medium
      n *= -1.;		// reverse normal (vrml wants normal to always point 
      // outside surface, RenderPark wants normal and in dir. on the same side)

      REFRACTIONINDEX index;
      BsdfIndexOfRefraction(outBsdf, &index);
      indexOut = complex(index.nr, index.ni);
    } else if (outBsdf && (void *)app == outBsdf->data) {
      // ray comes from outside surface
      // this time, inBsdf determines the medium outside the surface.
      REFRACTIONINDEX index;
      BsdfIndexOfRefraction(inBsdf, &index);
      indexOut = complex(index.nr, index.ni);
    }

    if (ctx) app->appearance->endquery(ctx);
    projector.set_hit(&hit);
    ctx = app->appearance->initquery(&app->world2object, &app->object2world,
				     p, n, indexOut, &projector);
  }
};
static struct context query;

// converts PhBRML color representation to 
// RenderPark color representation
// PhBRML uses CIE XYZ representation.
static COLOR f_cvtcol(Spectrum s)
{
  COLOR col;
  cvtcol(s, col);
  return col;
}

// converts VRML Vec3 to RenderPark VECTOR
static VECTOR f_cvtvec(Vec3 v)
{
  VECTOR vec;
  cvtvec(v, vec);
  return vec;
}

static int PhBRMLShadingFrame(VRML_APPEARANCE *app, HITREC *hit, VECTOR *X, VECTOR *Y, VECTOR *Z)
{
  query.update(app, hit, NULL, NULL);
  Mat3 xf = app->appearance->get_shadingframe(query.ctx);
  if (X) VECTORSET(*X, xf[0][0], xf[0][1], xf[0][2]);
  if (Y) VECTORSET(*Y, xf[1][0], xf[1][1], xf[1][2]);
  if (Z) VECTORSET(*Z, xf[2][0], xf[2][1], xf[2][2]);
  return TRUE;
}

static int PhBRMLIsTextured(VRML_APPEARANCE *app)
{
  return app->appearance->isTextured();
}

static COLOR PhBEDFEmittance(VRML_APPEARANCE *app, HITREC *hit, XXDFFLAGS flags)
{
  query.update(app, hit);
  // divide by WHITE_EFFICACY to convert lumen/m^2 to Watt/m^2
  return f_cvtcol(app->appearance->emittance(query.ctx, (int)flags) / WHITE_EFFICACY);
}

static COLOR PhBEDFEval(VRML_APPEARANCE *app, HITREC *hit, VECTOR *out, XXDFFLAGS flags, double *pdf)
{
  query.update(app, hit);

  float fpdf;
  Spectrum val = app->appearance->evaluateEDF(query.ctx,
 			           Vec3(out->x, out->y, out->z),
				   Spectrum(1.,1.,1.),
				   (int)flags,
				   pdf ? &fpdf : (float *)0) / WHITE_EFFICACY;
  if (pdf) *pdf = fpdf;
  return f_cvtcol(val);
}

static VECTOR PhBEDFSample(VRML_APPEARANCE *app, HITREC *hit,
			   XXDFFLAGS flags,
			   double xi1, double xi2,
			   COLOR *emitted_radiance, double *pdf)
{
  query.update(app, hit);

  float fpdf;
  Spectrum val;
  Vec3 dir = app->appearance->sampleEDF(query.ctx,
			     Spectrum(1.,1.,1.),
			     xi1, xi2,
			     (int)flags,
			     emitted_radiance ? &val : (Spectrum *)0,
			     pdf ? &fpdf : (float *)0);
  if (pdf) *pdf = fpdf;
  if (emitted_radiance) {
    val /= WHITE_EFFICACY;
    cvtcol(val, *emitted_radiance);
  }

  return f_cvtvec(dir);
}


static void PhBEDFPrint(FILE *out, VRML_APPEARANCE *app)
{
  cerr << "VRML Surface: " << app->appearance << "\nTransform:\n" << app->world2object << "\n";
  cerr << app->appearance << "\n";
}

static VRML_APPEARANCE *PhBEDFDuplicate(VRML_APPEARANCE *app)
{
  app->refcount ++;
  return app;
}

static void *PhBEDFCreateEditor(void *parent, VRML_APPEARANCE *app)
{
  xrml::Error("PhBEDFCreateEditor", "not yet implemented");
  return NULL;
}

static void PhBEDFDestroy(VRML_APPEARANCE *app)
{
  query.reset();

  app->refcount --;
  if (app->refcount <= 0)
    delete app;
}

static EDF_METHODS PhBEDFMethods = {
  (COLOR (*)(void *, HITREC *, XXDFFLAGS))PhBEDFEmittance,
  (int (*)(void *))PhBRMLIsTextured,
  (COLOR (*)(void *, HITREC *, VECTOR *, XXDFFLAGS, double *))PhBEDFEval,
  (VECTOR (*)(void *, HITREC *, XXDFFLAGS, double, double, COLOR *, double *))PhBEDFSample,
  (int (*)(void *, HITREC *, VECTOR *, VECTOR *, VECTOR *))PhBRMLShadingFrame,
  (void (*)(FILE *, void *))PhBEDFPrint,
  (void *(*)(void *))PhBEDFDuplicate,
  (void *(*)(void *, void *))PhBEDFCreateEditor,
  (void (*)(void *))PhBEDFDestroy
};

static EDF * PhBEDFCreate(VRML_APPEARANCE *app)
{
  if (!app->appearance->isLightSource()) {
    return NULL;
  } else {
    app->refcount++;
    return EdfCreate((void *)app, &PhBEDFMethods);
  }
}

static COLOR PhBBSDFAlbedo(VRML_APPEARANCE *app, HITREC *hit, VECTOR *in, BSDFFLAGS flags)
{
  query.update(app, hit, NULL, NULL);
  Vec3 inDir(in->x, in->y, in->z);
  return f_cvtcol(app->appearance->albedo(query.ctx, inDir, (int)flags));
}

static void PhBBSDFIndexOfRefraction(VRML_APPEARANCE *app, REFRACTIONINDEX *index)
{
  complex n = app->appearance->get_indexOfRefraction();
  index->nr = n.r; index->ni = n.i;
}

extern int debug_mat;	/* ui_debug.c TestBsdf routines */

static COLOR PhBBSDFEval(VRML_APPEARANCE *app, HITREC *hit,
			 BSDF *inBsdf, BSDF *outBsdf,
			 VECTOR *in, VECTOR *out,
			 BSDFFLAGS flags)
{
  query.update(app, hit, inBsdf, outBsdf);
  return f_cvtcol(app->appearance->evaluateBSDF(query.ctx,
					      Vec3(in->x, in->y, in->z),
					      Vec3(out->x, out->y, out->z),
					      Spectrum(1.,1.,1.),
					      (int)flags, 
					      (float *)0));
}


static VECTOR PhBBSDFSample(VRML_APPEARANCE *app, HITREC *hit,
			    BSDF *inBsdf, BSDF *outBsdf,
			    VECTOR *in,
			    int doRussianRoulette,
			    BSDFFLAGS flags,
			    double xi1, double xi2,
			    double *pdf)
{
  query.update(app, hit, inBsdf, outBsdf);
  Vec3 inDir(in->x, in->y, in->z);

  *pdf = 0.;
  float albedo = 0.;

  if (doRussianRoulette) {
    albedo = (app->appearance->albedo(query.ctx, inDir, (int)flags) 
	      & Spectrum(1.,1.,1.)) / 3.;
    if (albedo > 1.) albedo = 1.;
    if (albedo < EPSILON || xi1 >= albedo)
      return f_cvtvec(Vec3(0.,0.,0.));
    else // rescale xi1 to fill the interval [0,1] again
      xi1 /= albedo;
  }

  float fpdf;
  Vec3 outDir = app->appearance->sampleBSDF(query.ctx, inDir,
				 Spectrum(1.,1.,1.),
				 xi1, xi2,
				 (int)flags,
				 (Spectrum *)0, &fpdf);

  *pdf = fpdf;
  if (doRussianRoulette)
    *pdf *= albedo;

  return f_cvtvec(outDir);
}


static void PhBBSDFEvalPdf(VRML_APPEARANCE *app, HITREC *hit,
			   BSDF *inBsdf, BSDF *outBsdf,
			   VECTOR *in, VECTOR *out,
			   BSDFFLAGS flags,
			   double *pdf, double *pdfRR)
{
  query.update(app, hit, inBsdf, outBsdf);

  Vec3 inDir(in->x, in->y, in->z);
  Vec3 outDir(out->x, out->y, out->z);

  float fpdf = 0.;
  app->appearance->evaluateBSDF(query.ctx, inDir, outDir,
		     Spectrum(1.,1.,1.),
		     (int)flags,
		     (float *)&fpdf);
  *pdf = fpdf;

  float albedo = (app->appearance->albedo(query.ctx, inDir, (int)flags) 
		  & Spectrum(1.,1.,1.)) / 3.;
  *pdfRR = albedo;
}

static void PhBBSDFPrint(FILE *out, VRML_APPEARANCE *app)
{
  cerr << "VRML Appearance: " << app->appearance << "\nTransform:\n" << app->world2object << "\n";
  cerr << app->appearance << "\n";
}

static VRML_APPEARANCE *PhBBSDFDuplicate(VRML_APPEARANCE *app)
{
  app->refcount++;
  return app;
}

static void *PhBBSDFCreateEditor(void *parent, VRML_APPEARANCE *app)
{
  xrml::Error("PhBBSDFCreateEditor", "not yet implemented");
  return NULL;
}

static void PhBBSDFDestroy(VRML_APPEARANCE *app)
{
  query.reset();

  app->refcount --;
  if (app->refcount <= 0)
    delete app;
}

static BSDF_METHODS PhBBSDFMethods = {
  (COLOR (*)(void *, HITREC *, VECTOR *, BSDFFLAGS))PhBBSDFAlbedo,
  (int (*)(void *))PhBRMLIsTextured,
  (void (*)(void *, REFRACTIONINDEX *))PhBBSDFIndexOfRefraction,
  (COLOR (*)(void *, HITREC *, void *, void *, VECTOR *, VECTOR *, BSDFFLAGS))PhBBSDFEval, 
  (VECTOR (*)(void *, HITREC *, void *, void *, VECTOR *, int, BSDFFLAGS, double, double, double *))PhBBSDFSample,
  
  (void (*)(void *, HITREC *, void *, void *, VECTOR *, VECTOR *, BSDFFLAGS, double *, double *))PhBBSDFEvalPdf,
  (int (*)(void *, HITREC *, VECTOR *, VECTOR *, VECTOR *))PhBRMLShadingFrame,
  (void (*)(FILE *, void *))PhBBSDFPrint,
  (void *(*)(void *))PhBBSDFDuplicate,
  (void *(*)(void *, void *))PhBBSDFCreateEditor,
  (void (*)(void *))PhBBSDFDestroy
};

static BSDF * PhBBSDFCreate(VRML_APPEARANCE *app)
{
  if (!app->appearance->isScatterer())
    return NULL;
  else {
    app->refcount++;
    return BsdfCreate((void *)app, &PhBBSDFMethods);
  }
}

MATERIAL *PhBRMLMaterialCreate(char *name, PhBAppearance *appearance, 
			       const Mat4& world2object, const Mat4& object2world)
{
  // TODO: DEF'ed materials: re-use previous VRML_APPEARANCE if same transforms.
  VRML_APPEARANCE *app = new VRML_APPEARANCE;
  app->appearance = appearance;
  app->world2object = world2object;
  app->object2world = object2world;
  app->refcount = 0;

  return MaterialCreate(name,
			PhBEDFCreate(app),
			PhBBSDFCreate(app),
			1);
}

#endif /*NO_VRML*/
