/* 
A* -------------------------------------------------------------------
B* This file contains source code for the PyMOL computer program
C* copyright Schrodinger, LLC.
D* -------------------------------------------------------------------
E* It is unlawful to modify or remove this copyright notice.
F* -------------------------------------------------------------------
G* Please see the accompanying LICENSE file for further information. 
H* -------------------------------------------------------------------
I* Additional authors of this source file include:
-*
-* 
-*
Z* -------------------------------------------------------------------
*/

#include "os_gl.h"
#include "os_python.h"
#include <string.h>
#include <iostream>
#include "ShaderMgr.h"
#include "OOMac.h"
#include "ListMacros.h"
#include "PyMOLOptions.h"
#include "Feedback.h"
#include "MemoryDebug.h"
#include "Setting.h"
#include "Scene.h"
#include "Color.h"
#include "Vector.h"
#include "Util.h"
#include "Util2.h"
#include "Texture.h"
#include "File.h"
#include "Matrix.h"
#include "Scene.h"
#include "Parse.h"
#ifdef _WEBGL
#include "Matrix.h"
#include "WebPyMOLLibrary.h"
#endif
#define MAX_LOG_LEN 1024

#include <sstream>
#include <stack>
#include <vector>

#define SCENEGETIMAGESIZE SceneGetWidthHeight

using namespace std;

const float mat3identity[] = { 1., 0., 0., 0., 1., 0., 0., 0., 1. };

#ifdef _PYMOL_ARB_SHADERS
static
GLboolean ProgramStringIsNative(PyMOLGlobals * G,
                                       GLenum target, GLenum format,
                                       const string& shaderstr)
{
  GLint errorPos, isNative;
  glProgramStringARB(target, format, shaderstr.length(), shaderstr.c_str());
  glGetIntegerv(GL_PROGRAM_ERROR_POSITION_ARB, &errorPos);
  glGetProgramivARB(target, GL_PROGRAM_UNDER_NATIVE_LIMITS_ARB, &isNative);
  if((errorPos == -1) && (isNative == 1))
    return GL_TRUE;
  else if(errorPos >= 0) {
    if(Feedback(G, FB_OpenGL, FB_Errors)) {
      printf("OpenGL-Error: ARB shader error at char %d\n---->%s\n", errorPos,
             ((char *) shaderstr.c_str()) + errorPos);
    }
  }
  return GL_FALSE;
}

CShaderPrg *CShaderPrg::NewARB(PyMOLGlobals * G, const char * name, const string& vert, const string& frag) {
  /* BEGIN PROPRIETARY CODE SEGMENT (see disclaimer in "os_proprietary.h") */
#ifdef WIN32
  if(!(glGenProgramsARB && glBindProgramARB &&
       glDeleteProgramsARB && glProgramStringARB && glProgramEnvParameter4fARB)) {
    glGenProgramsARB = (PFNGLGENPROGRAMSARBPROC) wglGetProcAddress("glGenProgramsARB");
    glBindProgramARB = (PFNGLBINDPROGRAMARBPROC) wglGetProcAddress("glBindProgramARB");
    glDeleteProgramsARB =
      (PFNGLDELETEPROGRAMSARBPROC) wglGetProcAddress("glDeleteProgramsARB");
    glProgramStringARB =
      (PFNGLPROGRAMSTRINGARBPROC) wglGetProcAddress("glProgramStringARB");
    glProgramEnvParameter4fARB =
      (PFNGLPROGRAMENVPARAMETER4FARBPROC) wglGetProcAddress("glProgramEnvParameter4fARB");
    glGetProgramivARB = (PFNGLGETPROGRAMIVARBPROC) wglGetProcAddress("glGetProgramivARB");
  }

  if(glGenProgramsARB && glBindProgramARB &&
     glDeleteProgramsARB && glProgramStringARB && glProgramEnvParameter4fARB)
#endif
  {
    /* END PROPRIETARY CODE SEGMENT */

    int ok = true;
    GLuint programs[2];
    glGenProgramsARB(2, programs);
    
    /* load the vertex program */
    glBindProgramARB(GL_VERTEX_PROGRAM_ARB, programs[0]);
    
    ok = ok && (ProgramStringIsNative(G, GL_VERTEX_PROGRAM_ARB,
				      GL_PROGRAM_FORMAT_ASCII_ARB, vert));
    
    if(Feedback(G, FB_OpenGL, FB_Debugging))
      PyMOLCheckOpenGLErr("loading vertex program");
    
    /* load the fragment program */
    glBindProgramARB(GL_FRAGMENT_PROGRAM_ARB, programs[1]);
    
    ok = ok && (ProgramStringIsNative(G, GL_FRAGMENT_PROGRAM_ARB,
				      GL_PROGRAM_FORMAT_ASCII_ARB, frag));
    
    if(Feedback(G, FB_OpenGL, FB_Debugging))
      PyMOLCheckOpenGLErr("loading fragment program");
    if(ok) {
      CShaderPrg * I = new CShaderPrg(G, name, "", "");
      I->G = G;
      I->vid = programs[0];
      I->fid = programs[1];
      G->ShaderMgr->AddShaderPrg(I);
      return I;
    } else {
      glDeleteProgramsARB(2, programs);
    }
  }
  return nullptr;
}

int CShaderPrg::DisableARB() {
  G->ShaderMgr->current_shader = nullptr;
  glDisable(GL_FRAGMENT_PROGRAM_ARB);
  glDisable(GL_VERTEX_PROGRAM_ARB);
  return 1;
}
#endif


// -----------------------------------------------------------------------------
// Uniforms and Attributes


int CShaderPrg::Enable() {
  if (!id) return 0;

  /* linked? */
  if (!IsLinked() && !Link()) {
    // Link prints error message
    return 0;
  }
  /* if so, use the program */
  glUseProgram(id);
  // uniform 
  Set1i("isPicking", SettingGetGlobal_b(G, cSetting_pick_shading) ? 1 : G->ShaderMgr->is_picking);
  return 1;
}

int CShaderPrg::Disable() {
  glUseProgram(0);
  G->ShaderMgr->current_shader = nullptr;
  glBindTexture(GL_TEXTURE_2D, 0);
  glActiveTexture(GL_TEXTURE0);
  return 1;
}

int CShaderPrg::IsLinked() {
  GLint status = GL_FALSE;
  if (is_linked) {
    glGetProgramiv(id, GL_LINK_STATUS, &status);
  }
  return status==GL_TRUE;
}

int CShaderPrg::Link() {
  int howLong;

  glLinkProgram(id);
  is_linked = true;
  
  if (!IsLinked()) {
    if (G && G->Option && !G->Option->quiet) {
      GLint maxVarFloats;
      int infoLogLength = 0;

#ifndef PURE_OPENGL_ES_2
      glGetIntegerv(GL_MAX_VARYING_FLOATS, &maxVarFloats);
#endif
      PRINTFB(G, FB_ShaderMgr, FB_Errors)
	" CShaderPrg_Link-Error: Shader program failed to link name='%s'; GL_MAX_VARYING_FLOATS=%d log follows.\n", name.c_str(), maxVarFloats
	ENDFB(G);
      glGetProgramiv(id, GL_INFO_LOG_LENGTH, &infoLogLength);
      if (!glGetError() && infoLogLength>0){
	char *infoLog = pymol::malloc<char>(infoLogLength);
	glGetProgramInfoLog(id, infoLogLength, &howLong, infoLog);
	PRINTFB(G, FB_ShaderMgr, FB_Errors)
	  "%s\n", infoLog
	  ENDFB(G);
	FreeP(infoLog);
      }
    }
    return 0;
  }
  uniforms.clear();
  return 1;      
}

/*
 * Get a uniform location id by name. Caches ids for faster lookup (avoiding
 * expensive `glGetUniformLocation` calls).
 */
int CShaderPrg::GetUniformLocation(const char * name)
{
  if (!id)
    return -1;

  auto uloc = uniforms.find(name);
  if (uloc != uniforms.end())
    return uloc->second;

  GLint loc = glGetUniformLocation(id, name);
  uniforms[name] = loc;
  return loc;
}

/* accessors/mutators/uniform setters */
int CShaderPrg::Set1i(const char * name, int i)
{
  GLint loc = GetUniformLocation(name);
  if (loc < 0)
    return 0;
  glUniform1i(loc, i);
  return 1;
}

int CShaderPrg::Set3f(const char * name, float f1, float f2, float f3)
{
  GLint loc = GetUniformLocation(name);
  if (loc < 0)
    return 0;
  glUniform3f(loc, f1, f2, f3);
  return 1;
}

int CShaderPrg::Set2f(const char * name, float f1, float f2)
{
  GLint loc = GetUniformLocation(name);
  if (loc < 0)
    return 0;
  glUniform2f(loc, f1, f2);
  return 1;
}

/*
 * Set column major 3x3 matrix
 */
int CShaderPrg::SetMat3fc(const char * name, const GLfloat * m){
  GLint loc = GetUniformLocation(name);
  if (loc < 0)
    return 0;
  glUniformMatrix3fv(loc, 1, GL_FALSE, m);
  return 1;
}

/*
 * Set column major 4x4 matrix
 */
int CShaderPrg::SetMat4fc(const char * name, const GLfloat * m){
  GLint loc = GetUniformLocation(name);
  if (loc < 0)
    return 0;
  glUniformMatrix4fv(loc, 1, GL_FALSE, m);
  return 1;
}

int CShaderPrg::Set4fv(const char * name, const float *f){
  return (Set4f(name, f[0], f[1], f[2], f[3]));
}
int CShaderPrg::Set3fv(const char * name, const float *f){
  return (Set3f(name, f[0], f[1], f[2]));
}

int CShaderPrg::Set4f(const char * name, float f1, float f2, float f3, float f4)
{
  GLint loc = GetUniformLocation(name);
  if (loc < 0)
    return 0;
  glUniform4f(loc, f1, f2, f3, f4);
  return 1;
}

int CShaderPrg::Set1f(const char * name, float f)
{
  GLint loc = GetUniformLocation(name);
  if (loc < 0)
    return 0;
  glUniform1f(loc, f);
  return 1;
}

int CShaderPrg::GetAttribLocation(const char * name)
{
  GLint loc = -1;

  if (id && name) {
    loc = glGetAttribLocation(id, name);
    if (loc < 0)
      return -1;
  }
  return loc;
}

void CShaderPrg::SetAttrib4fLocation(const char * name, float f1, float f2, float f3, float f4){
  if (id){
    int attr = GetAttribLocation(name);
    if (attr>=0){
      glVertexAttrib4f(attr, f1, f2, f3, f4);
    }
  }
}

void CShaderPrg::SetAttrib1fLocation(const char * name, float f1){
  if (id){
    int attr = GetAttribLocation(name);
    if (attr>=0){
      glVertexAttrib1f(attr, f1);
    }
  }
}


void CShaderPrg::Invalidate() {
  if (!id)
    return;
  if (gid){
    glDetachShader(id, gid);
    glDeleteShader(gid);
    gid = 0;
  }
  if (vid){
    glDetachShader(id, vid);
    glDeleteShader(vid);
    vid = 0;
  }
  if (fid){
    glDetachShader(id, fid);
    glDeleteShader(fid);
    fid = 0;
  }
  glDeleteProgram(id);
  id = 0;
}


void CShaderPrg::Set_Matrices() {
  if (!(uniform_set & 2) &&
      SettingGetGlobal_b(G, cSetting_precomputed_lighting)) {
    Set1i("lightingTex", 1);
    uniform_set |= 2;
  }

  const float * mvm = SceneGetModelViewMatrix(G);

  // normalmatrix = transpose(inverse(mvm))
  // Since we only support orthogonal normal matrices (e.g. with our sphere
  // shader which uses a uniform radius), scaling by the length^2 of one row
  // is sufficient.
  float normalmatrix[9];
  copy44f33f(mvm, normalmatrix);
  float len_sq = lengthsq3f(normalmatrix);
  for (int i = 0; i < 9; ++i)
    normalmatrix[i] /= len_sq;

  SetMat3fc("g_NormalMatrix", normalmatrix);
  SetMat4fc("g_ModelViewMatrix", mvm);
  SetMat4fc("g_ProjectionMatrix", SceneGetProjectionMatrix(G));
}

int CShaderPrg::SetLightingEnabled(int lighting_enabled){
  return Set1i("lighting_enabled", lighting_enabled); 
}

void CShaderPrg::Set_AnaglyphMode(int mode) {
  extern float anaglyphR_constants[6][9];
  extern float anaglyphL_constants[6][9];
  /** Coefficients from: http://3dtv.at/Knowhow/AnaglyphComparison_en.aspx */
  /** anaglyph[R|L]_constants are found in Scene.c b/c of ray tracing */
  SetMat3fc("matL", G->ShaderMgr->stereo_flag < 0 ?
                          anaglyphL_constants[mode] :
                          anaglyphR_constants[mode]);
  Set1f("gamma", SettingGetGlobal_f(G, cSetting_gamma));
}

void CShaderPrg::Set_Stereo_And_AnaglyphMode() {
  int stereo, stereo_mode;

  stereo = SettingGetGlobal_i(G, cSetting_stereo);
  stereo_mode = SettingGetGlobal_i(G, cSetting_stereo_mode);

  if (stereo && stereo_mode==cStereo_anaglyph){
    Set_AnaglyphMode(SettingGetGlobal_i(G, cSetting_anaglyph_mode));
  } else {
    SetMat3fc("matL", (GLfloat*)mat3identity);
    Set1f("gamma", 1.0);
  }
  // for using one draw buffer, switch on which_pass between color and weights
  if (TM3_IS_ONEBUF)
    Set1f("which_pass", G->ShaderMgr->stereo_draw_buffer_pass ? 1.f : 0.f);
}

void CShaderPrg::Set_Specular_Values() {
  auto trans_oblique = SettingGet<float>(G, cSetting_ray_transparency_oblique);
  if (trans_oblique > R_SMALL4) {
    Set1f("trans_oblique", trans_oblique);
    Set1f("oblique_power", SettingGetGlobal_f(G, cSetting_ray_transparency_oblique_power));
  }

  if (SettingGetGlobal_b(G, cSetting_precomputed_lighting)) {
    glActiveTexture(GL_TEXTURE1);
    glBindTexture(GL_TEXTURE_CUBE_MAP, G->ShaderMgr->lightingTexture);
    return;
  }

  SceneProgramLighting(G, this);

  float settingSpecReflect, settingSpecDirect, settingSpecDirectPower, settingSpecPower;

  SceneGetAdjustedLightValues(G,
      &settingSpecReflect,
      &settingSpecPower,
      &settingSpecDirect,
      &settingSpecDirectPower);

  Set1f("spec_value_0", settingSpecDirect);
  Set1f("shininess_0", settingSpecDirectPower);
  Set1f("spec_value", settingSpecReflect);
  Set1f("shininess", settingSpecPower);
}

void CShaderPrg::SetBgUniforms() {
  auto bg_image_tilesize = SettingGet<const float*>(G, cSetting_bg_image_tilesize);
  int bg_width, bg_height;
  int scene_width, scene_height;

  Set3fv("bgSolidColor", ColorGet(G, SettingGet_color(G, nullptr, nullptr, cSetting_bg_rgb)));

  SceneGetWidthHeight(G, &scene_width, &scene_height);
  std::tie(bg_width, bg_height) = OrthoGetBackgroundSize(*G->Ortho);

  Set2f("tiledSize", bg_image_tilesize[0]/(float)scene_width, bg_image_tilesize[1]/(float)scene_height);
  Set2f("tileSize", 1.f/(float)bg_image_tilesize[0], 1.f/(float)bg_image_tilesize[1]);
  Set2f("viewImageSize", bg_width/(float)scene_width, bg_height/(float)scene_height);

  glActiveTexture(GL_TEXTURE4);
  glBindTexture(GL_TEXTURE_2D, OrthoGetBackgroundTextureID(G));
  if (!(uniform_set & 4)){
    Set1i("bgTextureMap", 4);
    uniform_set |= 4;
  }

  SceneSetFogUniforms(G, this);

  if (SettingGet<bool>(G, cSetting_chromadepth) &&
      !SettingGet<bool>(G, cSetting_orthoscopic)) {
    Set2f("clippingplanes",
        SceneGetCurrentFrontSafe(G),
        SceneGetCurrentBackSafe(G));
  }
}
