/*
 * Copyright (c) 2013-2014 Ruwen Hahn <palana@stunned.de>
 *                         Hugh "Jim" Bailey <obs.jim@gmail.com>
 *
 * Permission to use, copy, modify, and distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 */

#include "base.h"
#include "platform.h"
#include "dstr.h"

#include <dlfcn.h>
#include <time.h>
#include <unistd.h>

#include <CoreServices/CoreServices.h>
#include <mach/mach.h>
#include <mach/mach_time.h>

#include <IOKit/pwr_mgt/IOPMLib.h>

#import <Cocoa/Cocoa.h>

/* clock function selection taken from libc++ */
static uint64_t ns_time_simple()
{
	return mach_absolute_time();
}

static double ns_time_compute_factor()
{
	mach_timebase_info_data_t info = {1, 1};
	mach_timebase_info(&info);
	return ((double)info.numer) / info.denom;
}

static uint64_t ns_time_full()
{
	static double factor = 0.;
	if (factor == 0.) factor = ns_time_compute_factor();
	return (uint64_t)(mach_absolute_time() * factor);
}

typedef uint64_t (*time_func)();

static time_func ns_time_select_func()
{
	mach_timebase_info_data_t info = {1, 1};
	mach_timebase_info(&info);
	if (info.denom == info.numer)
		return ns_time_simple;
	return ns_time_full;
}

uint64_t os_gettime_ns(void)
{
	static time_func f = NULL;
	if (!f) f = ns_time_select_func();
	return f();
}

/* gets the location ~/Library/Application Support/[name] */
int os_get_config_path(char *dst, size_t size, const char *name)
{
	NSArray *paths = NSSearchPathForDirectoriesInDomains(
			NSApplicationSupportDirectory, NSUserDomainMask, YES);

	if([paths count] == 0)
		bcrash("Could not get home directory (platform-cocoa)");

	NSString *application_support = paths[0];
	const char *base_path = [application_support UTF8String];

	if (!name || !*name)
		return snprintf(dst, size, "%s", base_path);
	else
		return snprintf(dst, size, "%s/%s", base_path, name);
}

char *os_get_config_path_ptr(const char *name)
{
	NSArray *paths = NSSearchPathForDirectoriesInDomains(
			NSApplicationSupportDirectory, NSUserDomainMask, YES);

	if([paths count] == 0)
		bcrash("Could not get home directory (platform-cocoa)");

	NSString *application_support = paths[0];

	NSUInteger len = [application_support
		lengthOfBytesUsingEncoding:NSUTF8StringEncoding];

	char *path_ptr = bmalloc(len+1);

	path_ptr[len] = 0;

	memcpy(path_ptr, [application_support UTF8String], len);

	struct dstr path;
	dstr_init_move_array(&path, path_ptr);
	dstr_cat(&path, "/");
	dstr_cat(&path, name);
	return path.array;
}

struct os_cpu_usage_info {
	int64_t last_cpu_time;
	int64_t last_sys_time;
	int     core_count;
};

static inline void add_time_value(time_value_t *dst, time_value_t *a,
		time_value_t *b)
{
	dst->microseconds = a->microseconds + b->microseconds;
	dst->seconds      = a->seconds      + b->seconds;

	if (dst->microseconds >= 1000000) {
		dst->seconds      += dst->microseconds / 1000000;
		dst->microseconds %= 1000000;
	}
}

static bool get_time_info(int64_t *cpu_time, int64_t *sys_time)
{
	mach_port_t                   task = mach_task_self();
	struct task_thread_times_info thread_data;
	struct task_basic_info_64     task_data;
	mach_msg_type_number_t        count;
	kern_return_t                 kern_ret;
	time_value_t                  cur_time;

	*cpu_time = 0;
	*sys_time = 0;

	count = TASK_THREAD_TIMES_INFO_COUNT;
	kern_ret = task_info(task, TASK_THREAD_TIMES_INFO,
			(task_info_t)&thread_data, &count);
	if (kern_ret != KERN_SUCCESS)
		return false;

	count = TASK_BASIC_INFO_64_COUNT;
	kern_ret = task_info(task, TASK_BASIC_INFO_64,
			(task_info_t)&task_data, &count);
	if (kern_ret != KERN_SUCCESS)
		return false;

	add_time_value(&cur_time, &thread_data.user_time,
			&thread_data.system_time);
	add_time_value(&cur_time, &cur_time, &task_data.user_time);
	add_time_value(&cur_time, &cur_time, &task_data.system_time);

	*cpu_time = os_gettime_ns() / 1000;
	*sys_time = cur_time.seconds * 1000000 + cur_time.microseconds;
	return true;
}

os_cpu_usage_info_t *os_cpu_usage_info_start(void)
{
	struct os_cpu_usage_info *info = bmalloc(sizeof(*info));

	if (!get_time_info(&info->last_cpu_time, &info->last_sys_time)) {
		bfree(info);
		return NULL;
	}

	info->core_count = sysconf(_SC_NPROCESSORS_ONLN);
	return info;
}

double os_cpu_usage_info_query(os_cpu_usage_info_t *info)
{
	int64_t sys_time,       cpu_time;
	int64_t sys_time_delta, cpu_time_delta;

	if (!info || !get_time_info(&cpu_time, &sys_time))
		return 0.0;

	sys_time_delta = sys_time - info->last_sys_time;
	cpu_time_delta = cpu_time - info->last_cpu_time;

	if (cpu_time_delta == 0)
		return 0.0;

	info->last_sys_time = sys_time;
	info->last_cpu_time = cpu_time;

	return (double)sys_time_delta * 100.0 / (double)cpu_time_delta /
		(double)info->core_count;
}

void os_cpu_usage_info_destroy(os_cpu_usage_info_t *info)
{
	if (info)
		bfree(info);
}

os_performance_token_t *os_request_high_performance(const char *reason)
{
	@autoreleasepool {
		NSProcessInfo *pi = [NSProcessInfo processInfo];
		SEL sel = @selector(beginActivityWithOptions:reason:);
		if (![pi respondsToSelector:sel])
			return nil;

		//taken from http://stackoverflow.com/a/20100906
		id activity = [pi beginActivityWithOptions:0x00FFFFFF 
						    reason:@(reason)];

		return CFBridgingRetain(activity);
	}
}

void os_end_high_performance(os_performance_token_t *token)
{
	@autoreleasepool {
		NSProcessInfo *pi = [NSProcessInfo processInfo];
		SEL sel = @selector(beginActivityWithOptions:reason:);
		if (![pi respondsToSelector:sel])
			return;

		[pi endActivity:CFBridgingRelease(token)];
	}
}

struct os_inhibit_info {
	CFStringRef reason;
	IOPMAssertionID sleep_id;
	IOPMAssertionID user_id;
	bool active;
};

os_inhibit_t *os_inhibit_sleep_create(const char *reason)
{
	struct os_inhibit_info *info = bzalloc(sizeof(*info));
	if (!reason)
		info->reason = CFStringCreateWithCString(kCFAllocatorDefault,
				reason, kCFStringEncodingUTF8);
	else
		info->reason = CFStringCreateCopy(kCFAllocatorDefault,
				CFSTR(""));

	return info;
}

bool os_inhibit_sleep_set_active(os_inhibit_t *info, bool active)
{
	IOReturn success;

	if (!info)
		return false;
	if (info->active == active)
		return false;

	if (active) {
		IOPMAssertionDeclareUserActivity(info->reason,
				kIOPMUserActiveLocal, &info->user_id);
		success = IOPMAssertionCreateWithName(
				kIOPMAssertionTypeNoDisplaySleep,
				kIOPMAssertionLevelOn, info->reason,
				&info->sleep_id);

		if (success != kIOReturnSuccess) {
			blog(LOG_WARNING, "Failed to disable sleep");
			return false;
		}
	} else {
		IOPMAssertionRelease(info->sleep_id);
	}

	info->active = active;
	return true;
}

void os_inhibit_sleep_destroy(os_inhibit_t *info)
{
	if (info) {
		os_inhibit_sleep_set_active(info, false);
		CFRelease(info->reason);
		bfree(info);
	}
}