// Copyright 2021 DeepMind Technologies Limited
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

#include "render/render_gl2.h"

#include <math.h>
#include <string.h>
#include <stdio.h>

#include <mujoco/mjmacro.h>
#include <mujoco/mujoco.h>
#include "engine/engine_array_safety.h"
#include "render/glad/glad.h"

#define STRING_BUFSIZE 100


//------------------------- OpenGL framebuffer operations ------------------------------------------

// set OpenGL framebuffer for rendering: mjFB_WINDOW or mjFB_OFFSCREEN
//  if only one buffer is available, set that buffer and ignore framebuffer argument
void mjr_setBuffer(int framebuffer, mjrContext* con) {
  // no buffer: error
  if (!con->windowAvailable && !con->offFBO) {
    mju_error("No OpenGL framebuffer available");
  }

  // window only: set window
  else if (con->windowAvailable && !con->offFBO) {
    // bind window
    glBindFramebuffer(GL_FRAMEBUFFER, 0);
    glReadBuffer(con->windowDoublebuffer ? GL_BACK : GL_FRONT);
    glDrawBuffer(con->windowDoublebuffer ? GL_BACK : GL_FRONT);

    // save
    con->currentBuffer = mjFB_WINDOW;
  }

  // offscreen only: set offscreen
  else if (!con->windowAvailable && con->offFBO) {
    // bind offscreen
    glBindFramebuffer(GL_FRAMEBUFFER, con->offFBO);
    glReadBuffer(GL_COLOR_ATTACHMENT0);
    glDrawBuffer(GL_COLOR_ATTACHMENT0);

    // save
    con->currentBuffer = mjFB_OFFSCREEN;
  }

  // both available: use selected framebuffer
  else {
    // bind selected buffer
    if (framebuffer == mjFB_WINDOW) {
      glBindFramebuffer(GL_FRAMEBUFFER, 0);
      glReadBuffer(con->windowDoublebuffer ? GL_BACK : GL_FRONT);
      glDrawBuffer(con->windowDoublebuffer ? GL_BACK : GL_FRONT);
    } else {
      glBindFramebuffer(GL_FRAMEBUFFER, con->offFBO);
      glReadBuffer(GL_COLOR_ATTACHMENT0);
      glDrawBuffer(GL_COLOR_ATTACHMENT0);
    }

    // save
    con->currentBuffer = framebuffer;
  }
}



// make con->currentBuffer current again
void mjr_restoreBuffer(const mjrContext* con) {
  if (con->currentBuffer == mjFB_WINDOW) {
    glBindFramebuffer(GL_FRAMEBUFFER, 0);
    glReadBuffer(con->windowDoublebuffer ? GL_BACK : GL_FRONT);
    glDrawBuffer(con->windowDoublebuffer ? GL_BACK : GL_FRONT);
  } else {
    glBindFramebuffer(GL_FRAMEBUFFER, con->offFBO);
    glReadBuffer(GL_COLOR_ATTACHMENT0);
    glDrawBuffer(GL_COLOR_ATTACHMENT0);
  }
}



// emit one warning about mjGLAD_GL_ARB_clip_control
static void warnAboutARBClipControl(void) {
  // not thread safe, but the consequence is just too many log lines
  static int warned = 0;
  if (!warned) {
    warned = 1;
    mju_warning("ARB_clip_control unavailable while mjDEPTH_ZEROFAR requested, "
                "depth accuracy will be limited");
  }
}



// emit one warning about mjGLAD_GL_ARB_depth_buffer_float
static void warnAboutARBDepthBuffer(void) {
  // not thread safe, but the consequence is just too many log lines
  static int warned = 0;
  if (!warned) {
    warned = 1;
    mju_warning("ARB_depth_buffer_float unavailable while mjDEPTH_ZEROFAR requested, "
                "depth accuracy will be limited");
  }
}



static inline void flipDepthIfRequired(float* depth, mjrRect viewport, const mjrContext* con) {
  if (con->readDepthMap == mjDEPTH_ZERONEAR) {
    int npixel = viewport.width * viewport.height;
    for (int i = 0; i < npixel; i++) {
      depth[i] = 1.0 - depth[i];  // Reverse the reversed Z buffer
    }
  } else if (!mjGLAD_GL_ARB_clip_control) {
    warnAboutARBClipControl();
  } else if (!mjGLAD_GL_ARB_depth_buffer_float) {
    warnAboutARBDepthBuffer();
  }
}



// read pixels from current OpenGL framebuffer to client buffer
//  viewport is in OpenGL framebuffer; client buffer starts at (0,0)
void mjr_readPixels(unsigned char* rgb, float* depth,
                    mjrRect viewport, const mjrContext* con) {
  // construct mask resolve-blit
  GLbitfield mask = (rgb ? GL_COLOR_BUFFER_BIT : 0) |
                    (depth ? GL_DEPTH_BUFFER_BIT : 0);

  // make sure we have something to do
  if (!mask) {
    return;
  }

  // read from window
  if (con->currentBuffer == mjFB_WINDOW) {
    // read rgb and depth
    if (rgb) {
      glReadPixels(viewport.left, viewport.bottom, viewport.width, viewport.height,
                   GL_RGB, GL_UNSIGNED_BYTE, rgb);
    }
    if (depth) {
      glReadPixels(viewport.left, viewport.bottom, viewport.width, viewport.height,
                   GL_DEPTH_COMPONENT, GL_FLOAT, depth);
      flipDepthIfRequired(depth, viewport, con);
    }
  }

  // read from offscreen
  else {
    // multisample: blit to resolve buffer and read from there
    if (con->offSamples) {
      // make sure blit is supported
      if (!glBlitFramebuffer) {
        return;
      }

      // prepare for resolve-blit
      glBindFramebuffer(GL_READ_FRAMEBUFFER, con->offFBO);
      glReadBuffer(GL_COLOR_ATTACHMENT0);
      glBindFramebuffer(GL_DRAW_FRAMEBUFFER, con->offFBO_r);
      glDrawBuffer(GL_COLOR_ATTACHMENT0);

      // resolve-blit
      glBlitFramebuffer(viewport.left, viewport.bottom,
                        viewport.left+viewport.width, viewport.bottom+viewport.height,
                        viewport.left, viewport.bottom,
                        viewport.left+viewport.width, viewport.bottom+viewport.height,
                        mask, GL_NEAREST);

      // read from resolved
      glBindFramebuffer(GL_READ_FRAMEBUFFER, con->offFBO_r);
      glReadBuffer(GL_COLOR_ATTACHMENT0);
    }

    // no multisample: read from offscreen
    else {
      glBindFramebuffer(GL_READ_FRAMEBUFFER, con->offFBO);
      glReadBuffer(GL_COLOR_ATTACHMENT0);
    }

    // read rgb and depth
    if (rgb) {
      glReadPixels(viewport.left, viewport.bottom, viewport.width, viewport.height,
                   con->readPixelFormat, GL_UNSIGNED_BYTE, rgb);
    }
    if (depth) {
      glReadPixels(viewport.left, viewport.bottom, viewport.width, viewport.height,
                   GL_DEPTH_COMPONENT, GL_FLOAT, depth);
      flipDepthIfRequired(depth, viewport, con);
    }

    // restore currentBuffer
    mjr_restoreBuffer(con);
  }
}



// draw pixels from client buffer to current OpenGL framebuffer
//  viewport is in OpenGL framebuffer; client buffer starts at (0,0)
void mjr_drawPixels(const unsigned char* rgb, const float* depth,
                    mjrRect viewport, const mjrContext* con) {
  // set raster position
  glWindowPos2i(viewport.left, viewport.bottom);

  // write rgb and depth
  if (rgb) {
    glDrawPixels(viewport.width, viewport.height, GL_RGB, GL_UNSIGNED_BYTE, rgb);
  }
  if (depth) {
    glDrawPixels(viewport.width, viewport.height, GL_DEPTH_COMPONENT, GL_FLOAT, depth);
  }
}



// blit from src viewpoint in current framebuffer to dst viewport in other framebuffer
//  if src, dst have different size and flg_depth==0, color is interpolated with GL_LINEAR
void mjr_blitBuffer(mjrRect src, mjrRect dst,
                    int flg_color, int flg_depth, const mjrContext* con) {
  // construct mask and filter for blit
  GLbitfield mask = (flg_color ? GL_COLOR_BUFFER_BIT : 0) |
                    (flg_depth ? GL_DEPTH_BUFFER_BIT : 0);
  GLenum filter = (!flg_depth && (src.width != dst.width || src.height != dst.height)) ?
                  GL_LINEAR : GL_NEAREST;

  // make sure both buffers are available and we have something to do
  if (!con->windowAvailable || !con->offFBO || !mask) {
    return;
  }

  // make sure blit is supported
  if (!glBlitFramebuffer) {
    return;
  }

  // from window to offsreen
  if (con->currentBuffer == mjFB_WINDOW) {
    // offscreen is multisample: go through resolve
    if (con->offSamples) {
      // prepare to blit from window to resolve
      glBindFramebuffer(GL_READ_FRAMEBUFFER, 0);
      glReadBuffer(con->windowDoublebuffer ? GL_BACK : GL_FRONT);
      glBindFramebuffer(GL_DRAW_FRAMEBUFFER, con->offFBO_r);
      glDrawBuffer(GL_COLOR_ATTACHMENT0);

      // blit
      glBlitFramebuffer(src.left, src.bottom, src.left+src.width, src.bottom+src.height,
                        dst.left, dst.bottom, dst.left+dst.width, dst.bottom+dst.height,
                        mask, filter);

      // prepare to blit from resolve to offscreen
      glBindFramebuffer(GL_READ_FRAMEBUFFER, con->offFBO_r);
      glReadBuffer(GL_COLOR_ATTACHMENT0);
      glBindFramebuffer(GL_DRAW_FRAMEBUFFER, con->offFBO);
      glDrawBuffer(GL_COLOR_ATTACHMENT0);

      // blit: both dst
      glBlitFramebuffer(dst.left, dst.bottom, dst.left+dst.width, dst.bottom+dst.height,
                        dst.left, dst.bottom, dst.left+dst.width, dst.bottom+dst.height,
                        mask, filter);
    }

    // otherwise direct
    else {
      // prepare
      glBindFramebuffer(GL_READ_FRAMEBUFFER, 0);
      glReadBuffer(con->windowDoublebuffer ? GL_BACK : GL_FRONT);
      glBindFramebuffer(GL_DRAW_FRAMEBUFFER, con->offFBO);
      glDrawBuffer(GL_COLOR_ATTACHMENT0);

      // blit
      glBlitFramebuffer(src.left, src.bottom, src.left+src.width, src.bottom+src.height,
                        dst.left, dst.bottom, dst.left+dst.width, dst.bottom+dst.height,
                        mask, filter);
    }
  }

  // from offscreen to window
  else {
    // offscreen is multisample: go through resolve
    if (con->offSamples) {
      // prepare to blit from offscreen to resolve
      glBindFramebuffer(GL_READ_FRAMEBUFFER, con->offFBO);
      glReadBuffer(GL_COLOR_ATTACHMENT0);
      glBindFramebuffer(GL_DRAW_FRAMEBUFFER, con->offFBO_r);
      glDrawBuffer(GL_COLOR_ATTACHMENT0);

      // blit: both src
      glBlitFramebuffer(src.left, src.bottom, src.left+src.width, src.bottom+src.height,
                        src.left, src.bottom, src.left+src.width, src.bottom+src.height,
                        mask, filter);

      // prepare to blit from resolve to window
      glBindFramebuffer(GL_READ_FRAMEBUFFER, con->offFBO_r);
      glReadBuffer(GL_COLOR_ATTACHMENT0);
    }

    // otherwise direct
    else {
      glBindFramebuffer(GL_READ_FRAMEBUFFER, con->offFBO);
      glReadBuffer(GL_COLOR_ATTACHMENT0);
    }

    // set target and blit
    glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
    glDrawBuffer(con->windowDoublebuffer ? GL_BACK : GL_FRONT);
    glBlitFramebuffer(src.left, src.bottom, src.left+src.width, src.bottom+src.height,
                      dst.left, dst.bottom, dst.left+dst.width, dst.bottom+dst.height,
                      mask, filter);
  }

  // restore currentBuffer
  mjr_restoreBuffer(con);
}



// Set Aux buffer for custom OpenGL rendering (call restoreBuffer when done).
void mjr_setAux(int index, const mjrContext* con) {
  // check index
  if (index < 0 || index >= mjNAUX) {
    mju_error("Invalid aux buffer index");
  }

  // set
  if (con->auxFBO[index]) {
    glBindFramebuffer(GL_FRAMEBUFFER, con->auxFBO[index]);
    glReadBuffer(GL_COLOR_ATTACHMENT0);
    glDrawBuffer(GL_COLOR_ATTACHMENT0);
  } else {
    mju_error("auxFBO %d does not exist", index);
  }
}



// Blit from Aux buffer to con->currentBuffer.
void mjr_blitAux(int index, mjrRect src, int left, int bottom, const mjrContext* con) {
  // check index
  if (index < 0 || index >= mjNAUX) {
    mju_error("Invalid aux buffer index");
  }

  // make sure aux buffers is available
  if (!con->auxFBO[index]) {
    return;
  }

  // make sure blit is supported
  if (!glBlitFramebuffer) {
    return;
  }

  // resolve blit
  glBindFramebuffer(GL_READ_FRAMEBUFFER, con->auxFBO[index]);
  glReadBuffer(GL_COLOR_ATTACHMENT0);
  glBindFramebuffer(GL_DRAW_FRAMEBUFFER, con->auxFBO_r[index]);
  glDrawBuffer(GL_COLOR_ATTACHMENT0);
  glBlitFramebuffer(src.left, src.bottom, src.left+src.width, src.bottom+src.height,
                    src.left, src.bottom, src.left+src.width, src.bottom+src.height,
                    GL_COLOR_BUFFER_BIT, GL_NEAREST);

  // set source
  glBindFramebuffer(GL_READ_FRAMEBUFFER, con->auxFBO_r[index]);
  glReadBuffer(GL_COLOR_ATTACHMENT0);

  // set destination
  if (con->currentBuffer == mjFB_WINDOW) {
    glBindFramebuffer(GL_DRAW_FRAMEBUFFER, 0);
    glDrawBuffer(con->windowDoublebuffer ? GL_BACK : GL_FRONT);
  } else {
    glBindFramebuffer(GL_DRAW_FRAMEBUFFER, con->offFBO);
    glDrawBuffer(GL_COLOR_ATTACHMENT0);
  }

  // blit
  glBlitFramebuffer(src.left, src.bottom, src.left+src.width, src.bottom+src.height,
                    left, bottom, left+src.width, bottom+src.height,
                    GL_COLOR_BUFFER_BIT, GL_NEAREST);

  // restore
  mjr_restoreBuffer(con);
}



//---------------------------------- Text ----------------------------------------------------------

// init OpenGL for 2D
static void init2D(void) {
  // set OpenGL options
  glDisable(GL_NORMALIZE);
  glDisable(GL_DEPTH_TEST);
  glShadeModel(GL_FLAT);
  glDisable(GL_CULL_FACE);
  glDisable(GL_LIGHTING);
  glDisable(GL_COLOR_MATERIAL);
  glDisable(GL_BLEND);
  glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
  glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);

  // standard 2D projection
  glMatrixMode(GL_PROJECTION);
  glLoadIdentity();
  glOrtho(0, 1, 0, 1, -1, 1);
  glMatrixMode(GL_MODELVIEW);
  glLoadIdentity();
}



// actual draw text, do not initialize OpenGL options
void mjr_textActual(int font, const char* txt, const mjrContext* con,
                    float x, float y, float z, float r, float g, float b) {
  // return if font is not installed
  if (!con->rangeFont) {
    return;
  }

  // shadow text
  if (font == mjFONT_SHADOW) {
    // blend shadow with black
    glListBase(con->baseFontShadow);
    glColor4f(0, 0, 0, 0.5);
    glRasterPos3f(x, y, z);
    glCallLists((GLsizei)strlen(txt), GL_UNSIGNED_BYTE, txt);

    // render text
    glListBase(con->baseFontNormal);
    glColor4f(r, g, b, 1);
    glRasterPos3f(x, y, z);
    glCallLists((GLsizei)strlen(txt), GL_UNSIGNED_BYTE, txt);
  }

  // regular text (normal or big)
  else {
    glListBase(font == mjFONT_BIG ? con->baseFontBig : con->baseFontNormal);
    glColor4f(r, g, b, 1);
    glRasterPos3f(x, y, z);
    glCallLists((GLsizei)strlen(txt), GL_UNSIGNED_BYTE, txt);
  }
}



// draw text at (x,y) in relative coordinates; font is mjtFont
void mjr_text(int font, const char* txt, const mjrContext* con,
              float x, float y, float r, float g, float b) {
  // init OpenGL
  init2D();

  // call actual drawing function
  mjr_textActual(font, txt, con, x, y, 0, r, g, b);
}



// actual overlay drawing; used for each column; return width (used as skip for next call)
static int draw_overlay(int font, mjrRect viewport, int skip, int gridpos,
                        float red, float green, float blue,
                        const char* overlay, const mjrContext* con) {
  int pos, ncthis, nc, nr, W, H, flg_big = (font == mjFONT_BIG);
  int PAD = 5, sz = mjMIN(mjMAXOVERLAY, (int)strlen(overlay));
  char text[mjMAXOVERLAY];

  // count rows and columns of text rectangle in pixels
  nr = flg_big ? con->charHeightBig : con->charHeight;
  ncthis = nc = 0;
  for (int i=0; i < sz; i++) {
    // process this char
    if (overlay[i] != '\n') {
      ncthis += flg_big ? con->charWidthBig[(unsigned char)overlay[i]]
                        : con->charWidth[(unsigned char)overlay[i]];
      nc = mjMAX(nc, ncthis);
    }

    // advance to next line
    else {
      nr += PAD + (flg_big ? con->charHeightBig : con->charHeight);
      ncthis = 0;
    }
  }

  // viewport width and height
  W = PAD+nc+8;
  H = PAD+nr;

  // set viewport to specific grid position
  switch (gridpos) {
  case mjGRID_TOPLEFT:
    glViewport(viewport.left+skip+PAD,
               viewport.bottom+viewport.height-1-PAD-H, W, H);
    break;

  case mjGRID_TOPRIGHT:
    glViewport(viewport.left+viewport.width-skip-1-PAD-W,
               viewport.bottom+viewport.height-1-PAD-H, W, H);
    break;

  case mjGRID_BOTTOMLEFT:
    glViewport(viewport.left+skip+PAD,
               viewport.bottom+PAD, W, H);
    break;

  case mjGRID_BOTTOMRIGHT:
    glViewport(viewport.left+viewport.width-skip-1-PAD-W,
               viewport.bottom+PAD, W, H);
    break;

  case mjGRID_TOP:
    glViewport(viewport.left+(viewport.width-skip-W)/2-1-PAD,
               viewport.bottom+viewport.height-1-PAD-H, W, H);
    break;

  case mjGRID_BOTTOM:
    glViewport(viewport.left+(viewport.width-skip-W)/2-1-PAD,
               viewport.bottom+PAD, W, H);
    break;

  case mjGRID_LEFT:
    glViewport(viewport.left+skip+PAD,
               viewport.bottom+(viewport.height-H)/2-1-PAD, W, H);
    break;

  case mjGRID_RIGHT:
    glViewport(viewport.left+viewport.width-skip-1-PAD-W,
               viewport.bottom+(viewport.height-H)/2-1-PAD, W, H);
  }

  // set projection in pixels
  glMatrixMode(GL_PROJECTION);
  glLoadIdentity();
  glOrtho(0, W-1, 0, H-1, -1, 1);
  glMatrixMode(GL_MODELVIEW);
  glLoadIdentity();

  // blend with black background
  glEnable(GL_BLEND);
  glColor4d(0, 0, 0, 0.5);
  glBegin(GL_QUADS);
  glNormal3d(0, 0, 1);
  glVertex2d(0, 0);
  glVertex2d(0, H);
  glVertex2d(W, H);
  glVertex2d(W, 0);
  glEnd();
  glDisable(GL_BLEND);

  // draw text line by line
  nr = flg_big ? con->charHeightBig : con->charHeight;
  pos = 0;
  for (int i=0; i < sz; i++) {
    // terminate line and draw
    if (overlay[i] == '\n' || i == sz-1) {
      if (overlay[i] == '\n') {
        text[pos] = 0;
      } else {
        text[pos] = overlay[i];
        text[pos+1] = 0;
      }
      mjr_textActual(font, text, con, 3, H-nr, 0, red, green, blue);

      // advance to next line
      nr += PAD + (flg_big ? con->charHeightBig : con->charHeight);
      pos = 0;
    }

    // copy char
    else {
      text[pos++] = overlay[i];
    }
  }

  return W-2;
}



// draw text overlay; font is mjtFont; gridpos is mjtGridPos
void mjr_overlay(int font, int gridpos, mjrRect viewport,
                 const char* overlay, const char* overlay2, const mjrContext* con) {
  int skip = 0;

  // empty viewport: nothing to do
  if (viewport.width <= 0 || viewport.height <= 0) {
    return;
  }

  // init OpenGL once per overlay, set viewport later
  init2D();

  // two-column
  if (overlay2 && overlay2[0]) {
    // left side
    if (gridpos == mjGRID_TOPLEFT || gridpos == mjGRID_BOTTOMLEFT) {
      skip = draw_overlay(font, viewport, 0, gridpos, .7, .7, .7, overlay, con);
      draw_overlay(font, viewport, skip, gridpos, 1, 1, 1, overlay2, con);
    }

    // right side
    else {
      skip = draw_overlay(font, viewport, 0, gridpos, 1, 1, 1, overlay2, con);
      draw_overlay(font, viewport, skip, gridpos, .7, .7, .7, overlay, con);
    }
  }

  // one-column
  else {
    draw_overlay(font, viewport, 0, gridpos, 1, 1, 1, overlay, con);
  }
}



//------------------------------ 2D drawing --------------------------------------------------------

// get maximum viewport for active buffer
mjrRect mjr_maxViewport(const mjrContext* con) {
  // init with offscreen
  mjrRect res = {0, 0, con->offWidth, con->offHeight};

  // window: get from scissor box
  int dims[4];
  if (con->currentBuffer == mjFB_WINDOW) {
    glGetIntegerv(GL_SCISSOR_BOX, dims);
    res.width = dims[2];
    res.height = dims[3];
  }

  return res;
}



// draw rectangle
void mjr_rectangle(mjrRect viewport, float r, float g, float b, float a) {
  // empty viewport: nothing to do
  if (viewport.width <= 0 || viewport.height <= 0) {
    return;
  }

  // init OpenGL, set viewport
  init2D();
  glViewport(viewport.left, viewport.bottom, viewport.width, viewport.height);

  // draw rectangle
  glEnable(GL_BLEND);
  glColor4f(r, g, b, a);
  glBegin(GL_QUADS);
  glVertex2f(0, 0);
  glVertex2f(0, 1);
  glVertex2f(1, 1);
  glVertex2f(1, 0);
  glEnd();
  glDisable(GL_BLEND);
}



// draw rectangle with centered text
void mjr_label(mjrRect viewport, int font, const char* txt,
               float r, float g, float b, float a, float rt, float gt, float bt,
               const mjrContext* con) {
  // empty viewport: nothing to do
  if (viewport.width <= 0 || viewport.height <= 0) {
    return;
  }

  // set OpenGL options
  glDisable(GL_NORMALIZE);
  glDisable(GL_DEPTH_TEST);
  glDisable(GL_CULL_FACE);
  glDisable(GL_LIGHTING);
  glDisable(GL_COLOR_MATERIAL);
  glShadeModel(GL_FLAT);
  glEnable(GL_BLEND);
  glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
  glPolygonMode(GL_FRONT_AND_BACK, GL_FILL);

  // standard 2D projection, in framebuffer units
  glMatrixMode(GL_PROJECTION);
  glLoadIdentity();
  glOrtho(0, viewport.width, 0, viewport.height, -1, 1);
  glMatrixMode(GL_MODELVIEW);
  glLoadIdentity();

  // set viewport
  glViewport(viewport.left, viewport.bottom, viewport.width, viewport.height);

  // get sizes
  int W = viewport.width;
  int H = viewport.height;

  // render
  glBegin(GL_QUADS);
  glColor4f(r, g, b, a);
  glVertex2i(0, 0);
  glVertex2i(W, 0);
  glVertex2i(W, H);
  glVertex2i(0, H);
  glEnd();

  // draw text
  if (txt && con->rangeFont) {
    // compute width
    int i = 0, width = 0;
    if (font == mjFONT_BIG) {
      while (txt[i]) {
        width += con->charWidthBig[(unsigned char)txt[i++]];
      }
    } else {
      while (txt[i]) {
        width += con->charWidth[(unsigned char)txt[i++]];
      }
    }

    // compute center
    int cx = (W - width)/2;
    int cy = (H - (font == mjFONT_BIG ? con->charHeightBig : con->charHeight))/2;

    // draw
    glListBase(font == mjFONT_BIG ? con->baseFontBig : con->baseFontNormal);
    glColor3f(rt, gt, bt);
    glRasterPos2i(mjMAX(0, cx), cy);
    glCallLists((GLsizei)strlen(txt), GL_UNSIGNED_BYTE, txt);
  }
}



// make text, strip trailing zeros
static void maketext(const char* format, char* txt, float num, int txt_sz) {
  int i, j;

  // full text
  snprintf(txt, txt_sz, format, num);

  // locate trailing zeros
  i = strlen(txt);
  while (i > 0 && txt[i-1] == '0') {
    i--;
  }
  if (i <= 1) {
    return;
  }

  // strip if preceding char is '.'
  if (txt[i-1] == '.') {
    txt[i-1] = 0;
  }

  // otherwise find earlier '.'
  else {
    // find regular preceding digits
    j = i-1;
    while (j >= 0 && txt[j] >= '0' && txt[j] <= '9') {
      j--;
    }

    // '.' found: strip
    if (j >= 0 && txt[j] == '.') {
      txt[i] = 0;
    }
  }
}



// compute text width in pixels
static int textwidth(const mjrContext* con, const char* text) {
  int i = 0, width = 0;

  // add character widths
  while (text[i]) {
    width += con->charWidth[(unsigned char)text[i++]];
  }

  return width;
}



// draw 2D figure
void mjr_figure(mjrRect viewport, mjvFigure* fig, const mjrContext* con) {
  float range[2][2];
  float griddata[100];
  const float minrange = 1e-5, offset = 0.001;
  const int PAD = con->charHeight/2;
  char datatxt[STRING_BUFSIZE];

  // empty viewport: nothing to do
  if (viewport.width <= 0 || viewport.height <= 0) {
    return;
  }

  // init OpenGL, set viewport
  init2D();
  glViewport(viewport.left, viewport.bottom, viewport.width, viewport.height);

  // clear background and blend
  glEnable(GL_BLEND);
  glColor4fv(fig->figurergba);
  glBegin(GL_QUADS);
  glVertex2d(0, 0);
  glVertex2d(0, 1);
  glVertex2d(1, 1);
  glVertex2d(1, 0);
  glEnd();
  glDisable(GL_BLEND);

  // determine range along (x,y)
  for (int axis=0; axis < 2; axis++) {
    // init ranges, set flag
    range[axis][0] = fig->range[axis][0];
    range[axis][1] = fig->range[axis][1];
    int flg;
    if (fig->range[axis][0] < fig->range[axis][1]) {
      flg = 1;
    } else {
      flg = 0;
    }

    // determine range: scan line data, find min and max
    if (!flg || fig->flg_extend) {
      for (int n=0; n < mjMAXLINE; n++) {
        for (int i=0; i < fig->linepnt[n]; i++) {
          // get data
          float ldata = fig->linedata[n][2*i+axis];

          // skip if not finite
          if (!isfinite(ldata)) {
            continue;
          }

          // copy first finite data point
          if (!flg) {
            range[axis][0] = ldata;
            range[axis][1] = ldata;
            flg = 1;
          }

          // otherwise regular update
          else {
            // update minimum
            if (range[axis][0] > ldata) {
              range[axis][0] = ldata;
            }

            // update maximum
            if (range[axis][1] < ldata) {
              range[axis][1] = ldata;
            }
          }
        }
      }
    }

    // make sure range is not too small
    if (range[axis][1]-range[axis][0] < minrange) {
      float needed = minrange - (range[axis][1]-range[axis][0]);
      range[axis][0] -= 0.5f*needed;
      range[axis][1] += 0.5f*needed;
    }

    // special handling for Y-axis
    if (axis == 1) {
      // get current range
      float y_min = range[1][0];
      float y_max = range[1][1];
      float diff = y_max - y_min;

      // snap to nearest enclosing decimal bounds
      if (diff > 0) {
        float power = powf(10, floorf(log10f(diff)));
        range[1][0] = floorf(y_min / power) * power;
        range[1][1] = ceilf(y_max / power) * power;
      }

      // make range symmetric
      if (fig->flg_symmetric) {
        float y_sym = mjMAX(fabsf(range[1][0]), fabsf(range[1][1]));
        range[1][0] = -y_sym;
        range[1][1] = +y_sym;
      }
    }
  }

  // save ranges
  fig->xaxisdata[0] = range[0][0];
  fig->xaxisdata[1] = range[0][1];
  fig->yaxisdata[0] = range[1][0];
  fig->yaxisdata[1] = range[1][1];

  // set projection in pixels
  glMatrixMode(GL_PROJECTION);
  glLoadIdentity();
  if (viewport.width <= 1 || viewport.height <= 1) {
    return;
  }
  glOrtho(0, viewport.width-1, 0, viewport.height-1, -1, 1);

  // draw title, save vertical size
  int sy_title = 0;
  if (fig->title[0]) {
    // find address of selected subplot title
    int subadr = 0, subcnt = 0;
    while (subcnt < fig->subplot && subadr < 1000 && fig->title[subadr]) {
      // skip non-space
      while (subadr < 1000 && fig->title[subadr] && fig->title[subadr] != ' ') {
        subadr++;
      }

      // skip space, count
      int cntspace = 0;
      while (subadr < 1000 && fig->title[subadr] == ' ') {
        subadr++;
        cntspace++;
      }

      // count subplot if 2+ spaces
      if (cntspace > 1) {
        subcnt++;
      }
    }

    // find length of selected subplot title (skip non-space or single space)
    int sublen = 0;
    while ((subadr+sublen < 1000 &&
            fig->title[subadr+sublen] &&
            fig->title[subadr+sublen] != ' ') ||
           (subadr+sublen+1 < 1000 && fig->title[subadr+sublen+1] &&
            fig->title[subadr+sublen+1] != ' ')) {
      sublen++;
    }

    // proceed if non-empty
    if (sublen) {
      // save vertical size
      sy_title = con->charHeight;

      // get title size
      int sx_title = 0;
      for (int i=subadr; i < subadr+sublen; i++) {
        sx_title += con->charWidth[(unsigned char)fig->title[i]];
      }

      // compute left edge of title
      int left = mjMAX(0, viewport.width/2-sx_title/2);

      // compute left edge and address of extended title
      int left1 = left, subadr1 = subadr;
      while (subadr1 > 0 && left1 >= con->charWidth[(unsigned char)fig->title[subadr1-1]]) {
        left1 -= con->charWidth[(unsigned char)fig->title[subadr1-1]];
        subadr1--;
      }

      // compute right edge and length of extended title
      int sublen1 = 0, right1 = left1;
      while (sublen1+subadr1 < 1000 && fig->title[subadr1+sublen1] &&
             right1+con->charWidth[(unsigned char)fig->title[subadr1+sublen1]] < viewport.width) {
        right1 += con->charWidth[(unsigned char)fig->title[subadr1+sublen1]];
        sublen1++;
      }

      // extended title
      glListBase(con->baseFontNormal);
      glColor4f(0.4*fig->textrgb[0]+0.6*fig->figurergba[0],
                0.4*fig->textrgb[1]+0.6*fig->figurergba[1],
                0.4*fig->textrgb[2]+0.6*fig->figurergba[2], 1);
      glRasterPos3f(left1, viewport.height-PAD-sy_title, 0);
      glCallLists((GLsizei)sublen1, GL_UNSIGNED_BYTE, fig->title+subadr1);

      // actual title
      glListBase(con->baseFontNormal);
      glColor4f(fig->textrgb[0], fig->textrgb[1], fig->textrgb[2], 1);
      glRasterPos3f(left, viewport.height-PAD-sy_title, 0);
      glCallLists((GLsizei)sublen, GL_UNSIGNED_BYTE, fig->title+subadr);
    }
  }

  // draw xlabel, save size
  int sx_label = 0, sy_label = 0;
  if (fig->xlabel[0]) {
    // get xlabel size
    sx_label = textwidth(con, fig->xlabel);
    sy_label = con->charHeight;

    // render xlabel
    mjr_textActual(mjFONT_NORMAL, fig->xlabel, con,
                   mjMAX(0, viewport.width/2-sx_label/2), PAD, 0,
                   fig->textrgb[0], fig->textrgb[1], fig->textrgb[2]);
  }

  // reduce viewport to account for title and xlabel vertical size
  if (sy_title || sy_label) {
    // reduce viewport
    viewport.bottom += (sy_label ? sy_label + PAD/2 : 0);
    viewport.height -= (sy_label ? sy_label + PAD/2 : 0) + (sy_title ? sy_title + PAD : 0);
    if (viewport.width <= 0 || viewport.height <= 0) {
      return;
    }
    glViewport(viewport.left, viewport.bottom, viewport.width, viewport.height);

    // set matrix projection in pixels
    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    glOrtho(0, viewport.width-1, 0, viewport.height-1, -1, 1);
  }

  // draw optional tick labels, reduce viewport
  if (fig->gridsize[0] > 1 && fig->gridsize[1] > 1) {
    char txt[STRING_BUFSIZE];

    // determine xtick height
    int xtick_height = 0;
    if (fig->flg_ticklabel[0]) {
      xtick_height = con->charHeight + PAD;
    }

    // determine ytick width
    int ytick_width = 0;
    if (fig->flg_ticklabel[1]) {
      // start with minwidth
      if (fig->minwidth[0]) {
        ytick_width = textwidth(con, fig->minwidth);
      }

      // limit to 20 ticks
      int n = mjMIN(20, fig->gridsize[1]);

      // generate all strings, find max width
      for (int i=0; i < n; i++) {
        const int txt_sz = mjSIZEOFARRAY(txt);
        maketext(fig->yformat, txt, range[1][0] + (range[1][1]-range[1][0])*i/(float)(n-1),
                 txt_sz);
        ytick_width = mjMAX(ytick_width, textwidth(con, txt));
      }

      ytick_width += PAD;
    }

    // draw xtick
    if (fig->flg_ticklabel[0]) {
      // limit to 20 ticks
      int n = mjMIN(20, fig->gridsize[0]);

      // process ticks
      for (int i=0; i < n; i++) {
        // make text, get width
        {
          const int txt_sz = mjSIZEOFARRAY(txt);
          maketext(fig->xformat, txt,
                   range[0][0] + (range[0][1]-range[0][0])*i/(float)(n-1), txt_sz);
        }
        int w = textwidth(con, txt);

        // draw
        mjr_textActual(mjFONT_NORMAL, txt, con,
                       PAD+ytick_width+
                       (i == 0 ? 0 : (i == n-1 ? -w : -w/2))+
                       (viewport.width-2*PAD-ytick_width)*i/(float)(n-1),
                       PAD, 0,
                       fig->textrgb[0], fig->textrgb[0], fig->textrgb[0]);
      }
    }

    // draw ytick
    if (fig->flg_ticklabel[1]) {
      // limit to 20 ticks
      int n = mjMIN(20, fig->gridsize[1]);

      // process ticks
      for (int i=0; i < n; i++) {
        // make text, get width
        {
          const int txt_sz = mjSIZEOFARRAY(txt);
          maketext(fig->yformat, txt,
                   range[1][0] + (range[1][1]-range[1][0])*i/(float)(n-1), txt_sz);
        }
        int w = textwidth(con, txt);

        // draw
        mjr_textActual(mjFONT_NORMAL, txt, con,
                       ytick_width-w,
                       PAD+xtick_height-con->charHeight/2+
                       (viewport.height-2*PAD-xtick_height)*i/(float)(n-1), 0,
                       fig->textrgb[0], fig->textrgb[0], fig->textrgb[0]);
      }
    }

    // adjust viewport size
    viewport.left += ytick_width;
    viewport.width -= ytick_width;
    viewport.bottom += xtick_height;
    viewport.height -= xtick_height;
  }

  // set plot viewport
  viewport.left += PAD;
  viewport.width -= 2*PAD;
  viewport.bottom += PAD;
  viewport.height -= 2*PAD;
  if (viewport.width <= 0 || viewport.height <= 0) {
    return;
  }
  glViewport(viewport.left, viewport.bottom, viewport.width, viewport.height);

  // save range
  fig->xaxispixel[0] = viewport.left;
  fig->xaxispixel[1] = viewport.left + viewport.width - 1;
  fig->yaxispixel[0] = viewport.bottom;
  fig->yaxispixel[1] = viewport.bottom + viewport.height - 1;

  // set matrix projection to (0,1) + offset
  glMatrixMode(GL_PROJECTION);
  glLoadIdentity();
  glOrtho(-offset, 1 + offset, -offset, 1 + offset, -1, 1);

  // draw pane background
  glEnable(GL_BLEND);
  glColor4fv(fig->panergba);
  glBegin(GL_QUADS);
  glVertex2d(0, 0);
  glVertex2d(0, 1);
  glVertex2d(1, 1);
  glVertex2d(1, 0);
  glEnd();
  glDisable(GL_BLEND);

  // prepare to render lines
  glEnableClientState(GL_VERTEX_ARRAY);

  // draw grid
  if (fig->gridsize[0] > 1 && fig->gridsize[1] > 1) {
    // common GL state
    glVertexPointer(2, GL_FLOAT, 0, griddata);
    glColor3fv(fig->gridrgb);
    glLineWidth(fig->gridwidth);

    // prepare vertical lines
    int n = mjMIN(20, fig->gridsize[0]);
    for (int i=0; i < n; i++) {
      griddata[4*i] = i/(float)(n-1);
      griddata[4*i+1] = 0;
      griddata[4*i+2] = griddata[4*i];
      griddata[4*i+3] = 1;
    }

    // draw vertical lines
    glDrawArrays(GL_LINES, 0, 2*n);

    // prepare horizontal lines
    n = mjMIN(20, fig->gridsize[1]);
    for (int i=0; i < n; i++) {
      griddata[4*i] = 0;
      griddata[4*i+1] = i/(float)(n-1);
      griddata[4*i+2] = 1;
      griddata[4*i+3] = griddata[4*i+1];
    }

    // draw vertical lines
    glDrawArrays(GL_LINES, 0, 2*n);
  }

  // set matrix projection to range, with small padding
  float difx = range[0][1] - range[0][0];
  float dify = range[1][1] - range[1][0];
  glMatrixMode(GL_PROJECTION);
  glLoadIdentity();
  glOrtho(range[0][0] - difx*offset, range[0][1] + difx*offset,
          range[1][0] - dify*offset, range[1][1] + dify*offset, -1, 1);

  // draw lines: back to front
  for (int n=mjMAXLINE-1; n >= 0; n--)
    if (fig->linepnt[n]) {
      // GL state
      glVertexPointer(2, GL_FLOAT, 0, fig->linedata[n]);
      glColor3fv(fig->linergb[n]);
      glLineWidth(fig->linewidth);

      // draw line
      glDrawArrays(fig->flg_barplot ? GL_LINES : GL_LINE_STRIP, 0, fig->linepnt[n]);
    }

  // selection
  if (fig->flg_selection) {
    glColor3f(1, 1, 1);
    glLineWidth(3);
    glBegin(GL_LINES);
    glVertex2f(fig->selection, range[1][0]);
    glVertex2f(fig->selection, range[1][1]);
    glEnd();
  }

  // draw legend, find line to highlight
  int hlight = fig->highlightid;
  if (fig->flg_legend) {
    // set matrix projection in pixels
    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    if (viewport.width <= 1 || viewport.height <= 1) {
      return;
    }
    glOrtho(0, viewport.width-1, 0, viewport.height-1, -1, 1);

    // find legend size
    int lw = 0, lh = 0, cnt = 0;
    for (int n=fig->legendoffset; n < mjMAXLINE; n++) {
      if (fig->linename[n][0]) {
        // max width, accumulate height
        lw = mjMAX(lw, textwidth(con, fig->linename[n]));
        lh += con->charHeight;

        // count
        cnt++;

        // too big: correct and break
        if (lh+2*PAD >= viewport.height) {
          cnt--;
          lh -= con->charHeight;
          break;
        }
      }
    }

    // detect highlighted line
    int hcnt = -1;
    int hx = fig->highlight[0] - viewport.left;
    int hy = fig->highlight[1] - viewport.bottom;
    if (hx >= viewport.width-PAD-lw &&
        hx <= viewport.width-PAD &&
        hy >= viewport.height-PAD-lh &&
        hy <= viewport.height-PAD) {
      hcnt = (viewport.height-PAD-hy) / con->charHeight;
    }

    // draw background
    if (cnt) {
      glEnable(GL_BLEND);
      glColor4fv(fig->legendrgba);
      glBegin(GL_QUADS);
      glVertex2d(viewport.width-2*PAD-lw, viewport.height-2*PAD-lh);
      glVertex2d(viewport.width-2*PAD-lw, viewport.height);
      glVertex2d(viewport.width, viewport.height);
      glVertex2d(viewport.width, viewport.height-2*PAD-lh);
      glEnd();
      glDisable(GL_BLEND);
    }

    // find named lines and draw text
    cnt = 0;
    for (int n=fig->legendoffset; n < mjMAXLINE; n++) {
      if (fig->linename[n][0]) {
        // get width
        int width = textwidth(con, fig->linename[n]);

        // render right-aligned
        mjr_textActual(mjFONT_SHADOW, fig->linename[n], con,
                       viewport.width-PAD-width, viewport.height-PAD-(cnt+1)*con->charHeight, 0,
                       fig->linergb[n][0], fig->linergb[n][1], fig->linergb[n][2]);

        // save hlight
        if (hcnt == cnt) {
          hlight = n;
        }

        // count
        cnt++;

        // break if viewport is full
        if ((cnt+1)*con->charHeight+2*PAD >= viewport.height) {
          break;
        }
      }
    }
  }

  // draw highlight
  if (hlight >= 0 && hlight < mjMAXLINE) {
    // line-rendering projection
    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();
    glOrtho(range[0][0] - difx*offset, range[0][1] + difx*offset,
            range[1][0] - dify*offset, range[1][1] + dify*offset, -1, 1);

    // render selected line with extra width
    glVertexPointer(2, GL_FLOAT, 0, fig->linedata[hlight]);
    glColor3fv(fig->linergb[hlight]);
    glLineWidth(5*fig->linewidth);
    glDrawArrays(fig->flg_barplot ? GL_LINES : GL_LINE_STRIP, 0, fig->linepnt[hlight]);

    // print data coordinates
    if (fig->flg_selection) {
      // find nearest x-value
      int ibest = -1;
      float best = 0;
      for (int i=0; i < fig->linepnt[hlight]; i++) {
        if (ibest < 0 || best > fabs(fig->selection-fig->linedata[hlight][2*i])) {
          ibest = i;
          best = fabs(fig->selection-fig->linedata[hlight][2*i]);
        }
      }

      // show text
      mjSNPRINTF(datatxt, "( %.4g : %.4g )",
                 fig->linedata[hlight][2*ibest], fig->linedata[hlight][2*ibest+1]);
      mjr_textActual(mjFONT_SHADOW, datatxt, con,
                     0.9*range[0][0]+0.1*range[0][1], 0.9*range[1][0]+0.1*range[1][1], 0,
                     fig->textrgb[0], fig->textrgb[1], fig->textrgb[2]);
    }
  }

  // stop rendering lines
  glDisableClientState(GL_VERTEX_ARRAY);
}
