//Kevin Lynagh
//January 2010
//lightweb o3d instance that loads a single residue / cell (possibly neighbors)

o3djs.require('o3djs.util');
o3djs.require('o3djs.math');
o3djs.require('o3djs.rendergraph');
o3djs.require('o3djs.material');
o3djs.require('o3djs.primitives');


// global variables
var g_math;
var g_clock = 0.0;
var g_timeMult = 0.05; //the spinning rate

//keep a global hash of all the g_packs keyed by their div_id
var packs = new Object;
var clients = new Object;
var view_infos = new Object;
var g_o3ds = new Object;

$(document).ready(function(){
                    g_math = o3djs.math;
                  });

//in "real life" heavy atoms have a VdW radius of about 1.7 Angstroms
var atom_radius=0.3;
//RasMol cpk color scheme
var element_colors = new Array;
// Element      Color       Triple
// Carbon       light grey  [200,200,200]
element_colors['C'] = [0.784,0.784,0.784,1],
// Oxygen       red         [240,0,0]
element_colors['O'] = [0.941,0,0,1];
// Hydrogen     white       [255,255,255,1]
element_colors['H'] = [1,1,1,1];
element_colors['HH'] = element_colors['H'];
element_colors['HG'] = element_colors['H'];
element_colors['HD'] = element_colors['H'];
// Nitrogen     light blue  [143,143,255,1]
element_colors['N'] = [0.561,0.561,1,1];


function make_miniview(div_id, pdb_seqno){
  var div = $('#' + div_id);
  var g_client;
  var g_o3d;
  var g_pack;
  var view_info;
  var client_width, client_height;
  var top_transform;
  var atoms_transform;
  var bonds_transform;
  var cells_transform;

  //shared o3d shapes / materials
  var atom_material, bond_material, heavy_sph, hydrogen_sph, bond;
  if(packs[div_id] === undefined){//initialize it if we haven't made it yet
    o3djs.util.makeClients(initStep2, '', '', '', div_id);
  }else{
    reset_render_graph();
    if(pdb_seqno)
      get_residue_atoms(pdb_seqno);
  }



  function reset_render_graph(){
    g_pack = packs[div_id];
    g_client = clients[div_id];
    view_info = view_infos[div_id];
    g_o3d = g_o3ds[div_id];
    //put the transforms back into the namespace
    top_transform = g_pack.getObjects('top_transform', 'o3d.Transform')[0];
    atoms_transform = g_pack.getObjects('atoms_transform', 'o3d.Transform')[0];
    bonds_transform = g_pack.getObjects('bonds_transform', 'o3d.Transform')[0];
    cells_transform = g_pack.getObjects('cells_transform', 'o3d.Transform')[0];

    atom_material = g_pack.getObjects('atom_material', 'o3d.Material')[0];
    bond_material = g_pack.getObjects('bond_material', 'o3d.Material')[0];

    //clear out the old transformed shapes
    var stuff = jQuery.map([atoms_transform, bonds_transform], function(t){ return t.children;});
    jQuery.each(stuff, function(i, trans){
                  trans.parent = null;
                  g_pack.removeObject(trans);
                });

    //the cells_transform contains only shapes, no children
    jQuery.each(cells_transform.shapes, function(i, shape){
                  cells_transform.removeShape(shape);
                });


  }



  function initStep2(clientElements) {
    //this is the html element in the DOM
    var o3dElement = clientElements[0];
    g_client = o3dElement.client;
    clients[div_id] = g_client;
    g_o3d = o3dElement.o3d;
    g_o3ds[div_id] = g_o3d;
    client_width = g_client.width;
    client_height = g_client.height;


    //the pack holds all of the 3d objects in the scene
    g_pack = g_client.createPack();
    packs[div_id] = g_pack;

    view_info = o3djs.rendergraph.createBasicView(
      g_pack,
      g_client.root,
      g_client.renderGraphRoot);
    view_infos[div_id] = view_info;

    // Set up a simple perspective view.
    view_info.drawContext.projection = g_math.matrix4.perspective(
      g_math.degToRad(30), // 30 degree fov.
      client_width / client_height,
      1,                  // Near plane.
      5000);              // Far plane.

    // Set up our view transformation to look towards the world origin
    view_info.drawContext.view = g_math.matrix4.lookAt([0, 0, 0],  // eye
                                                       [0, 0, 0],  // target
                                                       [0, 0, 1]); // up)

    // set the background color to be the same as the page
    var rgb_str = $('body').css('background-color');
    var rgb = rgb_str.match(/\d+,\s*\d+,\s*\d+/)[0].split(',');
    if(rgb.length == 3){
    for(var i=0; i<3; i++){ rgb[i] = parseFloat(rgb[i]) / 255.0; }
    view_info.clearBuffer.clearColor = [rgb[0],rgb[1],rgb[2],1];
    }



    setup_shared_geometry();
    if(pdb_seqno)
      get_residue_atoms(pdb_seqno);
  }


  //Creates a material based on the given single color.
  //@param {!o3djs.math.Vector4} baseColor A 4-component vector with the R,G,B, and A components of a color.
  function create_material(baseColor) {
    // Create a new, empty Material object. The final 'true' allows it to be transparent
    var mat = o3djs.material.createConstantMaterial(g_pack, view_info, baseColor, true);
    return mat;
    //return o3djs.material.createBasicMaterial(g_pack, view_info, baseColor);
  }

  function setup_shared_geometry(){
    top_transform = g_pack.createObject('Transform');
    top_transform.parent = g_client.root;
    top_transform.name = 'top_transform';
    atoms_transform = g_pack.createObject('Transform');
    atoms_transform.name = 'atoms_transform';
    atoms_transform.parent = top_transform;

    //also construct a bonds_transform under it
    bonds_transform = g_pack.createObject('Transform');
    bonds_transform.parent = top_transform;
    bonds_transform.name = 'bonds_transform';
    cells_transform = g_pack.createObject('Transform');
    cells_transform.parent = top_transform;
    cells_transform.name = 'cells_transform';


    // Create a base material for the atoms
    atom_material = create_material([1, 1, 1, 1]);
    atom_material.name = 'atom_material';
    bond_material = create_material([1, 1, 1, 1]);
    bond_material.name = 'bond_material';

    // Create spheres to instance, the last argument is the quality of the sphere
    heavy_sph = o3djs.primitives.createSphere(g_pack, atom_material, atom_radius, 10, 8);
    heavy_sph.name = 'heavy_sph';
    hydrogen_sph = o3djs.primitives.createSphere(g_pack, atom_material, atom_radius*0.3, 10, 4);
    hydrogen_sph.name = 'hydrogen_sph';

    // Bonds; radius, height, 3 vertical divisions makes it a triangular prism, no divisions down the cylinder (i.e horizontal cuts)
    bond = o3djs.primitives.createCylinder(g_pack, bond_material, atom_radius*0.2, 1, 3, 1);
    bond.name = 'bond';
  }


  function get_residue_atoms(pdb_seqno) {
    $.getJSON( '/3d/json/' + pdb_seqno + '/atoms', function(r){
                var atoms = r.atoms;
                var alpha_carbon = atoms[' CA '];


                //we want to translate everything so the alpha carbon is at the origin
                top_transform.identity();
                top_transform.translate(-alpha_carbon.r[0],-alpha_carbon.r[1],-alpha_carbon.r[2]);

                //draw the atoms
                jQuery.each(atoms, function(atom_role, a){
                              var transform = g_pack.createObject('Transform');
                              transform.parent = atoms_transform;
                              //set the id to the atom serial
                              transform.createParam("atom_role", "o3d.ParamString").value = atom_role;

                              //sometimes hydrogens are like 'HH1', and sometimes more like ' HA '. Can't just match for H, since that's how you spell 'eta'.
                              if (atom_role.match(/^(H|\sH)/)){ //add hydrogens as small spheres
                                transform.addShape(g_pack.getObjects('hydrogen_sph', 'o3d.Shape')[0]);
                              }else{

                                transform.addShape(g_pack.getObjects('heavy_sph', 'o3d.Shape')[0]);
                              }
                              //move that sphere instance to the atom location
                              transform.translate(a.r[0],a.r[1],a.r[2]);

                              //add a color param (the slice makes a copy of the array, so we don't accidently modify the hardcoded color constants)

                              transform.createParam('emissive', 'ParamFloat4').value = (element_colors[atom_role.substring(0,2).replace(/^\s*/, '')] || [0, 0, 0, 1]).slice(0);
                            });

                //draw the bonds
                make_residue_bonds(r.residue_type, atoms);

                //draw the voronoi cell
                make_cell(r.cell_geometry);

                //look at the origin from the front (+Zhat)
                view_info.drawContext.view = g_math.matrix4.lookAt([0, 0, 30],  // eye
                                                                   [0, 0, 0],   //target
                                                                   [0, 1, 0]); // up


                // Set our render callback for animation.
                // This sets a function to be executed every time a frame is rendered.
                g_client.setRenderCallback(renderCallback);
              });

  }


  //create an o3d wireframe-ish shape from the 'lines' and 'vertexes' info from a json object
  function createWireframe(name, material, indicesArray, positionArray) {
    // Create a Shape object for the mesh.
    var wireframeShape = g_pack.createObject('Shape');
    wireframeShape.name = name;
    // Create the Primitive that will contain the geometry data
    var wireframePrimitive = g_pack.createObject('Primitive');

    // Create a StreamBank to hold the streams of vertex data.
    var streamBank = g_pack.createObject('StreamBank');

    // Assign the material that was passed in to the primitive.
    wireframePrimitive.material = material;

    // Assign the Primitive to the Shape.
    wireframePrimitive.owner = wireframeShape;

    // Assign the StreamBank to the Primitive.
    wireframePrimitive.streamBank = streamBank;

    //the wireframe is defined by a set of vertexes in the postiionArray and the lines connecting pairs of them, in the indicesArray
    wireframePrimitive.primitiveType = g_o3d.Primitive.LINELIST;
    wireframePrimitive.numberPrimitives = indicesArray.length / 2;
    wireframePrimitive.numberVertices = positionArray.length / 3;

    wireframePrimitive.createDrawElement(g_pack, null);

    var positionsBuffer = g_pack.createObject('VertexBuffer');
    var positionsField = positionsBuffer.createField('FloatField', 3);
    positionsBuffer.set(positionArray);

    var indexBuffer = g_pack.createObject('IndexBuffer');
    indexBuffer.set(indicesArray);

    // Associate the positions Buffer with the StreamBank.
    streamBank.setVertexStream(
      g_o3d.Stream.POSITION, // semantic: This stream stores vertex positions
      0,                     // semantic index: First (and only) position stream
      positionsField,        // field: the field this stream uses.
      0);                    // start_index: How many elements to skip in the field

    wireframePrimitive.indexBuffer = indexBuffer;
    return wireframeShape;
  }

  function make_cell(c){
    var mat = create_material( c.color || [1,1,1,1] );
    var wireframeShape = createWireframe('noname', mat, c.lines, c.vertexes);
    cells_transform.addShape(wireframeShape);
  }




  //a is a hash of atoms keyed by atom_role
  //nomenclature pulled manually from http://ligand-expo.rcsb.org/ld-search.html
  function make_residue_bonds(residue_type, a ){
    //backbone
    make_bond(a[' N  '], a[' CA '], 1);
    make_bond(a[' CA '], a[' C  '], 1);
    make_bond(a[' C  '], a[' O  '], 2);

    if(residue_type != 'gly'){
      make_bond(a[' CA '], a[' CB '], 1);
    }

    switch(residue_type){
    case 'arg':
      make_bond(a[' CB '], a[' CG '], 1);
      make_bond(a[' CG '], a[' CD '], 1);
      make_bond(a[' CD '], a[' NE '], 1);
      make_bond(a[' NE '], a[' CZ '], 1);
      make_bond(a[' CZ '], a[' NH1'], 1);
      make_bond(a[' CZ '], a[' NH2'], 1);

      break;
    }

  }

  //constructs a new transform, translates it to parent_atom, points bond to target
  function make_bond(parent_atom, target_atom, bond_order){
    var bond_transform = g_pack.createObject('Transform');
    bond_transform.parent = bonds_transform;

    var bond = g_pack.getObjects('bond', 'o3d.Shape')[0];

    //is this the most efficient way to make double bonds?
    if(bond_order == 2){
      var pi_sep = 0.2;
      //make a double bond by shifting the first one
      var left_bond = g_pack.createObject('Transform');
      var right_bond = g_pack.createObject('Transform');
      left_bond.parent = bond_transform;
      right_bond.parent = bond_transform;
      left_bond.addShape(bond);
      right_bond.addShape(bond);
      left_bond.translate([0, 0, -pi_sep/2]);
      right_bond.translate([0, 0, pi_sep/2]);
    }else{
      bond_transform.addShape(bond);
    }

    //move the bond to the parent_atom
    bond_transform.translate(parent_atom.r);


    //I'm pretty sure this isn't the most efficient way to "point" the cylinder to the other atom, but what I'll do is rotate it around the axis perpendicular to the vertical and the seperation vector (found using the cross product) until the cylinder, uh, falls onto the other atom
    var r_vec = g_math.subVector(target_atom.r, parent_atom.r);
    var r = g_math.distance(target_atom.r, parent_atom.r);
    var rot_vec = g_math.cross([0,1,0],g_math.normalize(r_vec));
    var angle = Math.acos(g_math.dot(g_math.divVectorScalar(r_vec, r), [0, 1, 0]));
    bond_transform.axisRotate(rot_vec,angle);
    bond_transform.translate(0, r/2, 0);
    bond_transform.scale(1,r,1); //the y-coordinate is parallel to the seperation vector, so we just scale by the seperation distance to stretch our cylinder of unit length to be between the two points
  }









  function renderCallback(renderEvent) {
    g_clock += renderEvent.elapsedTime * g_timeMult;
    //setClientSize(); //resize elements if we need to
    // Rotate around the vertical. Need to set the transform to .identity each time, otherwise the rotations pile up and things spin out of control
    g_client.root.identity();
    g_client.root.rotateY(5.0 * g_clock);
  }


  function setClientSize() {

    var newWidth  = parseInt(g_client.width);
    var newHeight = parseInt(g_client.height);
    if (newWidth != client_width || newHeight != client_height) {

      var size = Math.min(client_width, client_height);
      client_height = size;
      $(div).height(size);
      updateProjection();
    }
  }


  function updateProjection() {

    // Create a perspective projection matrix.
    view_info.drawContext.projection = g_math.matrix4.perspective(g_math.degToRad(30), client_width / client_height, 1, 5000);
  }

  //remove any callbacks so they don't get called after the page has unloaded.
  function uninit() {
    if (g_client) {
      g_client.cleanup();
    }
  }

}
