/// LSU EE 4702-1 (Fall 2018), GPU Programming
//
 ///  Homework 4 -- SOLUTION
//
 ///  Put solution in this file, most or all code in routine |fs_main|.
 //
 //   Assignment: https://www.ece.lsu.edu/koppel/gpup/2018/hw04.pdf

 //   Note: Code in this file is based on the code from
 //         links-shdr-sphere-true.cc.


// Specify version of OpenGL Shading Language.
//
#version 460 compatibility


layout ( binding = 1 ) buffer sr { mat4 sphere_rot[]; };
layout ( binding = 2 ) buffer spr { vec4 sphere_pos_rad[]; };
layout ( binding = 3 ) buffer sc { vec4 sphere_color[]; };

// Use this variable to debug your code. Press 'y' to toggle tryout.x
// and 'Y' to toggle debug_bool.y (between true and false).
//
layout ( location = 3 ) uniform bvec4 tryout;
layout ( location = 2 ) uniform int lighting_options;
layout ( location = 4 ) uniform float tryoutf;
layout ( location = 5 ) uniform bool mirrored;
layout ( location = 6 ) uniform mat4 trans_proj;
layout ( location = 7 ) uniform int opt_n_holes_eqt;
layout ( location = 8 ) uniform int opt_holes;

const int OHO_None = 0, OHO_Holes = 1, OHO_Lenses = 2;


#ifdef _VERTEX_SHADER_

// Interface block for vertex shader output / geometry shader input.
//
out Data_to_GS{ int vertex_id; };

void
vs_main()
{
  vertex_id = gl_VertexID;
}

#endif


#ifdef _GEOMETRY_SHADER_

in Data_to_GS { int vertex_id; } In[1];

out Data_to_FS
{
  flat int vertex_id;
  vec3 vertex_o;
};

// Type of primitive at geometry shader input.
//
layout ( points ) in;

// Type of primitives emitted geometry shader output.
//
layout ( triangle_strip, max_vertices = 4 ) out;

void
gs_main()
{
  vertex_id = In[0].vertex_id;
  vec4 pos_rad = sphere_pos_rad[vertex_id];
  vec3 ctr_o = pos_rad.xyz;
  float r = pos_rad.w;

  if ( mirrored ) ctr_o.y = -ctr_o.y;

  // Eye location in object space.
  vec3 e_o = vec3(gl_ModelViewMatrixInverse * vec4(0,0,0,1));

  // Vectors from eye to sphere center.
  vec3 ec_o = ctr_o - e_o;  // Eye to Center (of sphere).
  float ec_len = length(ec_o);
  vec3 ne = ec_o / ec_len;

  // Vectors orthogonal to ec_o.
  //
  vec3 atr_o = abs(ec_o);
  int min_idx = atr_o.x < atr_o.y ? ( atr_o.x < atr_o.z ? 0 : 2 )
    : ( atr_o.y < atr_o.z ? 1 : 2 );
  vec3 nx_raw = min_idx == 0 ? vec3(0,-ec_o.z,ec_o.y)
    : min_idx == 1 ? vec3(-ec_o.z,0,ec_o.x) : vec3(-ec_o.y,ec_o.x,0);
  vec3 nx = normalize(nx_raw);
  vec3 ny = cross(ne,nx);

  // Compute center of the limb, lmb_o, the circle formed by the most
  // distant visible parts of the sphere surface.
  float sin_theta = r / ec_len;
  float a = r * sin_theta;
  vec3 lmb_o = ctr_o - a * ne;

  // Compute axes to draw the limb.
  float b = r * sqrt( 1 - sin_theta * sin_theta );
  vec3 ax = b * nx;
  vec3 ay = b * ny;

  mat4 mvp = trans_proj * gl_ModelViewMatrix;

  // Emit a bounding square for the limb.
  for ( int i = -1; i < 2; i += 2 )
    for ( int j = -1; j < 2; j += 2 )
      {
        vertex_o = lmb_o + ax * i + ay * j;
        gl_Position = mvp * vec4(vertex_o,1);
        EmitVertex();
      }
  EndPrimitive();
}

#endif


#ifdef _FRAGMENT_SHADER_

uniform sampler2D tex_unit_0;

in Data_to_FS
{
  flat int vertex_id;
  vec3 vertex_o;
};

vec4 generic_lighting(vec4 vertex_e, vec4 color, vec3 normal_e);


void
fs_main()
{
  /// Put Homework 4 solution in this file.

  vec4 pos_rad = sphere_pos_rad[vertex_id];

  // Center of sphere in original object-space coördinates (oo) and possibly
  // reflected (o).
  vec3 ctr_oo = pos_rad.xyz;
  vec3 ctr_o = mirrored ? ctr_oo * vec3(1,-1,1) : ctr_oo;

  float rsq = pos_rad.w * pos_rad.w;

  // Eye location in object-space coördinates.
  vec3 e_o = gl_ModelViewMatrixInverse[3].xyz;

  // Prepare to compute intersection of ray from eye to through fragment with
  // sphere. That intersection is the point on the sphere corresponding
  // to this fragment.
  //
  vec3 ef = vertex_o - e_o;   // Eye to fragment.
  vec3 ce = e_o-ctr_o;
  float qfa = dot(ef,ef);
  float qfb = 2 * dot(ef,ce);
  float qfc = dot(ce,ce) - rsq;
  float discr = qfb*qfb - 4 * qfa * qfc;

  // If outside the limb, return.
  if ( discr < 0 ) discard;

  const float pi = 3.14159265359;
  const float two_pi = 2 * pi;

  const bool holes = opt_holes == OHO_Holes;    // If true sphere has holes
  const bool lenses = opt_holes == OHO_Lenses;
  const bool solid = !holes && !lenses;         // If true show complete sphere.


  /// SOLUTION  -- Organizing Front Surface / Back Surface Computation
  //
  //  Since this fragment can be on the front or the back of the sphere ..
  //  .. put code for finding the sphere coördinates in a loop.
  //
  //  First compute the coördinates of the front surface of the sphere ..
  //  .. if that point is visible (e.g., not in a hole), show it, ..
  //  .. otherwise find the coördinates of the back surface ..
  //  .. if that point is visible (e.g., not in a hole), show it, ..
  //  .. otherwise discard the fragment (don't use any coördinates).
  //
  //  To avoid code duplication ..
  //  .. put the sphere-coördinates computation in a loop.
  //

  vec4 sur_o, sur_oo;  // Global surface coördinates (front or back) of sphere.
  vec3 normal_oo, sur_l;
  bool front;          // If true, coördinates are of front of sphere.
  float theta, eta;    // Local angular coördinates of sphere.

  /// SOLUTION
  //
  //  Use loop to iterate over two possibilities ..
  //  .. front of sphere (dir=-1) and back of sphere (dir=1).
  //
  for ( float dir = -1;  dir <= 1;  dir+=2 )
    {
      front = dir == -1;

      // Finish computing coordinate of point for this fragment.
      //
      float t = ( -qfb + dir * sqrt( discr ) ) / ( 2 * qfa );

      // Compute true sphere surface coordinate.
      //
      sur_o = vec4(e_o + t * ef, 1);

      // Compute possibly reflected sphere surface coordinate.
      //
      sur_oo = mirrored ? sur_o * vec4(1,-1,1,1) : sur_o;
      normal_oo = normalize(sur_oo.xyz - ctr_oo);

      /// SOLUTION
      //
      //  Reverse normal if this is the back surface of the sphere.
      //
      if ( !front ) normal_oo = -normal_oo;

      // Use sphere-local coordinates to compute texture coordinates.
      //
      sur_l = normalize(mat3(sphere_rot[vertex_id]) * normal_oo);
      theta = atan(sur_l.x,sur_l.z);
      eta = acos(sur_l.y);

      /// SOLUTION
      //
      //  If this is just a normal sphere, we are finished.
      //
      if ( solid ) break;

      const float hole_frac = 0.8;
      const float radians_per_hole_eq = two_pi / opt_n_holes_eqt;
      const float hole_radius = 0.5 * hole_frac * radians_per_hole_eq;

      //
      /// SOLUTION -- Find coördinates of nearest hole.
      //
      //
      // Round surface eta coördinate to that of nearest hole.
      //
      float eta_hole = round( eta / radians_per_hole_eq ) * radians_per_hole_eq;
      //
      // Find distance around sphere at this latitude (value of eta).
      //
      float r = sin(eta_hole);
      //
      // Find number of holes that will fit around sphere at this latitude.
      //
      const float n_holes = floor( two_pi * r / radians_per_hole_eq );
      //
      // Don't try to find a hole if there are none this close to the pole.
      //
      if ( n_holes < 1 )
        {
          if ( holes ) break;
          if ( dir == 1 ) discard;
        }
      //
      // Round surface theta coördinate to that of nearest hole.
      //
      const float radians_per_hole = two_pi / n_holes;
      float theta_hole = round( theta / radians_per_hole ) * radians_per_hole;
      //
      // Compute local coördinates of center of hole.
      //
      vec3 hole_dir_l =
        vec3( r * sin(theta_hole), cos(eta_hole), r * cos(theta_hole) );

      /// SOLUTION -- Determine distance to hole and take appropriate action.
      //
      // Find distance from center of hole to sphere surface point ..
      //
      float dist = distance(hole_dir_l,sur_l);
      //
      // .. and check whether surface point is in hole.
      //
      bool found_hole = dist < hole_radius;
      //
      // If this part of the sphere is visible, break.
      //
      if ( lenses && found_hole || holes && !found_hole ) break;
      //
      // If we are already at the back of the sphere, don't show anything.
      //
      if ( !front ) discard;
    }

  vec3 normal_ee = normalize(gl_NormalMatrix * normal_oo);
  vec4 sur_ee = gl_ModelViewMatrix * sur_oo;

  // Compute clip-space depth. Take care so that compiler avoids full
  // matrix / vector multiplication.
  vec4 sur_e = gl_ModelViewMatrix * sur_o;
  vec4 sur_c = trans_proj * vec4(0,0,2*sur_e.z,1);
  gl_FragDepth = sur_c.z / sur_c.w;

  vec2 tcoord = vec2( ( 1.5 * pi + theta ) / two_pi, eta / pi );

  // Get filtered texel.
  //
  vec4 texel = front ? texture(tex_unit_0,tcoord) : vec4(1);

  vec4 color = sphere_color[vertex_id];
  vec4 color2 = front ? color : vec4(0.3,0,0,1);

  // Multiply filtered texel color with lighted color of fragment.
  //
  gl_FragColor = texel * generic_lighting(sur_ee, color2, normal_ee);
}


///
/// Routine used by Either Vertex or Fragment Shader
///

vec4
generic_lighting(vec4 vertex_e, vec4 color, vec3 normal_e)
{
  // Return lighted color of vertex_e.
  //

  vec3 nspc_color = color.rgb * gl_LightModel.ambient.rgb;
  vec3 spec_color = vec3(0);

  for ( int i=0; i<2; i++ )
    {
      if ( ( lighting_options & ( 1 << i ) ) == 0 ) continue;
      vec4 light_pos = gl_LightSource[i].position;
      vec3 v_vtx_light = light_pos.xyz - vertex_e.xyz;
      float dist = length(v_vtx_light);
      float dist_vl_inv = 1.0 / dist;
      vec3 v_vtx_l_n = v_vtx_light * dist_vl_inv;

      float d_n_vl = dot(normal_e, v_vtx_l_n);
      float phase_light = max(0,gl_FrontFacing != mirrored ? d_n_vl : -d_n_vl );

      vec3 ambient_light = gl_LightSource[i].ambient.rgb;
      vec3 diffuse_light = gl_LightSource[i].diffuse.rgb;
      float distsq = dist * dist;
      float atten_inv =
        gl_LightSource[i].constantAttenuation +
        gl_LightSource[i].linearAttenuation * dist +
        gl_LightSource[i].quadraticAttenuation * distsq;
      vec3 lighted_color =
        color.rgb
        * ( ambient_light + phase_light * diffuse_light ) / atten_inv;
      nspc_color += lighted_color;

      vec3 h = normalize( v_vtx_l_n - normalize(vertex_e.xyz) );

      spec_color +=
        pow(max(0.0,dot(normal_e,h)),16)
        * color.rgb
        * gl_LightSource[i].specular.rgb / atten_inv;
    }

  return vec4(nspc_color+spec_color,1);
}


#endif