#pragma once

#include "DeckLinkAPI_h.h"
#include "DeckLinkDisplayMode.h"
#include "DeckLinkFrameTransfer.h"
#include "DeckLinkVideoIOFormat.h"
#include "VideoIOFormat.h"
#include "VideoIOTypes.h"
#include "VideoPlayoutPolicy.h"
#include "VideoPlayoutScheduler.h"

#include <atlbase.h>
#include <deque>
#include <mutex>
#include <string>
#include <unordered_map>

class OpenGLComposite;

class DeckLinkSession : public VideoIODevice
{
public:
	DeckLinkSession() = default;
	~DeckLinkSession();

	void ReleaseResources() override;
	bool DiscoverDevicesAndModes(const VideoFormatSelection& videoModes, std::string& error) override;
	bool SelectPreferredFormats(const VideoFormatSelection& videoModes, bool outputAlphaRequired, std::string& error) override;
	bool ConfigureInput(InputFrameCallback callback, const VideoFormat& inputVideoMode, std::string& error) override;
	bool ConfigureOutput(OutputFrameCallback callback, const VideoFormat& outputVideoMode, bool externalKeyingEnabled, std::string& error) override;
	bool PrepareOutputSchedule() override;
	bool StartInputStreams() override;
	bool StartScheduledPlayback() override;
	bool Start() override;
	bool Stop() override;

	bool HasInputDevice() const { return mState.hasInputDevice; }
	bool HasInputSource() const { return mState.hasInputSource; }
	void SetInputSourceMissing(bool missing) { mState.hasInputSource = !missing; }
	bool InputOutputDimensionsDiffer() const { return mState.inputFrameSize != mState.outputFrameSize; }
	const FrameSize& InputFrameSize() const { return mState.inputFrameSize; }
	const FrameSize& OutputFrameSize() const { return mState.outputFrameSize; }
	unsigned InputFrameWidth() const { return mState.inputFrameSize.width; }
	unsigned InputFrameHeight() const { return mState.inputFrameSize.height; }
	unsigned OutputFrameWidth() const { return mState.outputFrameSize.width; }
	unsigned OutputFrameHeight() const { return mState.outputFrameSize.height; }
	VideoIOPixelFormat InputPixelFormat() const { return mState.inputPixelFormat; }
	VideoIOPixelFormat OutputPixelFormat() const { return mState.outputPixelFormat; }
	bool InputIsTenBit() const { return VideoIOPixelFormatIsTenBit(mState.inputPixelFormat); }
	bool OutputIsTenBit() const { return VideoIOPixelFormatIsTenBit(mState.outputPixelFormat); }
	unsigned InputFrameRowBytes() const { return mState.inputFrameRowBytes; }
	unsigned OutputFrameRowBytes() const { return mState.outputFrameRowBytes; }
	unsigned CaptureTextureWidth() const { return mState.captureTextureWidth; }
	unsigned OutputPackTextureWidth() const { return mState.outputPackTextureWidth; }
	const std::string& FormatStatusMessage() const { return mState.formatStatusMessage; }
	const std::string& InputDisplayModeName() const { return mState.inputDisplayModeName; }
	const std::string& OutputModelName() const { return mState.outputModelName; }
	bool SupportsInternalKeying() const { return mState.supportsInternalKeying; }
	bool SupportsExternalKeying() const { return mState.supportsExternalKeying; }
	bool KeyerInterfaceAvailable() const { return mState.keyerInterfaceAvailable; }
	bool ExternalKeyingActive() const { return mState.externalKeyingActive; }
	const std::string& StatusMessage() const { return mState.statusMessage; }
	void SetStatusMessage(const std::string& message) { mState.statusMessage = message; }
	const VideoIOState& State() const override { return mState; }
	VideoIOState& MutableState() override { return mState; }
	double FrameBudgetMilliseconds() const;
	VideoPlayoutRecoveryDecision AccountForCompletionResult(VideoIOCompletionResult completionResult, uint64_t readyQueueDepth) override;
	bool BeginOutputFrame(VideoIOOutputFrame& frame) override;
	void EndOutputFrame(VideoIOOutputFrame& frame) override;
	bool ScheduleOutputFrame(const VideoIOOutputFrame& frame) override;
	void HandleVideoInputFrame(IDeckLinkVideoInputFrame* inputFrame, bool hasNoInputSource);
	void HandlePlayoutFrameCompleted(IDeckLinkVideoFrame* completedFrame, BMDOutputFrameCompletionResult completionResult);

private:
	bool AcquireNextOutputVideoFrame(CComPtr<IDeckLinkMutableVideoFrame>& outputVideoFrame);
	bool PopulateOutputFrame(IDeckLinkMutableVideoFrame* outputVideoFrame, VideoIOOutputFrame& frame);
	bool ScheduleFrame(IDeckLinkMutableVideoFrame* outputVideoFrame);
	void UpdateScheduleLeadTelemetry();
	void MaybeRealignScheduleCursorForLowLead();
	void RealignScheduleCursorToPlayback();
	bool ScheduleSystemMemoryFrame(const VideoIOOutputFrame& frame);
	bool ScheduleBlackFrame(IDeckLinkMutableVideoFrame* outputVideoFrame);
	void RefreshBufferedVideoFrameCount();
	static VideoIOCompletionResult TranslateCompletionResult(BMDOutputFrameCompletionResult completionResult);

	CComPtr<CaptureDelegate>				captureDelegate;
	CComPtr<PlayoutDelegate>				playoutDelegate;
	CComPtr<IDeckLinkInput>					input;
	CComPtr<IDeckLinkOutput>				output;
	CComPtr<IDeckLinkKeyer>					keyer;
	std::deque<CComPtr<IDeckLinkMutableVideoFrame>> outputVideoFrameQueue;
	std::mutex								mScheduledSystemFrameMutex;
	std::unordered_map<IDeckLinkVideoFrame*, void*> mScheduledSystemFrameBuffers;
	VideoIOState							mState;
	VideoPlayoutPolicy						mPlayoutPolicy;
	VideoPlayoutScheduler					mScheduler;
	bool									mScheduleRealignmentPending = false;
	bool									mScheduleRealignmentArmed = true;
	bool									mProactiveScheduleRealignmentArmed = true;
	InputFrameCallback						mInputFrameCallback;
	OutputFrameCallback						mOutputFrameCallback;
};
