//
// Copyright (c) Double Symmetry GmbH
// Commercial use requires a license. See https://rntp.dev/pricing
//

import React
import MediaPlayer

@objc(TrackPlayer)
class TrackPlayer: RCTEventEmitter {

  // MARK: - Properties

  private var player = AudioPlayer()
  private var audioCache: AudioCache?
  private var remoteControlHandling: RemoteControlHandling = .native
  private var perCommandHandling: [String: RemoteControlHandling] = [:]
  private var forwardInterval: TimeInterval = 15
  private var backwardInterval: TimeInterval = 15
  private var progressSyncTimer: Timer?
  private var progressSyncIntervalSeconds: Double = 0
  private var progressSyncHttpUrl: String?
  private var autoUpdateMetadataFromStream: Bool = true
  private var lastEmittedMediaMetadata: MediaMetadataChangedEvent?
  private var progressSyncHttpHeaders: [String: String]?

  private var sleepTimerController: SleepTimerController?

  // MARK: - Initializers

  override init() {
    super.init()
    bindPlayerCallbacks()
  }

  private func bindPlayerCallbacks() {
    player.onStateChange = { [weak self] state in self?.handleAudioPlayerStateChange(state: state) }
    player.onCurrentItemChanged = { [weak self] item, index in
      self?.handleCurrentItemChanged(item: item, index: index)
    }
    player.onMetadataReceived = { [weak self] metadata in
      self?.handleMetadataReceived(metadata)
    }
    player.onPlaybackError = { [weak self] code, message in
      self?.emitEvent(event: PlaybackErrorEvent(code: code, message: message))
    }
  }

  // MARK: - RCTBridgeModule

  override class func requiresMainQueueSetup() -> Bool {
    return true
  }

  // MARK: - RCTEventEmitter

  override func supportedEvents() -> [String]! {
    return EmitEventType.all()
  }

  // MARK: - Setup

  @objc(setupPlayer:)
  public func setupPlayer(config: [String: Any]) {
    let contentType = config["contentType"] as? String ?? "music"
    let mode: AVAudioSession.Mode = contentType == "speech" ? .spokenAudio : .default
    let audioMixing = config["audioMixing"] as? String ?? "exclusive"
    var categoryOptions: AVAudioSession.CategoryOptions = []
    if audioMixing == "mix" {
      categoryOptions.insert(.mixWithOthers)
    }
    try? AVAudioSession.sharedInstance().setCategory(.playback, mode: mode, options: categoryOptions)

    var cache: AudioCache? = nil
    var preloadWindow = 0
    if let cacheConfig = config["cache"] as? [String: Any] {
      let maxSize = (cacheConfig["maxSizeBytes"] as? NSNumber)?.int64Value ?? (500 * 1024 * 1024)
      cache = AudioCache(maxSizeBytes: maxSize)
      if let preloadConfig = cacheConfig["preloading"] as? [String: Any] {
        preloadWindow = (preloadConfig["window"] as? NSNumber)?.intValue ?? 0
      }
    }
    audioCache = cache

    let handleNoisy = config["handleAudioBecomingNoisy"] as? Bool ?? true
    autoUpdateMetadataFromStream = config["autoUpdateMetadataFromStream"] as? Bool ?? true
    player = AudioPlayer(handleAudioBecomingNoisy: handleNoisy, cache: cache)
    player.preloadWindow = preloadWindow
    bindPlayerCallbacks()
    BrowseTreeStore.shared.player = player
    lastEmittedStateString = nil
    lastIsPlaying = false

    sleepTimerController = SleepTimerController(player: player)
    sleepTimerController?.onTriggered = { [weak self] type in
      self?.emitEvent(event: SleepTimerTriggeredEvent(sleepType: type))
    }

    if let progressSync = config["progressSync"] as? [String: Any] {
      progressSyncIntervalSeconds = progressSync["intervalSeconds"] as? Double ?? 0
      if let http = progressSync["http"] as? [String: Any] {
        progressSyncHttpUrl = http["url"] as? String
        progressSyncHttpHeaders = http["headers"] as? [String: String]
      }
    } else {
      progressSyncIntervalSeconds = 0
      progressSyncHttpUrl = nil
      progressSyncHttpHeaders = nil
    }

    setupRemoteCommandHandlers(capabilities: [PlayerCommand.PlayPause.rawValue])
  }

  // MARK: - Playback

  @objc(play)
  func play() {
    player.play()
  }

  @objc(pause)
  func pause() {
    player.pause()
  }

  @objc(stop)
  func stop() {
    player.stop()
  }

  @objc(seekTo:)
  func seekTo(position: Double) {
    player.seek(to: position)
  }

  @objc(seekBy:)
  func seekBy(offset: Double) {
    let newPosition = player.currentTime + offset
    player.seek(to: newPosition)
  }

  @objc(skipToNext)
  func skipToNext() {
    player.next()
  }

  @objc(skipToPrevious)
  func skipToPrevious() {
    player.previous()
  }

  @objc(skipToIndex:)
  func skipToIndex(index: Double) {
    player.skipTo(index: Int(index))
  }

  @objc(retry)
  func retry() {
    player.retry()
  }

  @objc(clearCache)
  func clearCache() {
    player.cancelAllDownloads()
    audioCache?.removeAll()
  }

  @objc(preload:duration:)
  func preload(item: [String: Any], duration: Double) {
    guard let urlString = extractUrl(from: item),
          let url = URL(string: urlString) else { return }

    let headers = item["headers"] as? [String: String]
    player.preloader?.preload(url: url, headers: headers, duration: duration)
  }

  @objc(cancelPreload:)
  func cancelPreload(item: [String: Any]) {
    guard let urlString = extractUrl(from: item),
          let url = URL(string: urlString) else { return }
    player.preloader?.cancel(url: url)
  }

  private func extractUrl(from item: [String: Any]) -> String? {
    if let url = item["url"] as? String { return url }
    if let urlObj = item["url"] as? [String: Any], let uri = urlObj["uri"] as? String { return uri }
    return nil
  }

  @objc(setPlaybackSpeed:)
  func setPlaybackSpeed(speed: Double) {
    player.rate = Float(speed)
  }

  @objc(setVolume:)
  func setVolume(volume: Double) {
    player.volume = Float(volume)
  }

  // MARK: - Queue - Set

  @objc(setMediaItem:)
  func setMediaItem(_ data: [String: Any]) {
    let mediaItem = MediaItem(data: data)
    player.clear()
    player.load(item: mediaItem)
    emitEvent(event: QueueChangedEvent())
  }

  @objc(setMediaItems:startIndex:)
  func setMediaItems(_ data: [[String: Any]], startIndex: Double) {
    player.clear()
    let mediaItems = data.map { MediaItem(data: $0) }
    player.add(items: mediaItems)
    let idx = Int(startIndex)
    if idx > 0 && idx < mediaItems.count {
      player.skipTo(index: idx)
    }
    emitEvent(event: QueueChangedEvent())
  }

  // MARK: - Queue - Add/Insert

  @objc(addMediaItem:)
  func addMediaItem(_ data: [String: Any]) {
    let mediaItem = MediaItem(data: data)
    player.add(items: [mediaItem])
    emitEvent(event: QueueChangedEvent())
  }

  @objc(addMediaItems:)
  func addMediaItems(_ data: [[String: Any]]) {
    let mediaItems = data.map { MediaItem(data: $0) }
    player.add(items: mediaItems)
    emitEvent(event: QueueChangedEvent())
  }

  @objc(insertMediaItem:mediaItem:)
  func insertMediaItem(_ index: Double, data: [String: Any]) {
    let mediaItem = MediaItem(data: data)
    try? player.add(items: [mediaItem], at: Int(index))
    emitEvent(event: QueueChangedEvent())
  }

  @objc(insertMediaItems:mediaItems:)
  func insertMediaItems(_ index: Double, data: [[String: Any]]) {
    let mediaItems = data.map { MediaItem(data: $0) }
    try? player.add(items: mediaItems, at: Int(index))
    emitEvent(event: QueueChangedEvent())
  }

  // MARK: - Queue - Remove

  @objc(removeMediaItem:)
  func removeMediaItem(_ index: Double) {
    try? player.removeItem(at: Int(index))
    emitEvent(event: QueueChangedEvent())
  }

  @objc(removeMediaItems:toIndex:)
  func removeMediaItems(_ fromIndex: Double, toIndex: Double) {
    for i in stride(from: Int(toIndex) - 1, through: Int(fromIndex), by: -1) {
      try? player.removeItem(at: i)
    }
    emitEvent(event: QueueChangedEvent())
  }

  @objc(clear)
  func clear() {
    // Cancel a media-item sleep timer first so the transition-to-nil can't fire it.
    if sleepTimerController?.sleepTimerType == "mediaItem" {
      sleepTimerController?.cancelInternal(restoreVolume: false)
    }
    player.clear()
    emitEvent(event: QueueChangedEvent())
  }

  // MARK: - Queue - Replace & Reorder

  @objc(replaceMediaItem:mediaItem:)
  func replaceMediaItem(_ index: Double, data: [String: Any]) {
    let mediaItem = MediaItem(data: data)
    player.replaceItem(at: Int(index), with: mediaItem)
    emitEvent(event: QueueChangedEvent())
  }

  @objc(moveMediaItem:toIndex:)
  func moveMediaItem(_ fromIndex: Double, toIndex: Double) {
    guard let item = player.items[safe: Int(fromIndex)] as? MediaItem else { return }
    try? player.removeItem(at: Int(fromIndex))
    try? player.add(items: [item], at: Int(toIndex))
    emitEvent(event: QueueChangedEvent())
  }

  // MARK: - Queue - Update Metadata

  @objc(updateMetadata:metadata:)
  func updateMetadata(_ index: Double, metadata: [String: Any]) {
    let idx = Int(index)
    guard idx >= 0 && idx < player.items.count,
          let existing = player.items[idx] as? MediaItem else { return }

    let updatedTitle: String?? = metadata.keys.contains("title")
      ? .some(metadata["title"] as? String)
      : nil
    let updatedArtist: String?? = metadata.keys.contains("artist")
      ? .some(metadata["artist"] as? String)
      : nil
    let updatedAlbumTitle: String?? = metadata.keys.contains("albumTitle")
      ? .some(metadata["albumTitle"] as? String)
      : nil
    let updatedArtworkUrl: MediaURL?? = metadata.keys.contains("artworkUrl")
      ? .some(MediaURL(object: metadata["artworkUrl"]))
      : nil

    let updated = existing.withMetadata(
      title: updatedTitle,
      artist: updatedArtist,
      albumTitle: updatedAlbumTitle,
      artworkUrl: updatedArtworkUrl
    )

    player.updateMetadata(at: idx, with: updated)
    if idx == player.currentIndex {
      emitMediaMetadataChangedIfNeeded(for: updated)
    }
  }

  // MARK: - State Getters (sync)

  @objc(getPlaybackState)
  func getPlaybackState() -> String {
    switch player.state {
    case .idle:
      return "idle"
    case .loading, .ready, .paused:
      return "ready"
    case .playing:
      return "ready"
    case .buffering:
      return "buffering"
    case .ended:
      return "ended"
    case .failed:
      return "error"
    }
  }

  @objc(isPlaying)
  func isPlaying() -> NSNumber {
    return NSNumber(value: player.state == .playing)
  }

  @objc(getProgress)
  func getProgress() -> [String: Any] {
    return [
      "position": player.currentTime,
      "duration": player.duration,
      "buffered": player.bufferedPosition,
      "cached": player.cachedPosition
    ]
  }

  @objc(getPlaybackSpeed)
  func getPlaybackSpeed() -> NSNumber {
    return NSNumber(value: player.rate)
  }

  @objc(getVolume)
  func getVolume() -> NSNumber {
    return NSNumber(value: player.volume)
  }

  @objc(getActiveMediaItem)
  func getActiveMediaItem() -> [String: Any]? {
    guard let currentItem = player.currentItem as? MediaItem else {
      return nil
    }
    return currentItem.toDictionary()
  }

  @objc(getActiveMediaItemIndex)
  func getActiveMediaItemIndex() -> NSNumber? {
    if player.currentItem == nil {
      return nil
    }
    return NSNumber(value: player.currentIndex)
  }

  @objc(getQueue)
  func getQueue() -> [[String: Any]] {
    return player.items.compactMap { item -> [String: Any]? in
      guard let mediaItem = item as? MediaItem else { return nil }
      return mediaItem.toDictionary()
    }
  }

  @objc(getRepeatMode)
  func getRepeatMode() -> String {
    switch player.repeatMode {
    case .off:
      return "off"
    case .track:
      return "one"
    case .queue:
      return "all"
    }
  }

  @objc(isShuffleEnabled)
  func isShuffleEnabled() -> NSNumber {
    return NSNumber(value: player.shuffleEnabled)
  }

  // MARK: - Player Options

  @objc(setCommands:)
  func setCommands(commands: [String: Any]) {
    let capabilities = commands["capabilities"] as? [String] ?? []

    let handlingStr = commands["handling"] as? String ?? remoteControlHandling.rawValue
    remoteControlHandling = RemoteControlHandling(rawValue: handlingStr) ?? .native

    if remoteControlHandling == .hybrid {
      if let perCommand = commands["perCommandHandling"] as? [String: String] {
        perCommandHandling = perCommand.compactMapValues { RemoteControlHandling(rawValue: $0) }
      }
    }

    if let fwd = commands["forwardInterval"] as? Double { forwardInterval = fwd }
    if let bwd = commands["backwardInterval"] as? Double { backwardInterval = bwd }

    setupRemoteCommandHandlers(capabilities: capabilities)
  }

  @objc(setRepeatMode:)
  func setRepeatMode(mode: String) {
    switch mode {
    case "one":
      player.repeatMode = .track
    case "all":
      player.repeatMode = .queue
    default:
      player.repeatMode = .off
    }
  }

  @objc(setShuffleEnabled:)
  func setShuffleEnabled(enabled: Bool) {
    player.shuffleEnabled = enabled
  }

  // MARK: - Sleep Timer

  @objc(sleepAfterTime:fadeOutSeconds:)
  func sleepAfterTime(seconds: Double, fadeOutSeconds: Double) {
    sleepTimerController?.sleepAfterTime(seconds: seconds, fadeOutSeconds: fadeOutSeconds)
  }

  @objc(sleepAfterMediaItemAtIndex:)
  func sleepAfterMediaItemAtIndex(index: Double) {
    sleepTimerController?.sleepAfterMediaItemAtIndex(index: Int(index))
  }

  @objc(getSleepTimer)
  func getSleepTimer() -> [String: Any]? {
    return sleepTimerController?.getState()
  }

  @objc(cancelSleepTimer)
  func cancelSleepTimer() {
    sleepTimerController?.cancel()
  }

  // MARK: - Browse Tree

  @objc(setBrowseTree:)
  func setBrowseTree(_ categories: [[String: Any]]) {
    BrowseTreeStore.shared.update(categories: categories)
  }

  // MARK: - Destroy

  @objc(destroy)
  func destroy() {
    BrowseTreeStore.shared.clear()
    sleepTimerController?.cancel()
    sleepTimerController = nil
    stopProgressSyncTimer(fireFinalTick: false)
    let commandCenter = MPRemoteCommandCenter.shared()
    commandCenter.togglePlayPauseCommand.removeTarget(nil)
    commandCenter.playCommand.removeTarget(nil)
    commandCenter.pauseCommand.removeTarget(nil)
    commandCenter.nextTrackCommand.removeTarget(nil)
    commandCenter.previousTrackCommand.removeTarget(nil)
    commandCenter.stopCommand.removeTarget(nil)
    commandCenter.changePlaybackPositionCommand.removeTarget(nil)
    commandCenter.skipForwardCommand.removeTarget(nil)
    commandCenter.skipBackwardCommand.removeTarget(nil)

    player.destroy()
  }


  // MARK: - Listeners

  private var lastIsPlaying: Bool = false
  private var lastEmittedStateString: String? = nil

  func handleAudioPlayerStateChange(state: PlayerState) {
    let stateString: String
    switch state {
    case .idle:
      stateString = "idle"
    case .loading, .ready, .paused:
      stateString = "ready"
    case .playing:
      stateString = "ready"
    case .buffering:
      stateString = "buffering"
    case .ended:
      stateString = "ended"
    case .failed:
      stateString = "error"
    }

    if stateString != lastEmittedStateString {
      lastEmittedStateString = stateString
      emitEvent(event: PlaybackStateChangedEvent(state: stateString))
    }

    let nowPlaying = state == .playing
    if nowPlaying != lastIsPlaying {
      if nowPlaying && progressSyncIntervalSeconds > 0 {
        startProgressSyncTimer()
      } else if !nowPlaying {
        stopProgressSyncTimer(fireFinalTick: true)
      }
      lastIsPlaying = nowPlaying
      emitEvent(event: IsPlayingChangedEvent(playing: nowPlaying))
    }
  }

  func handleCurrentItemChanged(item: AudioItem?, index: Int) {
    // Sleep timer: check if the target item just finished
    if sleepTimerController?.handleItemTransition(to: index) == true {
      // Defer pause to next run loop — calling during transition gets overridden
      DispatchQueue.main.async { [weak self] in
        self?.player.pause()
      }
    }
    let mediaItem = item as? MediaItem
    let dict = mediaItem?.toDictionary()
    BrowseTreeStore.shared.updateNowPlaying(mediaId: mediaItem?.mediaId)
    lastEmittedMediaMetadata = nil
    emitEvent(event: MediaItemTransitionEvent(item: dict, index: index))
    emitMediaMetadataChangedIfNeeded(for: mediaItem)
  }

  func handleMetadataReceived(_ metadata: StreamMetadata) {
    emitEvent(event: MetadataReceivedEvent(metadata: metadata))

    guard autoUpdateMetadataFromStream else { return }
    let idx = player.currentIndex
    guard idx >= 0 && idx < player.items.count,
          let existing = player.items[idx] as? MediaItem else { return }
    let updated = existing.withMetadata(
      title: metadata.title.map { .some($0) },
      artist: metadata.artist.map { .some($0) },
      albumTitle: metadata.albumTitle.map { .some($0) },
      artworkUrl: metadata.artworkUri.flatMap { MediaURL(object: $0) }.map { .some($0) }
    )
    player.updateMetadata(at: idx, with: updated)
    emitMediaMetadataChangedIfNeeded(for: updated)
  }

  /// iOS MediaItem has no `genre`; Android may include it in this event.
  private func emitMediaMetadataChangedIfNeeded(for item: MediaItem?) {
    guard let item else { return }
    let event = MediaMetadataChangedEvent(
      title: item.title,
      artist: item.artist,
      albumTitle: item.albumTitle,
      artworkUrl: item.artworkUrl.map { $0.isLocal ? $0.value.path : $0.value.absoluteString },
      genre: nil
    )
    if event.isEmpty { return }
    if event == lastEmittedMediaMetadata { return }
    lastEmittedMediaMetadata = event
    emitEvent(event: event)
  }

  // MARK: - Progress Sync

  private func startProgressSyncTimer() {
    progressSyncTimer?.invalidate()
    progressSyncTimer = Timer.scheduledTimer(
      withTimeInterval: progressSyncIntervalSeconds,
      repeats: true
    ) { [weak self] _ in
      self?.onProgressSyncTick()
    }
  }

  private func stopProgressSyncTimer(fireFinalTick: Bool) {
    if fireFinalTick && progressSyncTimer != nil {
      onProgressSyncTick()
    }
    progressSyncTimer?.invalidate()
    progressSyncTimer = nil
  }

  private func onProgressSyncTick() {
    guard let currentItem = player.currentItem as? MediaItem else { return }
    let mediaId = currentItem.mediaId
    let position = player.currentTime
    let duration = player.duration
    let timestamp = Double(Int64(Date().timeIntervalSince1970 * 1000))

    // Save to UserDefaults
    let dict: [String: Any] = [
      "mediaId": mediaId,
      "position": position,
      "duration": duration,
      "timestamp": timestamp
    ]
    UserDefaults.standard.set(dict, forKey: "TrackPlayerSavedProgress")

    // Emit event
    emitEvent(event: PlaybackProgressUpdatedEvent(
      mediaId: mediaId,
      position: position,
      duration: duration,
      timestamp: timestamp
    ))

    // HTTP POST
    guard let urlString = progressSyncHttpUrl, let url = URL(string: urlString) else { return }
    var request = URLRequest(url: url)
    request.httpMethod = "POST"
    request.setValue("application/json", forHTTPHeaderField: "Content-Type")
    progressSyncHttpHeaders?.forEach { key, value in
      request.setValue(value, forHTTPHeaderField: key)
    }
    request.httpBody = try? JSONSerialization.data(withJSONObject: dict)
    request.timeoutInterval = 10
    URLSession.shared.dataTask(with: request).resume()
  }

  @objc(updateProgressSyncHeaders:)
  func updateProgressSyncHeaders(_ headers: [String: Any]) {
    progressSyncHttpHeaders = headers as? [String: String]
  }

  // MARK: - Remote Command Handlers

  private func setupRemoteCommandHandlers(capabilities: [String]) {
    let commandCenter = MPRemoteCommandCenter.shared()
    let enabledCommands = Set(capabilities.compactMap { PlayerCommand(rawValue: $0) })

    commandCenter.togglePlayPauseCommand.removeTarget(nil)
    commandCenter.playCommand.removeTarget(nil)
    commandCenter.pauseCommand.removeTarget(nil)
    commandCenter.nextTrackCommand.removeTarget(nil)
    commandCenter.previousTrackCommand.removeTarget(nil)
    commandCenter.stopCommand.removeTarget(nil)
    commandCenter.changePlaybackPositionCommand.removeTarget(nil)
    commandCenter.skipForwardCommand.removeTarget(nil)
    commandCenter.skipBackwardCommand.removeTarget(nil)

    let hasPlayPause = enabledCommands.contains(.PlayPause)
    commandCenter.togglePlayPauseCommand.isEnabled = hasPlayPause
    commandCenter.playCommand.isEnabled = hasPlayPause
    commandCenter.pauseCommand.isEnabled = hasPlayPause
    commandCenter.nextTrackCommand.isEnabled = enabledCommands.contains(.Next)
    commandCenter.previousTrackCommand.isEnabled = enabledCommands.contains(.Previous)
    commandCenter.stopCommand.isEnabled = enabledCommands.contains(.Stop)
    commandCenter.changePlaybackPositionCommand.isEnabled = enabledCommands.contains(.Seek)

    let hasSkipForward = enabledCommands.contains(.SkipForward)
    commandCenter.skipForwardCommand.isEnabled = hasSkipForward
    if hasSkipForward {
      commandCenter.skipForwardCommand.preferredIntervals = [NSNumber(value: forwardInterval)]
    }

    let hasSkipBackward = enabledCommands.contains(.SkipBackward)
    commandCenter.skipBackwardCommand.isEnabled = hasSkipBackward
    if hasSkipBackward {
      commandCenter.skipBackwardCommand.preferredIntervals = [NSNumber(value: backwardInterval)]
    }

    commandCenter.togglePlayPauseCommand.addTarget { [weak self] _ in
      guard let self = self else { return .commandFailed }
      return self.handleRemoteCommand(.PlayPause, action: {
        if self.player.state == .playing {
          self.player.pause()
        } else {
          self.player.play()
        }
      })
    }

    commandCenter.playCommand.addTarget { [weak self] _ in
      guard let self = self else { return .commandFailed }
      return self.handleRemoteCommand(.PlayPause, action: { self.player.play() })
    }

    commandCenter.pauseCommand.addTarget { [weak self] _ in
      guard let self = self else { return .commandFailed }
      return self.handleRemoteCommand(.PlayPause, action: { self.player.pause() })
    }

    commandCenter.nextTrackCommand.addTarget { [weak self] _ in
      guard let self = self else { return .commandFailed }
      return self.handleRemoteCommand(.Next, action: { self.player.next() })
    }

    commandCenter.previousTrackCommand.addTarget { [weak self] _ in
      guard let self = self else { return .commandFailed }
      return self.handleRemoteCommand(.Previous, action: { self.player.previous() })
    }

    commandCenter.stopCommand.addTarget { [weak self] _ in
      guard let self = self else { return .commandFailed }
      return self.handleRemoteCommand(.Stop, action: { self.player.stop() })
    }

    commandCenter.changePlaybackPositionCommand.addTarget { [weak self] event in
      guard let self = self,
            let positionEvent = event as? MPChangePlaybackPositionCommandEvent else {
        return .commandFailed
      }
      return self.handleRemoteSeek(position: positionEvent.positionTime)
    }

    commandCenter.skipForwardCommand.addTarget { [weak self] event in
      guard let self = self,
            let skipEvent = event as? MPSkipIntervalCommandEvent else {
        return .commandFailed
      }
      return self.handleRemoteSkipForward(interval: skipEvent.interval)
    }

    commandCenter.skipBackwardCommand.addTarget { [weak self] event in
      guard let self = self,
            let skipEvent = event as? MPSkipIntervalCommandEvent else {
        return .commandFailed
      }
      return self.handleRemoteSkipBackward(interval: skipEvent.interval)
    }
  }

  private func shouldHandleNatively(_ command: PlayerCommand) -> Bool {
    if remoteControlHandling == .hybrid {
      if let perCommand = perCommandHandling[command.rawValue] {
        return perCommand == .native
      }
    }
    switch remoteControlHandling {
    case .native: return true
    case .js: return false
    case .hybrid: return true
    }
  }

  private func handleRemoteCommand(_ command: PlayerCommand, action: @escaping () -> Void) -> MPRemoteCommandHandlerStatus {
    if shouldHandleNatively(command) {
      action()
      if remoteControlHandling == .hybrid {
        emitRemoteEventIfNeeded(command: command)
      }
      return .success
    } else {
      emitRemoteEventIfNeeded(command: command)
      return .success
    }
  }

  private func handleRemoteSeek(position: TimeInterval) -> MPRemoteCommandHandlerStatus {
    let command = PlayerCommand.Seek
    if shouldHandleNatively(command) {
      player.seek(to: position)
      if remoteControlHandling == .hybrid {
        emitEvent(event: RemoteSeekEvent(position: position))
      }
      return .success
    } else {
      emitEvent(event: RemoteSeekEvent(position: position))
      return .success
    }
  }

  private func handleRemoteSkipForward(interval: TimeInterval) -> MPRemoteCommandHandlerStatus {
    if shouldHandleNatively(.SkipForward) {
      let newPosition = player.currentTime + interval
      player.seek(to: newPosition)
    }
    emitRemoteSkipEventIfNeeded(.SkipForward, interval: interval)
    return .success
  }

  private func handleRemoteSkipBackward(interval: TimeInterval) -> MPRemoteCommandHandlerStatus {
    if shouldHandleNatively(.SkipBackward) {
      let newPosition = max(0, player.currentTime - interval)
      player.seek(to: newPosition)
    }
    emitRemoteSkipEventIfNeeded(.SkipBackward, interval: interval)
    return .success
  }

  private func emitRemoteSkipEventIfNeeded(_ command: PlayerCommand, interval: TimeInterval) {
    let shouldEmit: Bool
    switch remoteControlHandling {
    case .native: shouldEmit = false
    case .js: shouldEmit = true
    case .hybrid:
      let perCommand = perCommandHandling[command.rawValue]
      shouldEmit = perCommand == .js
    }

    if shouldEmit {
      switch command {
      case .SkipForward:
        emitEvent(event: RemoteSkipForwardEvent(interval: interval))
      case .SkipBackward:
        emitEvent(event: RemoteSkipBackwardEvent(interval: interval))
      default:
        break
      }
    }
  }

  private func emitRemoteEventIfNeeded(command: PlayerCommand) {
    let shouldEmit: Bool
    switch remoteControlHandling {
    case .native: shouldEmit = false
    case .js: shouldEmit = true
    case .hybrid:
      let perCommand = perCommandHandling[command.rawValue]
      shouldEmit = perCommand == .js
    }

    if shouldEmit {
      let event: EmitEvent
      switch command {
      case .PlayPause:
        event = player.state == .playing ? RemotePauseEvent() : RemotePlayEvent()
      case .Next:
        event = RemoteNextEvent()
      case .Previous:
        event = RemotePreviousEvent()
      case .Stop:
        event = RemoteStopEvent()
      case .Seek:
        event = RemoteSeekEvent(position: player.currentTime)
      case .SkipForward, .SkipBackward:
        return
      }
      emitEvent(event: event)
    }
  }
}

extension TrackPlayer {
  func emitEvent(event: EmitEvent) {
    self.sendEvent(withName: event.eventType.rawValue, body: event.body)
  }
}

private extension Collection {
  subscript(safe index: Index) -> Element? {
    return indices.contains(index) ? self[index] : nil
  }
}
