/* -*- mode: c++; indent-tabs-mode: nil -*- */
/*
    Qore Programming Language

    Copyright (C) 2006 - 2022 Qore Technologies, s.r.o.

    This library is free software; you can redistribute it and/or
    modify it under the terms of the GNU Lesser General Public
    License as published by the Free Software Foundation; either
    version 2.1 of the License, or (at your option) any later version.

    This library is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
    Lesser General Public License for more details.

    You should have received a copy of the GNU Lesser General Public
    License along with this library; if not, write to the Free Software
    Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
*/

#include <qore/Qore.h>
#include "oracle.h"
#include "ocilib_internal.h"
#include "AQMessagePriv.h"

#include "AQQueuePriv.h"

#include <memory>

// handle external (oracle) threads for qore/aq
class AQCallbackThreadManager {
protected:
   pthread_t ora_thread;
   QoreThreadLock l;
   unsigned count;
   int tid;

public:
   DLLLOCAL AQCallbackThreadManager() : count(0), tid(0) {
   }

   DLLLOCAL ~AQCallbackThreadManager() {
      assert(!tid);
      assert(!count);
   }

   DLLLOCAL void inc() {
      AutoLocker al(l);
      if (!count) {
         assert(!tid);
         tid = q_reserve_foreign_thread_id();
         assert(tid > 0);
      }
      ++count;
   }

   DLLLOCAL void dec() {
      AutoLocker al(l);
      if (!--count) {
         q_release_reserved_foreign_thread_id(tid);
         tid = 0;
      }
   }

   DLLLOCAL int getTid() const {
      assert(tid);
      return tid;
   }

   DLLLOCAL unsigned getCount() const {
      return count;
   }
};

AQCallbackThreadManager aqCallbackThreadManager;

// periodically ping registered subscriptions and reconnect on demand
class AQSubscriptionManager {
protected:
   typedef std::set<AQQueuePriv*> t_list;

   pthread_t checkThread;
   QoreThreadLock l;
   QoreCondition c;
   t_list objects;
   bool quit;

public:
   DLLLOCAL AQSubscriptionManager() : quit(false) {
   }

   DLLLOCAL ~AQSubscriptionManager() {
      //printd(5, "~AQSubscriptionManager size: %d; tid: %d\n", objects.size(), tid);
      // std::set::size() is O(n) whereas set::empty() is O(1)
      assert(objects.empty());
   }

   DLLLOCAL int add(AQQueuePriv* o, ExceptionSink* xsink) {
      bool start = false;
      {
         AutoLocker al(l);
         if (objects.empty())
            start = true;
         objects.insert(o);
      }
      if (start) {
         int rc;
         if (((rc = pthread_create(&checkThread, NULL, &AQSubscriptionManager::thread_helper, this)))) {
            xsink->raiseErrnoException("THREAD-CREATION-FAILURE", rc, "could not create global subscription watchdog thread");
            AutoLocker al(l);
            objects.erase(o);
            return -1;
         }
      }
      return 0;
   }

   DLLLOCAL void remove(AQQueuePriv* o) {
      AutoLocker al(l);
      removeUnlocked(o);
   }

   DLLLOCAL void removeUnlocked(AQQueuePriv* o) {
      t_list::iterator i = objects.find(o);
      assert(i != objects.end());
      objects.erase(i);

      //printd(5, "AQSubscriptionManager::removeUnlocked(): removed %p size: %d\n", o, (int)objects.size());

      // stop polling thread if empty
      if (objects.empty()) {
         //printd(5, "AQSubscriptionManager::removeUnlocked(): stopping last thread\n");
         quit = true;
         c.signal();
      }
   }

   DLLLOCAL void ping() {
      t_list::iterator it = objects.begin();
      while (it != objects.end()) {
         // printd(5, "connection check: %p\n", (*it));
         if (!(*it)->ping()) {
            printd(5, "connection lost, restoring subscription: %p\n", (*it));

            AQQueuePriv *o = (*it);

            ExceptionSink xsink;
            // remove object from list if reconnection fails
            if (o->reconnectSubscription(&xsink)) {
               objects.erase(it);
               if (objects.empty())
                  quit = true;
               aqCallbackThreadManager.dec();
            }

            o->qoreObject()->evalMethod("onSubscriptionLost", 0, &xsink);
         }

         ++it;
      }
   }

   DLLLOCAL void run() {
      QoreForeignThreadHelper qfth;
      //printd(5, "AQSubscriptionManager::run(): starting connection watchdog thread\n");

      AutoLocker al(l);
      while (!quit) {
         ping();
         // poll every 25 seconds
         if (!quit)
            c.wait(l, 25000);
      }
   }

   DLLLOCAL static void* thread_helper(void* context) {
       ((AQSubscriptionManager*)context)->run();
       return 0;
   }
};

AQSubscriptionManager aqSubscriptionManager;

void on_async_message(OCI_Dequeue* deq) {
   QoreForeignThreadHelper qfth(aqCallbackThreadManager.getTid());

   //printd(5, "on_async_message() DEBUG> message arrived deq: %p obj: %p\n", deq, deq->user_ctx);
   ExceptionSink xsink;
   reinterpret_cast<QoreObject*>(deq->user_ctx)->evalMethod("onAsyncMessage", 0, &xsink);
}

const char* check_hash_key(const QoreHashNode* h, const char* key, const char* err, ExceptionSink *xsink) {
    QoreValue p = h->getKeyValue(key);
    if (p.isNothing())
        return nullptr;

    if (p.getType() != NT_STRING) {
        xsink->raiseException(err, "'%s' key is not type 'string' but is type '%s'", key, p.getTypeName());
        return nullptr;
    }
    return p.get<const QoreStringNode>()->c_str();
}

AQQueuePriv::AQQueuePriv(Datasource *ds, const QoreStringNode *tname, const QoreStringNode *qname, ExceptionSink *xsink)
    : m_conn(ds->getPrivateData<QoreOracleConnection>()),
      m_ds(ds),
      m_tname(tname->stringRefSelf()),
      m_qname(qname->stringRefSelf()),
      m_typeInfo(0),
      m_enqueue(0),
      m_dequeue(0),
      m_port(0),
      m_timeout(0),
      m_hasSubscription(false),
      valid(true) {
   //printd(5, "AQQueuePriv::AQQueuePriv() this: %p\n", this);
   connect(xsink);
}

AQQueuePriv::~AQQueuePriv() {
   //printd(5, "AQQueuePriv::~AQQueuePriv() this: %p\n", this);
   assert(!m_enqueue);
   assert(!m_dequeue);
   assert(!m_typeInfo);
   assert(!m_tname);
   assert(!m_qname);
   assert(!m_ds);
}

void AQQueuePriv::destructor(ExceptionSink* xsink) {
   //printd(5, "AQQueuePriv::destructor() this: %p\n", this);
   AutoLocker al(l);
   destructorUnlocked(xsink);
}

void AQQueuePriv::destructorUnlocked(ExceptionSink *xsink) {
   assert(valid);

   disconnectUnlocked(xsink);
   m_tname->deref();
   m_qname->deref();
   m_ds->close();
   delete m_ds;
#ifdef DEBUG
   m_tname = 0;
   m_qname = 0;
   m_ds = 0;
#endif

   valid = false;
}

int AQQueuePriv::reconnectUnlocked(ExceptionSink* xsink) {
   if (m_hasSubscription) {
      if (reconnectSubscriptionUnlocked(xsink)) {
         aqCallbackThreadManager.dec();
         aqSubscriptionManager.remove(this);
         return -1;
      }
      return 0;
   }

   disconnectUnlocked(0);
   return connectUnlocked(xsink);
}

int AQQueuePriv::reconnectSubscription(ExceptionSink* xsink) {
   AutoLocker al(l);
   return reconnectSubscriptionUnlocked(xsink);
}

int AQQueuePriv::reconnectSubscriptionUnlocked(ExceptionSink* xsink) {
   stopSubscriptionUnlocked(0);
   disconnectUnlocked(0);

   //printd(5, "AQQueuePriv::reconnectSubscriptionUnlocked() disconnected()\n");

   if (connectUnlocked(xsink))
      return -1;

   //printd(5, "AQQueuePriv::reconnectSubscriptionUnlocked() reconnected()\n");

   if (!checkDequeueUnlocked(qoreObject(), xsink))
      return -1;

   if (!OCI_DequeueSubscribe(&m_conn->ocilib, m_dequeue, m_port, m_timeout, on_async_message, xsink))
      return -1;

   m_hasSubscription = true;

   //printd(5, "AQQueuePriv::reconnectSubscriptionUnlocked() resubscribed()\n");
   return 0;
}

void AQQueuePriv::disconnect(ExceptionSink* xsink) {
   AutoLocker al(l);
   disconnectUnlocked(xsink);
}

void AQQueuePriv::disconnectUnlocked(ExceptionSink* xsink) {
   //printd(5, "AQQueuePriv::disconnectUnlocked() this: %p m_dequeue: %p hasSubscription: %d\n", this, m_dequeue, m_hasSubscription);
   if (m_enqueue) {
      OCI_EnqueueFree(&m_conn->ocilib, m_enqueue);
      m_enqueue = 0;
   }
   if (m_dequeue) {
      if (m_hasSubscription) {
         aqSubscriptionManager.removeUnlocked(this);
         stopSubscriptionUnlocked(xsink);
      }
      OCI_DequeueFree(&m_conn->ocilib, m_dequeue, xsink);
      m_dequeue = 0;
   }
   if (m_typeInfo) {
      OCI_TypeInfoFree2(&m_conn->ocilib, m_typeInfo);
      m_typeInfo = 0;
   }

   m_ds->close();
   m_conn = 0;
   //m_conn->logoff();
}

void AQQueuePriv::connect(ExceptionSink *xsink) {
   AutoLocker al(l);
   disconnectUnlocked(xsink);
   connectUnlocked(xsink);
}

int AQQueuePriv::connectUnlocked(ExceptionSink *xsink) {
   assert(!m_conn);

   if (m_ds->open(xsink)) {
      //printd(5, "AQQueuePriv::connectUnlocked() this: %p open failed\n", this);
      return -1;
   }
   m_conn = m_ds->getPrivateData<QoreOracleConnection>();

   //printd(5, "AQQueuePriv::connectUnlocked() this: %p qe: %p (%s)\n", this, m_conn->ds.getQoreEncoding(), m_conn->ds.getQoreEncoding()->getCode());

   // can only call Datasource::getQoreEncoding() after the call to Datasource::open()
   TempEncodingHelper tn(m_tname, m_conn->ds.getQoreEncoding(), xsink);
   if (*xsink)
      return -1;

   TempEncodingHelper qn(m_qname, m_conn->ds.getQoreEncoding(), xsink);
   if (*xsink)
      return -1;

   // use QoreString instead of a fixed-size buffer to deal with arbitrary-length strings
   QoreStringMaker value("Qore AQ Queue: %s", qn->getBuffer());
   OCI_SetTrace(&m_conn->ocilib, m_conn->ocilib_cn, OCI_TRC_MODULE, value.getBuffer(), xsink);
   if (*xsink)
      return -1;

   assert(!m_typeInfo);
   m_typeInfo = OCI_TypeInfoGet2(&m_conn->ocilib, m_conn->ocilib_cn, tn->getBuffer(), OCI_TIF_TYPE, xsink);

   if (!m_typeInfo) {
      if (!*xsink)
         xsink->raiseException("AQ-MESSAGE-ERROR", "Cannot get type info for '%s'", m_tname->getBuffer());
      destructorUnlocked(xsink);
      return -1;
   }

   return 0;
}

bool AQQueuePriv::pingUnlocked() {
   return OCI_Ping(&m_conn->ocilib, m_conn->ocilib_cn, 0);
}

bool AQQueuePriv::ping() {
   AutoLocker al(l);
   return pingUnlocked();
}

int AQQueuePriv::checkValidUnlocked(const char* m, ExceptionSink* xsink) {
   if (!valid) {
       xsink->raiseException("AQQUEUE-ERROR", "the AQQueue::%s() method cannot be executed because the AQQueue object has already been deleted in another thread", m);
      return -1;
   }
   if (!m_conn && connectUnlocked(xsink))
      return -1;

   return 0;
}

bool AQQueuePriv::post(AQMessagePriv *message, ExceptionSink *xsink) {
   // we have to dereference the message object when we return
   ReferenceHolder<AQMessagePriv> mholder(message, xsink);

   AutoLocker al(l);
   if (checkValidUnlocked("post", xsink))
      return false;

    if (!m_enqueue) {
       //printd(5, "Opening W queue: %s (%p)\n", m_qname->getBuffer(), m_typeInfo);
       m_enqueue = OCI_EnqueueCreate(&m_conn->ocilib, m_typeInfo, m_qname->getBuffer());
       if (!m_enqueue) {
          xsink->raiseException("AQQUEUE-ERROR", "failed to initialize queue");
          return false;
       }
    }

    ReferenceHolder<QoreHashNode> obj(message->getObject(), xsink);
    if (!obj) {
        xsink->raiseException("AQQUEUE-ERROR", "no oracle object (hash) provided in the message");
        return false;
    }

    OCI_Object *o = objBindQore(m_conn, *obj, xsink);
    if (!o) {
       xsink->raiseException("AQQUEUE-ERROR", "failed to create(bind) object");
       return false;
    }
    ON_BLOCK_EXIT(OCI_ObjectFree2, &m_conn->ocilib, o, xsink);

    OCI_Msg *m = OCI_MsgCreate(&m_conn->ocilib, m_typeInfo, xsink);
    if (!m) {
       if (!*xsink)
          xsink->raiseException("AQQUEUE-ERROR", "cannot allocate message object");
       return false;
    }
    ON_BLOCK_EXIT(OCI_MsgFree, &m_conn->ocilib, m, xsink);

    if (!OCI_MsgSetObject(&m_conn->ocilib, m, o, xsink)) {
       if (!*xsink)
          xsink->raiseException("AQQUEUE-ERROR", "cannot se message object");
       return false;
    }

    if (!OCI_MsgSetEnqueueDelay(&m_conn->ocilib, m, message->getEnqueueDelay(), xsink)) {
       if (!*xsink)
          xsink->raiseException("AQQUEUE-ERROR", "failed to set message delay");
       return false;
    }
    if (!OCI_MsgSetExpiration(&m_conn->ocilib, m, message->getExpiration(), xsink)) {
       if (!*xsink)
          xsink->raiseException("AQQUEUE-ERROR", "failed to set expiration");
       return false;
    }
    if (!OCI_MsgSetPriority(&m_conn->ocilib, m, message->getPriority(), xsink)) {
       if (!*xsink)
          xsink->raiseException("AQQUEUE-ERROR", "failed to set priority");
       return false;
    }

    QoreStringNodeHolder correlation(message->getCorrelation());

    if (*correlation) {
        TempEncodingHelper encoded(*correlation, m_conn->ds.getQoreEncoding(), xsink);
        if (*xsink)
           return false;
        if (!OCI_MsgSetCorrelation(&m_conn->ocilib, m, encoded->getBuffer(), xsink)) {
           if (!*xsink)
              xsink->raiseException("AQQUEUE-ERROR", "failed to set correlation");
           return false;
        }
    }

    if (!OCI_EnqueuePut(&m_conn->ocilib, m_enqueue, m, xsink)) {
        if (*xsink)
            return false;
    }

    return true;
}

bool AQQueuePriv::post(const QoreHashNode *h, ExceptionSink *xsink) {
   AutoLocker al(l);
   if (checkValidUnlocked("post", xsink))
      return false;

   if (!m_enqueue) {
      //printd(5, "Opening W queue: %s (%p)\n", m_qname->getBuffer(), m_typeInfo);
      m_enqueue = OCI_EnqueueCreate(&m_conn->ocilib, m_typeInfo, m_qname->getBuffer());
      if (!m_enqueue) {
         xsink->raiseException("AQQUEUE-ERROR", "failed to initialize queue");
         return false;
      }
   }

   OCI_Object* obj = objBindQore(m_conn, h, xsink);
   if (!obj) {
      xsink->raiseException("AQQUEUE-ERROR", "failed to create(bind) object");
      return false;
   }
   ON_BLOCK_EXIT(OCI_ObjectFree2, &m_conn->ocilib, obj, xsink);

   OCI_Msg* msg = OCI_MsgCreate(&m_conn->ocilib, m_typeInfo, xsink);
   if (!msg) {
      if (!*xsink)
         xsink->raiseException("AQQUEUE-ERROR", "cannot allocate message");
      return false;
   }
   ON_BLOCK_EXIT(OCI_MsgFree, &m_conn->ocilib, msg, xsink);

   if (!OCI_MsgSetObject(&m_conn->ocilib, msg, obj, xsink)) {
      if (!*xsink)
         xsink->raiseException("AQQUEUE-ERROR", "cannot set message object");
      return false;
   }

   if (!OCI_EnqueuePut(&m_conn->ocilib, m_enqueue, msg, xsink)) {
       if (*xsink)
           return false;
   }

   return true;
}

bool AQQueuePriv::commit(ExceptionSink *xsink) {
   AutoLocker al(l);
   if (checkValidUnlocked("commit", xsink))
      return false;

   return m_conn->commit(xsink);
}

bool AQQueuePriv::rollback(ExceptionSink *xsink) {
   AutoLocker al(l);
   if (checkValidUnlocked("rollback", xsink))
      return false;

   return m_conn->rollback(xsink);
}

QoreObject* AQQueuePriv::getMessage(QoreObject* self, int timeout, ExceptionSink *xsink) {
   AutoLocker al(l);
   if (checkValidUnlocked("getMessage", xsink))
      return 0;

   if (!checkDequeueUnlocked(self, xsink))
      return 0;

   //printd(5, "AQQueuePriv::getMessage() this: %p dequeue: %p msg: %p msg->typinfo: %p (%s)\n", this, m_dequeue, m_dequeue->msg, m_dequeue->msg->typinf, m_dequeue->msg->typinf->name);

   // set timeout if necessary
   if (!OCI_DequeueSetWaitTime(&m_conn->ocilib, m_dequeue, timeout, xsink)) {
      if (!*xsink)
         xsink->raiseException("AQQUEUE-ERROR", "failed to set dequeue timeout of %d second%s", timeout, timeout == 1 ? "" : "s");
      return 0;
   }

   OCI_Msg *msg;

   while (true) {
      //printd(5, "AQQueuePriv::getMessage() this: %p calling OCI_DequeueGet()\n");
      msg = OCI_DequeueGet(&m_conn->ocilib, m_dequeue, xsink);
      //printd(5, "AQQueuePriv::getMessage() this: %p msg: %p xsink: %d\n", this, msg, (bool)*xsink);

      if (msg == NULL) {
         if (*xsink) {
            // try to reconnect if connection was lost
            if (!pingUnlocked()) {
               printd(5, "AQQueuePriv::getMessage(): this: %p connection lost, reconnecting...\n", this);
               // use a temporary empty ExceptionSink object for the reconnect
               ExceptionSink tmp;
               // try to reconnect if possible
               if (reconnectUnlocked(&tmp)) {
                  xsink->assimilate(tmp);
                  return 0;
               }
               xsink->clear();
               // recreate dequeue object from new connection
               if (!checkDequeueUnlocked(self, xsink))
                  return 0;
               // we've successfully reconnected; now try again
               continue;
            }
         }

         //printd(5, "AQQueuePriv::getMessage() this: %p Message is NULL. NOTHING returned.\n", this);
         return 0;
      }
      break;
   }

   OCI_Object *o = OCI_MsgGetObject(&m_conn->ocilib, msg);

   if (!o) {
      printd(5, "AQQueuePriv::getMessage() object in message is NULL\n");
      return 0;
   }

   ON_BLOCK_EXIT(OCI_ObjectFree2, &m_conn->ocilib, o, xsink);

    ReferenceHolder<> obj(objToQore(m_conn, o, xsink), xsink);
    if (!obj) {
       xsink->raiseException("AQQUEUE-ERROR", "Cannot get qore hash from OCI object");
       return 0;
    }
    assert(obj->getType() == NT_HASH);

    AQMessagePriv *qop = new AQMessagePriv(reinterpret_cast<QoreHashNode*>(*obj), xsink);
    QoreObject *qo = new QoreObject(QC_AQMESSAGE, getProgram(), qop);
    printd(5, "AQQueuePriv::getMessage() this: %p returning obj: %p\n", this, qo);
    return qo;
}

bool AQQueuePriv::checkDequeueUnlocked(QoreObject* self, ExceptionSink *xsink) {
   if (!m_dequeue) {
      //printd(5, "AQQueuePriv::checkDequeueUnlocked() this: %p Opening R queue: %s (%p)\n", this, m_qname->getBuffer(), m_typeInfo);
      m_dequeue = OCI_DequeueCreate(&m_conn->ocilib, m_typeInfo, m_qname->getBuffer(), xsink);
      if (!m_dequeue) {
         if (!*xsink)
            xsink->raiseException("AQQUEUE-ERROR", "failed to initialize queue");
         return false;
      }
      m_dequeue->user_ctx = (void*)self;
      //printd(5, "AQQueuePriv::checkDequeueUnlocked() this: %p DEQUEUE ptr: %p msg: %p m_typeInfo: %p msg->typinfo: %p (%s)\n", this, m_dequeue, m_dequeue->msg, m_typeInfo, m_dequeue->msg->typinf, m_dequeue->msg->typinf->name);
   }
   return true;
   // TODO
   //OCI_DequeueSetConsumer for list
}

void AQQueuePriv::startSubscription(QoreObject* self, unsigned port, int64 timeout, ExceptionSink *xsink) {
   AutoLocker al(l);
   if (checkValidUnlocked("startSubscription", xsink))
      return;

   m_self = self;

   if (!checkDequeueUnlocked(self, xsink))
      return;

   if (m_hasSubscription) {
      xsink->raiseException("AQQUEUE-ERROR", "a subscription is already set with this AQQueue object; call AQQueue::stopSubscription() first before calling AQQueue::startSubscription()");
      return;
   }

   //printd(5, "AQQueuePriv::startSubscription() this: %p dequeue: %p self: %p port: %zd timeout: %zd\n", this, m_dequeue, self, port, timeout);

   // increment "oracle-thread active" count
   aqCallbackThreadManager.inc();

   if (!OCI_DequeueSubscribe(&m_conn->ocilib, m_dequeue, port, timeout, on_async_message, xsink)) {
      // decrement "oracle-thread active" count
      aqCallbackThreadManager.dec();
      if (!*xsink)
         xsink->raiseException("AQQUEUE-ERROR", "unknown error starting the subscription");
      return;
   }

   m_hasSubscription = true;
   m_port = port;
   m_timeout = timeout;

   if (aqSubscriptionManager.add(this, xsink)) {
      stopSubscriptionUnlocked(xsink);
      return;
   }

   //printd(5, "AQQueuePriv::startSubscription() this: %p dequeue: %p self: %p callback: %p\n", this, m_dequeue, self, m_dequeue->callback);
}

int AQQueuePriv::stopSubscriptionUnlocked(ExceptionSink *xsink) {
   //printd(5, "AQQueuePriv::stopSubscriptionUnlocked() this: %p xsink: %p\n", this, xsink);

   assert(m_dequeue);
   assert(m_hasSubscription);

   bool b = OCI_DequeueUnsubscribe(&m_conn->ocilib, m_dequeue, xsink);
   if (xsink)
      aqCallbackThreadManager.dec();
   m_hasSubscription = false;

   if (!b) {
      if (xsink && !*xsink)
         xsink->raiseException("AQQUEUE-ERROR", "unknown error stopping the subscription");
      return -1;
   }

   return 0;
}

int AQQueuePriv::stopSubscription(ExceptionSink *xsink) {
   AutoLocker al(l);
   if (checkValidUnlocked("stopSubscription", xsink))
      return false;

   //printd(5, "AQQueuePriv::stopSubscription() this: %p m_dequeue: %p m_hasSubscription: %d\n", this, m_dequeue, (int)m_hasSubscription);

   if (!m_hasSubscription)
      return 0;

   aqSubscriptionManager.remove(this);
   stopSubscriptionUnlocked(xsink);

   return 0;
}

static int ds_set_options(Datasource& ds, const QoreHashNode* opts, ExceptionSink* xsink) {
   ConstHashIterator hi(opts);
   while (hi.next()) {
      // skip "min" and "max" options
      if (!strcmp(hi.getKey(), "min") || !strcmp(hi.getKey(), "max"))
         continue;

      if (ds.setOption(hi.getKey(), hi.get(), xsink))
         return -1;
   }
   return 0;
}

static void aqqueue_constructor_hash(const QoreStringNode* queueName, const QoreStringNode* typeName, const QoreHashNode* opts, QoreObject* self, ExceptionSink* xsink) {
    static const char *DSC_ERR = "AQQUEUE-CONSTRUCTOR-ERROR";

    const char* str = check_hash_key(opts, "type", DSC_ERR, xsink);
    if (*xsink) return;

    if (str && strcmp(str, "oracle")) {
        xsink->raiseException(DSC_ERR, "if the 'type' key is present, then it must be 'oracle'; value provided: '%s'", str);
        return;
    }

    DBIDriver *db_driver = DBI.find("oracle", xsink);
    // driver must be laoded because it's being executed now :)
    assert(db_driver);

    std::unique_ptr<Datasource> ds(new Datasource(db_driver));

    str = check_hash_key(opts, "user", DSC_ERR, xsink);
    if (*xsink) return;
    if (str) ds->setPendingUsername(str);

    str = check_hash_key(opts, "pass", DSC_ERR, xsink);
    if (*xsink) return;
    if (str) ds->setPendingPassword(str);

    str = check_hash_key(opts, "db", DSC_ERR, xsink);
    if (*xsink) return;
    if (str) ds->setPendingDBName(str);

    str = check_hash_key(opts, "charset", DSC_ERR, xsink);
    if (*xsink) return;
    if (str) ds->setPendingDBEncoding(str);

    str = check_hash_key(opts, "host", DSC_ERR, xsink);
    if (*xsink) return;
    if (str) ds->setPendingHostName(str);

    bool found;
    int port = (int)opts->getKeyAsBigInt("port", found);
    if (port) {
        if (port < 0) {
            xsink->raiseException(DSC_ERR, "port value must be zero (meaning use the default port) or positive (value given: %d)", port);
            return;
        }

        ds->setPendingPort(port);
    }

    {
        QoreValue p = opts->getKeyValue("options");

        if (!p.isNothing()) {
            if (p.getType() != NT_HASH) {
                xsink->raiseException(DSC_ERR, "'options' key is not type 'hash' but is type '%s'", p.getTypeName());
                return;
            }
            const QoreHashNode* oh = p.get<const QoreHashNode>();
            if (ds_set_options(*ds.get(), oh, xsink))
                return;
        }
    }

    ds->open(xsink);
    if (*xsink) {
        return;
    }

    self->setPrivate(CID_AQQUEUE, new AQQueuePriv(ds.release(), typeName, queueName, xsink));
}
/* Qore class Qore::Oracle::AQQueue */

qore_classid_t CID_AQQUEUE;
QoreClass* QC_AQQUEUE;

// bool AQQueue::commit(){}
static QoreValue AQQueue_commit(QoreObject* self, AQQueuePriv* priv, const QoreListNode* args, q_rt_flags_t rtflags, ExceptionSink* xsink) {
# 898 "QC_AQQueue.qpp"
    return priv->commit(xsink);
}

// AQQueue::constructor(string queueName, string typeName, string desc) {}
static void AQQueue_constructor_VsVsVs(QoreObject* self, const QoreListNode* args, q_rt_flags_t rtflags, ExceptionSink* xsink) {
    const QoreStringNode* queueName = HARD_QORE_VALUE_STRING(args, 0);
    const QoreStringNode* typeName = HARD_QORE_VALUE_STRING(args, 1);
    const QoreStringNode* desc = HARD_QORE_VALUE_STRING(args, 2);
# 794 "QC_AQQueue.qpp"
ReferenceHolder<QoreHashNode> h(parseDatasource(desc->getBuffer(), xsink), xsink);
   if (!h) {
      assert(*xsink);
      return;
   }
   aqqueue_constructor_hash(queueName, typeName, *h, self, xsink);
   return;
}

// AQQueue::constructor(string queueName, string typeName, hash opts) {}
static void AQQueue_constructor_VsVsVh(QoreObject* self, const QoreListNode* args, q_rt_flags_t rtflags, ExceptionSink* xsink) {
    const QoreStringNode* queueName = HARD_QORE_VALUE_STRING(args, 0);
    const QoreStringNode* typeName = HARD_QORE_VALUE_STRING(args, 1);
    const QoreHashNode* opts = HARD_QORE_VALUE_HASH(args, 2);
# 820 "QC_AQQueue.qpp"
aqqueue_constructor_hash(queueName, typeName, opts, self, xsink);
}

// AQQueue::copy() {}
static void AQQueue_copy(QoreObject* self, QoreObject* old, AQQueuePriv* priv, ExceptionSink* xsink) {
# 836 "QC_AQQueue.qpp"
xsink->raiseException("AQQUEUE-COPY-ERROR", "copying AQQueue objects is currently not supported");
}

// AQQueue::destructor() {}
static void AQQueue_destructor(QoreObject* self, AQQueuePriv* priv, ExceptionSink* xsink) {
# 827 "QC_AQQueue.qpp"
priv->destructor(xsink);
   priv->deref();
}

// *AQMessage AQQueue::getMessage(softint timeout = 0){}
static QoreValue AQQueue_getMessage_vi(QoreObject* self, AQQueuePriv* priv, const QoreListNode* args, q_rt_flags_t rtflags, ExceptionSink* xsink) {
    int64 timeout = HARD_QORE_VALUE_INT(args, 0);
# 862 "QC_AQQueue.qpp"
    return priv->getMessage(self, timeout, xsink);
}

// nothing AQQueue::onAsyncMessage(){}
static QoreValue AQQueue_onAsyncMessage(QoreObject* self, AQQueuePriv* priv, const QoreListNode* args, q_rt_flags_t rtflags, ExceptionSink* xsink) {
# 917 "QC_AQQueue.qpp"
    xsink->raiseException("AQQUEUE-ONASYNCMESSAGE-ERROR", "abstract implementation of AQQueue::onAsyncMessage called");
    return QoreValue();
}

// nothing AQQueue::onSubscriptionLost(){}
static QoreValue AQQueue_onSubscriptionLost(QoreObject* self, AQQueuePriv* priv, const QoreListNode* args, q_rt_flags_t rtflags, ExceptionSink* xsink) {
# 929 "QC_AQQueue.qpp"
    //xsink->raiseException("AQQUEUE-ONSUBSCRIPTIONLOST-ERROR", "abstract implementation of AQQueue::onAsyncMessage called");
    return QoreValue();
}

// nothing AQQueue::post(AQMessage msg){}
static QoreValue AQQueue_post_C9AQMessage(QoreObject* self, AQQueuePriv* priv, const QoreListNode* args, q_rt_flags_t rtflags, ExceptionSink* xsink) {
    HARD_QORE_VALUE_OBJ_DATA(msg, AQMessagePriv, args, 0, CID_AQMESSAGE, "AQQueue::post()", "AQMessage", xsink);
    if (*xsink)
        return 0;
# 843 "QC_AQQueue.qpp"
    priv->post(msg, xsink);
    return QoreValue();
}

// nothing AQQueue::post(hash msg){}
static QoreValue AQQueue_post_Vh(QoreObject* self, AQQueuePriv* priv, const QoreListNode* args, q_rt_flags_t rtflags, ExceptionSink* xsink) {
    const QoreHashNode* msg = HARD_QORE_VALUE_HASH(args, 0);
# 853 "QC_AQQueue.qpp"
    priv->post(msg, xsink);
    return QoreValue();
}

// bool AQQueue::rollback(){}
static QoreValue AQQueue_rollback(QoreObject* self, AQQueuePriv* priv, const QoreListNode* args, q_rt_flags_t rtflags, ExceptionSink* xsink) {
# 905 "QC_AQQueue.qpp"
    return priv->rollback(xsink);
}

// nothing AQQueue::startSubscription(int port = 0, int timeout = 0){}
static QoreValue AQQueue_startSubscription_ViVi(QoreObject* self, AQQueuePriv* priv, const QoreListNode* args, q_rt_flags_t rtflags, ExceptionSink* xsink) {
    int64 port = HARD_QORE_VALUE_INT(args, 0);
    int64 timeout = HARD_QORE_VALUE_INT(args, 1);
# 872 "QC_AQQueue.qpp"
    if (port < 0) {
      xsink->raiseException("AQQUEUE-ERROR", "Port number has to be greater than or equal to 0");
      return QoreValue();
   }

   if (timeout < -1) {
      xsink->raiseException("AQQUEUE-ERROR", "Timeout value has to be greater than or equal to -1");
      return QoreValue();
   }

   priv->startSubscription(self, (unsigned)port, timeout, xsink);
    return QoreValue();
}

// nothing AQQueue::stopSubscription(){}
static QoreValue AQQueue_stopSubscription(QoreObject* self, AQQueuePriv* priv, const QoreListNode* args, q_rt_flags_t rtflags, ExceptionSink* xsink) {
# 891 "QC_AQQueue.qpp"
    priv->stopSubscription(xsink);
    return QoreValue();
}

DLLLOCAL void preinitAQQueueClass() {
    QC_AQQUEUE = new QoreClass("AQQueue", "::Qore::Oracle::AQQueue", QDOM_DATABASE);
    CID_AQQUEUE = QC_AQQUEUE->getID();
    QC_AQQUEUE->setSystem();
}

DLLLOCAL QoreClass* initAQQueueClass(QoreNamespace& ns) {
    if (!QC_AQQUEUE)
        preinitAQQueueClass();

    // bool AQQueue::commit(){}
    QC_AQQUEUE->addMethod("commit", (q_method_n_t)AQQueue_commit, Public, QCF_NO_FLAGS, QDOM_DEFAULT, boolTypeInfo);

    // AQQueue::constructor(string queueName, string typeName, string desc) {}
    QC_AQQUEUE->addConstructor(AQQueue_constructor_VsVsVs, Public, QCF_NO_FLAGS, QDOM_DEFAULT, 3, stringTypeInfo, QORE_PARAM_NO_ARG, "queueName", stringTypeInfo, QORE_PARAM_NO_ARG, "typeName", stringTypeInfo, QORE_PARAM_NO_ARG, "desc");

    // AQQueue::constructor(string queueName, string typeName, hash opts) {}
    QC_AQQUEUE->addConstructor(AQQueue_constructor_VsVsVh, Public, QCF_NO_FLAGS, QDOM_DEFAULT, 3, stringTypeInfo, QORE_PARAM_NO_ARG, "queueName", stringTypeInfo, QORE_PARAM_NO_ARG, "typeName", hashTypeInfo, QORE_PARAM_NO_ARG, "opts");

    // AQQueue::copy() {}
    QC_AQQUEUE->setCopy((q_copy_t)AQQueue_copy);

    // AQQueue::destructor() {}
    QC_AQQUEUE->setDestructor((q_destructor_t)AQQueue_destructor);

    // *AQMessage AQQueue::getMessage(softint timeout = 0){}
    QC_AQQUEUE->addMethod("getMessage", (q_method_n_t)AQQueue_getMessage_vi, Public, QCF_NO_FLAGS, QDOM_DEFAULT, QC_AQMESSAGE->getOrNothingTypeInfo(), 1, softBigIntTypeInfo, QoreSimpleValue().assign((int64)0), "timeout");

    // nothing AQQueue::onAsyncMessage(){}
    QC_AQQUEUE->addMethod("onAsyncMessage", (q_method_n_t)AQQueue_onAsyncMessage, Public, QCF_NO_FLAGS, QDOM_DEFAULT, nothingTypeInfo);

    // nothing AQQueue::onSubscriptionLost(){}
    QC_AQQUEUE->addMethod("onSubscriptionLost", (q_method_n_t)AQQueue_onSubscriptionLost, Public, QCF_NO_FLAGS, QDOM_DEFAULT, nothingTypeInfo);

    // nothing AQQueue::post(AQMessage msg){}
    QC_AQQUEUE->addMethod("post", (q_method_n_t)AQQueue_post_C9AQMessage, Public, QCF_NO_FLAGS, QDOM_DEFAULT, nothingTypeInfo, 1, QC_AQMESSAGE->getTypeInfo(), QORE_PARAM_NO_ARG, "msg");

    // nothing AQQueue::post(hash msg){}
    QC_AQQUEUE->addMethod("post", (q_method_n_t)AQQueue_post_Vh, Public, QCF_NO_FLAGS, QDOM_DEFAULT, nothingTypeInfo, 1, hashTypeInfo, QORE_PARAM_NO_ARG, "msg");

    // bool AQQueue::rollback(){}
    QC_AQQUEUE->addMethod("rollback", (q_method_n_t)AQQueue_rollback, Public, QCF_NO_FLAGS, QDOM_DEFAULT, boolTypeInfo);

    // nothing AQQueue::startSubscription(int port = 0, int timeout = 0){}
    QC_AQQUEUE->addMethod("startSubscription", (q_method_n_t)AQQueue_startSubscription_ViVi, Public, QCF_NO_FLAGS, QDOM_DEFAULT, nothingTypeInfo, 2, bigIntTypeInfo, QoreSimpleValue().assign((int64)0), "port", bigIntTypeInfo, QoreSimpleValue().assign((int64)0), "timeout");

    // nothing AQQueue::stopSubscription(){}
    QC_AQQUEUE->addMethod("stopSubscription", (q_method_n_t)AQQueue_stopSubscription, Public, QCF_NO_FLAGS, QDOM_DEFAULT, nothingTypeInfo);

    return QC_AQQUEUE;
}
