/* location-corelocation.m -- CoreLocation (OSX) location provider source
   This file is part of redshift-ng.

   redshift-ng 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 3 of the License, or
   (at your option) any later version.

   redshift-ng 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 redshift-ng.  If not, see <http://www.gnu.org/licenses/>.

   Copyright (c) 2014-2017  Jon Lund Steffensen <jonlst@gmail.com>
   Copyright (c) 2025       Mattias Andrée <m@maandree.se>
*/
#include "common.h"

#import <Foundation/Foundation.h>
#import <CoreLocation/CoreLocation.h>


struct location_state {
	NSThread *thread;
	NSLock *lock;
	int pipe_fd_read;
	int pipe_fd_write;
	int available;
	int error;
	struct location location;
};


@interface LocationDelegate : NSObject <CLLocationManagerDelegate>
	@property (strong, nonatomic) CLLocationManager *locationManager;
	@property (nonatomic) struct location_state *state;
@end

@implementation LocationDelegate;

- (void)start
{
	CLAuthorizationStatus authStatus;

	self.locationManager = [[CLLocationManager alloc] init];
	self.locationManager.delegate = self;
	self.locationManager.distanceFilter = 50000;
	self.locationManager.desiredAccuracy = kCLLocationAccuracyKilometer;

	authStatus = [CLLocationManager authorizationStatus];

	if (authStatus != kCLAuthorizationStatusNotDetermined &&
	    authStatus != kCLAuthorizationStatusAuthorized) {
		weprintf(_("Not authorized to obtain location from CoreLocation."));
		[self markError];
	} else {
		[self.locationManager startUpdatingLocation];
	}
}

- (void)markError
{
	[self.state->lock lock];
	self.state->error = 1;
	[self.state->lock unlock];

	pipeutils_signal(self.state->pipe_fd_write);
}

- (void)markUnavailable
{
	[self.state->lock lock];
	self.state->available = 0;
	[self.state->lock unlock];

	pipeutils_signal(self.state->pipe_fd_write);
}

- (void)locationManager:(CLLocationManager *)manager
        didUpdateLocations:(NSArray *)locations
{
	CLLocation *newLocation = [locations firstObject];

	[self.state->lock lock];

	self.state->location.lat = newLocation.coordinate.latitude;
	self.state->location.lon = newLocation.coordinate.longitude;
	self.state->available = 1;

	[self.state->lock unlock];

	pipeutils_signal(self.state->pipe_fd_write);
}

- (void)locationManager:(CLLocationManager *)manager
        didFailWithError:(NSError *)error
{
	weprintf(_("Error obtaining location from CoreLocation: %s"), [[error localizedDescription] UTF8String]);
	if ([error code] == kCLErrorDenied)
		[self markError];
	else
		[self markUnavailable];
}

- (void)locationManager:(CLLocationManager *)manager
        didChangeAuthorizationStatus:(CLAuthorizationStatus)status
{
	if (status == kCLAuthorizationStatusNotDetermined) {
		weprintf(_("Waiting for authorization to obtain location..."));
	} else if (status != kCLAuthorizationStatusAuthorized) {
		weprintf(_("Request for location was not authorized!"));
		[self markError];
	}
}

@end


// Callback when the pipe is closed.
//
// Stops the run loop causing the thread to end.
static void
pipe_close_callback(CFFileDescriptorRef fdref, CFOptionFlags callBackTypes, void *info)
{
	CFFileDescriptorInvalidate(fdref);
	CFRelease(fdref);

	CFRunLoopStop(CFRunLoopGetCurrent());
}


@interface LocationThread : NSThread
	@property (nonatomic) struct location_state *state;
@end

@implementation LocationThread;

// Run loop for location provider thread.
- (void)main
{
	@autoreleasepool {
		LocationDelegate *locationDelegate;
		CFFileDescriptorRef fdref;
		CFRunLoopSourceRef source;

		locationDelegate = [[LocationDelegate alloc] init];
		locationDelegate.state = self.state;

		// Start the location delegate on the run loop in this thread.
		[locationDelegate performSelector:@selector(start) withObject:nil afterDelay:0];

		// Create a callback that is triggered when the pipe is closed. This will
		// trigger the main loop to quit and the thread to stop.
		fdref = CFFileDescriptorCreate(kCFAllocatorDefault, self.state->pipe_fd_write,
		                               false, pipe_close_callback, NULL);
		CFFileDescriptorEnableCallBacks(fdref, kCFFileDescriptorReadCallBack);
		source = CFFileDescriptorCreateRunLoopSource(kCFAllocatorDefault, fdref, 0);
		CFRunLoopAddSource(CFRunLoopGetCurrent(), source, kCFRunLoopDefaultMode);

		// Run the loop
		CFRunLoopRun();

		close(self.state->pipe_fd_write);
	}
}

@end


static int
location_corelocation_init(struct location_state **state)
{
	*state = emalloc(sizeof(**state));
	return 0;
}

static int
location_corelocation_start(struct location_state *state)
{
	LocationThread *thread;
	int pipefds[2];

	state->pipe_fd_read = -1;
	state->pipe_fd_write = -1;

	state->available = 0;
	state->error = 0;
	state->location.lat = 0;
	state->location.lon = 0;

	if (pipeutils_create_nonblocking(pipefds)) {
		weprintf(_("Failed to start CoreLocation provider!"));
		return -1;
	}

	state->pipe_fd_read = pipefds[0];
	state->pipe_fd_write = pipefds[1];

	pipeutils_signal(state->pipe_fd_write);

	state->lock = [[NSLock alloc] init];

	thread = [[LocationThread alloc] init];
	thread.state = state;
	[thread start];
	state->thread = thread;

	return 0;
}

static void
location_corelocation_free(struct location_state *state)
{
	if (state->pipe_fd_read >= 0)
		close(state->pipe_fd_read);
	free(state);
}

static void
location_corelocation_print_help(FILE *f)
{
	fputs(_("Use the location as discovered by the Corelocation provider.\n"), f);
	fputs("\n", f);
}

static int
location_corelocation_set_option(
    struct location_state *state, const char *key, const char *value)
{
	weprintf(_("Unknown method parameter: `%s'."), key);
	return -1;
}

static int
location_corelocation_get_fd(struct location_state *state)
{
	return state->pipe_fd_read;
}

static int
location_corelocation_handle(struct location_state *state, location_t *location, int *available)
{
	int error;

	pipeutils_handle_signal(state->pipe_fd_read);

	[state->lock lock];
	error = state->error;
	*location->lat = state->location;
	*available = state->available;
	[state->lock unlock];

	return error ? -1 : 0;
}


const location_provider_t corelocation_location_provider = {
	"corelocation",
	&location_corelocation_init,
	&location_corelocation_start,
	&location_corelocation_free,
	&location_corelocation_print_help,
	&location_corelocation_set_option,
	&location_corelocation_get_fd,
	&location_corelocation_handle
};