// Copyright (C) 2010, Guy Barrand. All rights reserved.
// See the file tools.license for terms.

#ifndef tools_sg_GL_manager
#define tools_sg_GL_manager

#include "gl"

#include <tools/sg/render_manager>
#include <tools/mapmanip>

#include <cmath> //::sqrt

#include "../glbuf"

#ifdef TOOLS_MEM
#include <tools/mem>
#endif

namespace tools {
namespace sg {

class GL_manager : public virtual tools::sg::render_manager {
  typedef tools::sg::render_manager parent;
public:
  TOOLS_SCLASS(tools::sg::GL_manager)
  virtual void* cast(const std::string& a_class) const {
    if(void* p = tools::cmp_cast<GL_manager>(this,a_class)) {return p;}
    else return 0;
  }  
public:
  virtual bool begin_render(int a_x,int a_y,unsigned int a_ww,unsigned int a_wh,
                            float a_r,float a_g,float a_b,float a_a,bool a_clear = true) {
    gl_clear_errors();

#if TARGET_OS_IPHONE
#elif defined(ANDROID)
#elif _WIN32
#elif __APPLE__ // Cocoa
    // to avoid a 0x506 error message :
#ifdef TOOLS_HAS_GL_VBO
#if GL_ARB_framebuffer_object
    if(::glCheckFramebufferStatus(GL_FRAMEBUFFER)!=GL_FRAMEBUFFER_COMPLETE) {
      //m_out << "tools::sg::GL_manager::begin_render :"
      //      << " frame buffer not complete."
      //      << std::endl;
      return false;
    }
#endif //GL_ARB_framebuffer_object
#endif //TOOLS_HAS_GL_VBO
#else
#endif

    // WARNING : the values set here must match the default values in sg::state.

    // Antialiasing :
#if TARGET_OS_IPHONE
#elif defined(ANDROID)
    ::glEnable(GL_MULTISAMPLE);
#elif _WIN32
#elif __APPLE__	
    ::glEnable(GL_MULTISAMPLE); //Cocoa
#else
#endif

    /* NOTE : not GL-ES :
    //   ::glPolygonMode(GL_FRONT_AND_BACK,GL_FILL);
    // The upper is cruely lacking with povama solid+light.
    //   ::glEnable(GL_LINE_STIPPLE);

    ::glDisable(GL_POLYGON_SMOOTH);
    ::glAccum(GL_LOAD,1.0f);
    ::glAccum(GL_RETURN,1.0f);
    ::glReadBuffer(GL_FRONT);

    NOTE : GL_POLYGON_SMOOTH is here on Cocoa/AGL, Windows/WGL but it
           does nothing.           
    */

#if TARGET_OS_IPHONE
// GL-ES
#elif defined(ANDROID)
// GL-ES
#else
    ::glDisable(GL_POLYGON_STIPPLE); //CoinGL : reading a .wrl having Material::transparency may enable GL_POLYGON_STIPPLE.
#endif

    ::glEnable(GL_NORMALIZE);
    ::glShadeModel(GL_FLAT);
    //::glShadeModel(GL_SMOOTH); //Gouraud
    // GL-ES : ::glMaterialfv does not work. We then use :
    //         ::glEnable(GL_COLOR_MATERIAL) and ::glColor.
    ::glEnable(GL_COLOR_MATERIAL);
  
    // to handle transparency (same as SoGLRenderAction::SCREEN_DOOR ?) :
    //::glEnable(GL_BLEND);
    // NOTE : with Cocoa+AppleGL on macOS-10.14 (Mojave), it appears that points are blended ! (Even if alpha color is 1).
    //        Seen with some simulated catalog of galaxies done for LSST/DC2.
    //        To master this, we disable GL_BLEND by default. Then people wanting some transparency have to
    //        use the tools::sg::blend node in their scene graph.
    ::glDisable(GL_BLEND); //must be in sync with tools::sg::state.m_GL_BLEND and sg::blend node default.
    ::glBlendFunc(GL_SRC_ALPHA,GL_ONE_MINUS_SRC_ALPHA);

    //WARNING : the below glEnable/glDisable corresponds
    //          to defaults in tools::sg::state.
    ::glEnable(GL_DEPTH_TEST);
    ::glDisable(GL_LIGHTING);
    ::glFrontFace(GL_CCW);
    ::glEnable(GL_CULL_FACE);
    ::glDisable(GL_POLYGON_OFFSET_FILL);	
    ::glDisable(GL_TEXTURE_2D);

    ::glDisable(GL_POINT_SMOOTH);
    ::glPointSize(1);
    ::glDisable(GL_LINE_SMOOTH); //NOTE : it does not work on all platforms !
    ::glLineWidth(1);

    ::glViewport(a_x,a_y,a_ww,a_wh);

   // NOTE : iOS : glScissor logic does not work properly with Apple multisampling.
   //        But it appears that it is not needed for VR split views.
   //        Here a combination glViewport+[correct camera lrbt] does the clipping job (???).
   //        (But the clear color should be the same for both views).
   //::glEnable(GL_SCISSOR_TEST);
   //::glScissor(a_x,a_y,a_ww,a_wh);
  
    //NOTE : a=0 coworks with the below logic to handle transparent background.
    if(a_clear) {
      ::glClearColor(a_r,a_g,a_b,0);
      ::glClear(GL_COLOR_BUFFER_BIT);
      ::glClear(GL_DEPTH_BUFFER_BIT);
    }

    //WARNING : Android, iPhone : only one glPushMatrix ok !
    ::glMatrixMode(GL_PROJECTION); 
    ::glLoadIdentity();
  
    //WARNING : we take the convention that the current mode is MODELVIEW
    ::glMatrixMode(GL_MODELVIEW);
    ::glLoadIdentity();

    //m_clear_color.set_a(0.2);

    // to handle a transparent background :
    //NOTE : not tested on Android and iOS.
    // dst color :
    //    cr * alpha + cr * (1-alpha) = cr
    //    cg * alpha + cg * (1-alpha) = cg
    //    cb * alpha + cb * (1-alpha) = cb
    //    alpha*alpha + 0 * (1-alpha) = ca => alpha = sqrt(ca)

   {::glColor4f(a_r,a_g,a_b,::sqrt(a_a));
    float xyzs[12];
    xyzs[0] = -1;xyzs[ 1] = -1;xyzs[ 2] = 0;
    xyzs[3] =  1;xyzs[ 4] = -1;xyzs[ 5] = 0;
    xyzs[6] =  1;xyzs[ 7] =  1;xyzs[ 8] = 0;
    xyzs[9] = -1;xyzs[10] =  1;xyzs[11] = 0;
    ::glDisable(GL_DEPTH_TEST);
    ::glEnableClientState(GL_VERTEX_ARRAY);
    ::glVertexPointer(3,GL_FLOAT,0,xyzs);
    ::glDrawArrays(GL_TRIANGLE_FAN,0,4);
    ::glDisableClientState(GL_VERTEX_ARRAY);
    ::glEnable(GL_DEPTH_TEST);}
  
    return true;
  }

  virtual void end_render() {
    ::glFinish();
    gl_dump_if_errors(m_out,"tools::sg::GL_manager::end_render :");
  }

  ////////////////////////////////////////////////
  /// texture : //////////////////////////////////
  ////////////////////////////////////////////////
  virtual unsigned int create_texture(const tools::img_byte& a_img,bool a_NEAREST) {

    unsigned int gl_id;
    ::glGenTextures(1,&gl_id);
    if(!gl_id) return 0;

#ifdef TOOLS_MEM
    tools::mem::increment(tools::s_tex().c_str());
#endif
    unsigned int gsto_size = a_img.size();
    ::glBindTexture(GL_TEXTURE_2D,gl_id);
    bool status = true;
   {int sz;
    ::glGetIntegerv(GL_MAX_TEXTURE_SIZE,&sz);
  // MacBookPro : it returns 8192.
  // Android : it returns 2048.
  // iPad1 : it returns 2048.
  // iPod : it returns ?
  //::printf("debug : GL_MAX_TEXTURE_SIZE : %d\n",sz);
    tools::img_byte res;
    if(!sz) {
      m_out << "tools::sg::gl::tex_img : warning : GL_MAX_TEXTURE_SIZE is zero." << std::endl;
      status = gl_tex_img(m_out,a_img);
    } else {
    if(a_img.check_gl_limit(sz,res)) {
      if(res.is_empty()) { //a_img does not exceed.
        status = gl_tex_img(m_out,a_img);
      } else {
        m_out << "tools::sg::gl::tex_img : warning : img size > GL_MAX_TEXTURE_SIZE (" << sz << ")." << std::endl;
        status = gl_tex_img(m_out,res);
        gsto_size = res.size();
      }
    } else {
      m_out << "tools::sg::gl::tex_img :"
            << " warning : img size > GL_MAX_TEXTURE_SIZE (" << sz << ") but can't reduce."
            << std::endl;
      status = false;
    }}}

    if(a_NEAREST) {
      ::glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_NEAREST); //good to see astro images pixels.
      ::glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_NEAREST); //idem.
    } else {
      ::glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MIN_FILTER,GL_LINEAR);
      ::glTexParameteri(GL_TEXTURE_2D,GL_TEXTURE_MAG_FILTER,GL_LINEAR);
    }
    ::glBindTexture(GL_TEXTURE_2D,0);

    if(!status) {
      gl_dump_if_errors(m_out,"tools::sg::GL_manager::create_texture (1) :");
      ::glDeleteTextures(1,&gl_id);    
#ifdef TOOLS_MEM
      tools::mem::decrement(tools::s_tex().c_str());
#endif
      gl_id = 0;
      gl_clear_errors();
      return 0;
    }

    if(gl_dump_if_errors(m_out,"tools::sg::GL_manager::create_texture (2) :")) {
      ::glDeleteTextures(1,&gl_id);    
#ifdef TOOLS_MEM
      tools::mem::decrement(tools::s_tex().c_str());
#endif
      gl_id = 0;
      gl_clear_errors();
      return 0;
    }

    unsigned int _id = m_gen_id;m_gen_id++;
    m_gstos[_id] = new gsto_t(gsto_t::kind_texture,gl_id,gsto_size,0);
    return _id;
  }

  ////////////////////////////////////////////////
  /// VBO ////////////////////////////////////////
  ////////////////////////////////////////////////
  virtual unsigned int create_gsto_from_data(size_t a_floatn,const float* a_data) {
    if(!a_floatn) return 0;
    switch(m_gsto_mode) {
    case tools::sg::gsto_gl_vbo:{
#ifdef TOOLS_HAS_GL_VBO
      unsigned int gl_id = 0;
      ::glGenBuffers(1,&gl_id);
      if(!gl_id) {
        if(!m_warned) {
          m_warned = true;
          m_out << "tools::sg::GL_manager::create_gsto_from_data : glGenBuffers failed ()." << std::endl;
        }
        return 0;
      }
#ifdef TOOLS_MEM
      tools::mem::increment(tools::s_gsto().c_str());
#endif

      ::glBindBuffer(GL_ARRAY_BUFFER,gl_id);
      ::glBufferData(GL_ARRAY_BUFFER,
                     a_floatn*sizeof(float),a_data,
                     GL_STATIC_DRAW);
      ::glBindBuffer(GL_ARRAY_BUFFER,0);
  
      if(gl_dump_if_errors(m_out,"tools::sg::GL_manager::create_gsto_from_data :")) {
        ::glDeleteBuffers(1,&gl_id);
#ifdef TOOLS_MEM
        tools::mem::decrement(tools::s_gsto().c_str());
#endif
        gl_id = 0;
        gl_clear_errors();
        return 0;
      }

      unsigned int _id = m_gen_id;m_gen_id++;
      m_gstos[_id] = new gsto_t(gsto_t::kind_buffer,gl_id,a_floatn*sizeof(float),0);
      return _id;
#else //!TOOLS_HAS_GL_VBO
      m_out << "tools::sg::GL_manager::create_gsto_from_data :"
            << " gsto mode is gl_vbo but class not compiled with TOOLS_HAS_GL_VBO."
            << std::endl;
      return 0;
#endif //TOOLS_HAS_GL_VBO
      }break;
    case tools::sg::gsto_gl_list:{
      unsigned int gl_id = 0;
      unsigned int _id = m_gen_id;m_gen_id++;
      m_gstos[_id] = new gsto_t(gsto_t::kind_list,gl_id,a_floatn*sizeof(float),a_data);
      return _id;
      }break;
    case tools::sg::gsto_memory:{
      unsigned int gl_id = 0;
      unsigned int _id = m_gen_id;m_gen_id++;
      m_gstos[_id] = new gsto_t(gsto_t::kind_memory,gl_id,a_floatn*sizeof(float),a_data);
      return _id;
      }break;
    }
    return 0;
  }

  virtual bool is_gsto_id_valid(unsigned int a_id) const {
    std::map<unsigned int,gsto_t*>::const_iterator it = m_gstos.find(a_id);
    if(it==m_gstos.end()) return false;
    return (*it).second->is_valid();
  }

  virtual void delete_gsto(unsigned int a_id){
    tools::delete_key<unsigned int,gsto_t>(m_gstos,a_id);
  }

  ////////////////////////////////////////////////////////
  ////////////////////////////////////////////////////////
  ////////////////////////////////////////////////////////

  float* gsto_data(unsigned int a_id) const {
    std::map<unsigned int,gsto_t*>::const_iterator it = m_gstos.find(a_id);
    if(it==m_gstos.end()) return 0;
    return (*it).second->m_data;
  }

  unsigned int gsto_gl_list_id(unsigned int a_id,bool& a_created) const {
    std::map<unsigned int,gsto_t*>::const_iterator it = m_gstos.find(a_id);
    if(it==m_gstos.end()) {a_created = false;return 0;}
    if((*it).second->m_kind!=gsto_t::kind_list) {a_created = false;return 0;}
    if((*it).second->m_gl_id) {
      a_created = false;
      return (*it).second->m_gl_id;
    } else {
#ifdef TOOLS_HAS_GL_LIST
      unsigned int _id = ::glGenLists(1);
      if(!_id) {a_created = false;return 0;}
#ifdef TOOLS_MEM
      tools::mem::increment(tools::s_gsto().c_str());
#endif
      a_created = true;
      (*it).second->m_gl_id = _id;
      return _id;
#else
      a_created = false;
      return 0;
#endif
    }
  }

  ////////////////////////////////////////////////////////
  ////////////////////////////////////////////////////////
  ////////////////////////////////////////////////////////
  virtual tools::sg::gsto_mode get_gsto_mode() const {return m_gsto_mode;}

  virtual void set_gsto_mode(tools::sg::gsto_mode a_v) {
    if(a_v==m_gsto_mode) return;
    tools::safe_clear<unsigned int,gsto_t>(m_gstos);
    switch(a_v) {
    case tools::sg::gsto_gl_vbo:{
#ifdef TOOLS_HAS_GL_VBO
      m_gsto_mode = a_v;
#else
      m_gsto_mode = tools::sg::gsto_memory;
#endif
      }break;
    case tools::sg::gsto_gl_list:{
#ifdef TOOLS_HAS_GL_LIST
      m_gsto_mode = a_v;
#else
      m_gsto_mode = tools::sg::gsto_memory;
#endif
      }break;
    case tools::sg::gsto_memory:{
      m_gsto_mode = tools::sg::gsto_memory;
      }break;
    }
  }

  virtual void available_gsto_modes(std::vector<std::string>& a_vs) {
    a_vs.clear();
#ifdef TOOLS_HAS_GL_VBO
    a_vs.push_back(tools::sg::s_gsto_gl_vbo());
#endif
#ifdef TOOLS_HAS_GL_LIST
    a_vs.push_back(tools::sg::s_gsto_gl_list());
#endif
    a_vs.push_back(tools::sg::s_gsto_memory());
  }

  virtual void available_not_memory_gsto_mode(std::string& a_v) const {
    a_v.clear();
#ifdef TOOLS_HAS_GL_VBO
    a_v = tools::sg::s_gsto_gl_vbo();
#endif
#ifdef TOOLS_HAS_GL_LIST
    if(a_v.empty()) a_v = tools::sg::s_gsto_gl_list();
#endif
  }

  virtual size_t used_texture_memory() const {
    size_t sz = 0;
    std::map<unsigned int,gsto_t*>::const_iterator it;
    for(it=m_gstos.begin();it!=m_gstos.end();++it) {
      if((*it).second->m_kind==gsto_t::kind_texture) sz += (*it).second->m_size;
    }
    return sz;
  }

  virtual size_t gstos_size() const {
    size_t sz = 0;
    std::map<unsigned int,gsto_t*>::const_iterator it;
    for(it=m_gstos.begin();it!=m_gstos.end();++it) sz += (*it).second->m_size;
    return sz;
  }

public:
  GL_manager(std::ostream& a_out)
  :m_out(a_out)
  ,m_gen_id(1)
#ifdef TOOLS_HAS_GL_VBO
  ,m_gsto_mode(tools::sg::gsto_gl_vbo) //priority to GL VBOs.
#elif TOOLS_HAS_GL_LIST
  ,m_gsto_mode(tools::sg::gsto_gl_list)
#else
  ,m_gsto_mode(tools::sg::gsto_memory)
#endif
  ,m_warned(false)
  {}
  virtual ~GL_manager(){
    tools::safe_clear<unsigned int,gsto_t>(m_gstos);
  }
public:
  GL_manager(const GL_manager& a_from)
  :parent(a_from)
  ,m_out(a_from.m_out)
  ,m_gen_id(a_from.m_gen_id)
  ,m_gsto_mode(a_from.m_gsto_mode)
  ,m_warned(false)
  {}
  GL_manager& operator=(const GL_manager& a_from){
    if(&a_from==this) return *this;
    m_gen_id = a_from.m_gen_id;
    m_gsto_mode = a_from.m_gsto_mode;
    tools::safe_clear<unsigned int,gsto_t>(m_gstos);
    m_warned = false;
    return *this;
  }

public:
  void bind_gsto(unsigned int a_id) {
    std::map<unsigned int,gsto_t*>::const_iterator it = m_gstos.find(a_id);
    if(it==m_gstos.end()) return;
    (*it).second->bind();
  }

  void delete_gstos() {
    tools::safe_clear<unsigned int,gsto_t>(m_gstos);
  }

public:

  static unsigned char* get_rgbas(unsigned int a_w,unsigned int a_h) {
    //WARNING : it does OpenGL. Under Android it should be executed
    //          in the OpenGL thread. 
    //NOTE : Android, iOS : RGB produces a black image.
    unsigned char* rgbas = new unsigned char[4 * a_w * a_h];
    if(!rgbas) return 0;
    ::glPixelStorei(GL_PACK_ALIGNMENT,1); //needed with Cocoa.
    ::glReadPixels(0,0,a_w,a_h,GL_RGBA,GL_UNSIGNED_BYTE,rgbas);
 /*{size_t number = 4 * a_w * a_h;
    size_t count_not_255 = 0;
    for(size_t item=3;item<number;item+=4) {
      unsigned char a = rgbas[item];
      if(a!=255) {
        ::printf("%lu : %d\n",item,a);
        count_not_255++;
	rgbas[item] = 255;
      }
    }
    ::printf("not_255 : %lu\n",count_not_255);}*/
    return rgbas;
  }

  static unsigned char* get_rgbs(unsigned int a_w,unsigned int a_h) {
    //WARNING : it does OpenGL. Under Android it should be executed
    //          in the OpenGL thread. 
    //NOTE : Android, iOS : RGB produces a black image.
    unsigned char* rgbs = new unsigned char[3 * a_w * a_h];
    if(!rgbs) return 0;
    ::glPixelStorei(GL_PACK_ALIGNMENT,1); //needed with Cocoa.
    ::glReadPixels(0,0,a_w,a_h,GL_RGB,GL_UNSIGNED_BYTE,rgbs);
    return rgbs;
  }

protected:
  std::ostream& m_out;

  class gsto_t {
    TOOLS_SCLASS(GL_manager::gsto_t)
  public:
    enum kind {
      kind_texture,
      kind_buffer,
      kind_list,
      kind_memory
    };
  public:
    gsto_t(kind a_kind,int a_gl_id,size_t a_size,const float* a_data)
    :m_gl_id(a_gl_id)
    ,m_kind(a_kind)
    ,m_size(a_size)
    ,m_data(0)
    {
#ifdef TOOLS_MEM
      tools::mem::increment(s_class().c_str());
#endif
      if(a_data) {
        size_t num = m_size/sizeof(float);
        m_data = new float[num];
#ifdef TOOLS_MEM
        tools::mem::increment(tools::s_new().c_str());
#endif
        ::memcpy(m_data,a_data,m_size);
      }
    }
    virtual ~gsto_t() {
      if(m_kind==kind_texture) {
        ::glDeleteTextures(1,&m_gl_id);
#ifdef TOOLS_MEM
        tools::mem::decrement(tools::s_tex().c_str());
#endif
      } else if(m_kind==kind_buffer) {
#ifdef TOOLS_HAS_GL_VBO
        ::glDeleteBuffers(1,&m_gl_id);
#ifdef TOOLS_MEM
        tools::mem::decrement(tools::s_gsto().c_str());
#endif
#endif
      } else if(m_kind==kind_list) {
        if(m_gl_id) {
#ifdef TOOLS_HAS_GL_LIST
          ::glDeleteLists(m_gl_id,1);
#ifdef TOOLS_MEM
          tools::mem::decrement(tools::s_gsto().c_str());
#endif
#endif          
        }
      }

      if(m_data) {
        delete [] m_data;
#ifdef TOOLS_MEM
        tools::mem::decrement(tools::s_new().c_str());
#endif
      }
#ifdef TOOLS_MEM
      tools::mem::decrement(s_class().c_str());
#endif
    }    
  private:
    gsto_t(const gsto_t& a_from)
    :m_gl_id(a_from.m_gl_id)
    ,m_kind(a_from.m_kind)
    ,m_size(a_from.m_size)
    ,m_data(0)
    {
#ifdef TOOLS_MEM
      tools::mem::increment(s_class().c_str());
#endif
      if(a_from.m_data) {
        size_t num = m_size/sizeof(float);
        m_data = new float[num];
#ifdef TOOLS_MEM
        tools::mem::increment(tools::s_new().c_str());
#endif
        ::memcpy(m_data,a_from.m_data,m_size);
      }
    }
    gsto_t& operator=(const gsto_t& a_from){
      if(&a_from==this) return *this;
      m_gl_id = a_from.m_gl_id;
      m_kind = a_from.m_kind;
      m_size = a_from.m_size;
      if(m_data) {
        delete [] m_data;
#ifdef TOOLS_MEM
        tools::mem::decrement(tools::s_new().c_str());
#endif
        m_data = 0;
      }
      if(a_from.m_data) {
        size_t num = m_size/sizeof(float);
        m_data = new float[num];
#ifdef TOOLS_MEM
        tools::mem::increment(tools::s_new().c_str());
#endif
        ::memcpy(m_data,a_from.m_data,m_size);
      }
      return *this;
    }
  public:
    bool is_valid() const {
      if(m_kind==kind_texture) {
        return (::glIsTexture(m_gl_id)==GL_TRUE?true:false);
      } else if(m_kind==kind_buffer) {
#ifdef TOOLS_HAS_GL_VBO
        return (::glIsBuffer(m_gl_id)==GL_TRUE?true:false);
#endif
      } else if(m_kind==kind_list) {
#ifdef TOOLS_HAS_GL_LIST
        return (::glIsList(m_gl_id)==GL_TRUE?true:false);
#endif
      } else if(m_kind==kind_memory) {
        return true;
      }
      return false;
    }
    void bind() const {
      if(m_kind==kind_texture) {
        ::glBindTexture(GL_TEXTURE_2D,m_gl_id);
      } else if(m_kind==kind_buffer) {
#ifdef TOOLS_HAS_GL_VBO
        ::glBindBuffer(GL_ARRAY_BUFFER,m_gl_id);
#endif
      }
    }
  public:
    unsigned int m_gl_id;
    kind m_kind;
    size_t m_size;
    float* m_data;    
  };

  std::map<unsigned int,gsto_t*> m_gstos;

  bool m_gsto_on_card;
  unsigned int m_gen_id;
  tools::sg::gsto_mode m_gsto_mode;
  bool m_warned;
};

}}

#endif
