//
//  PlugIn.mm
//  obs-mac-virtualcam
//
//  Created by John Boiles  on 4/9/20.
//
//  obs-mac-virtualcam is free software: you can redistribute it and/or modify
//  it under the terms of the GNU General Public License as published by
//  the Free Software Foundation, either version 2 of the License, or
//  (at your option) any later version.
//
//  obs-mac-virtualcam is distributed in the hope that it will be useful,
//  but WITHOUT ANY WARRANTY; without even the implied warranty of
//  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
//  GNU General Public License for more details.
//
//  You should have received a copy of the GNU General Public License
//  along with obs-mac-virtualcam. If not, see <http://www.gnu.org/licenses/>.

#import "OBSDALPlugin.h"

#import <CoreMediaIO/CMIOHardwarePlugin.h>

#import "Logging.h"

typedef enum {
	PlugInStateNotStarted = 0,
	PlugInStateWaitingForServer,
	PlugInStateReceivingFrames,
} OBSDALPlugInState;

@interface OBSDALPlugin () <MachClientDelegate> {
	//! Serial queue for all state changes that need to be concerned with thread safety
	dispatch_queue_t _stateQueue;

	//! Repeated timer for driving the mach server re-connection
	dispatch_source_t _machConnectTimer;

	//! Timeout timer when we haven't received frames for 5s
	dispatch_source_t _timeoutTimer;
}
@property OBSDALPlugInState state;
@property OBSDALMachClient *machClient;

@end

@implementation OBSDALPlugin

+ (OBSDALPlugin *)SharedPlugIn
{
	static OBSDALPlugin *sPlugIn = nil;
	static dispatch_once_t sOnceToken;
	dispatch_once(&sOnceToken, ^{
		sPlugIn = [[self alloc] init];
	});
	return sPlugIn;
}

- (instancetype)init
{
	if (self = [super init]) {
		_stateQueue = dispatch_queue_create(
			"com.obsproject.obs-mac-virtualcam.dal.state",
			DISPATCH_QUEUE_SERIAL);

		_timeoutTimer = dispatch_source_create(
			DISPATCH_SOURCE_TYPE_TIMER, 0, 0, _stateQueue);
		__weak __typeof(self) weakSelf = self;
		dispatch_source_set_event_handler(_timeoutTimer, ^{
			if (weakSelf.state == PlugInStateReceivingFrames) {
				DLog(@"No frames received for 5s, restarting connection");
				[self stopStream];
				[self startStream];
			}
		});

		_machClient = [[OBSDALMachClient alloc] init];
		_machClient.delegate = self;

		_machConnectTimer = dispatch_source_create(
			DISPATCH_SOURCE_TYPE_TIMER, 0, 0, _stateQueue);
		dispatch_time_t startTime = dispatch_time(DISPATCH_TIME_NOW, 0);
		uint64_t intervalTime = (int64_t)(1 * NSEC_PER_SEC);
		dispatch_source_set_timer(_machConnectTimer, startTime,
					  intervalTime, 0);
		dispatch_source_set_event_handler(_machConnectTimer, ^{
			if (![[weakSelf machClient] isServerAvailable]) {
				DLog(@"Server is not available");
			} else if (weakSelf.state ==
				   PlugInStateWaitingForServer) {
				DLog(@"Attempting connection");
				[[weakSelf machClient] connectToServer];
			}
		});
	}
	return self;
}

- (void)startStream
{
	DLogFunc(@"");
	dispatch_async(_stateQueue, ^{
		if (_state == PlugInStateNotStarted) {
			dispatch_resume(_machConnectTimer);
			[self.stream startServingDefaultFrames];
			_state = PlugInStateWaitingForServer;
		}
	});
}

- (void)stopStream
{
	DLogFunc(@"");
	dispatch_async(_stateQueue, ^{
		if (_state == PlugInStateWaitingForServer) {
			dispatch_suspend(_machConnectTimer);
			[self.stream stopServingDefaultFrames];
		} else if (_state == PlugInStateReceivingFrames) {
			// TODO: Disconnect from the mach server?
			dispatch_suspend(_timeoutTimer);
		}
		_state = PlugInStateNotStarted;
	});
}

- (void)initialize
{
}

- (void)teardown
{
}

#pragma mark - CMIOObject

- (BOOL)hasPropertyWithAddress:(CMIOObjectPropertyAddress)address
{
	switch (address.mSelector) {
	case kCMIOObjectPropertyName:
		return true;
	default:
		DLog(@"PlugIn unhandled hasPropertyWithAddress for %@",
		     [OBSDALObjectStore
			     StringFromPropertySelector:address.mSelector]);
		return false;
	};
}

- (BOOL)isPropertySettableWithAddress:(CMIOObjectPropertyAddress)address
{
	switch (address.mSelector) {
	case kCMIOObjectPropertyName:
		return false;
	default:
		DLog(@"PlugIn unhandled isPropertySettableWithAddress for %@",
		     [OBSDALObjectStore
			     StringFromPropertySelector:address.mSelector]);
		return false;
	};
}

- (UInt32)getPropertyDataSizeWithAddress:(CMIOObjectPropertyAddress)address
		       qualifierDataSize:(UInt32)qualifierDataSize
			   qualifierData:(const void *)qualifierData
{
	switch (address.mSelector) {
	case kCMIOObjectPropertyName:
		return sizeof(CFStringRef);
	default:
		DLog(@"PlugIn unhandled getPropertyDataSizeWithAddress for %@",
		     [OBSDALObjectStore
			     StringFromPropertySelector:address.mSelector]);
		return 0;
	};
}

- (void)getPropertyDataWithAddress:(CMIOObjectPropertyAddress)address
		 qualifierDataSize:(UInt32)qualifierDataSize
		     qualifierData:(nonnull const void *)qualifierData
			  dataSize:(UInt32)dataSize
			  dataUsed:(nonnull UInt32 *)dataUsed
			      data:(nonnull void *)data
{
	switch (address.mSelector) {
	case kCMIOObjectPropertyName:
		*static_cast<CFStringRef *>(data) =
			CFSTR("OBS Virtual Camera Plugin");
		*dataUsed = sizeof(CFStringRef);
		return;
	default:
		DLog(@"PlugIn unhandled getPropertyDataWithAddress for %@",
		     [OBSDALObjectStore
			     StringFromPropertySelector:address.mSelector]);
		return;
	};
}

- (void)setPropertyDataWithAddress:(CMIOObjectPropertyAddress)address
		 qualifierDataSize:(UInt32)qualifierDataSize
		     qualifierData:(nonnull const void *)qualifierData
			  dataSize:(UInt32)dataSize
			      data:(nonnull const void *)data
{
	DLog(@"PlugIn unhandled setPropertyDataWithAddress for %@",
	     [OBSDALObjectStore StringFromPropertySelector:address.mSelector]);
}

#pragma mark - MachClientDelegate

- (void)receivedFrameWithSize:(NSSize)size
		    timestamp:(uint64_t)timestamp
		 fpsNumerator:(uint32_t)fpsNumerator
	       fpsDenominator:(uint32_t)fpsDenominator
		    frameData:(NSData *)frameData
{
	dispatch_sync(_stateQueue, ^{
		if (_state == PlugInStateWaitingForServer) {
			NSUserDefaults *defaults =
				[NSUserDefaults standardUserDefaults];
			[defaults setInteger:size.width
				      forKey:kTestCardWidthKey];
			[defaults setInteger:size.height
				      forKey:kTestCardHeightKey];
			[defaults setDouble:(double)fpsNumerator /
					    (double)fpsDenominator
				     forKey:kTestCardFPSKey];

			dispatch_suspend(_machConnectTimer);
			[self.stream stopServingDefaultFrames];
			dispatch_resume(_timeoutTimer);
			_state = PlugInStateReceivingFrames;
		}
	});

	// Add 5 more seconds onto the timeout timer
	dispatch_source_set_timer(
		_timeoutTimer,
		dispatch_time(DISPATCH_TIME_NOW, 5.0 * NSEC_PER_SEC),
		5.0 * NSEC_PER_SEC, (1ull * NSEC_PER_SEC) / 10);

	[self.stream queueFrameWithSize:size
			      timestamp:timestamp
			   fpsNumerator:fpsNumerator
			 fpsDenominator:fpsDenominator
			      frameData:frameData];
}

- (void)receivedStop
{
	DLogFunc(@"Restarting connection");
	[self stopStream];
	[self startStream];
}

@end
