//
//  PlayerFlutterPlatform.swift
//  nut_player_ios
//
//  Created by Nut.Tech on 25.09.2023.
//

import Foundation
import NutPlayer
import Flutter
import Combine

final class PlayerFlutterPlatform: NSObject, FlutterStreamHandler {
    
    enum FlutterStateEvent: String {
        case initialized
        case ready
        case bufferingStart
        case bufferingEnd
        case bufferingUpdate
        case isPlayingStateUpdate
        case completed
        case error
        case unknown
    }
    
    enum FlutterDataEvent: String {
        case updatedQualities
    }
    
    let playerId: Int64
    let channel: FlutterEventChannel
    
    private (set) var nutPlayer: NutPlayer?
    private var sinkEvent: FlutterEventSink?
    private var lastEvent: FlutterStateEvent = .unknown
    private var cancellable = Set<AnyCancellable>()
    
    private var playerContent: PlayerContent?
    let parameters: [String: Any]?
    
    // MARK: - Init
    
    init(_ rawContent: String, parameters: [String: Any]?, playerId: Int64, channel: FlutterEventChannel) {
        
        self.playerId = playerId
        self.channel = channel
        
        self.playerContent = Self.createPlayerContent(raw: rawContent)
        self.parameters = parameters
        
        super.init()
        
        channel.setStreamHandler(self)
    }
    
    private func subscribe(on player: NutPlayer) {
        
        player.state
            .receive(on: DispatchQueue.main)
            .sink { [weak self] state in
                guard let self else { return }
                self.endBufferisationifNeeded(for: state)
                switch state {
                case .ready(_, let duration):
                    self.lastEvent = .ready
                    let millisecondsDuration = (duration ?? 0.0) * 1000
                    var subtitles = [String: String]()
                    if let rootMenu = player.menu.first(where: { $0.group == "Subtitles" }),
                       case let .submenu(menu) = rootMenu.element { menu.forEach { subtitles[$0.id] = $0.title }
                    }
                    self.sinkEvent?([
                        "event": FlutterStateEvent.ready.rawValue,
                        "duration": Int(millisecondsDuration),
                        "subs": subtitles
                    ])
                case .playing:
                    self.lastEvent = .isPlayingStateUpdate
                    self.sinkEvent?([
                        "event": FlutterStateEvent.isPlayingStateUpdate.rawValue,
                        "isPlaying": true
                    ])
                case .paused:
                    self.lastEvent = .isPlayingStateUpdate
                    self.sinkEvent?([
                        "event": FlutterStateEvent.isPlayingStateUpdate.rawValue,
                        "isPlaying": false
                    ])
                case .completed:
                    self.lastEvent = .completed
                    self.sinkEvent?([
                        "event": FlutterStateEvent.completed.rawValue
                    ])
                case .buffering:
                    guard self.lastEvent != .unknown,
                          self.lastEvent != .bufferingStart else { return }
                    self.lastEvent = .bufferingStart
                    self.sinkEvent?([
                        "event": FlutterStateEvent.bufferingStart.rawValue
                    ])
                default:
                    break
                }
            }.store(in: &self.cancellable)
        
        player.cache
            .receive(on: DispatchQueue.main)
            .sink { [weak self] cachedTime in
                guard let self, self.lastEvent != .unknown else { return }
                let cachedInMilliseconds = cachedTime * 1000
                self.sinkEvent?([
                    "event": FlutterStateEvent.bufferingUpdate.rawValue,
                    "values": [[0, Int(cachedInMilliseconds)]]
                ])
            }
            .store(in: &self.cancellable)
        
        player.qualities
            .receive(on: DispatchQueue.main)
            .sink { [weak self] qualities in
                guard let self, qualities.count > 0,
                      let qualityMenu = self.extractSubmenu(for: "Qualities")  else { return }
                let titles = qualityMenu.compactMap { $0.title }
                let qualitiesDicts = Self.mapQualitiesToDictionaries(qualities: qualities, titles: titles)
                self.sinkEvent?([
                    "event": FlutterDataEvent.updatedQualities.rawValue,
                    "qualities": qualitiesDicts
                ])
            }
            .store(in: &self.cancellable)
    }
    
    private func endBufferisationifNeeded(for state: PlayerState) {
        guard self.lastEvent == .bufferingStart,
              state != .buffering,
              state != .loading else { return }
        self.lastEvent = .bufferingEnd
        self.sinkEvent?([
            "event": FlutterStateEvent.bufferingEnd.rawValue
        ])
    }
    
    private func initializePlayer() {

        guard let content = self.playerContent else {
            self.lastEvent = .error
            let flutterError = FlutterError(code: "NutPlayerError",
                                            message: "Unknown content",
                                            details: nil)
            self.sinkEvent?(flutterError)
            return
        }
            
        self.lastEvent = .initialized
        self.sinkEvent?([
            "event": FlutterStateEvent.initialized.rawValue
        ])
        
        let autostart: Bool
        let isPipAvailable: Bool
        let startPosition: TimeInterval
        var quality: QualityPresets? = nil
        
        if let parameters {
            autostart = parameters["enableAutostart"] as? Bool ?? false
            isPipAvailable = parameters["enablePip"] as? Bool ?? false
            startPosition = parameters["startPosition"] as? TimeInterval ?? 0
            if let qualityDict = parameters["quality"] as? [String: Int],
               let bandwidth = qualityDict["bandwidth"],
               let width = qualityDict["width"],
               let height = qualityDict["height"] {
                quality = QualityPresets(resolution: CGSize(width: width, height: height),
                                         bandwidth: bandwidth)
            }
        } else {
            autostart = false
            isPipAvailable = false
            startPosition = 0.0
        }
        
        let settings = NutPlayerSettings(isPipAvailable: isPipAvailable)
        let player = NutPlayer(settings: settings, locale: .rus)

        self.nutPlayer = player
        self.subscribe(on: player)
        
        let timeouts = self.createTimeouts(from: parameters)
        player.load(provider: CommonProvider(content: content),
                    timeouts: timeouts,
                    autoplay: autostart,
                    position: startPosition,
                    quality: quality)
    }
    
    private func createTimeouts(from params: [String: Any]?) -> PlayerTimeouts {
        let timeouts = parameters?["timeouts"] as? [String: Any]
        
        var playlistTimeout = timeouts?["playlist"] as? TimeInterval ?? 5000
        if playlistTimeout <= 0 {
            playlistTimeout = 5000
        }
        var trackTimeout = timeouts?["track"] as? TimeInterval ?? 3000
        if trackTimeout <= 0 {
            trackTimeout = 3000
        }
        return PlayerTimeouts(playlist: playlistTimeout, track: trackTimeout)
    }
    
    // MARK: - Interface
    
    var currentTime: Double { self.nutPlayer?.currentTime.value ?? 0.0 }
    
    var duration: Double { self.nutPlayer?.duration.value ?? 0.0 }
    
    func change(speed: Float) {
        if let rate = PlayerPlaybackRate(rawValue: speed) {
            self.nutPlayer?.change(rate: rate)
        }
    }
    
    func seek(time: Double) {
        guard let player = self.nutPlayer else { return }
        
        if time == player.duration.value,
           player.state.value == .completed {
            return
        }
        
        player.seek(time: time)
    }
    
    func change(volume: Float) {
        let value = volume < 0 ? 0 : ( volume > 1 ? 1 : volume )
        self.nutPlayer?.change(volume: value)
    }

    func change(subtitle: String) {
        guard let menu = self.extractSubmenu(for: "Subtitles"),
              menu.count > 0 else { return }
        
        let subFromMenu = menu.first(where: { $0.id == subtitle })
        guard let subFromMenu,
              case let .action(handler) = subFromMenu.element else { return }
        handler(subFromMenu)
    }
    
    func change(quality: String) {
        guard let menu = self.extractSubmenu(for: "Qualities"),
              menu.count > 0 else { return }
        
        let qualityFromMenu = menu
            .compactMap { $0 as? NutQualityPlugin.PluginSettingsRecord }
            .first(where: { $0.quality?.id == quality })
            
        guard let record = menu.first(where: { $0.id == qualityFromMenu?.id }),
              case let .action(handler) = record.element else { return }
        handler(record)
    }
    
    func play() { self.nutPlayer?.play() }
    
    func end() { self.nutPlayer?.end() }
    
    func pause() { 
        guard let player = self.nutPlayer,
              player.state.value != .completed else {
            return
        }
        
        player.pause()
    }
    
    func dispose() {
        self.nutPlayer?.stop()
        self.nutPlayer = nil
    }
    
    // MARK: - FlutterStreamHandler
    
    func onListen(withArguments arguments: Any?, eventSink events: @escaping FlutterEventSink) -> FlutterError? {
        self.sinkEvent = events
        self.initializePlayer()
        // print(arguments)
        return nil
    }
    
    func onCancel(withArguments arguments: Any?) -> FlutterError? {
        // print(arguments)
        return nil
    }
    
    private func extractSubmenu(for group: String) -> [PlayerSettingsRecord]? {
        guard let player = self.nutPlayer,
              let rootMenu = player.menu.first(where: { $0.group == group }),
              case let .submenu(menu) = rootMenu.element else { return [] }
        return menu
    }
    
    // MARK: - Static
    
    private static func createPlayerContent(raw: String) -> PlayerContent? {
        
        guard let data = raw.data(using: .utf8) else { return nil }
        
        do {
            let content = try JSONDecoder().decode(FlutterPlayerContent.self, from: data)
            return content
        } catch {
            print(error)
            return nil
        }
    }
    
    private static func mapQualitiesToDictionaries(qualities: [PlayerQualityRecord],
                                                   titles: [String]) -> [[String: Any]] {
        zip(qualities, titles).map { Self.mapToDictionary(quality: $0, title: $1) }
    }
    
    private static func mapToDictionary(quality: PlayerQualityRecord, title: String) -> [String: Any] {
        [
          "id": quality.id,
          "width": Int(quality.resolution.width),
          "height": Int(quality.resolution.height),
          "bandwidth": quality.bandwidth,
          "title": title
        ]
    }
    
}
