//
// © 2017-2020 Qualcomm Technologies, Inc. and/or its subsidiaries. All rights reserved.
//

import UIKit
import CocoaLumberjack

protocol SpinnerPresenterProtocol {
    func presentWaitSpinner()
    func dismissWaitSpinner()
    func start()
    func searchForSpeakers(failureIsError: Bool)
    func transitionToRunning()
}

// MARK: -

func appSpinnerPresenter() -> SpinnerPresenterProtocol? {
    guard let delegate = UIApplication.shared.delegate as? AppDelegate else {
        return nil
    }
    
    return delegate.spinnerManager
}

// Encapsulates state model for app start up

// Launch --> Wait For Terms --> Wait For WiFi --> Wait For Speakers --> running
//                                     ^                  |                 |
//                                     |                  |                 |
//                                     --------------------------------------
//
// The loss of WiFi is monitored by the Spinner Manager and the presentation of a spinner
// is automatic in that case - the wait for WiFi state may be entered at any time after terms have been accepted.
// Should Wifi be regained the spinner will change to a "searching for speakers" spinner. This may time out
// and the set up speakers dialog will be shown (state will be deemed to be running" in that case.
//

// There is an additional, non-automatic, on demand spinner for general "Please Wait" states. When this is shown the app is
// "running"

// MARK: -

enum AppState {
    case waitingTermsAcceptance
    case waitingForWiFi
    case waitingForSpeakersFailureIsHelp
    case waitingForSpeakersFailureIsError
    case speakersNotPresent
    case running
}

// MARK: -

class SpinnerManager: NSObject {
	// MARK: Private Properties
    private var spinner: SpinnerViewController? = nil
    private var wiFiObserver: ObserverToken? = nil
    private var PlayerGroupObserver: ObserverToken? = nil
    private let rootViewController: UIViewController
    private let playerController: PlayerControllerProtocol
    private let preferences: Preferences
    private var state: AppState = .waitingTermsAcceptance {
        didSet {
            // To hide the background during the initial launch searches we hide it here.
            // Simply showing a please wait does not trigger this as the app is still "running"
            rootViewController.view.alpha = (state == .running) ? 1.0 : 0.0
        }
    }
    // Weak because NSTimer causes retain cycles.
    private weak var speakerTimeoutTimer: Timer?
	private let speakerTimeoutDuration: TimeInterval = 10.0
	
	// MARK: Initializer
    required init(rootViewController viewController: UIViewController,
                  playerController apController: PlayerControllerProtocol,
                  preferences prefs: Preferences) {
        rootViewController = viewController
        playerController = apController
        preferences = prefs
    }
}

// MARK: - SpinnerPresenterProtocol
extension SpinnerManager: SpinnerPresenterProtocol {
    func start() {
        // We may be started
        if !preferences.hasUserAcceptedTerms() {
            // Create notifier so we know when the preferences have changed
            preferences.addObserver(self, using: { [weak self]  (prefs) in
                guard let self = self else {
                    return
                }
                if prefs.hasUserAcceptedTerms() {
                    prefs.removeObserver(self)
                    self.startMonitoringWiFi()
                }
            })
        } else {
            startMonitoringWiFi()
        }
    }
    
    func presentWaitSpinner() {
        if spinner != nil {
            return
        }
        
        presentSpinner(reason: .unspecified)
    }
    
    func dismissWaitSpinner() {
        dismissSpinner(reason: .unspecified)
    }
    
    // Public function to restart speaker search. Only proceeds if we have Wifi as speaker search automatic if we dont'
    func searchForSpeakers(failureIsError: Bool) {
        if state == .waitingForSpeakersFailureIsHelp && !failureIsError  {
            return
        }
        if state == .waitingForSpeakersFailureIsError && failureIsError  {
            return
        }
        waitForSpeakers(failureIsError: failureIsError)
    }
	
	func transitionToRunning() {
		dump()
		removePlayerGroupObserver()
		state = .running
		
		guard let sp = spinner else {
			return
		}
		dismissSpinner(reason: sp.reason)
	}
}

// MARK: - Private Methods
extension SpinnerManager {
    private func handleWiFiStateChange() {
		DDLogVerbose("handleWiFiStateChange: \(WiFiManager.sharedInstance.wifiAvailable)")
        if WiFiManager.sharedInstance.wifiAvailable {
            if state == .waitingForWiFi {
                waitForSpeakers(failureIsError: false)
            }
        } else {
            stopSpeakerTimeout()
            state = .waitingForWiFi
            presentSpinner(reason: .wiFi)
        }
    }
    
    private func startMonitoringWiFi() {
        state = .waitingForWiFi
        if wiFiObserver == nil {
            let wiFiHandler: (Notification) -> () = { [weak self] notification in
				DDLogVerbose("Handler")
                guard let self = self else { return }
                self.handleWiFiStateChange()
            }
			
			DDLogVerbose("Setting up observer")
            let wiFiToken = NotificationCenter.default.addObserver(forName: .wiFiStateDidChange,
                                                                   object: WiFiManager.sharedInstance,
                                                                   queue: OperationQueue.main,
                                                                   using: wiFiHandler)
            wiFiObserver = ObserverToken(notificationCenter: NotificationCenter.default, token: wiFiToken)
        }
        
        if WiFiManager.sharedInstance.wifiAvailable {
            waitForSpeakers(failureIsError: false)
        } else {
            presentSpinner(reason: .wiFi)
        }
    }
    
    private func waitForSpeakers(failureIsError: Bool) {
        if preferences.hasPrevioulyFoundConfiguredSpeaker() {
            transitionToRunning()
            return
        }
        
        state = failureIsError ? .waitingForSpeakersFailureIsError : .waitingForSpeakersFailureIsHelp
        if playerController.playerGroups.count == 0 {
            presentSpinner(reason: .speakers)
            startSpeakerTimeout()
            PlayerGroupObserver = NotificationCenter.default.addObserver(forType: PlayerGroupNotification.self,
                                                                  object: playerController,
                                                                  queue: OperationQueue.main,
                                                                  using: { [weak self] (note) in
                                                                    guard let self = self else { return }
                                                                    self.speakersChanged()
            })
        } else {
            transitionToRunning()
        }
    }
    
    private func removePlayerGroupObserver() {
        if PlayerGroupObserver != nil {
            NotificationCenter.default.removeObserver(PlayerGroupObserver!)
            PlayerGroupObserver = nil
        }
    }

    private func speakersChanged() {
        dump()
        if (state == .waitingForSpeakersFailureIsError || state == .waitingForSpeakersFailureIsHelp) &&
            playerController.playerGroups.count != 0 {
            // We are waiting for speakers and we have some now
            if speakerTimeoutTimer != nil {
                stopSpeakerTimeout()
            }
            transitionToRunning()
        }
    }
    
    private func startSpeakerTimeout () {
        if speakerTimeoutTimer != nil {
            stopSpeakerTimeout()
        }
        
        
        let timer  = Timer(timeInterval: speakerTimeoutDuration,
                           target: self,
                           selector: #selector(speakerTimerFired(_:)),
                           userInfo: nil,
                           repeats: false)
        RunLoop.current.add(timer, forMode: RunLoop.Mode.common)
        speakerTimeoutTimer = timer
    }
    
    @objc private func speakerTimerFired(_: Timer) {
        speakerTimeoutTimer = nil
        removePlayerGroupObserver()
        if state == .waitingForSpeakersFailureIsError {
            showSpeakerError()
        } else {
            startSpeakerSetup()
        }
    }
    
    private func dump() {
        let players = playerController.players
        let devices = playerController.devices
        let playerGroups = playerController.playerGroups
        
        DDLogVerbose ("PlayerGroups: \(playerGroups)")
        DDLogVerbose ("Players: \(players)")
        DDLogVerbose ("Devices: \(devices)")
    }
    
    private func stopSpeakerTimeout () {
        guard let timer = speakerTimeoutTimer else {
            return
        }
        
        timer.invalidate()
        speakerTimeoutTimer = nil
    }
    
    private func showSpeakerError() {
        let dialog = SpeakerErrorNavigationController.instantiateFromStoryboard(name: "Onboarding")
        showSpeakerDialog(viewController: dialog)
    }
    
    private func startSpeakerSetup() {
        let dialog = WiFiAdviceNavigationController.instantiateFromStoryboard(name: "Onboarding")
        showSpeakerDialog(viewController: dialog)
    }
    
    private func showSpeakerDialog(viewController: UIViewController) {
        state = .speakersNotPresent

        spinner?.presentingViewController?.dismiss(animated: true, completion: {
            self.spinner = nil
            viewController.modalTransitionStyle = .crossDissolve
            viewController.modalPresentationStyle = .overFullScreen
            let presenter = self.topPresentedViewController()
			presenter.present(viewController, animated: true)
        })
    }
    
    private func presentSpinner(reason: SpinnerReason) {
        if let spinner = spinner,
            spinner.presentingViewController != nil {
            spinner.reason = reason
            return
        }
        
        let sp = SpinnerViewController.instantiateFromStoryboard()
        sp.reason = reason
        sp.modalTransitionStyle = .crossDissolve
        sp.modalPresentationStyle = .overFullScreen
        let presenter = topPresentedViewController()
		presenter.present(sp, animated: true)
        spinner = sp
    }
    
    private func dismissSpinner(reason: SpinnerReason) {
        guard let sp = spinner else {
            return
        }
        
        guard sp.reason == reason else {
            return
        }
        
        sp.presentingViewController?.dismiss(animated: true, completion: {
            self.spinner = nil
        })
    }
    
    private func topPresentedViewController () -> UIViewController {
        if let presented = rootViewController.presentedViewController {
            return topPresentedViewController(fromController:presented)
        }
        
        guard let splitViewController = rootViewController as? SplitViewController else  {
            return rootViewController
        }
        
        if let master = splitViewController.viewControllers.first {
            let masterTop = topPresentedViewController(fromController: master)
            if masterTop != master {
                return master
            }
        }
        
        if splitViewController.viewControllers.count == 2 {
            if let detail = splitViewController.viewControllers.last {
                let detailTop = topPresentedViewController(fromController: detail)
                if detailTop != detail {
                    return detail
                }
            }
        }
        
        return splitViewController
    }
    
    private func topPresentedViewController(fromController controller: UIViewController) -> UIViewController{
        if let presented = controller.presentedViewController {
            return topPresentedViewController(fromController:presented)
        }
        
        return controller
    }
}
