

#include "tstencilcontrol.h"
#include "tgl.h"
#include "tthreadmessage.h"
#include <stack>

#include <QThreadStorage>

//-----------------------------------------------------------------------------------------

namespace {

// singleton
class StencilControlManager {
  QThreadStorage<TStencilControl *> m_storage;

  StencilControlManager() {}

public:
  static StencilControlManager *instance() {
    static StencilControlManager theInstance;
    return &theInstance;
  }

  TStencilControl *getCurrentStencilControl() {
    if (!m_storage.hasLocalData()) {
      m_storage.setLocalData(new TStencilControl);
    }

    return m_storage.localData();
  }

  ~StencilControlManager() {}
};

}  // Local namespace

//-----------------------------------------------------------------------------------------

class TStencilControl::Imp {
public:
  int m_stencilBitCount;
  int m_pushCount;
  int m_currentWriting;  // current stencil bit plane.
  // 0 is the first bit plane ; -1 means no stencil mask is writing

  int m_virtualState;
// the state of the (eventually virtual) top mask.
// A mask is virtual if overflows stencil buffer
// 0 is closed and disabled, 1 closed and enabled and 2 is opened

#ifdef _DEBUG
  std::stack<bool> fullState;
// state of each mask in the stack (except top mask).
// 'true' means opend; 'false' means close and enabled
// Used only for assert
#endif

  unsigned char m_writingMask;  // bit mask. The i-th bit=1 iff the i-th mask is
                                // opened to write
  unsigned char m_drawOnScreenMask;  // bitmsk.The ith bit=1 iff the ith mask
                                     // WRITE also ON SCREEN WHEN WRITE ON
                                     // STENCIL BIT PLANE
  unsigned char
      m_enabledMask;  // bit mask. The i-th bit=1 iff the i-th mask is enabled
  unsigned char
      m_inOrOutMask;  // bit mask. The i-th bit=1 iff the i-th mask is inside
  unsigned char m_drawOnlyOnceMask;

  Imp();

  void updateOpenGlState();

  void pushMask();
  // make a new stencil plane the current one.
  // if there is no plane available, increase a counter and does not push
  // (virtual masks)
  // So the same number of pop has no effect

  void popMask();
  // assert if the stencil stack contains only 0

  void beginMask(DrawMode drawMode);
  // clear the current stencil plane; start writing to it
  // if drawOnScreen is not 0, it writes also to the color buffer (or stencil
  // plane if another
  // mask is open). If drawOnScreen is 2, it drows only once (by stencil buffer)

  void endMask();
  // end writing to the stencil plane.

  void enableMask(MaskType maskType);
  void disableMask();
  // between enableMask()/disableMask() drawing is filtered by the values of the
  // current
  // stencil plane
  // n.b. enableMask()/disableMask() can be nidified. Between the inner pair
  // writing is enabled
  // according to the AND of all of them
};

//-----------------------------------------------------------------------------------------

TStencilControl::Imp::Imp()
    : m_stencilBitCount(0)
    , m_pushCount(1)
    , m_currentWriting(-1)
    , m_enabledMask(0)
    , m_writingMask(0)
    , m_inOrOutMask(0)
    , m_drawOnScreenMask(0)
    , m_drawOnlyOnceMask(0)
    , m_virtualState(0) {
  glGetIntegerv(GL_STENCIL_BITS, (GLint *)&m_stencilBitCount);

  glStencilMask(0xFFFFFFFF);
  // glClearStencil(0);
  glClear(GL_STENCIL_BUFFER_BIT);
}

//---------------------------------------------------------

TStencilControl *TStencilControl::instance() {
  StencilControlManager *instance = StencilControlManager::instance();
  return instance->getCurrentStencilControl();
}

//---------------------------------------------------------

TStencilControl::TStencilControl() : m_imp(new Imp) {}

//---------------------------------------------------------

TStencilControl::~TStencilControl() {}

//---------------------------------------------------------

void TStencilControl::Imp::updateOpenGlState() {
  if (m_currentWriting >= 0) {  // writing on stencil buffer
    unsigned char currentWritingMask = 1 << m_currentWriting;

    bool drawOnlyOnce = (currentWritingMask & m_drawOnlyOnceMask) != 0;

    if (currentWritingMask & m_drawOnScreenMask) {
      unsigned char lastWritingMask;
      int lastWriting = m_currentWriting - 1;
      for (; lastWriting >= 0; lastWriting--) {
        lastWritingMask = 1 << lastWriting;
        if ((lastWritingMask & m_writingMask) == lastWritingMask) break;
      }
      if (lastWriting < 0) {
        // glColorMask(1,1,1,1);

        if (drawOnlyOnce)
          m_enabledMask |= currentWritingMask;
        else
          m_enabledMask &= ~currentWritingMask;
      } else {
        tglMultColorMask(0, 0, 0, 0);
        // glDrawBuffer(GL_NONE);

        drawOnlyOnce = false;  // essendo solo un'effetto visivo, se sto
                               // scrivendo su una maschera e non a schermo, e'
                               // inutile
        currentWritingMask |= lastWritingMask;
      }
    } else
      tglMultColorMask(0, 0, 0, 0);
    // glDrawBuffer(GL_NONE);

    glStencilMask(currentWritingMask);

    if (drawOnlyOnce) {
      glStencilFunc(GL_EQUAL, m_inOrOutMask, m_enabledMask);
      glStencilOp(GL_KEEP, GL_INVERT, GL_INVERT);
    } else {
      glStencilFunc(GL_EQUAL, currentWritingMask | m_inOrOutMask,
                    m_enabledMask);
      glStencilOp(GL_KEEP, GL_REPLACE, GL_REPLACE);
    }

  } else {  // writing on screen buffer
    glStencilMask(0xFFFFFFFF);
    glStencilFunc(GL_EQUAL, m_inOrOutMask, m_enabledMask);
    glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP);
    // glColorMask(1,1,1,1);
  }

  if (!m_enabledMask && m_currentWriting < 0)
    glDisable(GL_STENCIL_TEST);
  else
    glEnable(GL_STENCIL_TEST);
}

//---------------------------------------------------------

void TStencilControl::Imp::pushMask() { m_pushCount++; }

//---------------------------------------------------------

void TStencilControl::Imp::beginMask(DrawMode drawMode) {
  m_currentWriting          = m_pushCount - 1;
  unsigned char currentMask = 1 << m_currentWriting;

  m_writingMask |= currentMask;

  if (drawMode == DRAW_ALSO_ON_SCREEN) {
    m_drawOnScreenMask |= currentMask;
  } else if (drawMode == DRAW_ON_SCREEN_ONLY_ONCE) {
    m_drawOnScreenMask |= currentMask;
    m_drawOnlyOnceMask |= currentMask;
  } else {
    m_drawOnScreenMask &= ~currentMask;
    m_drawOnlyOnceMask &= ~currentMask;
  }

  glEnable(GL_STENCIL_TEST);
  glStencilMask(currentMask);      // enabled to modify only current bitPlane
  glClear(GL_STENCIL_BUFFER_BIT);  // and clear it
  updateOpenGlState();
}

//---------------------------------------------------------

void TStencilControl::Imp::endMask() {
  assert(m_pushCount - 1 == m_currentWriting);

  unsigned char currentMask = 1 << (m_pushCount - 1);

  m_writingMask &= ~currentMask;  // stop writing
  m_enabledMask &= ~currentMask;
  m_drawOnScreenMask &= ~currentMask;
  m_drawOnlyOnceMask &= ~currentMask;

  //----------------------------------------------------------

  m_currentWriting--;
  for (; m_currentWriting >= 0; m_currentWriting--) {
    unsigned char currentWritingMask = 1 << m_currentWriting;
    if ((currentWritingMask & m_writingMask) == currentWritingMask) break;
  }

  updateOpenGlState();
}

//---------------------------------------------------------

void TStencilControl::Imp::enableMask(MaskType maskType) {
  unsigned char currentMask = 1 << (m_pushCount - 1);

  if ((m_enabledMask & currentMask) == 0) glPushAttrib(GL_ALL_ATTRIB_BITS);

  m_enabledMask |= currentMask;

  assert(maskType == SHOW_INSIDE || maskType == SHOW_OUTSIDE);

  if (maskType == SHOW_INSIDE)
    m_inOrOutMask |= currentMask;
  else
    m_inOrOutMask &= ~currentMask;

  updateOpenGlState();
}

//---------------------------------------------------------

void TStencilControl::Imp::disableMask() {
  unsigned char currentMask = 1 << (m_pushCount - 1);

  m_enabledMask &= ~currentMask;
  m_inOrOutMask &= ~currentMask;

  updateOpenGlState();
  glPopAttrib();
}

//---------------------------------------------------------

void TStencilControl::Imp::popMask() {
  --m_pushCount;
  assert(m_pushCount > 0);  // there is at least one mask in the stack
}

//---------------------------------------------------------
//---------------------------------------------------------

// questo forse e' un po' brutto:
// La maschera e' chiusa.
// Se e' abilitata,    open = push+open
// Se e' disabilitata, open =      open

void TStencilControl::beginMask(DrawMode drawMode) {
  glPushAttrib(GL_ALL_ATTRIB_BITS);

  if (m_imp->m_virtualState)  // opened or enabled
  {
#ifdef _DEBUG
    m_imp->fullState.push(m_imp->m_virtualState == 2);
#endif

    m_imp->pushMask();
  }

  m_imp->m_virtualState = 2;  // opened

  if (m_imp->m_pushCount <= m_imp->m_stencilBitCount)
    m_imp->beginMask(drawMode);
}

//---------------------------------------------------------

void TStencilControl::endMask() {
  if (!m_imp->m_virtualState)  // closed and disabled
  {
    m_imp->popMask();

#ifdef _DEBUG
    assert(m_imp->fullState.top());  // the state before last push must be
                                     // opened
    m_imp->fullState.pop();
#endif
  }

  assert(m_imp->m_virtualState != 1);  // yet closed

  m_imp->m_virtualState = 0;

  if (m_imp->m_pushCount <= m_imp->m_stencilBitCount) m_imp->endMask();

  glPopAttrib();
}

//---------------------------------------------------------

void TStencilControl::enableMask(MaskType maskType) {
  assert(m_imp->m_virtualState != 2);  // cannot enable an opened mask

  m_imp->m_virtualState = 1;  // enabled

  if (m_imp->m_pushCount <= m_imp->m_stencilBitCount)
    m_imp->enableMask(maskType);
}

//---------------------------------------------------------

void TStencilControl::disableMask() {
  assert(m_imp->m_virtualState != 2);  // cannot disable an opened mask

  if (!m_imp->m_virtualState)  // closed and disabled
  {
    m_imp->popMask();

#ifdef _DEBUG
    assert(
        !m_imp->fullState.top());  // the state before last push must be enabled
    m_imp->fullState.pop();
#endif
  }

  m_imp->m_virtualState = 0;  // close and disabled

  if (m_imp->m_pushCount <= m_imp->m_stencilBitCount) m_imp->disableMask();
}

//---------------------------------------------------------
