/*
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at https://mozilla.org/MPL/2.0/.
 */

#pragma once

#include "Math/ByteSize.hpp"
#include "Base/Event.hpp"
#include <atomic>
#include <condition_variable>
#include <filesystem>
#include <mutex>
#include <memory>
#include <string>
#include <thread>
#include <queue>

namespace OpenVulkano
{
	class IEventHandler;
	class MultiPartArchiveWriter;
}

namespace OpenVulkano::AR
{
	class ArSession;
	class ArFrame;

	enum class RecordingMode
	{
		/**
		 * Frames will only be saved when manually requested through ArFrame::Save()
		 */
		MANUAL,
		/**
		 * Frames will only be saved when the frame is manually requested through ArSession::GetFrame()
		 */
		FRAME_REQUEST,
		/**
		 * Every new frame will be stored automatically
		 */
		NEW_FRAME
	};

	struct RecordingSettings
	{
		RecordingMode recordingMode = RecordingMode::MANUAL;
		std::filesystem::path path;
		size_t archiveSize = 2_GiB;
		bool downsampleColor = true;
		bool highResFramesInSeparateArchive = true;
		bool saveHighResFrames = true;
		bool asyncRecording = true;
	};

	class ArRecorder final
	{
		struct AsyncProcessor final
		{
			ArRecorder* recorder;
			std::thread processingThread;
			std::queue<std::shared_ptr<ArFrame>> frameQueue, highResFrameQueue;
			std::mutex queueMutex;
			std::condition_variable newDataAvailable;
			std::atomic_bool requestExit{};

			explicit AsyncProcessor(ArRecorder* recorder);
			~AsyncProcessor();
			void Queue(const std::shared_ptr<ArFrame>& frame, bool highRes);
			void Handler();
		};

		ArSession* m_session;
		std::unique_ptr<MultiPartArchiveWriter> m_colorWriter, m_depthWriter, m_confidenceWriter, m_metadataWriter, m_highResWriter;
		RecordingSettings m_settings;
		bool m_recording = false, m_persistent = false;

		IEventHandler* m_newFrameHandler = nullptr;

		AsyncProcessor m_asyncProcessor;

		void Write(ArFrame* frame, bool highRes = false);
		void WriteMetadata(ArFrame* frame, MultiPartArchiveWriter* metaWriter);
		void WriteColorImage(ArFrame* arFrame, MultiPartArchiveWriter* colorWriter, const std::filesystem::path* path, bool highRes) const;
		void WriteDepthImage(ArFrame *arFrame, MultiPartArchiveWriter* depthWriter, MultiPartArchiveWriter* confWriter);

	public:
		ArRecorder(ArSession* session);

		~ArRecorder();

		void Save(const std::shared_ptr<ArFrame>& frame);

		void SaveHighResolution(const std::shared_ptr<ArFrame>& frame);

		void SaveToFile(const std::shared_ptr<ArFrame>& frame, const std::filesystem::path& path, bool downsample = false);

		/**
		 * Starts the recording of the owning AR session
		 */
		void Start();

		/**
		 * Stops the recording of the owning AR session
		 */
		void Stop();

		/**
		 * Sets the directory into which the AR recording should be stored.
		 * The path needs to be set to make the recording persistent.
		 * If path is changed after starting the recording, the already running recording will be moved to the new path.
		 * @param path The path to be used to store the recording
		 */
		void SetRecordingPath(const std::string& path)
		{
			std::filesystem::path p(path);
			SetRecordingPath(p);
		}

		/**
		 * Sets the directory into which the AR recording should be stored.
		 * The path needs to be set to make the recording persistent.
		 * If path is changed after starting the recording, the already running recording will be moved to the new path.
		 * @param path The path to be used to store the recording
		 */
		void SetRecordingPath(const std::filesystem::path& path);

		/**
		 * Gets the current recording path
		 * @return Current recording dir
		 */
		[[nodiscard]] const std::filesystem::path& GetRecordingPath() const { return m_settings.path; }

		/**
		 * Checks if a path to be used for the recording has been set.
		 * If no path has been set the recording will be stored in a temporary location and will be deleted once the recording is stopped.
		 * @return True if a path has been set and the recording will persist after stopping the recording.
		 */
		[[nodiscard]] bool IsPersistent() const { return m_persistent; }

		/**
		 * Checks if the recording of the AR session is running
		 * @return True if recording is started
		 */
		[[nodiscard]] bool IsRecording() const { return m_recording; }

		/**
		 * Sets the used recording mode
		 * @param mode The mode that should be used to record the current session
		 */
		void SetRecordingMode(RecordingMode mode);

		/**
		 * Checks the currently used recording mode for the AR session
		 * @return The currently used recording mode
		 */
		[[nodiscard]] RecordingMode GetRecordingMode() const { return m_settings.recordingMode; }

		/**
		 * If enabled color images will be reduced to 1/4 of the original resolution (both axes will have half the resolution).
		 * This option can be used to reduce the used storage space and storage bandwidth during recording.
		 *
		 * @param downsample true = downsample images; false = no downsampling
		 */
		void SetDownsampleColorImages(bool downsample = true) { m_settings.downsampleColor = downsample; }

		/**
		 * Checks if color image downsampling is enabled.
		 * @return true = downsample images; false = no downsampling
		 */
		bool GetDownsampleColorImages() const { return m_settings.downsampleColor; }

		/**
		 * If enabled the requested high resolution images will be stored in a separate archive ("highres.tar") instead of the normal ar recording archives.
		 * @param separateArchive true = using the separate archive file; false = using the normal archive files
		 */
		void SetHighResFramesInSeparateArchive(bool separateArchive = true) { m_settings.highResFramesInSeparateArchive = separateArchive; }

		/**
		 * Checks if a separate archive is used for the high res frames.
		 * @return true = using the separate archive file; false = using the normal archive files
		 */
		bool GetHighResFramesInSeparateArchive() const { return m_settings.highResFramesInSeparateArchive; }

		void SetArchivePartMaxFileSize(size_t maxPartSize = 2_GiB) { m_settings.archiveSize = maxPartSize; }

		size_t GetArchivePartMaxFileSize() const { return m_settings.archiveSize; }

		const RecordingSettings& GetRecordingSettings() const { return m_settings; }

		void SetRecordHighResImages(bool recHighRes = true) { m_settings.saveHighResFrames = recHighRes; }

		Event<ArRecorder*, bool> OnRecordingStateChanged;
	};
}
