#ifndef _datalog_h__
#define _datalog_h__

/** DataLog **
 * 
 * Create and parse binary logs of Dracal device channel data.
 * 
 * A data log is like a table: columns correspond to logged channels (and a timestamp),
 * and each row corresponds to a timestamped reading of channel quantities.
 * 
 * For example, when logging 3 channels, there are 4 columns, i.e. one column for the
 * timestamp and for each channel. If logging at a 1-second interval, there will be 10
 * rows after 10 seconds, or 60 rows after 60 seconds, etc.
 * 
 * We refer to log metadata and column definitions collectively as the "header".
 * 
 * A "row" consists of a timestamp and the quantities of all channels.
 * 
 * The struct DataLog contains the header and a single row of data. How we use this struct
 * depends on whether we are reading, writing or appending to/from a file.
 * 
 * When reading from a file:
 * 
 *   A new DataLog struct is allocated by calling datalog_open_read(). The file is opened
 *   read-only. The header is read from the file and the struct is populated accordingly.
 * 
 *   Each row can be read, sequentially, by calling datalog_read_row().
 *   With each call to this function, the DataLog struct is updated to reflect the row
 *   that was just read. The header stays constant, but the timestamp and channel
 *   quantities are updated. The cursor moves to the next row.
 * 
 *   The cursor starts at the first row, but it is possible to seek to a specific row index
 *   with datalog_seek_row().
 * 
 *   NOTE: Rows are indexed from 1 !!
 * 
 *   When finished, call datalog_close(). The DataLog is freed and the pointer becomes invalid.
 *
 * When writing to a file:
 * 
 *   You are responsible for creating and initializing your own struct DataLog. Make sure
 *   to set all header fields appropriately. In particular, add each Device that is being
 *   logged to the devices List. You must also add each logged Channel to the channels List,
 *   in the same order that they occur in the devices List.
 * 
 *   For example, if logging channels 2 and 3 of Device A, and logging channel 1 of Device B,
 *   the devices List should be [A, B] and the channels List should be
 * 
 *      [
 *          Channel 2 of Device A,
 *          Channel 3 of Device A,
 *          Channel 1 of Device B
 *      ]
 *   
 *   Failure to follow this convention will result in undefined behavior.
 * 
 *   After your DataLog struct is properly set up, you may call datalog_create() which
 *   creates a new file. At this point, the file contains the header only, and no rows yet.
 * 
 *   To append a row, first set the row data in the DataLog struct, i.e. set the timestamp and
 *   the quantities of all channels in the channels List. Then call datalog_append_row(), which
 *   appends the row data to the end of the file.
 * 
 *   When finished, call datalog_close(). The DataLog is NOT freed, since you created it yourself.
 *   You are responsible for freeing the struct and its members yourself.
 * 
 * When appending to an existing file:
 * 
 *   A new DataLog struct is allocated by calling datalog_open_append(). The file is opened
 *   for reading and writing. The header is read from the file and the struct is populated
 *   accordingly.
 * 
 *   At this point, the cursor is positioned after the last row.
 * 
 *   To append a row, first set the row data in the DataLog struct, i.e. set the timestamp and
 *   the quantities of all channels in the channels List. Then call datalog_append_row(), which
 *   appends the row data to the end of the file.
 * 
 *   To go back and read a given row, use datalog_seek_row() to seek to the desired row index,
 *   then call datalog_read_row(). The DataLog struct is now populated with the row.
 * 
 *   Calling datalog_append_row() while the cursor is not at the end will result in overwriting
 *   the current row, i.e. the row in the file corresponding to the cursor will be overwitten.
 *   Though this may be useful in some applications, always remember to seek past the end of the
 *   file if your intention is to append rather than to overwrite.
 * 
 *   NOTE: Rows are indexed from 1 !!
 * 
 *   When finished, call datalog_close(). The DataLog is freed and the pointer becomes invalid.
 *
 */

#include <stdio.h>
#include <stdint.h>

#include "list.h"
#include "device.h"

#ifdef __cplusplus
extern "C" {
#endif


/**
 * Struct used for reading/writing to/from binary data log files.
 * Read the top of this file for usage instructions.
 */
typedef struct DataLog {

    // Header
    int64_t creation_timestamp;
    uint32_t interval;
    char *comment;
    List devices;

    // Row
    int64_t timestamp;
    List channels;

    // For internal use. Don't touch!
    void *_handle;

} DataLog;


/**
 * Create a data log file at the given path, overwriting any existing file.
 * 
 * The datalog argument must point to a properly initialized struct DataLog.
 * You are responsible for allocating the struct and setting its members.
 * Refer to the top of this file for usage instructions.
 * 
 * Make sure to call datalog_close() when finished.
 * 
 * Return: non-zero if the file could not be created or the DataLog struct is invalid
 */
int datalog_create(const char *path, DataLog *datalog);

/**
 * Open a data log file read-only.
 * Refer to the top of this file for usage instructions.
 * 
 * Make sure to call datalog_close() when finished.
 * 
 * Return: a newly allocated DataLog struct with a populated header, with the cursor
 *         pointing to the first row
 */
DataLog *datalog_open_read(const char *path);

/**
 * Open a data log file read-write.
 * Refer to the top of this file for usage instructions.
 * 
 * Make sure to call datalog_close() when finished.
 * 
 * Return: a newly allocated DataLog struct with a populated header, with the cursor
 *         pointing past the last row
 */
DataLog *datalog_open_append(const char *path);

/**
 * Read the row at the current cursor position in the file and store results in the given
 * struct DataLog. The row cursor is incremented by 1.
 * 
 * Return: non-zero on error, or if the cursor is past the end of the file
 */
int datalog_read_row(DataLog *datalog);

/**
 * Write a row to the file at the current cursor position. The row data is obtained from
 * the given struct DataLog. The row cursor is incremented by 1. 
 * 
 * Return: non-zero on error
 */
int datalog_append_row(DataLog *datalog);

/**
 * Return the size (in bytes) of a single row.
 */
uint32_t datalog_row_size(DataLog *datalog);

/**
 * Return the total number of rows in the file.
 */
uint32_t datalog_row_count(DataLog *datalog);

/**
 * Return the index of the last row that was read or written.
 * 
 * NOTE: Rows are indexed from 1 !!
 * 
 * For example, before reading the first row, the returned index is 0.
 */
uint32_t datalog_row_index(DataLog *datalog);

/**
 * Position the cursor just behind the row at the given index, so that this row will be
 * the next to be read or written.
 * 
 * NOTE: Rows are indexed from 1 !!
 * 
 * For example, to seek to the first row, give index = 1 as argument.
 */
int datalog_seek_row(DataLog *datalog, uint32_t index);

/**
 * Close the open file handle and free all internally allocated memory.
 * 
 * If you allocated the DataLog yourself before calling datalog_create(), then you are
 * responsible for freeing the struct and its members yourself.
 * 
 * If you received the DataLog by calling datalog_open_read() or datalog_open_append(),
 * then the struct is automatically freed by this call and the pointer is now invalid.
 */
void datalog_close(DataLog *datalog);


#ifdef __cplusplus
}
#endif

#endif
