import eventManager from '../eventManager';
import { Notification } from '../models/Alarm';
import shim from '../shim';
import Setting from '../models/Setting';
const notifier = require('node-notifier');

interface Options {
	appName: string;
}

export default class AlarmServiceDriverNode {

	private appName_: string;
	private notifications_: any = {};
	private service_: any = null;

	constructor(options: Options) {
		// Note: appName is required to get the notification to work. It must be the same as the appId defined in package.json
		// https://github.com/mikaelbr/node-notifier/issues/144#issuecomment-319324058
		this.appName_ = options.appName;
	}

	setService(s: any) {
		this.service_ = s;
	}

	logger() {
		return this.service_.logger();
	}

	hasPersistentNotifications() {
		return false;
	}

	notificationIsSet(id: number) {
		return id in this.notifications_;
	}

	clearNotification(id: number) {
		if (!this.notificationIsSet(id)) return;
		shim.clearTimeout(this.notifications_[id].timeoutId);
		delete this.notifications_[id];
	}

	private displayDefaultNotification(notification: Notification) {
		const o: any = {
			appID: this.appName_,
			title: notification.title,
			icon: `${shim.electronBridge().electronApp().buildDir()}/icons/512x512.png`,
		};
		if ('body' in notification) o.message = notification.body;

		// Message is required on Windows 7 however we don't want to repeat the title so
		// make it an empty string.
		// https://github.com/laurent22/joplin/issues/2144
		if (!o.message) o.message = '-';

		this.logger().info('AlarmServiceDriverNode::scheduleNotification: Triggering notification (default):', o);

		notifier.notify(o, (error: any, response: any) => {
			this.logger().info('AlarmServiceDriverNode::scheduleNotification: node-notifier response:', error, response);
		});
	}

	private displayMacNotification(notification: Notification) {
		// On macOS, node-notifier is broken:
		//
		// https://github.com/mikaelbr/node-notifier/issues/352
		//
		// However we can use the native browser notification as described
		// there:
		//
		// https://www.electronjs.org/docs/tutorial/notifications
		//
		// In fact it's likely that we could use this on other platforms too
		try {
			const options: any = {
				body: notification.body ? notification.body : '-',
				onerror: (error: any) => {
					this.logger().error('AlarmServiceDriverNode::displayMacNotification', error);
				},
			};

			this.logger().info('AlarmServiceDriverNode::displayMacNotification: Triggering notification (macOS):', notification.title, options);

			new Notification(notification.title, options);
		} catch (error) {
			this.logger().error('AlarmServiceDriverNode::displayMacNotification', error);
		}
	}

	private async checkPermission() {
		if (shim.isMac() && shim.isElectron()) {
			this.logger().info(`AlarmServiceDriverNode::checkPermission: Permission in settings is "${Setting.value('notificationPermission')}"`);

			if (Setting.value('notificationPermission') !== '') return Setting.value('notificationPermission');

			// In theory `Notification.requestPermission()` should be used to
			// ask for permission but in practice this API is unreliable. In
			// particular, it returns "granted" immediately even when
			// notifications definitely aren't allowed (and creating a new
			// notification would fail).
			//
			// Because of that, our approach is to trigger a notification, which
			// should prompt macOS to ask for permission. Once this is done we
			// manually save the result in the settings. Of course it means that
			// if permission is changed afterwards, for example from the
			// notification center, we won't know it and notifications will
			// fail.
			//
			// All this means that for now this checkPermission function always
			// returns "granted" and the setting has only two values: "granted"
			// or "" (which means we need to do the check permission trick).
			//
			// The lack of "denied" value is acceptable in our context because
			// if a user doesn't want notifications, they can simply not set
			// alarms.

			new Notification('Checking permissions...', {
				body: 'Permission has been granted',
			});

			Setting.setValue('notificationPermission', 'granted');
		}

		return 'granted';
	}

	async scheduleNotification(notification: Notification) {
		const now = Date.now();
		const interval = notification.date.getTime() - now;
		if (interval < 0) return;

		if (isNaN(interval)) {
			throw new Error(`Trying to create a notification from an invalid object: ${JSON.stringify(notification)}`);
		}

		const permission = await this.checkPermission();
		if (permission !== 'granted') {
			this.logger().info(`AlarmServiceDriverNode::scheduleNotification: Notification ${notification.id}: Cancelled because permission was not granted.`);
			return;
		}

		this.logger().info(`AlarmServiceDriverNode::scheduleNotification: Notification ${notification.id} with interval: ${interval}ms`);

		if (this.notifications_[notification.id]) shim.clearTimeout(this.notifications_[notification.id].timeoutId);

		let timeoutId = null;

		// Note: setTimeout will break for values larger than Number.MAX_VALUE - in which case the timer
		// will fire immediately. So instead, if the interval is greater than a set max, reschedule after
		// that max interval.
		// https://stackoverflow.com/questions/3468607/why-does-settimeout-break-for-large-millisecond-delay-values/3468699

		const maxInterval = 60 * 60 * 1000;
		if (interval >= maxInterval) {
			this.logger().info(`AlarmServiceDriverNode::scheduleNotification: Notification interval is greater than ${maxInterval}ms - will reschedule in ${maxInterval}ms`);

			timeoutId = shim.setTimeout(() => {
				if (!this.notifications_[notification.id]) {
					this.logger().info(`AlarmServiceDriverNode::scheduleNotification: Notification ${notification.id} has been deleted - not rescheduling it`);
					return;
				}
				void this.scheduleNotification(this.notifications_[notification.id]);
			}, maxInterval);
		} else {
			timeoutId = shim.setTimeout(() => {
				if (shim.isMac() && shim.isElectron()) {
					this.displayMacNotification(notification);
				} else {
					this.displayDefaultNotification(notification);
				}

				this.clearNotification(notification.id);

				eventManager.emit('noteAlarmTrigger', { noteId: notification.noteId });
			}, interval);
		}

		this.notifications_[notification.id] = Object.assign({}, notification);
		this.notifications_[notification.id].timeoutId = timeoutId;
	}
}