# __DRIVER_CODE__ imports

cdef extern from "<msgpack.h>" nogil:
  ctypedef int (*msgpack_packer_write)(void* data, const char* buf, size_t len)
  ctypedef struct msgpack_sbuffer:
    size_t size
    char* data
    size_t alloc
  ctypedef struct msgpack_packer:
    void* data
    msgpack_packer_write callback
  void msgpack_sbuffer_destroy(msgpack_sbuffer* sbuf)
  void msgpack_sbuffer_clear(msgpack_sbuffer* sbuf)
  int msgpack_sbuffer_write(void* data, const char* buf, size_t len)
  void msgpack_sbuffer_init(msgpack_sbuffer* sbuf)
  int msgpack_pack_array(msgpack_packer* pk, size_t n)
  void msgpack_packer_init(msgpack_packer* pk, void* data, msgpack_packer_write callback)
  int msgpack_pack_uint8(msgpack_packer* pk, uint8_t d)
  int msgpack_pack_uint16(msgpack_packer* pk, uint16_t d)
  int msgpack_pack_uint32(msgpack_packer* pk, uint32_t d)
  int msgpack_pack_uint64(msgpack_packer* pk, uint64_t d)
  int msgpack_pack_int8(msgpack_packer* pk, int8_t d)
  int msgpack_pack_int16(msgpack_packer* pk, int16_t d)
  int msgpack_pack_int32(msgpack_packer* pk, int32_t d)
  int msgpack_pack_int64(msgpack_packer* pk, int64_t d)
  int msgpack_pack_float(msgpack_packer* pk, float d)
  int msgpack_pack_double(msgpack_packer* pk, double d)

cdef extern from "loggingStruct.h" nogil:
  ctypedef struct SQLTableElement:
    char *colName
    char *type
    short number
    char *unit
    void *data
    char *SQLtype

  ctypedef struct SQLTable:
    char* tableName
    short numCol
    SQLTableElement *columns

cdef extern from "<sqlite3.h>" nogil:
  ctypedef struct sqlite3:
    pass
  ctypedef struct sqlite3_stmt:
    pass
  ctypedef struct sqlite3_blob:
    pass
  int sqlite3_close(sqlite3 *)
  int sqlite3_exec(
    sqlite3*,                                  
    const char *sql,                           
    int (*callback)(void*,int,char**,char**),  
    void *,                                    
    char **errmsg                              
  )
  void sqlite3_free(void*)
  enum: SQLITE_OK
  ctypedef void (*destructor)(void*)
  destructor SQLITE_STATIC
  enum: SQLITE_OPEN_READWRITE
  int sqlite3_open_v2(
    const char *filename,   
    sqlite3 **ppDb,         
    int flags,              
    const char *zVfs        
  )

cdef extern from "sqlite3async.h" nogil:
  void sqlite3async_shutdown()
  void sqlite3async_run()
  int sqlite3async_initialize(const char *zParent, int isDefault)
  int sqlite3async_control(int op, ...)
  enum: SQLITEASYNC_HALT
  enum: SQLITEASYNC_HALT_IDLE

cdef extern from "sqlHelpers.h" nogil:
  void sql_bind_int(sqlite3_stmt *stmt, int index, const char* dtype, const void* value)
  void sql_bind_int64(sqlite3_stmt *stmt, int index, const void* value)
  void sql_bind_double(sqlite3_stmt *stmt, int index,  const char* dtype, const void* value)
  void sql_bind_text(sqlite3_stmt *stmt, int index, const void* value, int numBytes, destructor destruct)
  void sql_bind_blob(sqlite3_stmt *stmt, int index, const void* value, int numBytes, destructor destruct)
  void sql_prepare(sqlite3 *db, const char *zSql, int nByte, sqlite3_stmt **ppStmt, const char **pzTail)
  void sql_step(sqlite3_stmt *stmt)
  void sql_finalize(sqlite3_stmt *stmt)
  void openDatabase(sqlite3 **db, char *startName, int db_index, char* newNameLocation)
  void createTables(sqlite3 *db, SQLTable* databaseArr)
  enum: NUM_BUF_S
  enum: NUM_MS_IN_S # TODO replace ms with ticks or just remove
  cdef const char *INSERT_STR
  cdef const char *VALUES_STR
  cdef const char *OPEN_BRACE
  cdef const char *QUESTION
  cdef const char *CLOSE_BRACE_SEMI
  cdef const char *CLOSE_BRACE
  cdef const char *COMMA


cdef extern from "stdatomic.h":
  enum memory_order:
    memory_order_relaxed,
    memory_order_consume,
    memory_order_acquire,
    memory_order_release,
    memory_order_acq_rel,
    memory_order_seq_cst
  void atomic_thread_fence(memory_order)

cdef extern from "constants.h":
  enum: NUM_TABLES
  cdef const char *DATALOGGER_SIGNALS_TABLE_NAME
  enum: NEW_DB_NUM_TICKS
  enum: SQL_LOGGER_FLUSH


# __DRIVER_CODE__ variables

cdef bool newDB = False
cdef int ret
cdef int rc
cdef char currDbName[32]
# worker thread vars
cdef char *zErrMsg
cdef void * retVal
# numpy signal data structs to hold one tick of data
ctypedef struct signalsTickData:
{%- for sig,dtype in sig_types.items() %}
  {%- if sig in source_outputs %}
  {{dtype}} {{sig}}[{{in_signals[sig]['schema']['data']['size'] * 2}}]
  uint32_t {{sig}}NumSamplesRecvd
  {%- else %}
  {{dtype}} {{sig}}[{{in_signals[sig]['packet_size']}}]
  {%- endif %}
{%- endfor %}
cdef signalsTickData *pSignalsStructCur
cdef signalsTickData *pSignalsStructStart
cdef signalsTickData *pSignalsStructEnd
# tick buffers to enable batching
cdef signalsTickData *signalsBufStrt
cdef signalsTickData *signalsBufEnd
# size of intermediary logging buffer in blocks of signalMsData 
cdef int signalsBufSize

# sqlite variables
cdef sqlite3 *db
cdef sqlite3_stmt *contStmt
cdef sqlite3_stmt *signalsStmt
cdef SQLTable databaseArray[NUM_TABLES]
cdef SQLTableElement signalsTableArr[{{in_signals|length}} + {{raw_vec_sigs['total']}}]

# sqlite variables
cdef int eNewHalt
cdef int queryLen
cdef char *signalsQuery

# message pack
{%- for sig in msgpack_sigs %}
cdef msgpack_sbuffer mpSignals{{sig|capitalize}}Buf
cdef msgpack_packer mpSignals{{sig|capitalize}}Pk
{%- endfor %}

cdef int db_index = 0
cdef int num_extra_cols = 0 # counter for extra columns added to db
tick_count = 0
cdef uint32_t ticks_written = 0

#all the memcpys happen here now
cdef void* bufferSignals():
  global signalsBufStrt, signalsBufEnd, pSignalsStructCur

  # for each tick buffered, copy each signal from buffer into pSignalsStructCur
  {%- for sig,dtype in sig_types.items() %}
  # TODO this logic should have some validation
  # in the case of max_packets_per_tick != 1, must be saved as a blob or be in its own table
  {%- if sig in source_outputs %}
  memcpy(pSignalsStructCur.{{sig}}, <uint8_t *>{{sig}}.__array_interface__["data"][0], {{sig}}Len * sizeof({{dtype}}))
  {%- else %}
  memcpy(pSignalsStructCur.{{sig}}, <uint8_t *><long>({{sig}}.__array_interface__["data"][0]), {{sig}}Len * sizeof({{dtype}}))
  # pSignalsStructCur.{{sig}}NumSamplesRecvd = {{sig}}Len
  {%- endif %}
  {%- endfor %}

  atomic_thread_fence(memory_order_seq_cst)

  #increment in values buffer
  pSignalsStructCur += 1
  if (pSignalsStructCur == signalsBufEnd):
    pSignalsStructCur = signalsBufStrt


cdef bool flushToDisk(bool newDB=False) nogil:
  global zErrMsg, db, newDB, db_index, num_extra_cols, exitMask, signalsQuery, signalsStmt, signalsBufStrt, signalsBufEnd, ticks_written, \
         {% for sig in msgpack_sigs %} mpSignals{{sig|capitalize}}Buf, {% endfor %} \
         pSignalsStructStart, pSignalsStructEnd, pSignalsStructCur
  complete = True

  # create new database if time limit reached
  if (newDB):
    ticks_written = 0
    db_index += 1
    openDatabase(&db, "{{ out_signal['args']['save_file']}}", db_index, currDbName)
    sqlite3_exec(db, "PRAGMA journal_mode = MEMORY", NULL, NULL, &zErrMsg)
    createTables(db, databaseArray)

  ret = sqlite3_exec(db, "BEGIN TRANSACTION", NULL, NULL, &zErrMsg)
  if (ret != SQLITE_OK):
    # print sql error msg
    sqlite3_free(zErrMsg)
    die("SQL error")

  while (pSignalsStructStart != pSignalsStructEnd):
    sql_prepare(db, signalsQuery, -1, &signalsStmt, NULL)

    num_extra_cols = 0 # restart counter for each row logged

  {%- for sig, args in in_signals.items() %}

  {%- if sig in msgpack_sigs %}
    # msgpack signal
    msgpack_sbuffer_clear(&mpSignals{{sig|capitalize}}Buf)
    {%- if sig in source_outputs %}
    msgpack_pack_array(&mpSignals{{sig|capitalize}}Pk, pSignalsStructStart.{{sig}}NumSamplesRecvd)
    for i in range (pSignalsStructStart.{{sig}}NumSamplesRecvd):
    {%- else %}
    msgpack_pack_array(&mpSignals{{sig|capitalize}}Pk, {{args['packet_size']}})
    for i in range({{args['packet_size']}}):
    {%- endif %}
      msgpack_pack_{{args['dtype_msgpack']}}(&mpSignals{{sig|capitalize}}Pk, pSignalsStructStart.{{sig}}[i])
    sql_bind_blob(signalsStmt, {{loop.index}} + num_extra_cols, mpSignals{{sig|capitalize}}Buf.data, mpSignals{{sig|capitalize}}Buf.size, SQLITE_STATIC)
  
  {%- elif sig in raw_vec_sigs %}
    # vector signal
    for i in range({{raw_vec_sigs[sig]}}):
      if "int" in '{{args['dtype']}}':
        if "64" in '{{args['dtype']}}':
          sql_bind_int64(signalsStmt, {{loop.index}} + num_extra_cols, &(pSignalsStructStart.{{sig}}[i]))
        else:
          sql_bind_int(signalsStmt, {{loop.index}} + num_extra_cols, "{{args['dtype']}}", &(pSignalsStructStart.{{sig}}[i]))
      else: # float
        sql_bind_double(signalsStmt, {{loop.index}} + num_extra_cols, "{{args['dtype']}}", &(pSignalsStructStart.{{sig}}[i]))
      num_extra_cols += 1
    num_extra_cols -= 1

  {%- elif sig in raw_text_sigs %}
    # text signal
    sql_bind_text(signalsStmt, {{loop.index}} + num_extra_cols, pSignalsStructStart.{{sig}}, {{raw_text_sigs[sig]}} * {{args['packet_size']}}, SQLITE_STATIC)

  {%- elif sig in raw_num_sigs %}
    # raw number signal
    if "int" in "{{args['dtype']}}":
      if "64" in "{{args['dtype']}}":
        sql_bind_int64(signalsStmt, {{loop.index}} + num_extra_cols, pSignalsStructStart.{{sig}})
      else:
        sql_bind_int(signalsStmt, {{loop.index}} + num_extra_cols, "{{args['dtype']}}", pSignalsStructStart.{{sig}})
    else: # float
      sql_bind_double(signalsStmt, {{loop.index}} + num_extra_cols, "{{args['dtype']}}", pSignalsStructStart.{{sig}})

  {%- endif %}
  {%- endfor %}

    sql_step(signalsStmt)
    sql_finalize(signalsStmt)
    
    pSignalsStructStart += 1
    if (pSignalsStructStart == signalsBufEnd):
      pSignalsStructStart = signalsBufStrt

    ticks_written += 1
    if (ticks_written % NEW_DB_NUM_TICKS == 0):
      complete = False
      break


  ret = sqlite3_exec(db, "END TRANSACTION", NULL, NULL, &zErrMsg)
  if (ret != SQLITE_OK):
    # print sql error msg
    sqlite3_free(zErrMsg)
    die("SQL error")

  sqlite3async_run()

  if not complete:
    flushToDisk(newDB=True)


# __DRIVER_CODE__ setup

# initialize sqlite3async
rc = sqlite3async_initialize(NULL, 1)
if (rc != SQLITE_OK):
  die("sqlite3async init failed\n")
# control sqlite3async: set attributes
eNewHalt = SQLITEASYNC_HALT_IDLE
rc = sqlite3async_control(SQLITEASYNC_HALT, eNewHalt)
if (rc != SQLITE_OK):
  die("sqlite3async control failed\n")

# create numpy signals table sql insert queries
queryLen = (
  strlen(INSERT_STR) + strlen(DATALOGGER_SIGNALS_TABLE_NAME) + 
  strlen(VALUES_STR) + strlen(OPEN_BRACE) + 
{%- for sig in in_signals %}
  strlen(QUESTION){{" + strlen(COMMA) + " if not loop.last}}
{%- endfor %} +
{%- for i in range(raw_vec_sigs['total']) %}
  strlen(COMMA) + strlen(QUESTION) +
{%- endfor %}
  strlen(CLOSE_BRACE_SEMI) 
)
signalsQuery = <char *> malloc(queryLen + 1)
strcpy(signalsQuery, INSERT_STR)
strcat(signalsQuery, DATALOGGER_SIGNALS_TABLE_NAME)
strcat(signalsQuery, VALUES_STR)
strcat(signalsQuery, OPEN_BRACE)
{%- for sig in in_signals %}
strcat(signalsQuery, QUESTION)
{%- if not loop.last %}
strcat(signalsQuery, COMMA)
{%- endif %}
{%- endfor %}
# don't forget extra columns for vector signals
{%- for i in range(raw_vec_sigs['total']) %}
strcat(signalsQuery, COMMA)
strcat(signalsQuery, QUESTION)
{%- endfor %}
strcat(signalsQuery, CLOSE_BRACE_SEMI)
# signals buffer
signalsBufSize = NUM_BUF_S * NUM_MS_IN_S # TODO this should be signal size
signalsBufSizeBytes = (sizeof(signalsTickData) * signalsBufSize)
signalsBufStrt = <signalsTickData *>malloc(signalsBufSizeBytes)
signalsBufEnd = signalsBufStrt + signalsBufSize
pSignalsStructCur = signalsBufStrt
pSignalsStructStart = signalsBufStrt

openDatabase(&db, "{{ out_signal['args']['save_file'] }}", db_index, currDbName)
sqlite3_exec(db, "PRAGMA journal_mode = MEMORY", NULL, NULL, &zErrMsg)

# prepare SQL database table
num_extra_cols = 0 # counter for number of extra added columns
{%- for sig,args in in_signals.items() %}
# signalsTableArr[{{loop.index0}}].type = "uint64_t"
# signalsTableArr[{{loop.index0}}].number = {{args['packet_size']}}
# signalsTableArr[{{loop.index0}}].unit = "TODO"
# signalsTableArr[{{loop.index0}}].data = NULL

{%- if sig in msgpack_sigs %}
  # msgpack sigs
signalsTableArr[{{loop.index0}} + num_extra_cols].colName = "m_{{args['dtype_short']}}_{{sig}}"
signalsTableArr[{{loop.index0}} + num_extra_cols].SQLtype = "BLOB"

{%- elif sig in raw_vec_sigs %}
  # vector sigs
curr_index = {{loop.index0}}
{%- for i in range(raw_vec_sigs[sig]) %}
{%- if args['log_storage'] and ('suffixes' in args['log_storage']) %} # if suffixes specified
signalsTableArr[curr_index + num_extra_cols].colName = "v_{{args['dtype_short']}}_{{sig}}_{{args['log_storage']['suffixes'][i]}}"
{%- else %}
signalsTableArr[curr_index + num_extra_cols].colName = "v_{{args['dtype_short']}}_{{sig}}_{{i}}"
{%- endif %}
if 'int' in '{{args['dtype']}}':
  signalsTableArr[curr_index + num_extra_cols].SQLtype = "INTEGER"
else: # float
  signalsTableArr[curr_index + num_extra_cols].SQLtype = "REAL"

num_extra_cols += 1
{%- endfor %}
num_extra_cols -= 1 # only count *extra* added cols

{%- elif sig in raw_text_sigs %}
  # text sigs
signalsTableArr[{{loop.index0}} + num_extra_cols].colName = "t_{{args['dtype_short']}}_{{sig}}"
signalsTableArr[{{loop.index0}} + num_extra_cols].SQLtype = "TEXT"

{%- elif sig in raw_num_sigs %}

  # raw number sigs
signalsTableArr[{{loop.index0}} + num_extra_cols].colName = "r_{{args['dtype_short']}}_{{sig}}"
if 'int' in '{{args['dtype']}}':
  signalsTableArr[{{loop.index0}} + num_extra_cols].SQLtype = "INTEGER"
else: # float
  signalsTableArr[{{loop.index0}} + num_extra_cols].SQLtype = "REAL"
{%- endif %}

{%- endfor %} 
databaseArray[0].tableName = DATALOGGER_SIGNALS_TABLE_NAME
databaseArray[0].numCol = {{in_signals|length}} + {{raw_vec_sigs['total']}}
databaseArray[0].columns = signalsTableArr
createTables(db, databaseArray)

# flush sqlite buffer before execution start
sqlite3async_run()


# initialize message pack for numpy signals
{%- for sig, args in in_signals.items() %}
{%- if sig in msgpack_sigs %}
msgpack_sbuffer_init(&mpSignals{{sig|capitalize}}Buf)
msgpack_packer_init(&mpSignals{{sig|capitalize}}Pk, &mpSignals{{sig|capitalize}}Buf, msgpack_sbuffer_write)
# enlarge buffer before main execution
{%- if sig in source_outputs %}
# this should really be on a packet level
msgpack_pack_array(&mpSignals{{sig|capitalize}}Pk, {{2 * args['packet_size']}})
for i in range({{2 * args['packet_size']}}):
  msgpack_pack_{{args['dtype_msgpack']}}(&mpSignals{{sig|capitalize}}Pk, 0)
{%- else %}
msgpack_pack_array(&mpSignals{{sig|capitalize}}Pk, {{args['packet_size']}})
for i in range({{args['packet_size']}}):
  msgpack_pack_{{args['dtype_msgpack']}}(&mpSignals{{sig|capitalize}}Pk, 0)
{%- endif %}
{%- endif %}
{%- endfor %}


# __DRIVER_CODE__ write

  # for now we assume signals are synchronous and we get one signal each tick.
  # signals can be put in separate tables if needed for multiple async.
  # sufficient ticks must be buffered for async to work. error handling??
  # parsing happens on async side, so need previous tick signals exposed here
  bufferSignals()


  # after SQL_LOGGER_FLUSH ticks, flush buffered data to DB
  # TODO this has to be a different var since numTicks is not accurate for async
  tick_count += 1
  if (tick_count % SQL_LOGGER_FLUSH == 0):
    pSignalsStructEnd = pSignalsStructStart + SQL_LOGGER_FLUSH
    flushToDisk()




# __DRIVER_CODE__ exit_handler
  global signalsBufStrt, tick_count, pNumTicks, pSignalsStructStart, pSignalsStructEnd # TODO add msgpack Buf vars

  # perform final flush of buffered data
  pSignalsStructEnd = pSignalsStructCur
  flushToDisk()

  free(signalsBufStrt)

  # destroy msgpack signals
{% for sig in msgpack_sigs %}
  msgpack_sbuffer_destroy(&mpSignals{{sig|capitalize}}Buf)
{%- endfor %}

  # clean up sqlite db
  sqlite3_close(db)
  sqlite3async_shutdown()
