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

import Foundation
import CocoaLumberjack

protocol PlayerControllerProtocol {
    var playerGroups: [IoTPlayerGroup] { get }
    var playingPlayerGroups: [IoTPlayerGroup] { get }
    var availablePlayerGroups: [IoTPlayerGroup] { get }
	func playerGroupForPlayer(_ player: IoTPlayer) -> IoTPlayerGroup?
    
    var players: [IoTPlayer] { get }
	func player(id: String) -> IoTPlayer?
    var isStarted: Bool { get }
    var devices: [IoTDevice] { get }
	func device(id: String) -> IoTDevice?
    func player(device: IoTDevice) -> IoTPlayer?
    func device(player: IoTPlayer) -> IoTDevice?

    func start()
    func stop()
    func restart()

    func create(playerGroupName: String,
                completion: ((_ success: Bool, _ groupID: String? , _ error: Error?)->())?,
                queue: DispatchQueue?)

    func delete(playerGroupID: String,
                completion: ((_ success: Bool, _ error: Error?)->())?,
                queue: DispatchQueue?)

    func editMembers(playerGroupID: String,
                     playerIDs:[String],
                     completion: ((_ success: Bool, _ error: Error?)->())?,
                     queue: DispatchQueue?)

    func rename(playerGroupID: String,
                newName:String,
                completion: ((_ success: Bool, _ error: Error?)->())?,
                queue: DispatchQueue?)

    func supportsBluetooth(playerGroupID: String)->Bool
    func hasBTDevices(playerGroupID: String)->Bool
    func supportsZigbee(playerGroupID: String)->Bool
    func zigbeeCoordinatorExists()->Bool
    func zigbeeCoordinator(playerGroupID: String)->IoTDevice?
    func hasZigbeeDevices(playerGroupID: String)->Bool
}

// MARK: -

extension IoTPlayerGroup {
    func hasCurrentMediaItem() -> Bool {
        if let currentItem = playlist?.currentItem {
        	return !currentItem.isEmpty
        } else {
            return false
        }
    }
}

// MARK: -

// Notification types sent by the controller class.
extension Notification.Name {
	static var PlayerGroupStateDidChangeNotification = Notification.Name("PlayerGroupStateDidChangeNotification")
	static var PlaylistStateDidChangeNotification = Notification.Name("PlaylistStateDidChangeNotification")
	static var PlayerStateDidChangeNotification = Notification.Name("PlayerStateDidChangeNotification")
	static var DeviceStateDidChangeNotification = Notification.Name("PlayDeviceStateDidChangeNotification")
    static var BluetoothStateDidChangeNotification = Notification.Name("BluetoothStateDidChangeNotification")
    static var VoiceUIStateDidChangeNotification = Notification.Name("VoiceUIStateDidChangeNotification")
    static var ZigbeeStateDidChangeNotification = Notification.Name("ZigbeeStateDidChangeNotification")
	static var HomeTheaterStateDidChangeNotification = Notification.Name("HomeTheaterStateDidChangeNotification")
}


// MARK: - PlayerController

class PlayerController: NSObject  {
	// MARK: Private Properties
    private let iotClient: IoTClient
    private let notificationCenter: NotificationCenter
    private let preferences: Preferences

	// MARK: Init/Deinit
    init(notificationCenter: NotificationCenter,
         preferences: Preferences) {
        self.iotClient = IoTClient(applicationName: "Jukebox")
        self.notificationCenter = notificationCenter
        self.preferences = preferences

        super.init()

		startObservingWiFi()
    }
    
    convenience init(preferences: Preferences) {
        self.init(notificationCenter: NotificationCenter.default,
                  preferences: preferences)
    }
	
	deinit {
		stopObservingWiFi()
	}
}

// MARK: - Public Methods
extension PlayerController: PlayerControllerProtocol {
    var playerGroups: [IoTPlayerGroup] {
        return iotClient.getPlayerGroups()
    }
    
    var playingPlayerGroups: [IoTPlayerGroup] {
        let allPlayerGroups = playerGroups
        return allPlayerGroups.filter{ $0.hasCurrentMediaItem() }
    }
    
    var availablePlayerGroups: [IoTPlayerGroup] {
        let allPlayerGroups = playerGroups
        return allPlayerGroups.filter{ !$0.hasCurrentMediaItem() }
    }
	
	func playerGroupForPlayer(_ player: IoTPlayer) -> IoTPlayerGroup? {
		let allPlayerGroups = playerGroups
		for playerGroup in allPlayerGroups {
			if playerGroup.members.contains(where: { $0.playerID == player.playerID }) {
				return playerGroup
			}
		}
		return nil
	}
    
    var players: [IoTPlayer] {
        return iotClient.getPlayers()
    }
	
	func player(id: String) -> IoTPlayer? {
		return iotClient.player(forID: id)
	}
    
    var isStarted: Bool {
        return iotClient.isStarted
    }

    var devices: [IoTDevice] {
        return iotClient.getDevices()
    }
	
	func device(id: String) -> IoTDevice? {
        return iotClient.device(forID: id)
	}

    func player(device: IoTDevice) -> IoTPlayer? {
        return iotClient.player(for: device)
    }

    func device(player: IoTPlayer) -> IoTDevice? {
        return iotClient.device(for: player)
    }

    func start() {
        if !iotClient.isStarted {
            iotClient.delegate = self;
            iotClient.start()
        }
    }
    
    func stop() {
        if iotClient.isStarted {
            iotClient.stop()
        }
    }
    
    func restart() {
        stop()
        start()
    }


    func create(playerGroupName: String,
                completion: ((_ success: Bool, _ groupID: String? , _ error: Error?)->())?,
                queue: DispatchQueue?) {
        iotClient.createPlayerGroup(withName: playerGroupName,
                                    completion: completion,
                                    completionQueue: queue)
    }

    func delete(playerGroupID: String,
                completion: ((_ success: Bool, _ error: Error?)->())?,
                queue: DispatchQueue?) {
        iotClient.deletePlayerGroup(withID: playerGroupID,
                                    completion: completion,
                                    completionQueue: queue)
    }

    func editMembers(playerGroupID: String,
                     playerIDs:[String],
                     completion: ((_ success: Bool, _ error: Error?)->())?,
                     queue: DispatchQueue?) {
        if let group = playerGroups.first(where: { return $0.groupID == playerGroupID }) {
			let membersIDs = Set(group.members.map {
				return $0.playerID
            })
            let requestedMembers = Set(playerIDs)
            let toAdd = requestedMembers.subtracting(membersIDs)
            let toRemove = membersIDs.subtracting(requestedMembers)
			let group = DispatchGroup()
            if toRemove.count > 0 {
                group.enter()
                iotClient.removePlayers(withIDs: toRemove,
                                        fromPlayerGroupWithCompletion: { (_, _) in
                                            group.leave()
                }, completionQueue: DispatchQueue.main)
            }
            if toAdd.count > 0 {
                group.enter()
                iotClient.addPlayers(withIDs: toAdd,
                                     toPlayerGroupWithID: playerGroupID,
                                     completion: { (_, _) in
                                        group.leave()
                }, completionQueue: DispatchQueue.main)
            }
            group.notify(queue: DispatchQueue.main) {
                if let q = queue,
                    let c = completion {
                    q.async {
                        c(true, nil)
                    }
                }
            }
        }
    }

    func rename(playerGroupID: String,
                newName:String,
                completion: ((_ success: Bool, _ error: Error?)->())?,
                queue: DispatchQueue?) {
		iotClient.renamePlayerGroup(withID: playerGroupID,
                                    newName: newName,
                                    completion: completion,
                                    completionQueue: queue)
    }

    func supportsBluetooth(playerGroupID: String)->Bool {
        if let group = playerGroups.first(where: { return $0.groupID == playerGroupID }) {
            let members = group.members
            let devices = members.compactMap { (member) -> IoTDevice? in
                if let player = player(id: member.playerID),
                    let device = device(player: player),
                    device.bluetoothSupported {
                    return device
                }
                return nil
            }
            return devices.count > 0
        }
        return false
    }

    func hasBTDevices(playerGroupID: String)->Bool {
        if let group = playerGroups.first(where: { return $0.groupID == playerGroupID }) {
            let members = group.members
            let devices = members.compactMap { (member) -> IoTDevice? in
                if let player = player(id: member.playerID),
                    let device = device(player: player),
                    device.bluetoothSupported,
                    device.pairedBluetoothDevices.count > 0 {
                    return device
                }
                return nil
            }
            return devices.count > 0
        }
        return false
    }

    func supportsZigbee(playerGroupID: String)->Bool {
        if let group = playerGroups.first(where: { return $0.groupID == playerGroupID }) {
            let members = group.members
            let devices = members.compactMap { (member) -> IoTDevice? in
                if let player = player(id: member.playerID),
                    let device = device(player: player),
                    device.zigbeeSupported {
                    return device
                }
                return nil
            }
            return devices.count > 0
        }
        return false
    }

    func zigbeeCoordinator(playerGroupID: String)->IoTDevice? {
        if let group = playerGroups.first(where: { return $0.groupID == playerGroupID }) {
            let members = group.members
            let devices = members.compactMap { (member) -> IoTDevice? in
                if let player = player(id: member.playerID),
                    let device = device(player: player),
                    device.zigbeeSupported,
                    device.zigbeeCoordinatorState == .nominated {
                    return device
                }
                return nil
            }
            return devices.first
        }
        return nil
    }

    func zigbeeCoordinatorExists()->Bool {
        return playerGroups.contains { (group) -> Bool in
            return zigbeeCoordinator(playerGroupID: group.groupID) != nil
        }
    }

    func hasZigbeeDevices(playerGroupID: String)->Bool {
        if let coordinator = zigbeeCoordinator(playerGroupID: playerGroupID) {
            return coordinator.zigbeeDevices.count > 0
        } else {
            return false
        }
    }
}

// MARK: - IoTAppListenerProtocol Conformity
extension PlayerController: IoTAppListenerProtocol {
	// MARK: PlayerGroup
	func playerGroupAdded(_ playerGroup: IoTPlayerGroup) {
		dispatchPlayerGroupNotification(playerGroup, reason: .added)
	}
	
	func playerGroupRemoved(_ playerGroup: IoTPlayerGroup) {
		dispatchPlayerGroupNotification(playerGroup, reason: .removed)
	}
	
    func playerGroup(_ group: IoTPlayerGroup, didChange state: IoTPlayState) {
		dispatchPlayerGroupNotification(group, reason: .playStateChanged, payload: state)
	}
	
    func playerGroupDidChangePlayerList(_ group: IoTPlayerGroup) {
		dispatchPlayerGroupNotification(group, reason: .playerListChanged)
	}

    func playerGroupDidChangeSourcePlayer(_ group: IoTPlayerGroup) {
        dispatchPlayerGroupNotification(group, reason: .sourcePlayerChanged)
    }

    func playerGroup(_ group: IoTPlayerGroup, didChangeName name: String) {
        dispatchPlayerGroupNotification(group, reason: .nameChanged, payload: name)
    }

    func playerGroupDidChangeEnabledControls(_ group: IoTPlayerGroup) {
		dispatchPlayerGroupNotification(group, reason: .controlsEnabledChanged)
	}

    func playerGroup(_ group: IoTPlayerGroup, didChangeVolume volume: Double) {
		dispatchPlayerGroupNotification(group, reason: .volumeStateChanged)
	}

    func playerGroup(_ group: IoTPlayerGroup, didChangeMuteState muted: Bool) {
		dispatchPlayerGroupNotification(group, reason: .muteStateChanged, payload: muted)
	}

    func playerGroup(_ group: IoTPlayerGroup, didChangeInputSelector inputSelector: IoTInputOutputSelector) {
        dispatchPlayerGroupNotification(group, reason: .inputSelectorChanged)
    }
	
	// MARK: Playlist

    func playlistDidChange(_ playlist: IoTPlaylist) {
		dispatchPlaylistNotification(playlist, reason: .changed)
	}

    func playlist(_ playlist: IoTPlaylist, didChange loopMode: IoTLoopMode) {
		dispatchPlaylistNotification(playlist, reason: .loopModeChanged, payload: loopMode)
	}

    func playlist(_ playlist: IoTPlaylist, didChange shuffleMode: IoTShuffleMode) {
		dispatchPlaylistNotification(playlist, reason: .shuffleModeChanged, payload: shuffleMode)
	}
	
	// MARK: Player

    func player(_ player: IoTPlayer, didChangeDisplayName name: String) {
		dispatchPlayerNotification(player, reason: .displayNameChanged, payload: name)
	}

    func player(_ player: IoTPlayer, didChangeVolume volume: Double) {
		dispatchPlayerNotification(player, reason: .volumeStateChanged, payload: volume)
	}

    func player(_ player: IoTPlayer, didChangeMuteState muted: Bool) {
		dispatchPlayerNotification(player, reason: .muteStateChanged, payload: muted)
	}

    func player(_ player: IoTPlayer, didChangeInputSelector inputSelector: IoTInputOutputSelector) {
		dispatchPlayerNotification(player, reason: .inputSelectorChanged)
	}

    func player(_ player: IoTPlayer, didChange outputSelectors: [IoTInputOutputSelector]) {
        dispatchPlayerNotification(player, reason: .outputSelectorsChanged)
    }

    func playerDidChangeAudioConfiguration(_ player: IoTPlayer) {
        dispatchPlayerNotification(player, reason: .audioConfigurationChanged)
    }
	
	// MARK: Device

	func deviceAdded(_ device: IoTDevice) {
		dispatchDeviceNotification(device, reason: .added)
	}
	
	func deviceRemoved(_ device: IoTDevice) {
		dispatchDeviceNotification(device, reason: .removed)
	}
	
    func device(_ device: IoTDevice, didChangeName name: String) {
		dispatchDeviceNotification(device, reason: .nameChanged, payload: name)
	}

    func deviceDidChangeBatteryState(_ device: IoTDevice) {
        dispatchDeviceNotification(device, reason: .batteryStateChanged, payload: nil)
    }

    func deviceDidChangeWiFiState(_ device: IoTDevice) {
        dispatchDeviceNotification(device, reason: .wifiStateChanged, payload: nil)
    }

    func deviceDidChangeEthernetState(_ device: IoTDevice) {
        dispatchDeviceNotification(device, reason: .ethernetStateChanged, payload: nil)
    }

    func deviceDidChangeAccessPointState(_ device: IoTDevice) {
        dispatchDeviceNotification(device, reason: .accessPointStateChanged, payload: nil)
    }

// Bluetooth
    func device(_ device: IoTDevice, btScanDidDiscoverBtDevice btDevice: IoTBluetoothDevice) {
        dispatchBluetoothNotification(device, reason: .deviceDiscovered, payload: btDevice)
    }

    func device(_ device: IoTDevice, btScanStateDidChange scanning: Bool) {
        dispatchBluetoothNotification(device, reason: .scanStateChanged , payload: scanning)
    }

    func device(_ device: IoTDevice, btAdapterStateDidChange adapterState: IoTBluetoothAdapterState) {
        dispatchBluetoothNotification(device, reason: .adapterStateChanged , payload: adapterState)
    }

    func device(_ device: IoTDevice, btDiscoverabilityDidChange discoverability: IoTDiscoverable) {
        dispatchBluetoothNotification(device, reason: .discoverabilityChanged , payload: discoverability)
    }

    func device(_ device: IoTDevice, btDidConnect btDevice: IoTBluetoothDevice) {
        dispatchBluetoothNotification(device, reason: .deviceConnected, payload: btDevice)
    }

    func device(_ device: IoTDevice, btDidDisconnectDevice btDevice: IoTBluetoothDevice) {
        dispatchBluetoothNotification(device, reason: .deviceDisconnected, payload: btDevice)
    }

    func device(_ device: IoTDevice, btPairedDevicesDidChange pairedDevices: [IoTBluetoothDevice]) {
        dispatchBluetoothNotification(device, reason: .pairedDevicesChanged, payload: pairedDevices)
    }

// Voice UI

    func device(_ device: IoTDevice, voiceUIEnabledStateDidChange enabled: Bool) {
        dispatchVoiceUINotification(device, reason: .enabledStateChanged, payload: enabled)
    }

    func device(_ device: IoTDevice, voiceUIClientsDidChange voiceUIClients: [IoTVoiceUIClient]) {
        dispatchVoiceUINotification(device, reason: .clientsChanged, payload: voiceUIClients)
    }

    func device(_ device: IoTDevice, voiceUIDefaultClientDidChange voiceUIClient: IoTVoiceUIClient?) {
        dispatchVoiceUINotification(device, reason: .defaultClientChanged, payload: voiceUIClient)
    }

    func device(_ device: IoTDevice, voiceUIDidProvideAVSAuthenticationCode code: String, url: String) {
        let payload = VoiceUIAuthenticationInfo(code: code, url: url)
        dispatchVoiceUINotification(device, reason: .authenticationInfoGenerated, payload: payload)
    }

    func device(_ device: IoTDevice, voiceUIOnboardingDidErrorWithTimeout didTimeout: Bool, reattempt: Int32) {
        let payload = VoiceUIAuthenticationErrorInfo(didTimeout: didTimeout, reattempt: reattempt)
        dispatchVoiceUINotification(device, reason: .authenticationError, payload: payload)
    }

// Zigbee
    func device(_ device: IoTDevice, zbAdapterStateDidChange adapterState: IoTZigbeeAdapterState) {
        dispatchZigbeeNotification(device, reason: .enabledChanged, payload: adapterState)
    }

    func device(_ device: IoTDevice, zbCoordinatorStateDidChange coordinatorState: IoTCoordinatorState) {
        dispatchZigbeeNotification(device, reason: .coordinatorStateChanged, payload: coordinatorState)
    }

    func device(_ device: IoTDevice, zbJoiningStateDidChange joiningAllowed: Bool) {
        dispatchZigbeeNotification(device, reason: .joiningStateChanged, payload: joiningAllowed)
    }

    func device(_ device: IoTDevice, zbJoinedDevicesDidChange joinedDevices: [IoTZigbeeDevice]) {
        dispatchZigbeeNotification(device, reason: .joinedDevicesChanged, payload: joinedDevices)
    }

// Home Theater
    func homeTheaterChannelAvailabilityChanged(_ channel: IoTHomeTheaterChannel, player: IoTPlayer) {
		let payload = HomeTheaterNotificationPayload(channel: channel)
		dispatchHomeTheaterNotification(player, reason: .availabilityChanged, payload: payload)
	}

    func homeTheaterChannelVolumeChanged(_ channel: IoTHomeTheaterChannel, player: IoTPlayer) {
		let payload = HomeTheaterNotificationPayload(channel: channel)
		dispatchHomeTheaterNotification(player, reason: .volumeChanged, payload: payload)
	}
}

// MARK: - Private Methods - WiFi
extension PlayerController {
	private struct WiFiInfo {
		static var previousIP: String?
		static var previousSSID: String?
	}
	
	private func startObservingWiFi() {
		NotificationCenter.default.addObserver(self,
											   selector: #selector(wifiChanged(_:)),
											   name: .wiFiStateDidChange,
											   object: WiFiManager.sharedInstance)
	}
	
	private func stopObservingWiFi() {
		NotificationCenter.default.removeObserver(self,
												  name: .wiFiStateDidChange,
												  object: nil)
	}
	
	@objc private func wifiChanged(_ note: Notification) {
		if WiFiManager.sharedInstance.wifiAvailable {
			// Are we using the same WiFi as before
			checkForWifiChange()
		} else {
			stop()
		}
	}
	
	private func checkForWifiChange() {
		if !isStarted {
			start()
		} else if wifiChanged() {
			restart()
		}
		
		// update wifi info
		WiFiInfo.previousIP = WiFiManager.sharedInstance.currentWiFiIPAddress()
	}
	
	private func wifiChanged() -> Bool {
		if WiFiInfo.previousIP != WiFiManager.sharedInstance.currentWiFiIPAddress() {
			return true
		}
		
		return false
	}
}

// MARK: - Private Methods - Notification Dispatch
extension PlayerController {
	private func dispatchPlayerGroupNotification(_ playerGroup: IoTPlayerGroup,
										  reason: PlayerGroupNotificationReason,
										  payload: Any? = nil) {
		let notification = PlayerGroupNotification(sender: self,
												   playerGroup: playerGroup,
												   reason: reason,
												   payload: payload)
		DispatchQueue.main.async { [weak self] in
            guard let self = self else { return }
			DDLogDebug("PlayerGroup notification sent - reason: \(reason) - \(playerGroup)")
			self.notificationCenter.post(notification)
		}
	}
	
	private func dispatchPlaylistNotification(_ playlist: IoTPlaylist,
											  reason: PlaylistNotificationReason,
											  payload: Any? = nil) {
		let notification = PlaylistNotification(sender: self,
													   playlist: playlist,
													   reason: reason,
													   payload: payload)
		
        DispatchQueue.main.async { [weak self] in
            guard let self = self else { return }
			DDLogDebug("Playlist notification sent - reason: \(reason)")
			self.notificationCenter.post(notification)
		}
	}
	
	private func dispatchPlayerNotification(_ player: IoTPlayer,
									reason: PlayerNotificationReason,
									payload: Any? = nil) {
		let notification = PlayerNotification(sender: self,
													 player: player,
													 reason: reason,
													 payload: payload)
		
        DispatchQueue.main.async { [weak self] in
            guard let self = self else { return }
			DDLogDebug("Player notification sent - reason: \(reason)")
			self.notificationCenter.post(notification)
		}
	}
	
	private func dispatchDeviceNotification(_ device: IoTDevice,
									reason: DeviceNotificationReason,
									payload: Any? = nil) {
		let notification = DeviceNotification(sender: self,
													 device: device,
													 reason: reason,
													 payload: payload)
		
        DispatchQueue.main.async { [weak self] in
            guard let self = self else { return }
			DDLogDebug("Device notification sent - reason: \(reason) - \(device)")
			self.notificationCenter.post(notification)
		}
	}

    private func dispatchBluetoothNotification(_ device: IoTDevice,
                                               reason: BluetoothNotificationReason,
                                               payload: Any? = nil) {
        let notification = BluetoothNotification(sender: self,
                                                 device: device,
                                                 reason: reason,
                                                 payload: payload)

        DispatchQueue.main.async { [weak self] in
            guard let self = self else { return }
            DDLogDebug("Bluetooth notification sent - reason: \(reason) - \(device)")
            self.notificationCenter.post(notification)
        }
    }

    private func dispatchZigbeeNotification(_ device: IoTDevice,
                                            reason: ZigbeeNotificationReason,
                                            payload: Any? = nil) {
        let notification = ZigbeeNotification(sender: self,
                                              device: device,
                                              reason: reason,
                                              payload: payload)

        DispatchQueue.main.async { [weak self] in
            guard let self = self else { return }
            DDLogDebug("Zigbee notification sent - reason: \(reason) - \(device)")
            self.notificationCenter.post(notification)
        }
    }

    private func dispatchVoiceUINotification(_ device: IoTDevice,
                                             reason: VoiceUINotificationReason,
                                             payload: Any? = nil) {
        let notification = VoiceUINotification(sender: self,
                                               device: device,
                                               reason: reason,
                                               payload: payload)

        DispatchQueue.main.async { [weak self] in
            guard let self = self else { return }
            DDLogDebug("VoiceUI notification sent - reason: \(reason) - \(device)")
            self.notificationCenter.post(notification)
        }
    }

    private func dispatchHomeTheaterNotification(_ player: IoTPlayer,
                                                 reason: HomeTheaterNotificationReason,
                                                 payload: HomeTheaterNotificationPayload) {
        let notification = HomeTheaterNotification(sender: self,
                                                   player: player,
                                                   reason: reason,
                                                   payload: payload)

        DispatchQueue.main.async { [weak self] in
            guard let self = self else { return }
			DDLogDebug("Home Theater notification sent - reason: \(reason)")
			self.notificationCenter.post(notification)
		}
	}
}
