/*
 * 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 "ArType.hpp"
#include "ArConstans.hpp"
#include "ArTrackingState.hpp"
#include "ArSessionMetadata.hpp"
#include "ArRecorder.hpp"
#include "Math/Range.hpp"
#include "Base/Event.hpp"
#include <memory>
#include <string>
#include <vector>
#include <optional>
#include <future>

namespace OpenVulkano
{
	class IRenderer;

	namespace Scene
	{
		class Texture;
	}
}

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

	enum class ArSessionType
	{
		NATIVE, PLAYBACK, NETWORK_STREAM
	};

	class ArSessionCapabilities final
	{
		ArType type = ArType::UNKNOWN;
		ArSessionType sessionType = ArSessionType::NATIVE;
		bool uncompressed = true;
		bool depthSupported = false;
		bool supportsExposureLocking = false;
		bool supportsWhitebalanceLocking = false;
		bool hasFlashlight = false;

	public:
		ArSessionCapabilities() = default;

		ArSessionCapabilities(const ArType type, const ArSessionType sessionType, const bool uncompressed, const bool depthSupported, const bool supportsExposureLocking, const bool supportsWhitebalanceLocking, const bool hasFlash)
			: type(type), sessionType(sessionType), uncompressed(uncompressed), depthSupported(depthSupported), supportsExposureLocking(supportsExposureLocking), supportsWhitebalanceLocking(supportsWhitebalanceLocking), hasFlashlight(hasFlash)
		{}

		[[nodiscard]] bool IsUncompressed() const { return uncompressed; }
		[[nodiscard]] bool IsDepthSupported() const { return depthSupported; }
		[[nodiscard]] bool IsExposureLockSupported() const { return supportsExposureLocking; }
		[[nodiscard]] bool IsWhitebalanceLockSupported() const { return supportsWhitebalanceLocking; }
		[[nodiscard]] bool HasFlashlight() const { return hasFlashlight; }
		[[nodiscard]] bool IsNative() const { return sessionType == ArSessionType::NATIVE; }
		[[nodiscard]] bool IsPlayback() const { return sessionType == ArSessionType::PLAYBACK; }
		[[nodiscard]] bool IsStream() const { return sessionType == ArSessionType::NETWORK_STREAM; }
		[[nodiscard]] ArType GetArType() const { return type; }
		[[nodiscard]] ArSessionType GetSessionType() const { return sessionType; }
	};

	struct ArSessionConfig
	{
		bool enableDepth = true;
		bool autoFocus = true;
		bool preferHDR = true;
		bool sceneReconstruction = false;
		bool planeDetection = false;
		int preferredResolution = 0;
	};

	struct ArCreateResult
	{
		enum Status { SUCCESS, FAILED_UNKNOWN, FAILED_INCOMPATIBLE_CONFIG, FAILED_ALREADY_CREATED, FAILED_NOT_SUPPORTED, FAILED_INVALID_PATH, FAILED_TIMEOUT, FAILED_INVALID_SERVER };

		const std::shared_ptr<ArSession> session;
		const Status status;
		const std::string statusMessage;

		[[nodiscard]] bool IsSuccess() const { return status == SUCCESS; }
	};

	class ArSession
	{
		friend ArFrame;

	protected:
		ArSession(const ArSessionMetadata& metadata) : metadata(metadata), recorder(metadata.playback ? nullptr : this) {}

		virtual Scene::Texture* MakeTexture(ArFrame* frame) = 0;

		virtual void ReturnTexture(Scene::Texture* texture) = 0;

	public:
		/**
		 * Creates a platform native AR session. nullptr if failed to create session.
		 * @return ArCreateResult about the status of the AR session creation. The session pointer will always be nullptr unless the status is SUCCESS or FAILED_ALREADY_CREATED (in this case the session pointer will point to the previously created instance).
		 */
		[[nodiscard]] static ArCreateResult Create(const ArSessionConfig& config = {});

		/**
		 * Creates a playback AR session. nullptr if failed to create session for given path.
		 * @param recordingPath Path to a previously recorded AR session.
		 * @param autoAdvance If set to true the playback will advance based on the stored timestamps. If set to false it will only advance if a new frame is requested.
		 * @return ArCreateResult about the status of the AR session creation. The session pointer will always be nullptr unless the status is SUCCESS.
		 */
		[[nodiscard]] static ArCreateResult CreatePlayback(const std::string& recordingPath, bool autoAdvance = true, bool loadImages = true, bool loadDepth = true);

		/**
		 * Creates a network streamed AR session. nullptr if failed to create session for given address. This will block till the connection with the remote host has been established.
		 * @param serverAddress The address of the server that is hosting the AR session.
		 * @param requestConfig An optional config to be requested. It is not guaranteed that this will be honored (especially in multi guest sessions).
		 * @return ArCreateResult about the status of the AR session creation. The session pointer will always be nullptr unless the status is SUCCESS.
		 */
		[[nodiscard]] static ArCreateResult CreateNetwork(const std::string& serverAddress, std::optional<ArSessionConfig> requestConfig = std::nullopt);

		/**
		 * Creates a network streamed AR session. nullptr if failed to create session for given address.
		 * @param serverAddress The address of the server that is hosting the AR session.
		 * @param requestConfig An optional config to be requested. It is not guaranteed that this will be honored (especially in multi guest sessions).
		 * @return ArCreateResult about the status of the AR session creation. The session pointer will always be nullptr unless the status is SUCCESS.
		 */
		[[nodiscard]] static std::future<ArCreateResult> CreateNetworkAsync(const std::string& serverAddress, std::optional<ArSessionConfig> requestConfig = std::nullopt);

		/**
		 * Gets the primary session. This is the longest running session.
		 * @return The primary ar session. nullptr if no active session.
		 */
		[[nodiscard]] static std::shared_ptr<ArSession> GetPrimarySession();

		/**
		 * Gets all the active ar sessions.
		 * @return Vector of active ar sessions.
		 */
		[[nodiscard]] static std::vector<std::shared_ptr<ArSession>> GetActiveSessions();

		/**
		 * Gets the capabilities of the native AR system.
		 * @return ArSessionCapabilities for the current system.
		 */
		[[nodiscard]] static const ArSessionCapabilities& GetNativeCapabilities();

		/**
		 * Checks if a native AR implementation is available for this system. If false the Create(...) call will fail and only CreatePlayback(...) and CreateNetwork(...) are available.
		 * @return true if there is a native AR system available. false if not.
		 */
		static bool IsNativeArAvailable();

		virtual ~ArSession() = default;

		/**
		 * Starts the AR session.
		 */
		virtual void Start() = 0;

		/**
		 * Stops the AR session.
		 */
		virtual void Stop() = 0;

		/**
		 * Pauses the AR session. The behaviour of this will depend on the type of the AR session.
		 * The type can be checked with the GetSessionType() method.
		 * NATIVE:   The session will continue to run, but GetFrame() will block until continued and events will not be triggered.
		 * PLAYBACK: There will be no new frames being read from the recording until continued. As a result GetFrame() will block until continued and no events will be triggered.
		 * NETWORK_STREAM: The session on the remote host will stay active. Data might still be sent to the client (in multi client mode). But no data will be forwarded to app and thus GetFrame() will block until continued and no events will be triggered.
		 */
		virtual void Pause() { paused = true; }

		/**
		 * Checks if the AR session is running. Will be true while paused!
		 * @return True if the AR session is running. False if not.
		 */
		[[nodiscard]] virtual bool IsRunning() const { return running; };

		/**
		 * Checks if the AR session is paused.
		 * @return True if the AR session is paused. False if not.
		 */
		[[nodiscard]] virtual bool IsPaused() const { return paused; };

		/**
		 * Gets the next ArFrame. Is guaranteed to be a new frame. Will block till new frame is available or an internal timeout is reached.
		 * @return Next ArFrame, or nullptr if no more frames are available or internal timeout was reached.
		 */
		[[nodiscard]] virtual std::shared_ptr<ArFrame> GetFrame() = 0;

		/**
		 * The ArRecorder for this ArSession.
		 * @return ArRecorder instance or nullptr if the ArSession does not support recording.
		 */
		[[nodiscard]] ArRecorder& GetRecorder() { return recorder; };

		/**
		 * Gets the type of the AR session.
		 * @return Type of the AR session.
		 */
		[[nodiscard]] virtual ArSessionType GetSessionType() = 0;

		/**
		 * Gets the type of the AR system backed by this session.
		 * @return The type of the AR system used by this session.
		 */
		[[nodiscard]] virtual ArType GetArType() = 0;

		virtual void RequestHighResolutionFrame() {}

		/**
		 * Sets the renderer that should be used for texture creation.
		 * @param renderer The renderer to be used to create textures.
		 */
		virtual void SetRenderer(IRenderer* renderer) = 0;
		
		virtual void LockExposureTime(bool locked) {};
		
		virtual void LockWhitebalance(bool locked) {};
		
		virtual void SetFlashlightOn(bool on) {};

		/**
		 * Gets the capabilities for this ArSession.
		 * @return The capabilities for the current AR session.
		 */
		[[nodiscard]] const ArSessionCapabilities& GetCapabilities() const { return capabilities; };

		[[nodiscard]] const ArConstants& GetConstants() const { return constants; }

		[[nodiscard]] bool GetShouldAttemptRelocalization() { return shouldAttemptRelocalization; }

		[[nodiscard]] const ArSessionMetadata& GetSessionMetadata() const { return metadata; }

		void SetShouldAttemptRelocalization(bool attemptReloc) { shouldAttemptRelocalization = attemptReloc; }

		Event<> OnNewFrameAvailable;
		Event<const std::shared_ptr<ArFrame>&> OnNewFrame;
		Event<const std::shared_ptr<ArFrame>&> OnNewFrameHighResolution;
		Event<bool> OnSessionInterruptionChange;
		Event<ArTrackingState> OnTrackingStateChanged;
		Event<const Math::Matrix4f&> OnNewCameraTransformation;
		Event<const Math::Matrix4f&> OnNewCameraViewMatrix;
		Event<const std::string&> OnSessionFailed;
		Event<> OnAnchorsUpdated;
		Event<> OnStopped;

	protected:
		bool running = false, paused = false, shouldAttemptRelocalization = false;
		ArSessionCapabilities capabilities;
		ArConstants constants;
		ArSessionMetadata metadata;

	private:
		ArRecorder recorder;

		static std::vector<std::weak_ptr<ArSession>> sessions;
		static std::weak_ptr<ArSession> nativeSession;
	};
}