#ifndef DV_INPUT_HPP
#define DV_INPUT_HPP

#include "../../include/dv-sdk/cross/portable_endian.h"

#include "../output/dv_io.hpp"
#include "filebuffer.hpp"

#include <boost/filesystem.hpp>
#include <boost/nowide/fstream.hpp>

#include <cassert>
#include <fstream>
#include <map>
#include <thread>

namespace dv {

struct InputStream {
	int32_t id;
	std::string name;
	std::string typeIdentifier;
	dv::Types::Type type;
};

struct InputInformation {
	size_t fileSize;
	dv::CompressionType compression;
	int64_t dataTablePosition;
	size_t dataTableSize;
	int64_t timeLowest;
	int64_t timeHighest;
	int64_t timeDifference;
	int64_t timeShift;
	std::vector<InputStream> streams;
	std::unique_ptr<dv::FileDataTable> dataTable;
};

struct DecompressionData {
	/// Type of compression to apply.
	bool compressionIsLZ4;
	/// Support LZ4 compression.
	lz4DecompressionSupport compressionLZ4;
	/// Support Zstd compression.
	zstdDecompressionSupport compressionZstd;
	/// Buffers to hold data while decoding.
	std::vector<char> readBuffer;
	std::vector<char> decompressBuffer;
};

class InputDecoder {
private:
	/// Input stream to read data from.
	std::istream *inputStream;
	/// Input information from header.
	const InputInformation *inputInfo;
	/// Module data for outputs access.
	dvModuleData moduleData;
	/// Configuration access.
	dv::RuntimeConfig *config;
	/// Logging access.
	dv::Logger *log;
	/// Decompression support.
	dv::DecompressionData dataCompress;
	/// Cache for decompressed data.
	dv::FileBuffer cacheBuffer;
	/// Limit consecutive log messages.
	bool slowdownWarningSent;

	// Ensure source attributes in info part of node are updated to new format:
	// "DAVIS346_12345678", instead of old: "DAVIS346 [SN: 12345678, USB: X:Y]".
	// The USB information is not useful for streams, and all the special
	// characters make it hard to use in file names or config-tree node names.
	static void rewriteSourceAttribute(dv::Config::Node mNode) {
		if (!mNode.exists<dv::CfgType::STRING>("source")) {
			return;
		}

		// Get current value of source attribute.
		auto currSource = mNode.get<dv::CfgType::STRING>("source");

		// Search for old format with 'SN: ' component.
		auto snStartPos = currSource.find("SN: ");

		if (snStartPos == std::string::npos) {
			// SN not found, leave unchanged.
			return;
		}

		// SN found, find ending.
		snStartPos    += 4;
		auto snEndPos  = currSource.find(',', snStartPos);

		dv::runtime_assert(
			[snEndPos] {
				return snEndPos != std::string::npos;
			},
			[] {
				return "Source serial number string incompatible";
			}); // All sources with 'SN: ' must have ',' after.

		// Now also extract device name. It's always followed by a space.
		auto nameEndPos = currSource.find(' ');

		dv::runtime_assert(
			[nameEndPos] {
				return nameEndPos != std::string::npos;
			},
			[] {
				return "Source serial number string incompatible";
			}); // All sources with 'SN: ' must have a device name.

		// Generate new, concise source string.
		auto newSource = currSource.substr(0, nameEndPos) + "_" + currSource.substr(snStartPos, snEndPos - snStartPos);

		// Update configuration attribute.
		mNode.updateReadOnly<dv::CfgType::STRING>("source", newSource);
	}

	static std::unique_ptr<dv::FileDataTable> rebuildFileDataTable(
		boost::nowide::ifstream &fStream, const dv::InputInformation &fInfo) {
		// Remember offset to go back here after.
		const auto initialOffset = fStream.tellg();

		auto dataTable = std::make_unique<dv::FileDataTable>();

		// We should be right after the header here, so the first thing we encounter
		// is a dv::PacketHeader. Let's read them one by one until the end and build
		// our temporary data table that way.
		auto nextRead = static_cast<size_t>(initialOffset);
		dv::PacketHeader header;
		dv::DecompressionData dataCompress;

		// Init decompression.
		// Build LZ4 decompression context.
		if ((fInfo.compression == CompressionType::LZ4) || (fInfo.compression == CompressionType::LZ4_HIGH)) {
			dataCompress.compressionIsLZ4       = true;
			dataCompress.compressionLZ4.context = lz4InitDecompressionContext();
		}

		// Build Zstd decompression context.
		if ((fInfo.compression == CompressionType::ZSTD) || (fInfo.compression == CompressionType::ZSTD_HIGH)) {
			dataCompress.compressionIsLZ4        = false;
			dataCompress.compressionZstd.context = zstdInitDecompressionContext();
		}

		while (nextRead < fInfo.fileSize) {
			nextRead += sizeof(dv::PacketHeader);
			if (nextRead >= fInfo.fileSize) {
				break;
			}

			fStream.read(reinterpret_cast<char *>(&header), sizeof(dv::PacketHeader));

			const auto dataOffset = nextRead;

			nextRead += static_cast<size_t>(header.Size());
			if (nextRead >= fInfo.fileSize) {
				break;
			}

			dataCompress.readBuffer.resize(static_cast<size_t>(header.Size()));

			fStream.read(dataCompress.readBuffer.data(), static_cast<std::streamsize>(dataCompress.readBuffer.size()));

			auto dataSize = dataCompress.readBuffer.size();
			auto dataPtr  = dataCompress.readBuffer.data();

			// Decompress packet.
			if (fInfo.compression != dv::CompressionType::NONE) {
				if (!dv::InputDecoder::decompressData(dataPtr, dataSize, dataCompress)) {
					// Skip this packet due to decompression error.
					continue;
				}

				dataSize = dataCompress.decompressBuffer.size();
				dataPtr  = dataCompress.decompressBuffer.data();
			}

			// dataPtr and dataSize now contain the uncompressed, raw flatbuffer.
			const auto identifier = flatbuffers::GetBufferIdentifier(dataPtr, true);

			const auto fbPtr = flatbuffers::GetSizePrefixedRoot<void>(dataPtr);

			const auto typeInfo = dvTypeSystemGetInfoByIdentifier(identifier);

			auto outPacket = typeInfo.construct(typeInfo.sizeOfType);

			typeInfo.unpack(outPacket, fbPtr);

			const auto extractedInfo = typeInfo.timeElementExtractor(outPacket);

			typeInfo.destruct(outPacket);

			// Only add to table if reading and parsing was fully possible.
			dataTable->Table.emplace_back(dataOffset, header, extractedInfo.numElements, extractedInfo.startTimestamp,
				extractedInfo.endTimestamp);
		}

		// Reset file position to initial one.
		fStream.seekg(initialOffset);

		return dataTable;
	}

	static std::unique_ptr<dv::FileDataTable> loadFileDataTable(
		boost::nowide::ifstream &fStream, const dv::InputInformation &fInfo) {
		// Only proceed if data table is present.
		if (fInfo.dataTablePosition < 0) {
			// Rebuild table if not present.
			dv::Log(dv::logLevel::WARNING, "Data table not present, possibly corrupt file. Will rebuild a temporary "
										   "data table, this may take some time ...");
			return rebuildFileDataTable(fStream, fInfo);
		}

		// Remember offset to go back here after.
		const auto initialOffset = fStream.tellg();

		// Read file table data from file.
		std::vector<char> dataTableBuffer;
		dataTableBuffer.resize(fInfo.dataTableSize);

		fStream.seekg(fInfo.dataTablePosition);
		fStream.read(dataTableBuffer.data(), static_cast<int64_t>(fInfo.dataTableSize));

		// Decompress.
		size_t dataTableSize     = dataTableBuffer.size();
		const char *dataTablePtr = dataTableBuffer.data();

		std::vector<char> decompressBuffer;
		decompressBuffer.reserve(dataTableSize);

		if (fInfo.compression != dv::CompressionType::NONE) {
			if (fInfo.compression == dv::CompressionType::LZ4 || fInfo.compression == dv::CompressionType::LZ4_HIGH) {
				auto ctx = dv::InputDecoder::lz4InitDecompressionContext();

				dv::InputDecoder::decompressLZ4(dataTablePtr, dataTableSize, decompressBuffer, ctx.get());
			}

			if (fInfo.compression == dv::CompressionType::ZSTD || fInfo.compression == dv::CompressionType::ZSTD_HIGH) {
				auto ctx = dv::InputDecoder::zstdInitDecompressionContext();

				dv::InputDecoder::decompressZstd(dataTablePtr, dataTableSize, decompressBuffer, ctx.get());
			}

			dataTablePtr  = decompressBuffer.data();
			dataTableSize = decompressBuffer.size();
		}

		// Verify data table is valid.
		flatbuffers::Verifier dataTableVerify(
			reinterpret_cast<const uint8_t *>(dataTablePtr), dataTableSize, 64, INT32_MAX);

		if (!dv::VerifySizePrefixedFileDataTableBuffer(dataTableVerify)) {
			throw std::runtime_error("AEDAT4.0: could not verify FileDataTable contents.");
		}

		// Unpack table.
		auto dataTableFlatbuffer = dv::GetSizePrefixedFileDataTable(dataTablePtr);
		auto dataTable           = std::unique_ptr<dv::FileDataTable>(dataTableFlatbuffer->UnPack());

		// Reset file position to initial one.
		fStream.seekg(initialOffset);

		return dataTable;
	}

public:
	static dv::InputInformation parseHeader(
		boost::nowide::ifstream &fStream, dv::Config::Node mNode, const bool verifyType) {
		// Extract AEDAT version.
		char aedatVersion[static_cast<std::underlying_type_t<dv::Constants>>(dv::Constants::AEDAT_VERSION_LENGTH)];
		fStream.read(
			aedatVersion, static_cast<std::underlying_type_t<dv::Constants>>(dv::Constants::AEDAT_VERSION_LENGTH));

		// Check if file is really AEDAT 4.0.
		if (memcmp(aedatVersion, "#!AER-DAT4.0\r\n",
				static_cast<std::underlying_type_t<dv::Constants>>(dv::Constants::AEDAT_VERSION_LENGTH))
			!= 0) {
			throw std::runtime_error("AEDAT4.0: no valid version line found.");
		}

		dv::InputInformation result;

		// We want to understand what data is in this file,
		// and return actionable elements to our caller.
		// Parse version 4.0 header content.
		// Uses IOHeader flatbuffer. Get its size first.
		flatbuffers::uoffset_t ioHeaderSize;

		fStream.read(reinterpret_cast<char *>(&ioHeaderSize), sizeof(ioHeaderSize));

		// Size is little-endian.
		ioHeaderSize = le32toh(ioHeaderSize);

		// Allocate memory for header and read it.
		auto ioHeader = std::make_unique<uint8_t[]>(ioHeaderSize);

		fStream.read(reinterpret_cast<char *>(ioHeader.get()), ioHeaderSize);

		// Get file size (should always work in binary mode).
		fStream.seekg(0, std::ios::end);
		size_t fileSize = static_cast<size_t>(fStream.tellg());

		// Back to right after the header.
		fStream.seekg(static_cast<std::underlying_type_t<dv::Constants>>(dv::Constants::AEDAT_VERSION_LENGTH)
						  + sizeof(ioHeaderSize) + ioHeaderSize,
			std::ios::beg);

		// Verify header content is valid.
		flatbuffers::Verifier ioHeaderVerify(ioHeader.get(), ioHeaderSize);

		if (!dv::VerifyIOHeaderBuffer(ioHeaderVerify)) {
			throw std::runtime_error("AEDAT4.0: could not verify IOHeader contents.");
		}

		// Parse header into C++ class.
		auto header = dv::UnPackIOHeader(ioHeader.get());

		// Set file-level return information.
		result.fileSize          = fileSize;
		result.compression       = header->compression;
		result.dataTablePosition = header->dataTablePosition;
		result.dataTableSize
			= (header->dataTablePosition < 0) ? (0) : (fileSize - static_cast<size_t>(header->dataTablePosition));

		// Publish file size to tree too.
		if (mNode) {
			mNode.updateReadOnly<dv::CfgType::LONG>("fileSize", static_cast<int64_t>(result.fileSize));
		}

		// Get file data table to determine seek position.
		result.dataTable = loadFileDataTable(fStream, result);

		if (!result.dataTable->Table.empty()) {
			int64_t lowestTimestamp  = INT64_MAX;
			int64_t highestTimestamp = 0;

			for (const auto &inputDef : result.dataTable->Table) {
				// No timestamp is denoted by -1.
				if (inputDef.TimestampStart == -1) {
					continue;
				}

				// Determine lowest and highest timestamps present in file.
				if (inputDef.TimestampStart < lowestTimestamp) {
					lowestTimestamp = inputDef.TimestampStart;
				}

				if (inputDef.TimestampEnd > highestTimestamp) {
					highestTimestamp = inputDef.TimestampEnd;
				}
			}

			result.timeLowest  = lowestTimestamp;
			result.timeHighest = highestTimestamp;
		}
		else {
			result.timeLowest  = 0;
			result.timeHighest = INT64_MAX;
		}

		result.timeDifference = result.timeHighest - result.timeLowest;
		result.timeShift      = result.timeLowest;

		if (mNode) {
			mNode.create<dv::CfgType::LONG>(
				"seek", 0, {0, result.timeDifference}, dv::CfgFlags::NORMAL, "Current playback point.");
			mNode.create<dv::CfgType::LONG>(
				"seekStart", 0, {0, result.timeDifference}, dv::CfgFlags::NORMAL, "Start playback point.");
			mNode.create<dv::CfgType::LONG>("seekEnd", result.timeDifference, {0, result.timeDifference},
				dv::CfgFlags::NORMAL, "End playback point.");
		}

		// Create empty ConfigTree and parse the infoNode into it.
		auto infoTree = dv::Config::Tree::newTree();

		auto importedNode = infoTree.getRootNode();
		importedNode.importSubTreeFromXMLString(header->infoNode.c_str(), false);

		// Map shared amongst files to count up for each type and
		// give the event streams good names if conflicts arise.
		std::map<std::string, int> nameClashTracker;

		for (const auto &childNode : importedNode.getChildren()) {
			// Check type first.
			auto typeIdentifier = childNode.getString("typeIdentifier");

			if (typeIdentifier == dv::Types::nullIdentifier) {
				// Unknown type, skip.
				continue;
			}

			if (verifyType && (dvTypeSystemGetInfoByIdentifier(typeIdentifier.c_str()).id == dv::Types::nullId)) {
				// Type not available, skip.
				continue;
			}

			// Get original stream name.
			auto streamName = childNode.getString("originalOutputName");

			// Check if this collides with anything already there.
			if (nameClashTracker.count(streamName) == 0) {
				// Not present, add it and keep this name.
				nameClashTracker[streamName] = 1;
			}
			else {
				// Name already present, add _X suffix.
				streamName += ("_" + std::to_string(nameClashTracker[streamName]++));
			}

			// Set stream-level return information.
			dv::InputStream st;
			st.name           = streamName;
			st.typeIdentifier = typeIdentifier;
			st.id             = std::stoi(childNode.getName());

			result.streams.push_back(st);

			if (mNode) {
				// All info gathered, create corresponding output.
				// We only have the module node available here, so we just create
				// the sub-nodes and register the outputs later on.
				auto outputNode = mNode.getRelativeNode("outputs/" + streamName + "/");

				childNode.copyTo(outputNode);

				childNode.getRelativeNode("info/").copyTo(outputNode.getRelativeNode("info/"));

				// Rewrite 'source' string attribute to new, more concise and useful format.
				rewriteSourceAttribute(outputNode.getRelativeNode("info/"));
			}
		}

		// Get rid of temporary tree.
		infoTree.deleteTree();

		return (result);
	}

	static boost::nowide::ifstream openFile(const boost::filesystem::path &fPath) {
		if (!boost::filesystem::exists(fPath) || !boost::filesystem::is_regular_file(fPath)) {
			throw std::runtime_error("File doesn't exist or cannot be accessed.");
		}

		if (fPath.extension().string() != dv::io::support::AEDAT4_FILE_EXTENSION) {
			throw std::runtime_error("Unknown file extension '" + fPath.extension().string() + "'.");
		}

		boost::nowide::ifstream fStream;

		// Enable exceptions on failure to open file.
		fStream.exceptions(std::ifstream::badbit | std::ifstream::failbit | std::ifstream::eofbit);

		fStream.open(fPath, std::ios::in | std::ios::binary);

		return (fStream);
	}

	InputDecoder(dvModuleData mData, dv::RuntimeConfig *cfg, dv::Logger *logger) :
		moduleData(mData),
		config(cfg),
		log(logger) {
	}

	static std::shared_ptr<struct LZ4F_dctx_s> lz4InitDecompressionContext() {
		// Create LZ4 decompression context.
		struct LZ4F_dctx_s *ctx = nullptr;
		auto ret                = LZ4F_createDecompressionContext(&ctx, LZ4F_VERSION);
		if (ret != 0) {
			throw std::bad_alloc();
		}

		return (std::shared_ptr<struct LZ4F_dctx_s>(ctx, [](struct LZ4F_dctx_s *c) {
			LZ4F_freeDecompressionContext(c);
		}));
	}

	static std::shared_ptr<struct ZSTD_DCtx_s> zstdInitDecompressionContext() {
		// Create Zstd decompression context.
		struct ZSTD_DCtx_s *ctx = ZSTD_createDCtx();
		if (ctx == nullptr) {
			throw std::bad_alloc();
		}

		return (std::shared_ptr<struct ZSTD_DCtx_s>(ctx, [](struct ZSTD_DCtx_s *c) {
			ZSTD_freeDCtx(c);
		}));
	}

	void setup(std::istream *inStream, const InputInformation *inInfo) {
		inputStream = inStream;
		inputInfo   = inInfo;
		cacheBuffer = FileBuffer(inputInfo->dataTable->Table);

		// Build LZ4 decompression context.
		if (inputInfo->compression == CompressionType::LZ4 || inputInfo->compression == CompressionType::LZ4_HIGH) {
			dataCompress.compressionIsLZ4       = true;
			dataCompress.compressionLZ4.context = lz4InitDecompressionContext();
		}

		// Build Zstd decompression context.
		if (inputInfo->compression == CompressionType::ZSTD || inputInfo->compression == CompressionType::ZSTD_HIGH) {
			dataCompress.compressionIsLZ4        = false;
			dataCompress.compressionZstd.context = zstdInitDecompressionContext();
		}
	}

	static void decompressLZ4(const char *dataPtr, size_t dataSize, std::vector<char> &decompressBuffer,
		struct LZ4F_dctx_s *decompressContext) {
		size_t decompressedSize = 0;
		size_t retVal           = 1;

		while ((dataSize > 0) && (retVal != 0)) {
			decompressBuffer.resize(decompressedSize + lz4CompressionChunkSize);
			size_t dstSize = lz4CompressionChunkSize;

			size_t srcSize = dataSize;

			retVal = LZ4F_decompress(
				decompressContext, decompressBuffer.data() + decompressedSize, &dstSize, dataPtr, &srcSize, nullptr);

			if (LZ4F_isError(retVal)) {
				throw std::runtime_error(std::string("LZ4 decompression error: ") + LZ4F_getErrorName(retVal));
			}

			decompressedSize += dstSize;

			dataSize -= srcSize;
			dataPtr  += srcSize;
		}

		decompressBuffer.resize(decompressedSize);
	}

	static void decompressZstd(const char *dataPtr, size_t dataSize, std::vector<char> &decompressBuffer,
		struct ZSTD_DCtx_s *decompressContext) {
		auto decompressedSize = ZSTD_getFrameContentSize(dataPtr, dataSize);
		if (decompressedSize == ZSTD_CONTENTSIZE_UNKNOWN) {
			throw std::runtime_error("Zstd decompression error: unknown content size");
		}
		if (decompressedSize == ZSTD_CONTENTSIZE_ERROR) {
			throw std::runtime_error("Zstd decompression error: content size error");
		}

		decompressBuffer.resize(decompressedSize);

		auto retVal
			= ZSTD_decompressDCtx(decompressContext, decompressBuffer.data(), decompressedSize, dataPtr, dataSize);

		if (ZSTD_isError(retVal)) {
			throw std::runtime_error(std::string("Zstd decompression error: ") + ZSTD_getErrorName(retVal));
		}
		else {
			decompressBuffer.resize(retVal);
		}
	}

	static bool decompressData(const char *dataPtr, const size_t dataSize, dv::DecompressionData &decompress) {
		if (decompress.compressionIsLZ4) {
			try {
				decompressLZ4(dataPtr, dataSize, decompress.decompressBuffer, decompress.compressionLZ4.context.get());
			}
			catch (const std::runtime_error &ex) {
				// Decompression error, ignore this packet.
#if defined(LZ4_VERSION_NUMBER) && LZ4_VERSION_NUMBER >= 10800
				LZ4F_resetDecompressionContext(decompress.compressionLZ4.context.get());
#else
				decompress.compressionLZ4.context.reset();
				decompress.compressionLZ4.context = lz4InitDecompressionContext();
#endif
				return false;
			}
		}

		if (!decompress.compressionIsLZ4) {
			try {
				decompressZstd(
					dataPtr, dataSize, decompress.decompressBuffer, decompress.compressionZstd.context.get());
			}
			catch (const std::runtime_error &ex) {
				// Decompression error, ignore this packet.
#if defined(ZSTD_VERSION_NUMBER) && ZSTD_VERSION_NUMBER >= 10400
				ZSTD_DCtx_reset(decompress.compressionZstd.context.get(), ZSTD_reset_session_only);
#else
				decompress.compressionZstd.context.reset();
				decompress.compressionZstd.context = zstdInitDecompressionContext();
#endif
				return false;
			}
		}

		return true;
	}

	void run() {
		std::chrono::time_point<std::chrono::steady_clock> prepareDataStart = std::chrono::steady_clock::now();

		int64_t startTimePos = 0;
		int64_t endTimePos   = 0;
		bool hitEnd          = false;

		if (config->getString("mode") == "timeInterval") {
			startTimePos = config->getLong("seek");
			endTimePos   = startTimePos + config->getLong("timeInterval") - 1;

			if (endTimePos >= inputInfo->timeDifference) {
				endTimePos = inputInfo->timeDifference;
				hitEnd     = true;
			}

			if ((startTimePos <= config->getLong("seekEnd")) && (endTimePos >= config->getLong("seekEnd"))) {
				endTimePos = config->getLong("seekEnd");
				hitEnd     = true;
			}

			int64_t startTimestamp = startTimePos + inputInfo->timeShift;
			int64_t endTimestamp   = endTimePos + inputInfo->timeShift;

			log->debug.format(
				"Generating time-bound packet for StartTS: {:d}, EndTS: {:d}.", startTimestamp, endTimestamp);

			// Now we know how much time we need to extract, from when to when.
			// First let's see if there is even something for that time interval for
			// each of the streams.
			for (const auto &st : inputInfo->streams) {
				cacheBuffer.updatePacketsTimeRange(startTimestamp, endTimestamp, st.id);
				auto packetsToRead = cacheBuffer.getInRange();

				if (packetsToRead.empty()) {
					// No data for this stream and time interval, skip.
					continue;
				}

				log->debug.format("Stream {:s} ({:s}): found {:d} packet(s) involved in time-interval with StartTS: "
								  "{:d}, EndTS: {:d}.",
					st.name, st.typeIdentifier, packetsToRead.size(), startTimestamp, endTimestamp);

				for (auto iter = packetsToRead.cbegin(); iter != packetsToRead.cend(); iter++) {
					const char *dataPtr;
					std::size_t dataSize;

					if (iter->cached) {
						dataSize = cacheBuffer.getDataSizeCache(*iter);
						dataPtr  = cacheBuffer.getDataPtrCache(*iter).data();
					}
					else {
						inputStream->seekg(iter->packet.ByteOffset, std::ios_base::beg);

						dataCompress.readBuffer.resize(static_cast<size_t>(iter->packet.PacketInfo.Size()));

						inputStream->read(dataCompress.readBuffer.data(), iter->packet.PacketInfo.Size());

						dataSize = dataCompress.readBuffer.size();
						dataPtr  = dataCompress.readBuffer.data();

						// Decompress packet.
						if (inputInfo->compression != dv::CompressionType::NONE) {
							if (!decompressData(dataPtr, dataSize, dataCompress)) {
								// Skip this packet due to decompression error.
								continue;
							}

							dataSize = dataCompress.decompressBuffer.size();
							dataPtr  = dataCompress.decompressBuffer.data();
						}

						// add dataPtr and dataSize to cache
						std::vector<char> temp(dataPtr, dataPtr + dataSize);
						cacheBuffer.addToCache(*iter, temp, dataSize);
					}

					// dataPtr and dataSize now contain the uncompressed, raw flatbuffer.
					if (!flatbuffers::BufferHasIdentifier(dataPtr, st.typeIdentifier.c_str(), true)) {
						// Wrong type identifier for this flatbuffer (file_identifier field).
						// This should never happen, ignore packet.
						log->error.format("Wrong type identifier for packet: '{:s}', expected: '{:s}'.",
							flatbuffers::GetBufferIdentifier(dataPtr, true), st.typeIdentifier);
						continue;
					}

					auto fbPtr = flatbuffers::GetSizePrefixedRoot<void>(dataPtr);

					auto outPacket = dvModuleOutputAllocate(moduleData, st.name.c_str());

					bool earlyCommit
						= st.type.unpackTimeElementRange(outPacket->obj, fbPtr, {startTimestamp, endTimestamp, -1});

					bool lastPacket = (std::next(iter) == packetsToRead.cend());

					// Commit when done extracting a packet.
					if (earlyCommit || lastPacket) {
						auto extractedInfo = st.type.timeElementExtractor(outPacket->obj);

						if (extractedInfo.numElements > 0) {
							dvModuleOutputCommit(moduleData, st.name.c_str());
						}
					}
				}
			}

			// If the next read would be outside existing time in this file, we've reached the end.
			if (hitEnd) {
				throw std::ios::failure("EOF");
			}

			// Only update to next seek point if we're not paused.
			if (!config->getBool("pause")) {
				config->setLong("seek", endTimePos + 1);
			}
		}
		else {
			// TODO: implement this later.
			int64_t requiredElements = config->getLong("eventNumber");
		}

		std::chrono::time_point<std::chrono::steady_clock> prepareDataEnd = std::chrono::steady_clock::now();
		const auto timeSpentPreparingData
			= std::chrono::duration_cast<std::chrono::microseconds>(prepareDataEnd - prepareDataStart).count();

		if (timeSpentPreparingData >= config->getLong("timeDelay")) {
			if (!slowdownWarningSent) {
				slowdownWarningSent = true;

				log->warning("Cannot keep up with requested time delay during playback (at position "
							 + std::to_string(startTimePos) + ").");
			}
		}
		else {
			slowdownWarningSent = false;

			// Configurable time delay for real-time playback.
			std::this_thread::sleep_for(
				std::chrono::microseconds(config->getLong("timeDelay") - timeSpentPreparingData));
		}
	}
};

} // namespace dv

#endif // DV_INPUT_HPP
