#pragma once

#include "../../../core/utils.hpp"
#include "../../../data/event_base.hpp"
#include "../../../data/frame_base.hpp"
#include "../../../data/imu_base.hpp"
#include "../../../data/trigger_base.hpp"

#include <functional>
#include <span>

namespace dv::io::camera::parser {

struct ParsedData {
	dv::EventPacket events{};
	std::vector<dv::Frame> frames{};
	dv::IMUPacket imu{};
	dv::TriggerPacket triggers{};

	void clear() {
		events.elements.clear();
		frames.clear();
		imu.elements.clear();
		triggers.elements.clear();
	}
};

using ParserLoggerCallback     = std::function<void(bool debug, std::string_view message)>;
using ParserTimeInitCallback   = std::function<void()>;
using ParserDataCommitCallback = std::function<void(ParsedData buffers)>;

class ParserBase {
public:
	struct eDataLost {};

	virtual ~ParserBase() = default;

	virtual void parseData(std::span<const uint8_t> buffer, bool dataLost) = 0;

	void setLogger(ParserLoggerCallback loggerFunction) {
		mLogger = std::move(loggerFunction);
	}

	void setDataCommitCallback(ParserDataCommitCallback dataCommitCallback) {
		mDataCommitCallback = std::move(dataCommitCallback);
	}

	void setTimeInitCallback(ParserTimeInitCallback timeInitCallback) {
		mTimeInitCallback = std::move(timeInitCallback);
	}

	void setTimeInterval(const std::chrono::microseconds timeInterval) {
		if (timeInterval.count() < 0) {
			throw dv::exceptions::RuntimeError{"Time interval cannot be negative."};
		}

		mTimestamps.commitInterval = timeInterval.count();
	}

	[[nodiscard]] std::chrono::microseconds getTimeInterval() const {
		return std::chrono::microseconds{mTimestamps.commitInterval};
	}

	void setSystemOffset(const std::chrono::microseconds systemOffset) {
		if (systemOffset.count() < 0) {
			throw dv::exceptions::RuntimeError{"System offset cannot be negative."};
		}

		mTimestamps.systemOffset = systemOffset.count();

		mTimestamps.resetTimingAsync = true;
	}

	[[nodiscard]] std::chrono::microseconds getSystemOffset() const {
		return std::chrono::microseconds{mTimestamps.systemOffset};
	}

	void adjustTimestamps(const std::chrono::microseconds adjust) {
		// Just sum here, actual adjustment is done in updateTimestamp().
		mTimestamps.adjust += adjust.count(); // Can be negative for backwards adjustment.
	}

	[[nodiscard]] std::chrono::microseconds getOutstandingTimestampAdjustment() const {
		return std::chrono::microseconds{mTimestamps.adjust};
	}

protected:
	ParserLoggerCallback mLogger{};

	ParsedData mBuffers{};

	[[nodiscard]] int64_t getCurrentTimestamp() const {
		return mTimestamps.current;
	}

	/**
	 * Update timestamp tracking with the latest timestamp from the device.
	 * Always implies a dataCommit() too.
	 * @tparam STRICT_MONOTONIC_TIMESTAMP wether the timestamps from device increase strictly or not.
	 * @param timestamp latest timestamp from device.
	 */
	template<bool STRICT_MONOTONIC_TIMESTAMP = true>
	void updateTimestamp(const int64_t timestamp) {
		if (mTimestamps.resetTimingAsync) {
			resetTiming();
		}

		// Check monotonicity of timestamps. In cases we cannot guarantee strict monotonicity (always increases),
		// we track that with the 'timestampDidIncrease' flag.
		bool timestampDidIncrease = true;

		if constexpr (STRICT_MONOTONIC_TIMESTAMP) {
			if (mTimestamps.lastDevice >= timestamp) [[unlikely]] {
				throw dv::exceptions::RuntimeError{fmt::format("non strictly-monotonic device timestamp detected: "
															   "lastTimestamp={}, currentTimestamp={}, difference={}",
					mTimestamps.lastDevice, timestamp, (mTimestamps.lastDevice - timestamp))};
			}
		}
		else {
			if (mTimestamps.lastDevice > timestamp) [[unlikely]] {
				throw dv::exceptions::RuntimeError{fmt::format(
					"non monotonic device timestamp detected: lastTimestamp={}, currentTimestamp={}, difference={}",
					mTimestamps.lastDevice, timestamp, (mTimestamps.lastDevice - timestamp))};
			}

			timestampDidIncrease = (mTimestamps.lastDevice != timestamp);
		}

		mTimestamps.lastDevice = timestamp;

		// Smear out adjustment on each timestamp update, instead of all at once. This leads to smoother behavior
		// and makes it possible to 'go back' by slowing down time updates instead of jumping, guaranteeing
		// monotonic behavior of timestamps (their value never jumps back).
		if (mTimestamps.adjust != 0) {
			// Adjust forwards: time goes up by one. This is the easy case, we can just increase the system offset
			// by one and current timestamp will be updated right away. This also guarantees monotonicity.
			// We could add the full adjustment amount in one go, but we don't to avoid big jumps.
			if (mTimestamps.adjust > 0) {
				mTimestamps.systemOffset++;

				mTimestamps.adjust--;
			}

			// Adjust backwards: time stays still. We can't just decrease the offset by any arbitrary amount, as
			// that might make the final timestamp go backwards and violate monotonicity. So we decrease by one and
			// smear this out over multiple updates, which is safe as the device timestamp always increases by at
			// least one (we track strict monotonic increase with the flag 'timestampDidIncrease'), and paired with an
			// offset reduction by one it means the final timestamp either stays the same or increases (monotonic
			// behavior).
			if ((mTimestamps.adjust < 0) && timestampDidIncrease) {
				mTimestamps.systemOffset--;

				mTimestamps.adjust++;
			}
		}

		// Compute full timestamp for usage by parser.
		mTimestamps.current = mTimestamps.systemOffset + timestamp;

		dataCommit();
	}

	/**
	 * Send all data currently available to the consumer, up to the current timestamp excluded.
	 * Usually called on timestamp update.
	 * All data previous to this timestamp should be inside the data buffers, but in some cases this is not possible,
	 * currently only the case with DAVIS frames.
	 */
	void dataCommit() {
		// If time tracking not initialized yet, do nothing. timestampInit() has to do that.
		if ((mTimestamps.nextCommit == -1) || mTimestamps.resetTimingAsync) {
			mBuffers.clear();
			return;
		}

		// Time interval hasn't passed yet. No buffer commit at this time.
		if (mTimestamps.current <= mTimestamps.nextCommit) {
			return;
		}

		// Commit buffers to output, if at least one has data.
		if (mDataCommitCallback
			&& ((!mBuffers.events.elements.empty()) || (!mBuffers.frames.empty()) || (!mBuffers.imu.elements.empty())
				|| (!mBuffers.triggers.elements.empty()))) {
			mDataCommitCallback(std::move(mBuffers));
		}

		mBuffers.clear();

		// Update time. Always jump forward by multiples of the time interval so we chunk the stream up in
		// time-interval sized bites that are served to the user.
		const int64_t timeInterval = mTimestamps.commitInterval;

		while (mTimestamps.current > mTimestamps.nextCommit) {
			mTimestamps.nextCommit += timeInterval;
		}
	}

	/**
	 * Call on first timestamp initialization and timestamp resets.
	 * This will ensure the system offset is set correctly.
	 * NOTE: callers must guarantee their own internal timestamp tracking is fully initialized and ready when
	 * calling this function!
	 *
	 * @param firstTimestamp first timestamp parsed from data stream from the device's point-of-view.
	 */
	void timestampInit(const std::chrono::microseconds firstTimestamp) {
		if (firstTimestamp.count() < 0) {
			throw dv::exceptions::RuntimeError{"First timestamp cannot be negative."};
		}

		// Ensure system offset is always set, subtract an initial offset from the device's point-of-view if it exists.
		mTimestamps.systemOffset = dv::now() - firstTimestamp.count();
		resetTiming();

		// And then add back the device-side offset if it exists.
		if (firstTimestamp.count() > 0) {
			mTimestamps.lastDevice = firstTimestamp.count();
			mTimestamps.current    = mTimestamps.systemOffset + firstTimestamp.count();
		}

		if (mTimeInitCallback) {
			mTimeInitCallback();
		}
	}

private:
	ParserDataCommitCallback mDataCommitCallback{};
	ParserTimeInitCallback mTimeInitCallback{};

	struct {
		std::atomic<int64_t> commitInterval{10000};
		std::atomic<int64_t> systemOffset{0};
		std::atomic<int64_t> adjust{0};
		int64_t lastDevice{0};
		int64_t current{0};
		int64_t nextCommit{-1};
		std::atomic<bool> resetTimingAsync{false};
	} mTimestamps;

	/**
	 * Reset timestamping. Only called in the parser thread (via async flag)!
	 * systemOffset must have been set prior to calling this.
	 */
	void resetTiming() {
		// Reset adjust too if we reset the whole timestamp tracking anyway.
		mTimestamps.adjust = 0;

		mTimestamps.lastDevice = 0;

		// Setup current timestamp so time information is immediately available for users.
		mTimestamps.current = mTimestamps.systemOffset;

		// Initialize time interval tracking. System offset would be initialized/changed in the previous callback.
		mTimestamps.nextCommit = mTimestamps.current + mTimestamps.commitInterval - 1;

		// Clear stale data, as it might have wrong timestamp information.
		mBuffers.clear();

		mTimestamps.resetTimingAsync = false;
	}
};

} // namespace dv::io::camera::parser
