//
//  SafariExtensionHandler.swift
//  Subscribe to Feed
//
//  Created by Daniel Jalkut on 6/11/18.
//  Copyright © 2018 Ranchero Software. All rights reserved.
//

import SafariServices

class SafariExtensionHandler: SFSafariExtensionHandler {

	// Safari App Extensions don't support any reasonable means of detecting whether a
	// specific Safari page was loaded with the benefit of the extension's injected
	// JavaScript. For this reason a condition can easily be reached where the toolbar
	// icon is active for a page, but the expected supporting code is not loaded into
	// the page. To detect this and disable our icon, we use a kind of "ping" trick
	// to verify whether our code is installed.

	// I tried to use a NSMapTable from String to the closure directly, but Swift
	// complains that the object has to be a class type.
	typealias ValidationHandler = (Bool, String) -> Void
	class ValidationWrapper {
		let validationHandler: ValidationHandler

		init(validationHandler: @escaping ValidationHandler) {
			self.validationHandler = validationHandler
		}
	}

	// Maps from UUID to a validation wrapper
	static var gPingPongMap = Dictionary<String, ValidationWrapper>()
	static var validationQueue = DispatchQueue(label: "Toolbar Validation")

	// Bottleneck for calling through to a validation handler we have saved, and removing it from the list.
	static func callValidationHandler(forHandlerID handlerID: String, withShouldValidate shouldValidate: Bool) {
		if let validationWrapper = gPingPongMap[handlerID] {
			validationWrapper.validationHandler(shouldValidate, "")
			gPingPongMap.removeValue(forKey: handlerID)
		}
	}

	override func messageReceived(withName messageName: String, from page: SFSafariPage, userInfo: [String : Any]?) {
		if (messageName == "subscribeToFeed") {
			if var feedURLString = userInfo?["url"] as? String {
				var openInDefaultBrowser = false

				// Ask for the user's choice for whether to open the feed URL using whatever the system
				// configured default is, or to always hard-code it to have NetNewsWire handle it itself.
				if let appGroupID = Bundle.main.object(forInfoDictionaryKey: "AppGroup") as? String {
					if let groupDefaults = UserDefaults(suiteName: appGroupID) {
						openInDefaultBrowser = groupDefaults.bool(forKey: "subscribeToFeedsInDefaultBrowser")
					}
				}

				if openInDefaultBrowser == false {
						feedURLString = feedURLString.replacingOccurrences(of: "feed:", with: "x-netnewswire-feed:")
				}

				if let feedURL = URL(string: feedURLString) {
					NSWorkspace.shared.open(feedURL)
				}
			}
		}
		else if (messageName == "pong") {
			if let validationIDString = userInfo?["validationID"] as? String {
				// Should we validate the button?
				let shouldValidate = userInfo?["shouldValidate"] as? Bool ?? false
				SafariExtensionHandler.callValidationHandler(forHandlerID: validationIDString, withShouldValidate:shouldValidate)
			}
		}
    }
    
    override func toolbarItemClicked(in window: SFSafariWindow) {
		window.getActiveTab { (activeTab) in
			activeTab?.getActivePage(completionHandler: { (activePage) in
				activePage?.dispatchMessageToScript(withName: "toolbarButtonClicked", userInfo: nil)
			})
		}
    }

    override func validateToolbarItem(in window: SFSafariWindow, validationHandler: @escaping ((Bool, String) -> Void)) {

		let uniqueValidationID = NSUUID().uuidString

		SafariExtensionHandler.validationQueue.sync {

			// Save it right away to eliminate any doubt of whether the handler gets deallocated while
			// we are waiting for a callback from the getActiveTab or getActivatePage methods below.
			let validationWrapper = ValidationWrapper(validationHandler: validationHandler)
			SafariExtensionHandler.gPingPongMap[uniqueValidationID] = validationWrapper

			// To avoid problems with validation handlers dispatched after we've, for example,
			// switched to a new tab, we aggressively clear out the map of any pending validations,
			// and focus only on the newest validation request we've been asked for.
			for thisValidationID in SafariExtensionHandler.gPingPongMap.keys {
				if thisValidationID != uniqueValidationID {
					// Default to valid ... we'll know soon enough whether the latest state
					// is actually still valid or not...
					SafariExtensionHandler.callValidationHandler(forHandlerID: thisValidationID, withShouldValidate: true);

				}
			}

			// See comments above where gPingPongMap is declared. Upon being asked to validate the
			// toolbar icon for a specific page, we save the validationHandler and postpone calling
			// it until we have either received a response from our installed JavaScript, or until
			// a timeout period has elapsed
			window.getActiveTab { (activeTab) in
				guard let activeTab = activeTab else {
					SafariExtensionHandler.callValidationHandler(forHandlerID: uniqueValidationID, withShouldValidate:false);
					return
				}

				activeTab.getActivePage { (activePage) in
					guard let activePage = activePage else {
						SafariExtensionHandler.callValidationHandler(forHandlerID: uniqueValidationID, withShouldValidate:false);
						return						
					}

					activePage.getPropertiesWithCompletionHandler { (pageProperties) in
						if let isActive = pageProperties?.isActive {
							if isActive {
								// Capture the uniqueValidationID to ensure it doesn't change out from under us on a future call
								activePage.dispatchMessageToScript(withName: "ping", userInfo: ["validationID": uniqueValidationID])

								let pongTimeoutInNanoseconds = Int(Double(NSEC_PER_SEC) * 0.5)
								let timeoutDeadline = DispatchTime.now() + DispatchTimeInterval.nanoseconds(pongTimeoutInNanoseconds)
								DispatchQueue.main.asyncAfter(deadline: timeoutDeadline, execute: { [timedOutValidationID = uniqueValidationID] in
									SafariExtensionHandler.callValidationHandler(forHandlerID: timedOutValidationID, withShouldValidate:false)
								})
							}
						}
					}
				}
			}
		}
    }
}
