/*-
 * redshift-ng - Automatically adjust display colour temperature according the Sun
 * 
 * Copyright (c) 2009-2018        Jon Lund Steffensen <jonlst@gmail.com>
 * Copyright (c) 2014-2016, 2025  Mattias Andrée <m@maandree.se>
 * 
 * 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/>.
 */
#include "common.h"

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


/**
 * Location data
 */
struct location_data {
	/**
	 * The user's geographical location
	 */
	struct location location;

	/**
	 * Whether the location provider is available
	 */
	int available;

	/**
	 * Whether an unrecoverable error has occurred
	 */
	int error;
};


struct location_state {
	/**
	 * Slave thread, used to receive location updates
	 */
	NSThread *thread;

	/**
	 * Read-end of piped used to send location data
	 * from the slave thread to the master thread
	 */
	int pipe_fd_read;

	/**
	 * Write-end of piped used to send location data
	 * from the slave thread to the master thread
	 */
	int pipe_fd_write;

	/**
	 * Location data available from the slave thread
	 */
	struct location_data data;

	/**
	 * Location data sent to the master thread
	 */
	struct location_data saved_data;
};


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


static void
send_data(struct location_state *state)
{
	while (write(state->pipe_fd_write, &state->data, sizeof(state->data)) == -1 && errno == EINTR);
}


@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->data.error = 1;
	send_data(self.state);
}

- (void)markUnavailable
{
	self.state->data.available = 0;
	send_data(self.state);
}

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

	self.state->data.location.lat = newLocation.coordinate.latitude;
	self.state->data.location.lon = newLocation.coordinate.longitude;
	self.state->data.available = 1;
	send_data(self.state);
}

- (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
corelocation_create(struct location_state **state_out)
{
	*state_out = emalloc(sizeof(**state_out));
	return 0;
}


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

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

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

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

	send_data(state); /* TODO why? */

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

	return 0;
}


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


static void
corelocation_print_help(void)
{
	printf(_("Use the location as discovered by the Corelocation provider.\n"));
	printf("\n");
}


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


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


static int
corelocation_fetch(struct location_state *state, struct location *location_out, int *available_out)
{
	struct location_data data;
	ssize_t r;

	for (;;) {
		r = read(state->pipe_fd_read, &data, sizeof(data));
		if (r == (ssize_t)sizeof(data)) {
			state->saved_data = data;
		} else if (r > 0) {
			/* writes of 512 bytes or less are always atomic on pipes */
			weprintf("read <pipe>: %s", _("Unexpected message size"));
		} else if (!r || errno == EAGAIN) {
			break;
		} else if (errno != EINTR) {
			weprintf("read <pipe>:");
			state->saved_data.error = 1;
			break;
		}
	}

	*location_out = state->saved_data.location;
	*available_out = state->saved_data.available;
	return state->saved_data.error ? -1 : 0;
}


const location_provider_t corelocation_location_provider = LOCATION_PROVIDER_INIT("corelocation", corelocation);