/*
 * PyMOL COLLADA export
 *
 * (c) 2014 Jared Sampson
 * (c) 2014 Schrodinger, Inc.
 */

#include "os_predef.h"
#include "os_std.h"

#include "Feedback.h"
#include "Ray.h"

#ifdef _HAVE_LIBXML

#include <libxml/xmlwriter.h>
#include <libxml/encoding.h>

#include <time.h>

#include "Setting.h"
#include "Ortho.h"
#include "Color.h"
#include "Util.h"
#include "Version.h"
#include "MemoryDebug.h"
#include "Sphere.h"
#include "Scene.h"
#include "CGO.h"


#define XML_VERSION "1.0"
#define XML_ENCODING "UTF-8"

#define PRECISION 0.001f
#define TRANS_PRECISION 0.01f

/*
 * Size of individual text nodes must be less than the maximum size for
 * the LIBXML Parser node: 10MB. Text nodes longer than this will be split.
 */
#define XML_NODE_SIZE_LIMIT 1000000

/* For debugging output. */
#define COLLADA_DEBUG 0

/* Total number of `collada_geometry_mode` settings implemented. */
#define COLLADA_GEOM_MODE_COUNT 2


/* Returns a standard XML-formatted timestamp */
static char *GetTimestamp(){
  time_t now;
  struct tm *local;
  char buffer[20];

  time(&now);
  local = localtime(&now);
  strftime(buffer, 20, "%Y-%m-%dT%H:%M:%S", local);
  return strdup(buffer);
}

/*
 * Returns index of the first float in an array matching the given value to
 * the given precision.
 */
static int GetFloatPositionInArray(float val, float *arr, int len,
    float precision)
{
  int i;
  for (i = 0; i < len; i++) {
    if (precision > fabsf(val - arr[i])){
      return i;
    }
  }
  return -1;  // not found
}


/* Returns 1 if any of the 4 VLA strings has gotten too big, else 0. */
static int ReachedXmlNodeSizeLimit(int pos_str_cc, int norm_str_cc,
    int col_str_cc, int p_str_cc)
{
  int r = 0;
  if(pos_str_cc >= XML_NODE_SIZE_LIMIT)
    r = 1;
  if(norm_str_cc >= XML_NODE_SIZE_LIMIT)
    r = 1;
  if(col_str_cc >= XML_NODE_SIZE_LIMIT)
    r = 1;
  if(p_str_cc >= XML_NODE_SIZE_LIMIT)
    r = 1;
  return r;
}


/* Writes global <asset> element with file meta information. */
static void ColladaWriteAssetElement(xmlTextWriterPtr w)
{
  xmlTextWriterStartElement(w, BAD_CAST "asset");
  xmlTextWriterStartElement(w, BAD_CAST "contributor");
  xmlTextWriterWriteElement(w, BAD_CAST "author",
      BAD_CAST "PyMOL User");
#ifdef _PyMOL_VERSION
  char *vers = (char *)"PyMOL " _PyMOL_VERSION;
#else
  char *vers = (char *)"PyMOL";
#endif
  xmlTextWriterWriteElement(w, BAD_CAST "authoring_tool",
      BAD_CAST vers);
  xmlTextWriterEndElement(w);  // contributor

  char *ts = GetTimestamp();

  xmlTextWriterWriteElement(w, BAD_CAST "created", BAD_CAST ts);
  xmlTextWriterWriteElement(w, BAD_CAST "modified", BAD_CAST ts);
  free(ts);

  xmlTextWriterStartElement(w, BAD_CAST "unit");
  xmlTextWriterWriteAttribute(w, BAD_CAST "meter", BAD_CAST "1e-10");
  xmlTextWriterWriteAttribute(w, BAD_CAST "name", BAD_CAST "Angstrom");
  xmlTextWriterEndElement(w);  // unit
  xmlTextWriterWriteElement(w, BAD_CAST "up_axis", BAD_CAST "Y_UP");
  xmlTextWriterEndElement(w);  // asset
}


/* Writes the <library_cameras> element, including PyMOL's viewport camera. */
static void ColladaWriteLibraryCameras(xmlTextWriterPtr w, PyMOLGlobals *G, int width,
    int height, float fov, float front, float back)
{
  SceneViewType view;
  SceneGetView(G, view);

  float aspect_ratio = (float)width / (float)height;
  int ortho = SettingGetGlobal_i(G, cSetting_ortho);
  int ray_ortho = SettingGetGlobal_i(G, cSetting_ray_orthoscopic);

  if (ray_ortho == -1) {
    ray_ortho = ortho;
  }

  xmlTextWriterStartElement(w, BAD_CAST "library_cameras");

  xmlTextWriterStartElement(w, BAD_CAST "camera");
  xmlTextWriterWriteAttribute(w, BAD_CAST "id", BAD_CAST "camera");
  xmlTextWriterStartElement(w, BAD_CAST "optics");
  xmlTextWriterStartElement(w, BAD_CAST "technique_common");

  if (ray_ortho) {
    float ymag = SettingGetGlobal_i(G, cSetting_field_of_view) / 2;
    float ymag_scale = -view[18] / 50;  // scale fov based on camera distance
    float ortho_adjust = 0.88f;  // prevent apparent size change
    ymag = ymag * ymag_scale * ortho_adjust;

    xmlTextWriterStartElement(w, BAD_CAST "orthographic");
    xmlTextWriterWriteFormatElement(w, BAD_CAST "ymag", "%6.4f", ymag);
  } else {
    xmlTextWriterStartElement(w, BAD_CAST "perspective");
    xmlTextWriterWriteFormatElement(w, BAD_CAST "yfov", "%6.4f", fov);
  }

  xmlTextWriterWriteFormatElement(w, BAD_CAST "aspect_ratio",
      "%6.4f", aspect_ratio);
  xmlTextWriterWriteFormatElement(w, BAD_CAST "znear", "%6.4f", front);
  xmlTextWriterWriteFormatElement(w, BAD_CAST "zfar", "%6.4f", back);
  xmlTextWriterEndElement(w);  // perspective or orthographic

  xmlTextWriterEndElement(w);  // technique_common
  xmlTextWriterEndElement(w);  // optics
  xmlTextWriterEndElement(w);  // camera

  xmlTextWriterEndElement(w);  // library_cameras
}


/* Writes the <library_lights> element, including ambient and directional
 * PyMOL lights. */
static void ColladaWriteLibraryLights(xmlTextWriterPtr w, CRay *I, PyMOLGlobals *G)
{
  xmlTextWriterStartElement(w, BAD_CAST "library_lights");

  /* Ambient light */
  float ambient = SettingGetGlobal_f(I->G, cSetting_ambient);
  if(ambient > 0.5){
    ambient = 0.5;
  }

  /* RGB = "0.5 0.5 0.5" seems to work well; `ambient` setting is handled in
   * default effect */
  xmlTextWriterStartElement(w, BAD_CAST "light");
  xmlTextWriterWriteAttribute(w, BAD_CAST "id", BAD_CAST "ambient-light");
  xmlTextWriterStartElement(w, BAD_CAST "technique_common");
  xmlTextWriterStartElement(w, BAD_CAST "ambient");
  xmlTextWriterWriteElement(w, BAD_CAST "color", BAD_CAST "0.5 0.5 0.5");
  xmlTextWriterEndElement(w);  // directional
  xmlTextWriterEndElement(w);  // technique_common
  xmlTextWriterEndElement(w);  // light

  /* All other lights are directional, and their intensities are equal. Use
   * instances of this light in the <node> section below. */
  xmlTextWriterStartElement(w, BAD_CAST "light");
  xmlTextWriterWriteAttribute(w, BAD_CAST "id", BAD_CAST "pymol-light");
  xmlTextWriterStartElement(w, BAD_CAST "technique_common");
  xmlTextWriterStartElement(w, BAD_CAST "directional");

  /* Intensity decreases with increasing number of lights. */
  float intensity = SceneGetSpecularValue(G, 0.6, 10);
  char *value = (char *) malloc(sizeof(char) * 30);
  sprintf(value, "%5.3f %5.3f %5.3f", intensity, intensity, intensity);
  xmlTextWriterWriteElement(w, BAD_CAST "color", BAD_CAST value);
  free(value);

  xmlTextWriterEndElement(w);  // directional
  xmlTextWriterEndElement(w);  // technique_common
  xmlTextWriterEndElement(w);  // light

  xmlTextWriterEndElement(w);  // library_lights
}


/* Writes a <color>R G B</color> element. */
static void ColladaWriteCommonColorElement(xmlTextWriterPtr w, char *name,
    char *sid, char *value)
{
  xmlTextWriterStartElement(w, BAD_CAST name);
  xmlTextWriterStartElement(w, BAD_CAST "color");
  if(sid) {
    xmlTextWriterWriteAttribute(w, BAD_CAST "sid", BAD_CAST sid);
  }
  else {
    xmlTextWriterWriteAttribute(w, BAD_CAST "sid", BAD_CAST name);
  }
  xmlTextWriterWriteString(w, BAD_CAST value);
  xmlTextWriterEndElement(w);  // color
  xmlTextWriterEndElement(w);  // element
}


/* Writes a <float>value</float> element. */
static void ColladaWriteCommonFloatElement(xmlTextWriterPtr w, char *name,
    char *sid, char *value)
{
  xmlTextWriterStartElement(w, BAD_CAST name);
  xmlTextWriterStartElement(w, BAD_CAST "float");
  if(sid) {
    xmlTextWriterWriteAttribute(w, BAD_CAST "sid", BAD_CAST sid);
  }
  else {
    xmlTextWriterWriteAttribute(w, BAD_CAST "sid", BAD_CAST name);
  }
  xmlTextWriterWriteString(w, BAD_CAST value);
  xmlTextWriterEndElement(w);  // float
  xmlTextWriterEndElement(w);  // element
}


/* Writes a set of <phong> shader parameter elements. */
static void ColladaWritePhongEffect(xmlTextWriterPtr w, char *id,
    float amb, float spec, float shin,
    float trans, float iref)
{
  char *value = (char *) malloc(100 * sizeof(char));

  xmlTextWriterStartElement(w, BAD_CAST "effect");
  xmlTextWriterWriteAttribute(w, BAD_CAST "id",
      BAD_CAST id);
  xmlTextWriterStartElement(w, BAD_CAST "profile_COMMON");
  xmlTextWriterStartElement(w, BAD_CAST "technique");
  xmlTextWriterWriteAttribute(w, BAD_CAST "sid",
      BAD_CAST "common");

  xmlTextWriterStartElement(w, BAD_CAST "phong");

  if (amb > PRECISION) {
    sprintf(value, "0.5 0.5 0.5 %5.3f", amb);
    ColladaWriteCommonColorElement(w, (char *)"ambient", NULL, value);
  }

  if (spec > PRECISION) {
    sprintf(value, "0.5 0.5 0.5 %5.3f", spec);
    ColladaWriteCommonColorElement(w, (char *)"specular", NULL, value);
  }

  if(shin > PRECISION) {
    sprintf(value, "%5.3f", shin);
    ColladaWriteCommonFloatElement(w, (char *)"shininess", NULL, value);
  }

  if(trans > PRECISION) {
    sprintf(value, "%5.3f", trans);
    ColladaWriteCommonFloatElement(w, (char *)"transparency", NULL, value);
  }

  if(iref > PRECISION) {
    sprintf(value, "%5.3f", iref);
    ColladaWriteCommonFloatElement(w, (char *)"index_of_refraction", NULL, value);
  }

  xmlTextWriterEndElement(w);  // phong
  xmlTextWriterEndElement(w);  // technique
  xmlTextWriterEndElement(w);  // profile_COMMON
  xmlTextWriterEndElement(w);  // effect

  free(value);
}


/* Writes a <material> element as an instance of a specific <effect> by URL. */
static void ColladaWriteInstanceEffectMaterial(xmlTextWriterPtr w,
    char *id, char *url)
{
  xmlTextWriterStartElement(w, BAD_CAST "material");
  xmlTextWriterWriteAttribute(w, BAD_CAST "id", BAD_CAST id);
  xmlTextWriterStartElement(w, BAD_CAST "instance_effect");
  xmlTextWriterWriteAttribute(w, BAD_CAST "url", BAD_CAST url);
  xmlTextWriterEndElement(w);  // instance_effect
  xmlTextWriterEndElement(w);  // material
}


/* Opens a <geometry id="..."><mesh> element. */
static void ColladaBeginGeometryMesh(xmlTextWriterPtr w, int geom){
  xmlTextWriterStartElement(w, BAD_CAST "geometry");
  xmlTextWriterWriteFormatAttribute(w, BAD_CAST "id", "geom%i", geom);
  xmlTextWriterStartElement(w, BAD_CAST "mesh");
}


/* Closes a </mesh></geometry> element. */
static void ColladaEndGeometryMesh(xmlTextWriterPtr w){
  xmlTextWriterEndElement(w);  // mesh
  xmlTextWriterEndElement(w);  // geometry
}


/* Writes the <library_effects> element, including transparency values. */
static void ColladaWriteLibraryEffects(xmlTextWriterPtr w, PyMOLGlobals *G,
    int trans_len, float *trans)
{
  xmlTextWriterStartElement(w, BAD_CAST "library_effects");

  float amb = SettingGetGlobal_f(G, cSetting_ambient);
  if(amb > 0.5){
    amb = 0.5;
  }

  float spec = SettingGetGlobal_f(G, cSetting_specular_intensity);

  float shin_factor = 5.0f;
  float shin = SettingGetGlobal_f(G, cSetting_shininess) / shin_factor;

  /* Default effect */
  ColladaWritePhongEffect(w, (char *)"default-effect", amb, spec, shin, 1, 1);

  /* Background effect */
  ColladaWritePhongEffect(w, (char *)"bg-effect", 0.5f, 0, 0, 0, 0);

  /* Transparency effects */
  int i;
  char *name = (char *) malloc(100 * sizeof(char));
  for (i = 0; i < trans_len; i++) {
    sprintf(name, "transparency-%1.2f-effect", trans[i]);
    ColladaWritePhongEffect(w, name, amb, spec, shin, 1 - trans[i], 1);
#if COLLADA_DEBUG
    printf("Wrote Phong effect: %s\n", name);
#endif
  }
  xmlTextWriterEndElement(w);  // library_effects
  free(name);
}


/* Writes the <library_materials> element, including transparent materials. */
static void ColladaWriteLibraryMaterials(xmlTextWriterPtr w, int trans_len, float *trans)
{
  xmlTextWriterStartElement(w, BAD_CAST "library_materials");

  /* Default material */
  ColladaWriteInstanceEffectMaterial(w, (char *)"default-material",
      (char *)"#default-effect");
  ColladaWriteInstanceEffectMaterial(w, (char *)"bg-material",
      (char *)"#bg-effect");

  /* Transparent materials */
  int i;
  char *name = (char *) malloc(100 * sizeof(char));
  char *url = (char *) malloc(100 * sizeof(char));
  for (i = 0; i < trans_len; i++) {

    sprintf(name, "transparency-%1.2f-material", trans[i]);
    sprintf(url, "#transparency-%1.2f-effect", trans[i]);

    ColladaWriteInstanceEffectMaterial(w, name, url);
#if COLLADA_DEBUG
    printf("Wrote material: %s for effect: %s\n", name, url);
#endif

  }
  xmlTextWriterEndElement(w);  // library_materials
  free(name);
  free(url);
}


/*
 * Writes a 3-dimensional <source> element, e.g. XYZ or RGB, to define
 * position, normal, or color source data.
 *
 * Note: "count" should be the number of accessible elements (e.g. vertices,
 * normals; NOT individual float values).  The length of the float array will
 * be 3x this value.
 */
static void ColladaWrite3DSource(xmlTextWriterPtr w, int geom, char *name, int count,
    char *data_str, char *dim)
{
  xmlTextWriterStartElement(w, BAD_CAST "source");
  xmlTextWriterWriteFormatAttribute(w, BAD_CAST "id",
      "geom%i-mesh-%s", geom, name);

  xmlTextWriterStartElement(w, BAD_CAST "float_array");
  xmlTextWriterWriteFormatAttribute(w, BAD_CAST "id",
      "geom%i-mesh-%s-array", geom, name);
  xmlTextWriterWriteFormatAttribute(w, BAD_CAST "count", "%i", count * 3);
  xmlTextWriterWriteString(w, BAD_CAST data_str);
  xmlTextWriterEndElement(w);  // float_array

  xmlTextWriterStartElement(w, BAD_CAST "technique_common");
  xmlTextWriterStartElement(w, BAD_CAST "accessor");
  xmlTextWriterWriteFormatAttribute(w, BAD_CAST "source",
      "#geom%i-mesh-%s-array", geom, name);
  xmlTextWriterWriteFormatAttribute(w, BAD_CAST "count", "%i", count);
  xmlTextWriterWriteAttribute(w, BAD_CAST "stride", BAD_CAST "3");

  char n[2] = "\0";
  sprintf(n, "%c", dim[0]);
  xmlTextWriterStartElement(w, BAD_CAST "param");
  xmlTextWriterWriteAttribute(w, BAD_CAST "name", BAD_CAST n);
  xmlTextWriterWriteAttribute(w, BAD_CAST "type", BAD_CAST "float");
  xmlTextWriterEndElement(w);

  sprintf(n, "%c", dim[1]);
  xmlTextWriterStartElement(w, BAD_CAST "param");
  xmlTextWriterWriteAttribute(w, BAD_CAST "name", BAD_CAST n);
  xmlTextWriterWriteAttribute(w, BAD_CAST "type", BAD_CAST "float");
  xmlTextWriterEndElement(w);

  sprintf(n, "%c", dim[2]);
  xmlTextWriterStartElement(w, BAD_CAST "param");
  xmlTextWriterWriteAttribute(w, BAD_CAST "name", BAD_CAST n);
  xmlTextWriterWriteAttribute(w, BAD_CAST "type", BAD_CAST "float");
  xmlTextWriterEndElement(w);

  xmlTextWriterEndElement(w);  // accessor
  xmlTextWriterEndElement(w);  // technique_common
  xmlTextWriterEndElement(w);  // source
}


/* Write a <vertices> element using the current geometry's positions source. */
static void ColladaWriteVertices(xmlTextWriterPtr w, int geom)
{
  xmlTextWriterStartElement(w, BAD_CAST "vertices");
  xmlTextWriterWriteFormatAttribute(w, BAD_CAST "id",
      "geom%i-mesh-vertices", geom);
  xmlTextWriterStartElement(w, BAD_CAST "input");
  xmlTextWriterWriteAttribute(w, BAD_CAST "semantic",
      BAD_CAST "POSITION");
  xmlTextWriterWriteFormatAttribute(w, BAD_CAST "source",
      "#geom%i-mesh-positions", geom);
  xmlTextWriterEndElement(w);  // input
  xmlTextWriterEndElement(w);  // vertices
}


/* Writes vertex, normal, and color <input> elements for the given geometry. */
static void ColladaWriteVNCInputs(xmlTextWriterPtr w, int geom)
{
  xmlTextWriterStartElement(w, BAD_CAST "input");
  xmlTextWriterWriteAttribute(w, BAD_CAST "offset", BAD_CAST "0");
  xmlTextWriterWriteAttribute(w, BAD_CAST "semantic",
      BAD_CAST "VERTEX");
  xmlTextWriterWriteFormatAttribute(w, BAD_CAST "source",
      "#geom%i-mesh-vertices", geom);
  xmlTextWriterEndElement(w);  // input

  xmlTextWriterStartElement(w, BAD_CAST "input");
  xmlTextWriterWriteAttribute(w, BAD_CAST "offset", BAD_CAST "1");
  xmlTextWriterWriteAttribute(w, BAD_CAST "semantic",
      BAD_CAST "NORMAL");
  xmlTextWriterWriteFormatAttribute(w, BAD_CAST "source",
      "#geom%i-mesh-normals", geom);
  xmlTextWriterEndElement(w);  // input

  xmlTextWriterStartElement(w, BAD_CAST "input");
  xmlTextWriterWriteAttribute(w, BAD_CAST "offset", BAD_CAST "2");
  xmlTextWriterWriteAttribute(w, BAD_CAST "semantic",
      BAD_CAST "COLOR");
  xmlTextWriterWriteFormatAttribute(w, BAD_CAST "source",
      "#geom%i-mesh-colors", geom);
  xmlTextWriterEndElement(w);  // input
}


/* Writes a <vcount> element with the given string as its value. */
static void ColladaWriteVCountElement(xmlTextWriterPtr w, char *vcount_str)
{
  xmlTextWriterStartElement(w, BAD_CAST "vcount");
  xmlTextWriterWriteFormatString(w, "%s", vcount_str);
  xmlTextWriterEndElement(w);
}


/* Writes a <vcount> element for the given number of triangles. */
static void ColladaWriteTrianglesVCountElement(xmlTextWriterPtr w, int tri)
{
  int i;
  char *vc_str = VLACalloc(char, 1000);
  ov_size cc = 0;
  char *next = (char *) malloc(10 * sizeof(char));

  for(i = 0; i < tri; i++){
    sprintf(next, "3 ");  // all triangles
    UtilConcatVLA(&vc_str, &cc, next);
  }

  ColladaWriteVCountElement(w, vc_str);

  VLAFree(vc_str);
  free(next);
}


/* Writes a <p> element with the given string as its value. */
static void ColladaWritePrimitiveElement(xmlTextWriterPtr w, char *p_str)
{
  xmlTextWriterStartElement(w, BAD_CAST "p");
  xmlTextWriterWriteFormatString(w, "%s", p_str);
  xmlTextWriterEndElement(w);  // p
}


/* Writes a <triangles> element with the current primitive (<p>) string. */
static void ColladaWriteTrianglesElement(xmlTextWriterPtr w, int geom, int tri,
    char *p_str, int mode=0)
{
  xmlTextWriterStartElement(w, BAD_CAST (mode == 1 ? "polylist" : "triangles"));
  xmlTextWriterWriteFormatAttribute(w, BAD_CAST "count", "%i", tri);
  xmlTextWriterWriteFormatAttribute(w, BAD_CAST "material",
      "geom%i-material", geom);

  ColladaWriteVNCInputs(w, geom);

  if (mode == 1) {
    ColladaWriteTrianglesVCountElement(w, tri);
  }

  ColladaWritePrimitiveElement(w, p_str);

  xmlTextWriterEndElement(w);  // triangles
}


/* Writes a <polylist> element consisting entirely of triangles. */
static void ColladaWriteTrianglesPolylistElement(xmlTextWriterPtr w, int geom, int tri,
    char *p_str)
{
  ColladaWriteTrianglesElement(w, geom, tri, p_str, 1);
}


/*
 * Opens a <polylist> element, including <input>s.  Must be followed by at
 * least one <p> element and then closed.
 */
static void ColladaBeginPolylistElement(xmlTextWriterPtr w, int geom, int count)
{
  xmlTextWriterStartElement(w, BAD_CAST "polylist");
  xmlTextWriterWriteFormatAttribute(w, BAD_CAST "count", "%i", count);
  xmlTextWriterWriteFormatAttribute(w, BAD_CAST "material",
      "geom%i-material", geom);

  ColladaWriteVNCInputs(w, geom);
}


/* Closes a <polylist> element. */
static void ColladaEndPolylistElement(xmlTextWriterPtr w)
{
  xmlTextWriterEndElement(w);
}


/*
 * Opens a <tristrips> element, including inputs.  Must be followed by at
 * least one <p> element.  Multiple <p> elements can be used to have
 * multiple triangle strips using the same vertices.
 */
static void ColladaBeginTristripsElement(xmlTextWriterPtr w, int geom, int num_strips)
{
  xmlTextWriterStartElement(w, BAD_CAST "tristrips");
  xmlTextWriterWriteFormatAttribute(w, BAD_CAST "count",
      "%i", num_strips);
  xmlTextWriterWriteFormatAttribute(w, BAD_CAST "material",
      "geom%i-material", geom);

  ColladaWriteVNCInputs(w, geom);
}


/* Closes a <tristrips> element. */
static void ColladaEndTristripsElement(xmlTextWriterPtr w)
{
  xmlTextWriterEndElement(w);
}


#if 0
/*
 * Opens a <trifans> element, including inputs.  Must be followed by at
 * least one <p> element.
 */
static void ColladaBeginTrifansElement(xmlTextWriterPtr w, int geom, int num_fans)
{
  xmlTextWriterStartElement(w, BAD_CAST "trifans");
  xmlTextWriterWriteFormatAttribute(w, BAD_CAST "count",
      "%i", num_fans);
  xmlTextWriterWriteFormatAttribute(w, BAD_CAST "material",
      "geom%i-material", geom);

  ColladaWriteVNCInputs(w, geom);
}


/* Closes a <trifans> element. */
static void ColladaEndTrifansElement(xmlTextWriterPtr w)
{
  xmlTextWriterEndElement(w);
}
#endif


/* Writes a complete <geometry> element for a triangle mesh. */
static void ColladaWriteMeshGeometry(xmlTextWriterPtr w, int geom,
    int pos, char* positions_str,
    int norm, char* normals_str,
    int col, char* colors_str,
    int tri, char* p_str, int mode)
{
  ColladaBeginGeometryMesh(w, geom);
  ColladaWrite3DSource(w, geom, (char *)"positions", pos, positions_str,
      (char *)"XYZ");
  ColladaWrite3DSource(w, geom, (char *)"normals", norm, normals_str,
      (char *)"XYZ");
  ColladaWrite3DSource(w, geom, (char *)"colors", col, colors_str,
      (char *)"RGB");
  ColladaWriteVertices(w, geom);

  ColladaWriteTrianglesElement(w, geom, tri, p_str, mode);

  ColladaEndGeometryMesh(w);
}

#endif // _HAVE_LIBXML

/* Generates COLLADA output and appends it to `vla_ptr`. */
void RayRenderCOLLADA(CRay * I, int width, int height,
    char **vla_ptr, float front, float back,
    float fov)
{
#ifndef _HAVE_LIBXML
  PRINTFB(I->G, FB_Ray, FB_Errors)
    " ColladaRender-Error: No libxml2 support, can't output COLLADA file.\n"
    ENDFB(I->G);
#elif !(defined(LIBXML_WRITER_ENABLED) && defined(LIBXML_OUTPUT_ENABLED))
  PRINTFB(I->G, FB_Ray, FB_Errors)
    " ColladaRender-Error: libxml2 library not properly configured " \
    "with writer and output support, can't output COLLADA file.\n"
    ENDFB(I->G);
#else

  char *vla = *vla_ptr;
  ov_size cc = 0;               /* character count */
  PyMOLGlobals *G = I->G;


  /*
   * Setting: geometry_export_mode
   * -----------------------------
   * 0 = Output everything. (default)
   * 1 = Output geometry and materials only; exclude lighting and camera
   *     information from scene.
   */
  int identity = (SettingGetGlobal_i(I->G, cSetting_geometry_export_mode) == 1);

  /*
   * Setting: collada_background_box
   * -------------------------------
   * 0 = Do not include a background box. (default)
   * 1 = Include the background box for more accurate reproduction of the scene.
   * NB: This setting is overridden by geometry_export_mode == 1, in which case
   * no background box will be generated.
   */
  int bgbox = SettingGetGlobal_i(I->G, cSetting_collada_background_box);

  /*
   * Setting: collada_geometry_mode
   * ------------------------------
   * 0 = Valid COLLADA 1.4.1. (default)
   * 1 = Blender-compatible (only <polylist> geometries are used).
   */
  int geom_mode = SettingGetGlobal_i(I->G, cSetting_collada_geometry_mode);
  if (geom_mode >= COLLADA_GEOM_MODE_COUNT || geom_mode < 0) {
    geom_mode = 0;
  }

  /*
   * Setting: collada_export_lighting
   * --------------------------------
   * 0 = No lighting information included in output.  Best for
   *     interactive scenes. (default)
   * 1 = Lighting information (<library_lights> and light <node>s) included
   *     in output.
   */
  int lighting = (SettingGetGlobal_i(I->G, cSetting_collada_export_lighting) == 1);
  int lc = SettingGetGlobal_i(I->G, cSetting_light_count);


  /* Ray trace */
  RayExpandPrimitives(I);
  RayTransformFirst(I, 0, identity);
  RayComputeBox(I);

  /* initialize for XML writing */
  int rc;  // return codes for error handling
  xmlTextWriterPtr w;
  xmlDocPtr doc;
  xmlChar *tmp;
  int buffersize;

  /* Create a new XML DOM tree, to which the COLLADA document will be
   * written */
  doc = xmlNewDoc(BAD_CAST XML_VERSION);
  if (doc == NULL) {
    printf("ColladaRender: Error creating the xml document tree (xmlNewDoc).\n");
    return;
  }

  /* Create a new XmlWriter */
  w = xmlNewTextWriterTree(doc, NULL, 0);
  if (w == NULL) {
    printf("ColladaRender: Error creating the xml writer (xmlNewTextWriterTree).\n");
    return;
  }

  /* Start the XML document */
  rc = xmlTextWriterStartDocument(w, NULL, XML_ENCODING, NULL);
  if (rc < 0) {
    printf("ColladaRender: Error at xmlTextWriterStartDocument\n");
    return;
  }

  /* Begin COLLADA */
  xmlTextWriterStartElement(w, BAD_CAST "COLLADA");
  xmlTextWriterWriteAttribute(w, BAD_CAST "xmlns",
      BAD_CAST "http://www.collada.org/2005/11/COLLADASchema");
  xmlTextWriterWriteAttribute(w, BAD_CAST "version", BAD_CAST "1.4.1");

  /* Asset */
  ColladaWriteAssetElement(w);

  /* Cameras */
  if (!identity) {
    ColladaWriteLibraryCameras(w, G, width, height, fov, front, back);
  }

  /* Lights */
  if (!identity && lighting) {
    ColladaWriteLibraryLights(w, I, G);
  }

  /* Geometries */
  int geom = 0;

  /*
   * Track transparency levels used on a per-geom basis to be added to
   * additional library_effects and library_materials elements.
   */
  float *trans = (float *) malloc(sizeof(float));  // store transparency values
  int trans_len = 0;

  /* Associate geometries with transparency values. */
  int *geom_trans = (int *) malloc(sizeof(int));
  int geom_trans_len = 1;

  {
    xmlTextWriterStartElement(w, BAD_CAST "library_geometries");

    int a, tri, pos, norm, col;
    CPrimitive *prim;
    int mesh_obj = false;
    int largest_dim = 10;
    float cur_trans = 0.0f;

    /* Initialize data string VLAs and character counts. */
    char *positions_str = VLACalloc(char, 1000);
    char   *normals_str = VLACalloc(char, 1000);
    char    *colors_str = VLACalloc(char, 1000);
    char         *p_str = VLACalloc(char, 1000);
    ov_size  pos_str_cc = 0;
    ov_size norm_str_cc = 0;
    ov_size  col_str_cc = 0;
    ov_size    p_str_cc = 0;

    /*
     * Loop through primitives, plus one extra time to finish writing a
     * triangle mesh if necessary.
     */
    for (a = 0; a <= I->NPrimitive; a++) {
      prim = I->Primitive + a;

      /* Handle transitions between/after triangle meshes. */
      if (mesh_obj) {
        if (a == I->NPrimitive ||
            prim->type != cPrimTriangle ||
            TRANS_PRECISION <= fabsf(prim->trans - cur_trans))
        {

          /*
           * Not a triangle primitive, but the previous mesh is still
           * active; OR, transparency level is different; OR the final
           * primitive was part of a triangle mesh that needs to be
           * closed.
           *
           * Write the previous triangle mesh from its data strings
           * and counters, reset mesh tracking variables.
           */

          ColladaWriteMeshGeometry(w, geom, pos, positions_str,
              norm, normals_str, col, colors_str, tri, p_str, geom_mode);

          geom += 1;
          mesh_obj = false;
        }
      }
      if(!mesh_obj) {
        /* First triangle primitive or any other primititve. */

        /* Allocate data strings */
        pos_str_cc = 0;
        norm_str_cc = 0;
        col_str_cc = 0;
        p_str_cc = 0;


        if(a < I->NPrimitive){

          /* Reset counters */
          tri = 0;
          pos = 0;
          norm = 0;
          col = 0;
          cur_trans = prim->trans;

          /* Increase geom_trans with geom, realloc exponentially. */
          while (geom >= geom_trans_len) {
            geom_trans_len *= 2;
            geom_trans = (int *) realloc(geom_trans,
                (geom_trans_len) * sizeof(int));
#if COLLADA_DEBUG
            printf("  increased geom_trans_len to %i\n", geom_trans_len);
#endif
          }

          /* Record transparency for each geometry. */
          int p = GetFloatPositionInArray(cur_trans, trans,
              trans_len, TRANS_PRECISION);

          if (0 > p) {
            /* Not found, add new transparency level. */
            trans = (float *) realloc(trans, (trans_len + 1) * sizeof(float));
            trans[trans_len] = cur_trans;
#if COLLADA_DEBUG
            printf("  Added new trans[%i] = %1.2f\n", trans_len,
                trans[trans_len]);
#endif
            geom_trans[geom] = trans_len;
            trans_len++;
          } else {
            /* Reference existing transparency level. */
            geom_trans[geom] = p;
          }

#if COLLADA_DEBUG
          printf("geom_trans[%i] = %i\n", geom, geom_trans[geom]);
#endif

          if(prim->type == cPrimTriangle){

            /* Track the open triangle mesh. */
            mesh_obj = true;
          }
        }
        else {
          /*
           * No active mesh object, and the last primitive has already been
           * processed.
           */

          /* Generate bounding box geometry for background. */
          /* Only if collada_background_box == 1 and geometry_export_mode == 0 */
          if (!identity && bgbox) {
            /* Ensure camera is inside bounding box */
            int i;
            for (i = 0; i < 3; i++) {
              if (fabsf(I->Pos[i]) > largest_dim) {
                largest_dim = fabsf(I->Pos[i]);
              }
            }

            /* Leave plenty of room */
            largest_dim *= 100;

            /* Allocate data strings */
            pos_str_cc = 0;
            norm_str_cc = 0;
            col_str_cc = 0;
            p_str_cc = 0;

            int cube_coords[24] = {
              1,  1,  1,
              1, -1,  1,
              -1, -1,  1,
              -1,  1,  1,
              -1,  1, -1,
              -1, -1, -1,
              1, -1, -1,
              1,  1, -1 };

            int cube_face_verts[24] = {
              0, 1, 2, 3,
              3, 2, 5, 4,
              4, 5, 6, 7,
              7, 6, 1, 0,
              0, 3, 4, 7,
              1, 6, 5, 2 };

            char *next = (char *) malloc(200 * sizeof(int));

            /* positions */
            for(i = 0; i < 24; i++){
              sprintf(next, "%i ", cube_coords[i] * largest_dim);
              UtilConcatVLA(&positions_str, &pos_str_cc, next);
            }

            /* normals */
            for(i = 0; i < 24; i++){
              sprintf(next, "%i ", -cube_coords[i]);
              UtilConcatVLA(&normals_str, &norm_str_cc, next);
            }

            /* color */
            const float *bg_color;
            bg_color = ColorGet(G, SettingGet_color(G, NULL, NULL, cSetting_bg_rgb));

            sprintf(next, "%6.4f %6.4f %6.4f", bg_color[0], bg_color[1], bg_color[2]);
            UtilConcatVLA(&colors_str, &col_str_cc, next);

            /* p */
            for(i = 0; i < 6; i++){
              sprintf(next, "%i %i 0 %i %i 0 %i %i 0 %i %i 0  ",
                  cube_face_verts[4 * i], cube_face_verts[4 * i],
                  cube_face_verts[4 * i + 1], cube_face_verts[4 * i + 1],
                  cube_face_verts[4 * i + 2], cube_face_verts[4 * i + 2],
                  cube_face_verts[4 * i + 3], cube_face_verts[4 * i + 3]);
              UtilConcatVLA(&p_str, &p_str_cc, next);
            }

            xmlTextWriterStartElement(w, BAD_CAST "geometry");
            xmlTextWriterWriteAttribute(w, BAD_CAST "id", BAD_CAST "geom-bg");
            xmlTextWriterStartElement(w, BAD_CAST "mesh");

            ColladaWrite3DSource(w, geom, (char *)"positions", 8,
                positions_str, (char *)"XYZ");
            ColladaWrite3DSource(w, geom, (char *)"normals", 8,
                normals_str, (char *)"XYZ");
            ColladaWrite3DSource(w, geom, (char *)"colors", 1,
                colors_str, (char *)"RGB");
            ColladaWriteVertices(w, geom);

            xmlTextWriterStartElement(w, BAD_CAST "polylist");
            xmlTextWriterWriteAttribute(w, BAD_CAST "count", BAD_CAST "6");
            xmlTextWriterWriteAttribute(w, BAD_CAST "material", BAD_CAST "geom-bg-material");

            ColladaWriteVNCInputs(w, geom);
            xmlTextWriterWriteElement(w, BAD_CAST "vcount", BAD_CAST "4 4 4 4 4 4");
            ColladaWritePrimitiveElement(w, p_str);
            xmlTextWriterEndElement(w);  // polylist

            xmlTextWriterEndElement(w);  // mesh
            xmlTextWriterEndElement(w);  // geometry

            free(next);

          }


          /* Finished with geometries. */
          break;
        }
      }

      /* Update largest dimension - assumes distance from origin for most
       * objects will be much greater than e.g. sphere/cylinder/cone radius
       * and camera won't be "too far" away. */
      {
        int i;
        for(i = 0; i < 3; i++) {
          switch (prim->type) {
            /* 3 vertices defined */
            case cPrimTriangle:
              if (largest_dim < prim->v3[i]) {
                largest_dim = prim->v3[i];
              }
              /* 2 vertices defined */
            case cPrimCone:
            case cPrimCylinder:
              if (largest_dim < prim->v2[i]) {
                largest_dim = prim->v2[i];
              }
              /* only 1 vertex defined */
            case cPrimSphere:
            default:
              if (largest_dim < prim->v1[i]) {
                largest_dim = prim->v1[i];
              }
              break;
          }
        }
      }

      /* Process the primitive according to its type. */
      switch(prim->type){
        case cPrimSphere:         // 1
          {
#if COLLADA_DEBUG > 1
            printf("Primitive %i: cPrimSphere\n", a);
#endif

            /* sphere_quality range: 0 to (NUMBER_OF_SPHERE_LEVELS - 1). */
            int sq = (SettingGetGlobal_i(I->G, cSetting_sphere_quality));

            SphereRec *sp;
            int i, j;
            int *q, *s;
            char *next = (char *) malloc(200 * sizeof(char));  // enough for 9 %6.4 floats

            sp = I->G->Sphere->Sphere[sq];
            q = sp->Sequence;
            s = sp->StripLen;


            /* Generate <source> elements */

            /* For each tristrip, add positions and normals to their respective
             * data strings. */
            for(i = 0; i < sp->NStrip; i++) {
#if COLLADA_DEBUG > 1
              printf("\ni = %i, StripLen = %i\n", i, *s);
#endif

              for(j = 0; j < (*s); j++) {
                /* positions */
                sprintf(next, "%6.4f %6.4f %6.4f ",
                    prim->v1[0] + (prim->r1 * sp->dot[*q][0]),
                    prim->v1[1] + (prim->r1 * sp->dot[*q][1]),
                    prim->v1[2] + (prim->r1 * sp->dot[*q][2]));
#if COLLADA_DEBUG > 1
                printf("positions j = %02i: %s\n", j, next);
#endif
                UtilConcatVLA(&positions_str, &pos_str_cc, next);

                /* normals */
                sprintf(next, "%6.4f %6.4f %6.4f ",
                    sp->dot[*q][0], sp->dot[*q][1], sp->dot[*q][2]);
                UtilConcatVLA(&normals_str, &norm_str_cc, next);
                q++;  // next position in sequence

              }
              s++;  // next strip

            }

            /* Colors: only one color per sphere. */
            sprintf(next, "%6.4f %6.4f %6.4f",
                prim->c1[0], prim->c1[1], prim->c1[2]);
            UtilConcatVLA(&colors_str, &col_str_cc, next);



            ColladaBeginGeometryMesh(w, geom);
            ColladaWrite3DSource(w, geom, (char *)"positions", sp->NVertTot,
                positions_str, (char *)"XYZ");
            ColladaWrite3DSource(w, geom, (char *)"normals", sp->NVertTot,
                normals_str, (char *)"XYZ");
            ColladaWrite3DSource(w, geom, (char *)"colors", 1,
                colors_str, (char *)"RGB");
            ColladaWriteVertices(w, geom);

            /* Reset to the beginning of the list of strips. */
            q = sp->Sequence;
            s = sp->StripLen;

            int vert_ct;
            vert_ct = 0;

            if (geom_mode == 1) {

              /* polylists */
              int tri = 0;

              for (i = 0; i < sp->NStrip; i++) {

                /* Each triangle has the last 2 vertices of previous one as its
                 * first 2 vertices.  Color is the same for all vertices. */
                for(j = 2; j < (*s); j++) {

                  if (j % 2 == 0) {
                    /* even, normals correct as-is */
                    sprintf(next, "%i %i 0 %i %i 0 %i %i 0 ",
                        vert_ct + j - 2, vert_ct + j - 2,
                        vert_ct + j - 1, vert_ct + j - 1,
                        vert_ct + j, vert_ct + j);
                  } else {
                    /* odd, switch order to maintain correct normal */
                    sprintf(next, "%i %i 0 %i %i 0 %i %i 0 ",
                        vert_ct + j - 1, vert_ct + j - 1,
                        vert_ct + j - 2, vert_ct + j - 2,
                        vert_ct + j, vert_ct + j);
                  }

                  UtilConcatVLA(&p_str, &p_str_cc, next);
                  q++;
                  tri++;
                }

                ColladaWriteTrianglesPolylistElement(w, geom, tri, p_str);

                vert_ct += (*s);
                s++;

                /* Get a fresh p_str for the next strip. */
                VLAFree(p_str);
                p_str = VLACalloc(char, 1000);
                p_str_cc = 0;
                tri = 0;

              }


            } else {

              /* tristrips */
              ColladaBeginTristripsElement(w, geom, sp->NStrip);

              /* For each strip, write a <p> element. */
              for(i = 0; i < sp->NStrip; i++) {

                /* Write the 2 initial vertices. */
                sprintf(next, "%i %i 0 %i %i 0 ",
                    vert_ct, vert_ct, vert_ct + 1, vert_ct + 1);
                UtilConcatVLA(&p_str, &p_str_cc, next);

                /* After the first 2, each new vertex adds a new triangle.
                 * Color is the same for all vertices. */
                for(j = 2; j < (*s); j++) {
                  sprintf(next, "%i %i 0 ", vert_ct + j, vert_ct + j);
                  UtilConcatVLA(&p_str, &p_str_cc, next);
                  q++;
                }

                ColladaWritePrimitiveElement(w, p_str);

                /* Start the next strip at the next vertex. */
                vert_ct += *s;
                s++;

                /* Get a fresh p_str for the next strip. */
                VLAFree(p_str);
                p_str = VLACalloc(char, 1000);
                p_str_cc = 0;

              }

              ColladaEndTristripsElement(w);
            }

            free(next);

            ColladaEndGeometryMesh(w);
            geom += 1;
            break;

          }  // cPrimSphere

        case cPrimCylinder:       // 2
        case cPrimSausage:        // 4
        case cPrimCone:           // 7
          {
#if COLLADA_DEBUG > 1
            if(prim->type == cPrimCylinder) {
              printf("Primitive %i: cPrimCylinder\n", a);
            } else if(prim->type == cPrimSausage) {
              printf("Primitive %i: cPrimSausage\n", a);
            } else {
              printf("Primitive %i: cPrimCone\n", a);
            }
#endif

#define DAE_MAX_EDGE 50

            /* Local vars */
            float d[3], t[3], p0[3], p1[3], p2[3];
            float v_buf[9], *v, vv1[3], vv2[3], vvv1[3], vvv2[3];
            float x[DAE_MAX_EDGE + 1], y[DAE_MAX_EDGE + 1];
            float overlap, overlap2, nub, nub2, r2 = 0.F;
            int nEdge, c; //colorFlag;
            int i;
            char *next = (char *) malloc(200 * sizeof(char));

            char *cap1_p_str = VLACalloc(char, 1000);
            ov_size cap1_cc = 0;

            bool stick_round_nub = SettingGetGlobal_i(I->G, cSetting_stick_round_nub);
            const int j_arr[] = {2, 3, 2};
            int nCapTri = 0;
            int captype[2] = {prim->cap1, prim->cap2};
            CGO *cgocap[2] = {NULL, NULL};

            if(prim->type == cPrimSausage) {
              captype[0] = captype[1] = cCylCapRound;
            }

            v = v_buf;
            nEdge = SettingGetGlobal_i(I->G, cSetting_stick_quality);
            overlap = prim->r1 * SettingGetGlobal_f(I->G, cSetting_stick_overlap);
            nub = prim->r1 * SettingGetGlobal_f(I->G, cSetting_stick_nub);
            if(prim->type == cPrimCone) {
              r2 = prim->r2;
              overlap2 = prim->r2 * SettingGetGlobal_f(I->G, cSetting_stick_overlap);
              nub2 = prim->r2 * SettingGetGlobal_f(I->G, cSetting_stick_nub);
            }
            else {
              r2 = prim->r1;
              overlap2 = overlap;
              nub2 = nub;
            }

            if(nEdge > DAE_MAX_EDGE)
              nEdge = DAE_MAX_EDGE;
            subdivide(nEdge, x, y);

            //colorFlag = (prim->c1 != prim->c2) && prim->c2;

            /* primary axis vector p0 */
            p0[0] = (prim->v2[0] - prim->v1[0]);
            p0[1] = (prim->v2[1] - prim->v1[1]);
            p0[2] = (prim->v2[2] - prim->v1[2]);

            normalize3f(p0);

            /* cap1 */
            copy3f(prim->v1, vv1);
            copy3f(vv1, vvv1);

            /* cap2 */
            copy3f(prim->v2, vv2);
            copy3f(vv2, vvv2);

            d[0] = (vv2[0] - vv1[0]);
            d[1] = (vv2[1] - vv1[1]);
            d[2] = (vv2[2] - vv1[2]);

            get_divergent3f(d, t);

            /* orthogonal vectors p1, p2 */
            cross_product3f(d, t, p1);
            normalize3f(p1);

            cross_product3f(d, p1, p2);
            normalize3f(p2);

            /* now we have a coordinate system */

            /* cap1 */
            if(captype[0] == cCylCapRound) {
              if (stick_round_nub) {
                cgocap[0] = CGONew(I->G);
                CGORoundNub(cgocap[0], prim->v1, p0, p1, p2, -1, nEdge, prim->r1);
              } else {
                for(i = 0; i < 3; i++) {
                  vv1[i] -= p0[i] * overlap;
                  vvv1[i] = vv1[i] - p0[i] * nub;
                }
              }
            }

            /* cap2 */
            if(captype[1] == cCylCapRound) {
              if (stick_round_nub) {
                cgocap[1] = CGONew(I->G);
                CGORoundNub(cgocap[1], prim->v2, p0, p1, p2, 1, nEdge, prim->r1);
              } else {
                for(i = 0; i < 3; i++) {
                  vv2[i] += p0[i] * overlap2;
                  vvv2[i] = vv2[i] + p0[i] * nub2;
                }
              }
            }

            /* colors */
            sprintf(next, "%6.4f %6.4f %6.4f %6.4f %6.4f %6.4f ",
                prim->c1[0], prim->c1[1], prim->c1[2],
                prim->c2[0], prim->c2[1], prim->c2[2]);
            UtilConcatVLA(&colors_str, &col_str_cc, next);

            /* Generate the data strings */
            char *vcount_str = VLACalloc(char, 100);
            ov_size vc_cc = 0;
            {

              /* Write values for each edge. The first edge is also the last
               * edge. */
              for(c = nEdge; c >= 0; c--) {

                /* vector only, not positioned yet */
                v[0] = p1[0] * x[c] + p2[0] * y[c];
                v[1] = p1[1] * x[c] + p2[1] * y[c];
                v[2] = p1[2] * x[c] + p2[2] * y[c];

                /* vertices */
                v[3] = vv1[0] + v[0] * prim->r1;
                v[4] = vv1[1] + v[1] * prim->r1;
                v[5] = vv1[2] + v[2] * prim->r1;
                v[6] = vv2[0] + v[0] * r2;
                v[7] = vv2[1] + v[1] * r2;
                v[8] = vv2[2] + v[2] * r2;


                /* positions */
                sprintf(next, "%6.4f %6.4f %6.4f %6.4f %6.4f %6.4f ",
                    v[3], v[4], v[5], v[6], v[7], v[8]);
                UtilConcatVLA(&positions_str, &pos_str_cc, next);

                /* normals: Both vertices have the same normal. */
                sprintf(next, "%6.4f %6.4f %6.4f ",
                    v[0], v[1], v[2]);
                UtilConcatVLA(&normals_str, &norm_str_cc, next);

                if (c > 0) {
                  /* vcount */
                  sprintf(next, "4 ");
                  UtilConcatVLA(&vcount_str, &vc_cc, next);

                  /* p for polylist */
                  sprintf(next, "%i %i %i %i %i %i %i %i %i %i %i %i ",
                      pos, norm, col,
                      pos + 1, norm, col + 1,
                      pos + 3, norm + 1, col,
                      pos + 2, norm + 1, col + 1);
                  UtilConcatVLA(&p_str, &p_str_cc, next);
                }
                pos += 2;
                norm += 1;

              }

              /* Caps */
              {
                /* add another vertex-normal-color set for the center of each
                 * cap if r > 0 */

#define CONE_MIN_RADIUS 10e-6f

                if(prim->r1 > CONE_MIN_RADIUS && !cgocap[0] && captype[0] != cCylCapNone){
                  /* positions */
                  sprintf(next, "%6.4f %6.4f %6.4f ",
                      vvv1[0], vvv1[1], vvv1[2]);
                  UtilConcatVLA(&positions_str, &pos_str_cc, next);

                  /* normals */
                  sprintf(next, "%6.4f %6.4f %6.4f ",
                      -p0[0], -p0[1], -p0[2]);
                  UtilConcatVLA(&normals_str, &norm_str_cc, next);

                  /* p */
                  for(i = 0; i < nEdge; i++) {
                    sprintf(next, "%i %i %i %i %i %i %i %i %i ",
                        pos, norm, col,
                        2*i, i, col,
                        2*i+2, i+1, col);
                    UtilConcatVLA(&cap1_p_str, &cap1_cc, next);
                    ++nCapTri;
                  }

                  ++pos;
                  ++norm;
                }

                if(r2 > CONE_MIN_RADIUS && !cgocap[1] && captype[1] != cCylCapNone){
                  /* positions */
                  sprintf(next, "%6.4f %6.4f %6.4f ",
                      vvv2[0], vvv2[1], vvv2[2]);
                  UtilConcatVLA(&positions_str, &pos_str_cc, next);

                  /* normals */
                  sprintf(next, "%6.4f %6.4f %6.4f ",
                      p0[0], p0[1], p0[2]);
                  UtilConcatVLA(&normals_str, &norm_str_cc, next);

                  /* p */
                  for(i = 0; i < nEdge; i++) {
                    /* reverse order for other end */
                    sprintf(next, "%i %i %i %i %i %i %i %i %i ",
                        pos, norm, col + 1,
                        2*i+3, i+1, col + 1,
                        2*i+1, i, col + 1);
                    UtilConcatVLA(&cap1_p_str, &cap1_cc, next);
                    ++nCapTri;
                  }

                  ++pos;
                  ++norm;
                }
              }

#if COLLADA_DEBUG > 1
              printf("positions: %s\n", positions_str);
              printf("normals:   %s\n", normals_str);
              printf("colors:    %s\n", colors_str);
              printf("p:         %s\n", p_str);
#endif

              for (i = 0; i < 2; ++i) {
                if (!cgocap[i])
                  continue;

                int pos0 = -1;

                for (auto it = cgocap[i]->begin(); !it.is_stop(); ++it) {
                  auto pc = it.data();
                  int op = it.op_code();
                  switch (op){
                    case CGO_BEGIN:
                      pos0 = pos;
                      break;
                    case CGO_NORMAL:
                      /* normals */
                      sprintf(next, "%6.4f %6.4f %6.4f ", pc[0], pc[1], pc[2]);
                      UtilConcatVLA(&normals_str, &norm_str_cc, next);
                      ++norm;
                      break;
                    case CGO_VERTEX:
                      /* positions */
                      sprintf(next, "%6.4f %6.4f %6.4f ", pc[0], pc[1], pc[2]);
                      UtilConcatVLA(&positions_str, &pos_str_cc, next);
                      ++pos;

                      /* unroll the triangle strip */
                      if (pos > pos0 + 2) {
                        const int * j = j_arr + ((pos - pos0) % 2);
                        sprintf(next, "%i %i %i %i %i %i %i %i %i ",
                            pos - j[0], norm - j[0], col + i,
                            pos - j[1], norm - j[1], col + i,
                            pos - 1,    norm - 1,    col + i);
                        UtilConcatVLA(&cap1_p_str, &cap1_cc, next);
                        ++nCapTri;
                      }

                      break;
                  }
                }

                CGOFree(cgocap[i]);
              }
            }

            col += 2;

            ColladaBeginGeometryMesh(w, geom);
            ColladaWrite3DSource(w, geom, (char *)"positions", pos,
                positions_str, (char *)"XYZ");
            ColladaWrite3DSource(w, geom, (char *)"normals", norm,
                normals_str, (char *)"XYZ");
            ColladaWrite3DSource(w, geom, (char *)"colors", col,
                colors_str, (char *)"RGB");
            ColladaWriteVertices(w, geom);

            /* polylist for cylinder shaft */
            ColladaBeginPolylistElement(w, geom, nEdge);
            ColladaWriteVCountElement(w, vcount_str);
            ColladaWritePrimitiveElement(w, p_str);
            ColladaEndPolylistElement(w);

            if (nCapTri) {
              ColladaWriteTrianglesElement(w, geom, nCapTri, cap1_p_str, geom_mode);
            }

            ColladaEndGeometryMesh(w);
            geom += 1;


            VLAFree(vcount_str);
            VLAFree(cap1_p_str);

            free(next);

            break;
          }  // cPrimCylinder

        case cPrimTriangle:       // 3
          {
#if COLLADA_DEBUG > 1
            printf("Primitive %i: cPrimTriangle\n", a);
#endif

            char *next = (char *) malloc(200 * sizeof(char));  // enough for 9 color floats

            /*** Positions ***/
            sprintf(next, "%6.4f %6.4f %6.4f %6.4f %6.4f %6.4f %6.4f %6.4f %6.4f ",
                prim->v1[0], prim->v1[1], prim->v1[2],
                prim->v2[0], prim->v2[1], prim->v2[2],
                prim->v3[0], prim->v3[1], prim->v3[2]);
            UtilConcatVLA(&positions_str, &pos_str_cc, (char *)next);

            /*** Normals ***/
            /* prim->n0 is a face normal; prim->n1/2/3 are vertex normals. */
            sprintf(next, "%6.4f %6.4f %6.4f %6.4f %6.4f %6.4f %6.4f %6.4f %6.4f ",
                prim->n1[0], prim->n1[1], prim->n1[2],
                prim->n2[0], prim->n2[1], prim->n2[2],
                prim->n3[0], prim->n3[1], prim->n3[2]);
            UtilConcatVLA(&normals_str, &norm_str_cc, (char *)next);

            /* Colors */
            /* R, G, B per vertex */
            sprintf(next, "%6.4f %6.4f %6.4f %6.4f %6.4f %6.4f %6.4f %6.4f %6.4f ",
                prim->c1[0], prim->c1[1], prim->c1[2],    // vertex 1
                prim->c2[0], prim->c2[1], prim->c2[2],    // vertex 2
                prim->c3[0], prim->c3[1], prim->c3[2]);   // vertex 3
            UtilConcatVLA(&colors_str, &col_str_cc, next);

            /* <p> indices */
            if (TriangleReverse(prim)) {
              sprintf(next, "%i %i %i %i %i %i %i %i %i ",
                  pos, norm, col,
                  pos + 2, norm + 2, col + 2,
                  pos + 1, norm + 1, col + 1);
            } else {
              sprintf(next, "%i %i %i %i %i %i %i %i %i ",
                  pos, norm, col,
                  pos + 1, norm + 1, col + 1,
                  pos + 2, norm + 2, col + 2);
            }
            UtilConcatVLA(&p_str, &p_str_cc, next);

            pos += 3;
            norm += 3;
            col += 3;
            tri += 1;

            free(next);

            if (ReachedXmlNodeSizeLimit(pos_str_cc, norm_str_cc,
                  col_str_cc, p_str_cc)) {

              /* xmlTextWriter uses the LibXML parser module, which enforces a node size
               * limit (10MB as of June 2014). Split oversize nodes into multiple nodes. */
#if COLLADA_DEBUG
              printf("Reached XML_NODE_SIZE_LIMIT (%i) at a=%i, geom=%i\n",
                  XML_NODE_SIZE_LIMIT, a, geom);
#endif

              /* Write the current mesh object and clean up */
              ColladaWriteMeshGeometry(w, geom, pos, positions_str,
                  norm, normals_str, col, colors_str, tri, p_str, geom_mode);

              geom += 1;
              mesh_obj = false;
            }

            break;
          }  // cPrimTriangle

          /* Character and Ellipsoid not implemented */
        case cPrimCharacter:      // 5
          {
#if COLLADA_DEBUG > 1
            printf("Primitive %i: cPrimCharacter\n", a);
#endif
            break;
          }  // cPrimCharacter

        case cPrimEllipsoid:      // 6
          {
#if COLLADA_DEBUG > 1
            printf("Primitive %i: cPrimEllipsoid\n", a);
#endif
            break;
          }  // cPrimEllipsoid

      }  // switch
    }  // for

    xmlTextWriterEndElement(w);  // library_geometries

    VLAFree(positions_str);
    VLAFree(normals_str);
    VLAFree(colors_str);
    VLAFree(p_str);
  }

  /* Effects */
  ColladaWriteLibraryEffects(w, G, trans_len, trans);

  /* Materials */
  ColladaWriteLibraryMaterials(w, trans_len, trans);

  /* Visual Scenes */
  {
    xmlTextWriterStartElement(w, BAD_CAST "library_visual_scenes");

    // TODO: support for multiple scenes (e.g. stored scenes)
    xmlTextWriterStartElement(w, BAD_CAST "visual_scene");
    xmlTextWriterWriteAttribute(w, BAD_CAST "id", BAD_CAST "scene");
    xmlTextWriterWriteAttribute(w, BAD_CAST "name", BAD_CAST "scene");

    /* One node per geometry element. */
    int i, j;
    char *mat = (char *) malloc(100 * sizeof(char));

    for(i = 0; i < geom; i++){
      xmlTextWriterStartElement(w, BAD_CAST "node");
      xmlTextWriterWriteFormatAttribute(w, BAD_CAST "id", "node-geom%i", i);

      /* set center screen pixel as origin */
      float offset[3];
      SceneOriginGet(G, offset);
      invert3f(offset);
      for (j = 0; j < 2; j++) {
        /* camera XY offset (Z handled in camera node) */
        offset[j] += I->Pos[j];
      }
      char *tmp = (char *) malloc(50 * sizeof(char));
      sprintf(tmp, "%5.3f %5.3f %5.3f", offset[0], offset[1], offset[2]);
      xmlTextWriterStartElement(w, BAD_CAST "translate");
      xmlTextWriterWriteAttribute(w, BAD_CAST "sid", BAD_CAST "location");
      xmlTextWriterWriteString(w, BAD_CAST tmp);
      xmlTextWriterEndElement(w);  // translate
      free(tmp);

      xmlTextWriterStartElement(w, BAD_CAST "rotate");
      xmlTextWriterWriteAttribute(w, BAD_CAST "sid", BAD_CAST "rotationZ");
      xmlTextWriterWriteString(w, BAD_CAST "0 0 1 0");
      xmlTextWriterEndElement(w);  // rotate

      xmlTextWriterStartElement(w, BAD_CAST "rotate");
      xmlTextWriterWriteAttribute(w, BAD_CAST "sid", BAD_CAST "rotationY");
      xmlTextWriterWriteString(w, BAD_CAST "0 1 0 0");
      xmlTextWriterEndElement(w);  // rotate

      xmlTextWriterStartElement(w, BAD_CAST "rotate");
      xmlTextWriterWriteAttribute(w, BAD_CAST "sid", BAD_CAST "rotationX");
      xmlTextWriterWriteString(w, BAD_CAST "1 0 0 0");
      xmlTextWriterEndElement(w);  // rotate

      xmlTextWriterStartElement(w, BAD_CAST "scale");
      xmlTextWriterWriteAttribute(w, BAD_CAST "sid", BAD_CAST "scale");
      xmlTextWriterWriteString(w, BAD_CAST "1 1 1");
      xmlTextWriterEndElement(w);  // scale

      xmlTextWriterStartElement(w, BAD_CAST "instance_geometry");
      xmlTextWriterWriteFormatAttribute(w, BAD_CAST "url", "#geom%i", i);
      xmlTextWriterStartElement(w, BAD_CAST "bind_material");
      xmlTextWriterStartElement(w, BAD_CAST "technique_common");
      xmlTextWriterStartElement(w, BAD_CAST "instance_material");
      xmlTextWriterWriteFormatAttribute(w, BAD_CAST "symbol",
          "geom%i-material", i);

      if (TRANS_PRECISION > geom_trans[i]) {
        sprintf(mat, "#default-material");
      }
      else {
        sprintf(mat, "#transparency-%1.2f-material", trans[geom_trans[i]]);
      }
      xmlTextWriterWriteAttribute(w, BAD_CAST "target", BAD_CAST mat);

      xmlTextWriterEndElement(w);  // instance_material
      xmlTextWriterEndElement(w);  // technique_common
      xmlTextWriterEndElement(w);  // bind_material
      xmlTextWriterEndElement(w);  // instance_geometry

      xmlTextWriterEndElement(w);  // node
    }
    free(mat);

    /* background geometry */
    if (!identity && bgbox) {
      xmlTextWriterStartElement(w, BAD_CAST "node");
      xmlTextWriterWriteAttribute(w, BAD_CAST "id", BAD_CAST "node-geom-bg");

      xmlTextWriterStartElement(w, BAD_CAST "translate");
      xmlTextWriterWriteAttribute(w, BAD_CAST "sid", BAD_CAST "location");
      xmlTextWriterWriteString(w, BAD_CAST "0 0 0");
      xmlTextWriterEndElement(w);  // translate

      xmlTextWriterStartElement(w, BAD_CAST "rotate");
      xmlTextWriterWriteAttribute(w, BAD_CAST "sid", BAD_CAST "rotationZ");
      xmlTextWriterWriteString(w, BAD_CAST "0 0 1 0");
      xmlTextWriterEndElement(w);  // rotate

      xmlTextWriterStartElement(w, BAD_CAST "rotate");
      xmlTextWriterWriteAttribute(w, BAD_CAST "sid", BAD_CAST "rotationY");
      xmlTextWriterWriteString(w, BAD_CAST "0 1 0 0");
      xmlTextWriterEndElement(w);  // rotate

      xmlTextWriterStartElement(w, BAD_CAST "rotate");
      xmlTextWriterWriteAttribute(w, BAD_CAST "sid", BAD_CAST "rotationX");
      xmlTextWriterWriteString(w, BAD_CAST "1 0 0 0");
      xmlTextWriterEndElement(w);  // rotate

      xmlTextWriterStartElement(w, BAD_CAST "scale");
      xmlTextWriterWriteAttribute(w, BAD_CAST "sid", BAD_CAST "scale");
      xmlTextWriterWriteString(w, BAD_CAST "1 1 1");
      xmlTextWriterEndElement(w);  // scale

      xmlTextWriterStartElement(w, BAD_CAST "instance_geometry");
      xmlTextWriterWriteAttribute(w, BAD_CAST "url", BAD_CAST "#geom-bg");
      xmlTextWriterStartElement(w, BAD_CAST "bind_material");
      xmlTextWriterStartElement(w, BAD_CAST "technique_common");
      xmlTextWriterStartElement(w, BAD_CAST "instance_material");
      xmlTextWriterWriteAttribute(w, BAD_CAST "symbol",
          BAD_CAST "geom-bg-material");
      xmlTextWriterWriteAttribute(w, BAD_CAST "target",
          BAD_CAST "#bg-material");  // TODO: custom materials
      xmlTextWriterEndElement(w);  // instance_material
      xmlTextWriterEndElement(w);  // technique_common
      xmlTextWriterEndElement(w);  // bind_material
      xmlTextWriterEndElement(w);  // instance_geometry

      xmlTextWriterEndElement(w);  // node
    }

    /* Camera node */
    char *matrix = (char *) malloc(400 * sizeof(char));  // enough for 16 floats
    if (!identity) {
      float m1[16];

      // m1 = RotMatrix * T(0, 0, -Pos[z])
      identity44f(m1);

      // camera z
      m1[11] = -I->Pos[2];

      // RotMatrix
      left_multiply44f44f(SceneGetMatrix(G), m1);

      sprintf(matrix,
          "\n%5.3f %5.3f %5.3f %5.3f "
          "\n%5.3f %5.3f %5.3f %5.3f "
          "\n%5.3f %5.3f %5.3f %5.3f "
          "\n%5.3f %5.3f %5.3f %5.3f ",
          m1[0], m1[1], m1[2], m1[3],
          m1[4], m1[5], m1[6], m1[7],
          m1[8], m1[9], m1[10], m1[11],
          m1[12], m1[13], m1[14], m1[15]);

      xmlTextWriterStartElement(w, BAD_CAST "node");
      xmlTextWriterWriteAttribute(w, BAD_CAST "id", BAD_CAST "node-camera");

      xmlTextWriterWriteElement(w, BAD_CAST "matrix", BAD_CAST matrix);

      xmlTextWriterStartElement(w, BAD_CAST "scale");
      xmlTextWriterWriteAttribute(w, BAD_CAST "sid", BAD_CAST "scale");
      xmlTextWriterWriteString(w, BAD_CAST "1 1 1");
      xmlTextWriterEndElement(w);  // scale

      xmlTextWriterStartElement(w, BAD_CAST "instance_camera");
      xmlTextWriterWriteAttribute(w, BAD_CAST "url", BAD_CAST "#camera");
      xmlTextWriterEndElement(w);  // instance_camera

      xmlTextWriterEndElement(w);  // node

    }

    /* Light nodes */
    if (!identity && lighting) {
      if(lc > 0){

        /* First light is the ambient light.  For light_count of 2-10,
         * we add a directional light, e.g. light, light2, etc. */

        int i;

        xmlTextWriterStartElement(w, BAD_CAST "node");
        xmlTextWriterWriteFormatAttribute(w, BAD_CAST "id", "node-ambient-light");
        xmlTextWriterWriteAttribute(w, BAD_CAST "type", BAD_CAST "NODE");
        xmlTextWriterStartElement(w, BAD_CAST "instance_light");
        xmlTextWriterWriteAttribute(w, BAD_CAST "url", BAD_CAST "#ambient-light");
        xmlTextWriterEndElement(w);  // instance_light

        xmlTextWriterEndElement(w);  // node

        const float *light_pos_ptr;
        float light_pos[3];
        for(i = 1; i < lc; i++){
          switch(i){
            case 1:
              light_pos_ptr = SettingGetGlobal_3fv(G, cSetting_light);
              break;
            case 2:
              light_pos_ptr = SettingGetGlobal_3fv(G, cSetting_light2);
              break;
            case 3:
              light_pos_ptr = SettingGetGlobal_3fv(G, cSetting_light3);
              break;
            case 4:
              light_pos_ptr = SettingGetGlobal_3fv(G, cSetting_light4);
              break;
            case 5:
              light_pos_ptr = SettingGetGlobal_3fv(G, cSetting_light5);
              break;
            case 6:
              light_pos_ptr = SettingGetGlobal_3fv(G, cSetting_light6);
              break;
            case 7:
              light_pos_ptr = SettingGetGlobal_3fv(G, cSetting_light7);
              break;
            case 8:
              light_pos_ptr = SettingGetGlobal_3fv(G, cSetting_light8);
              break;
            default:
              light_pos_ptr = SettingGetGlobal_3fv(G, cSetting_light9);
              break;
          }

          copy3f(light_pos_ptr, light_pos);
          normalize3f(light_pos);

          xmlTextWriterStartElement(w, BAD_CAST "node");
          xmlTextWriterWriteFormatAttribute(w, BAD_CAST "id", "node-pymol-light%i", i);
          xmlTextWriterWriteAttribute(w, BAD_CAST "type", BAD_CAST "NODE");

          char *lookat = (char *) malloc(sizeof(char) * 50);
          sprintf(lookat,
              "%5.3f %5.3f %5.3f "  // position of light on unit sphere
              "0 0 0 "  // pointed toward origin
              "0 1 0",  // positive y axis up (arbitrary)
              /* negative because PyMOL gives direction of light, not position */
              -light_pos[0], -light_pos[1], -light_pos[2]);

          xmlTextWriterWriteElement(w, BAD_CAST "lookat", BAD_CAST lookat);

          xmlTextWriterWriteElement(w, BAD_CAST "matrix", BAD_CAST matrix);

          xmlTextWriterStartElement(w, BAD_CAST "instance_light");
          xmlTextWriterWriteAttribute(w, BAD_CAST "url", BAD_CAST "#pymol-light");
          xmlTextWriterEndElement(w);  // instance_light

          xmlTextWriterEndElement(w);  // node
        }
      }
    }

    free(matrix);

    xmlTextWriterEndElement(w);  // visual_scene
    xmlTextWriterEndElement(w);  // library_visual_scenes
  }

  /* Scene */
  {
    xmlTextWriterStartElement(w, BAD_CAST "scene");
    xmlTextWriterStartElement(w, BAD_CAST "instance_visual_scene");
    xmlTextWriterWriteAttribute(w, BAD_CAST "url", BAD_CAST "#scene");
    xmlTextWriterEndElement(w);  // instance_visual_scene
    xmlTextWriterEndElement(w);  // scene
  }


  xmlTextWriterEndElement(w);  // COLLADA

  /* Close the document */
  rc = xmlTextWriterEndDocument(w);
  if (rc < 0) {
    printf("ColladaRender: Error at xmlTextWriterEndDocument.\n");
    // return;
  }

  /*
   * Dump the document to buffer
   */
  xmlDocDumpFormatMemory(doc, &tmp, &buffersize, 1);

  /* Concat buffer to VLA output */
  UtilConcatVLA(&vla, &cc, (char *) tmp);

  /*
   * Free associated memory.
   */
  free(trans);
  free(geom_trans);

  xmlFree(tmp);
  xmlFreeTextWriter(w);
  xmlFreeDoc(doc);

  *vla_ptr = vla;

#endif // _HAVE_LIBXML
}
