import 'dart:async';
import 'dart:math';

import 'package:audio_service/audio_service.dart';
import 'package:collection/collection.dart';
import 'package:drift/drift.dart' show Value;
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:just_audio/just_audio.dart';
import 'package:pool/pool.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:rxdart/rxdart.dart';
import 'package:synchronized/synchronized.dart';

import '../cache/image_cache.dart';
import '../database/database.dart';
import '../log.dart';
import '../models/music.dart';
import '../models/query.dart';
import '../models/support.dart';
import '../sources/music_source.dart';
import '../state/settings.dart';
import 'cache_service.dart';
import 'settings_service.dart';

part 'audio_service.g.dart';

class QueueSourceItem {
  final MediaItem mediaItem;
  final UriAudioSource audioSource;
  final QueueData queueData;

  const QueueSourceItem({
    required this.mediaItem,
    required this.audioSource,
    required this.queueData,
  });
}

class QueueSlice {
  final QueueSourceItem? prev;
  final QueueSourceItem? current;
  final QueueSourceItem? next;

  const QueueSlice(this.prev, this.current, this.next);
}

@Riverpod(keepAlive: true)
FutureOr<AudioControl> audioControlInit(AudioControlInitRef ref) async {
  final imageCache = ref.watch(imageCacheProvider);

  return AudioService.init(
    builder: () => AudioControl(AudioPlayer(), ref),
    config: const AudioServiceConfig(
      androidNotificationChannelId: 'com.subtracks2.channel.audio',
      androidNotificationChannelName: 'Music playback',
      androidNotificationIcon: 'drawable/ic_stat_name',
    ),
    cacheManager: imageCache,
    cacheKeyResolver: (mediaItem) =>
        mediaItem.data.artCache?.thumbnailArtCacheKey ?? '',
  );
}

@Riverpod(keepAlive: true)
AudioControl audioControl(AudioControlRef ref) {
  return ref.watch(audioControlInitProvider).requireValue;
}

class AudioControl extends BaseAudioHandler with QueueHandler, SeekHandler {
  static const radioLength = 10;

  Stream<Duration> get position => _player.positionStream;

  BehaviorSubject<QueueMode> queueMode = BehaviorSubject.seeded(QueueMode.user);
  BehaviorSubject<List<int>?> shuffleIndicies = BehaviorSubject.seeded(null);
  BehaviorSubject<AudioServiceRepeatMode> repeatMode =
      BehaviorSubject.seeded(AudioServiceRepeatMode.none);

  final Ref _ref;
  final AudioPlayer _player;

  int? _queueLength;
  int? _previousCurrentTrackIndex;

  final _syncPool = Pool(1);
  final _playLock = Lock();
  final _currentIndexIgnore = <int?>[null, 0];

  final ConcatenatingAudioSource _audioSource =
      ConcatenatingAudioSource(children: []);

  SubtracksDatabase get _db => _ref.read(databaseProvider);
  CacheService get _cache => _ref.read(cacheServiceProvider);
  MusicSource get _source => _ref.read(musicSourceProvider);
  int get _sourceId => _ref.read(sourceIdProvider);

  AudioControl(this._player, this._ref) {
    _player.playbackEventStream.listen((PlaybackEvent event) {
      final playing = _player.playing;
      playbackState.add(playbackState.value.copyWith(
        controls: [
          MediaControl.skipToPrevious,
          if (playing) MediaControl.pause else MediaControl.play,
          MediaControl.stop,
          MediaControl.skipToNext,
        ],
        systemActions: const {
          MediaAction.seek,
        },
        androidCompactActionIndices: const [0, 1, 3],
        processingState: const {
          ProcessingState.idle: AudioProcessingState.idle,
          ProcessingState.loading: AudioProcessingState.loading,
          ProcessingState.buffering: AudioProcessingState.buffering,
          ProcessingState.ready: AudioProcessingState.ready,
          ProcessingState.completed: AudioProcessingState.completed,
        }[_player.processingState]!,
        playing: playing,
        updatePosition: _player.position,
        bufferedPosition: _player.bufferedPosition,
        queueIndex: event.currentIndex,
      ));
    });

    _player.playbackEventStream.doOnError((e, st) async {
      log.warning('playbackEventStream', e, st);
    });

    shuffleIndicies.listen((value) {
      playbackState.add(playbackState.value.copyWith(
        shuffleMode: value != null
            ? AudioServiceShuffleMode.all
            : AudioServiceShuffleMode.none,
      ));
    });

    repeatMode.listen((value) {
      playbackState.add(playbackState.value.copyWith(repeatMode: value));
    });

    _player.processingStateStream.listen((event) async {
      if (event == ProcessingState.completed) {
        if (_audioSource.length > 0) {
          log.fine('completed');
          await stop();
          await seek(Duration.zero);
        }
      }
    });

    _ref.listen(sourceIdProvider, (_, __) async {
      await _clearAudioSource(true);
      await _db.clearQueue();
    });

    _ref.listen(maxBitrateProvider, (prev, next) async {
      if (prev?.valueOrNull != next.valueOrNull) {
        await _resyncQueue(true);
      }
    });

    _ref.listen(
        settingsServiceProvider.select((value) => value.app.streamFormat),
        (prev, next) async {
      await _resyncQueue(true);
    });

    _player.durationStream.listen((duration) {
      if (mediaItem.valueOrNull == null) return;

      final index = queue.valueOrNull?.indexOf(mediaItem.value!);

      final updated = mediaItem.value!.copyWith(duration: duration);
      mediaItem.add(updated);

      if (index != null) {
        queue.add(queue.value..replaceRange(index, index + 1, [updated]));
      }
    });

    _player.currentIndexStream.listen((index) async {
      if (_currentIndexIgnore.contains(index)) {
        _currentIndexIgnore.remove(index);
        return;
      }
      if (index == null || index >= _audioSource.sequence.length) return;

      final queueIndex = _audioSource.sequence[index].tag;
      if (queueIndex != null) {
        await _db.setCurrentTrack(queueIndex);
      }
    });

    _db.currentTrackIndex().watchSingleOrNull().listen((index) {
      // distict() except for when in loop one mode
      if (repeatMode.value != AudioServiceRepeatMode.one &&
          _previousCurrentTrackIndex == index) {
        return;
      }

      _previousCurrentTrackIndex = index;
      _syncPool.withResource(() => _syncQueue(index));
    });
  }

  Future<void> init() async {
    await _player.setAudioSource(_audioSource, preload: false);

    final last = await _db.getLastAudioState().getSingleOrNull();
    if (last == null) return;

    _queueLength = await _db.queueLength().getSingleOrNull();
    final repeat = {
      RepeatMode.none: AudioServiceRepeatMode.none,
      RepeatMode.all: AudioServiceRepeatMode.all,
      RepeatMode.one: AudioServiceRepeatMode.one,
    }[last.repeat]!;
    repeatMode.add(repeat);
    await repeatMode.firstWhere((e) => e == repeat);

    queueMode.add(last.queueMode);
    await queueMode.firstWhere((e) => e == last.queueMode);

    if (last.shuffleIndicies != null) {
      final indicies = last.shuffleIndicies!.unlock;
      shuffleIndicies.add(indicies);
      await shuffleIndicies.firstWhere((e) => e == indicies);
    }

    final startIndex = await _db.currentTrackIndex().getSingleOrNull();

    if (startIndex != null && _queueLength != null) {
      await _preparePlayer(startIndex);
    }
  }

  Future<void> playSongs({
    QueueMode mode = QueueMode.user,
    required QueueContextType context,
    String? contextId,
    required ListQuery query,
    required FutureOr<Iterable<Song>> Function(ListQuery query) getSongs,
    int? startIndex,
    bool? shuffle,
  }) async {
    if (!_playLock.locked) {
      return _playLock.synchronized(
        () => _playSongs(
          mode: mode,
          context: context,
          contextId: contextId,
          query: query,
          getSongs: getSongs,
          startIndex: startIndex,
          shuffle: shuffle,
        ),
      );
    }
  }

  Future<void> playRadio({
    required QueueContextType context,
    String? contextId,
    ListQuery query = const ListQuery(),
    required FutureOr<Iterable<Song>> Function(ListQuery query) getSongs,
  }) async {
    await playSongs(
      mode: QueueMode.radio,
      context: QueueContextType.library,
      contextId: contextId,
      query: query.copyWith(
        sort: SortBy(
          column: 'SIN(songs.ROWID + ${Random().nextInt(10000)})',
        ),
      ),
      getSongs: getSongs,
      startIndex: 0,
    );
  }

  Future<void> _playSongs({
    QueueMode mode = QueueMode.user,
    required QueueContextType context,
    String? contextId,
    required ListQuery query,
    required FutureOr<Iterable<Song>> Function(ListQuery query) getSongs,
    int? startIndex,
    bool? shuffle,
  }) async {
    shuffle = shuffle ?? shuffleIndicies.valueOrNull != null;
    queueMode.add(mode);

    if (mode == QueueMode.radio) {
      if (repeatMode.value != AudioServiceRepeatMode.none) {
        await _loop(AudioServiceRepeatMode.none);
      }
      if (shuffleIndicies.value != null) {
        await _shuffle(unshuffle: true);
      }
    }

    await _clearAudioSource();

    const limit = 500;
    _queueLength = 0;

    if ((startIndex == null || startIndex >= limit) && shuffle) {
      startIndex = Random().nextInt(limit);
    } else {
      startIndex ??= 0;
    }

    // clear the queue and load the initial songs only up to the startIndex
    await _db.transaction(() async {
      await _db.clearQueue();

      while (_queueLength! <= startIndex!) {
        final songs = await getSongs(query.copyWith(
          page: Pagination(limit: limit, offset: _queueLength!),
        ));
        await _loadQueueSongs(songs, _queueLength!, context, contextId);
        if (songs.length < limit) {
          break;
        }
      }
    });

    // if there are less songs than the limit and we're shuffling,
    // choose a new random startIndex
    if (startIndex >= _queueLength!) {
      startIndex = Random().nextInt(_queueLength!);
    }

    await _preparePlayer(startIndex, shuffle);
    await _db.setCurrentTrack(startIndex);
    play();

    const maxLength = 10000;

    // no need to do extra loading if we've already loaded everything
    if (_queueLength! < limit) return;
    while (true) {
      final songs = await getSongs(query.copyWith(
        page: Pagination(limit: limit, offset: _queueLength!),
      ));
      await _loadQueueSongs(songs, _queueLength!, context, contextId);
      if (songs.length < limit || _queueLength! >= maxLength) {
        break;
      }
    }
  }

  Future<void> _loadQueueSongs(
    Iterable<Song> songs,
    int total,
    QueueContextType context,
    String? contextId,
  ) async {
    await _db.insertQueue(songs.mapIndexed(
      (i, song) => QueueCompanion.insert(
        index: Value(i + (_queueLength ?? 0)),
        sourceId: song.sourceId,
        id: song.id,
        context: context,
        contextId: Value(contextId),
      ),
    ));

    _queueLength = (_queueLength ?? 0) + songs.length;
    if (shuffleIndicies.valueOrNull != null) {
      await _generateShuffleIndicies(startIndex: _player.currentIndex);
    }
  }

  Future<void> _preparePlayer(int startIndex, [bool? shuffle]) async {
    if (shuffle == true) {
      await _shuffle(startIndex: startIndex);
    } else if (shuffle == false) {
      await _shuffle(unshuffle: true);
    }

    final slice = await _getQueueSlice(startIndex);
    if (slice == null) {
      throw StateError('Could not get queue slice!');
    }

    final list =
        [slice.prev, slice.current, slice.next].whereNotNull().toList();

    mediaItem.add(slice.current!.mediaItem);
    queue.add(list.map((e) => e.mediaItem).toList());

    log.fine('addAll');
    await _audioSource.addAll(list.map((e) => e.audioSource).toList());
    await _player.seek(Duration.zero, index: list.indexOf(slice.current!));
  }

  Future<void> _syncQueue(int? index) async {
    if (index == null || _queueLength == null) return;

    final slice = await _getQueueSlice(index);
    if (slice == null || slice.current == null) return;

    mediaItem.add(slice.current!.mediaItem);
    queue.add(
      [slice.prev, slice.current, slice.next]
          .map((e) => e?.mediaItem)
          .whereNotNull()
          .toList(),
    );

    final sourceIndex = _player.currentIndex;
    final sourceNeedsNext = sourceIndex == _audioSource.length - 1;
    final sourceNeedsPrev = sourceIndex == 0;

    if (sourceNeedsNext && slice.next != null) {
      log.fine('add');
      await _audioSource.add(slice.next!.audioSource);
    }
    if (sourceNeedsPrev && slice.prev != null) {
      await _insertFirstAudioSource(slice.prev!.audioSource);
    }
  }

  Future<void> _loop(AudioServiceRepeatMode mode) async {
    repeatMode.add(mode);
    await repeatMode.firstWhere((e) => e == mode);

    await _resyncQueue();
  }

  Future<void> _generateShuffleIndicies({
    bool unshuffle = false,
    int? startIndex,
  }) async {
    final indicies = unshuffle
        ? null
        : (List.generate(_queueLength!, (i) => i + 1)
          ..insert(0, 0)
          ..removeLast()
          ..shuffle());

    if (indicies != null && startIndex != null) {
      indicies.removeAt(indicies.indexOf(startIndex));
      indicies.insert(0, startIndex);
    }
    shuffleIndicies.add(indicies);
    await shuffleIndicies.firstWhere((e) => e == indicies);
  }

  Future<void> _shuffle({bool unshuffle = false, int? startIndex}) async {
    await _generateShuffleIndicies(
      unshuffle: unshuffle,
      startIndex: startIndex,
    );

    await _resyncQueue();
  }

  Future<void> _resyncQueue([bool reloadCurrent = false]) async {
    return _syncPool.withResource(() async {
      final currentSource =
          _player.sequenceState?.currentSource as UriAudioSource?;
      if (currentSource == null) return;

      final currentSourceIndex = _player.sequence!.indexOf(currentSource);
      await _pruneAudioSources(currentSourceIndex);

      if (reloadCurrent && !currentSource.uri.isScheme('file')) {
        final position = _player.position;

        final item = (await _getQueueItems([currentSource.tag]))[0];

        await _audioSource.clear();
        await _audioSource.add(item.audioSource);
        await seek(position);
      }

      await _syncQueue(currentSource.tag);
    });
  }

  int _realIndex(int index) {
    if (shuffleIndicies.valueOrNull == null) {
      return index;
    }

    if (index < 0 || index >= shuffleIndicies.value!.length) {
      return -1;
    }

    return shuffleIndicies.value![index];
  }

  int _effectiveIndex(int index) {
    if (shuffleIndicies.valueOrNull == null) {
      return index;
    }

    return shuffleIndicies.value!.indexOf(index);
  }

  Future<void> _insertFirstAudioSource(AudioSource source) {
    log.fine('insert');
    final wait = _audioSource.insert(0, source);
    _currentIndexIgnore.add(1);
    return wait;
  }

  Future<void> _pruneAudioSources(int keepIndex) async {
    if (keepIndex > 0) {
      log.fine('removeRange 0');
      final wait = _audioSource.removeRange(0, keepIndex);
      _currentIndexIgnore.add(0);
      await wait;
    }
    if (_audioSource.length > 1) {
      log.fine('removeRange 1');
      await _audioSource.removeRange(1, _audioSource.length);
    }
  }

  Future<void> _clearAudioSource([bool clearMetadata = false]) async {
    // await _player.stop();
    log.fine('_clearAudioSource');
    await _audioSource.clear();

    if (clearMetadata) {
      mediaItem.add(null);
      queue.add([]);
      queueTitle.add('');
    }
  }

  Future<QueueSlice?> _getQueueSlice(int index) async {
    if (_queueLength == null) {
      return null;
    }

    final effectiveIndex = _effectiveIndex(index);

    int nextIndex;
    int prevIndex;
    if (repeatMode.value == AudioServiceRepeatMode.none) {
      nextIndex = _realIndex(effectiveIndex + 1);
      prevIndex = _realIndex(effectiveIndex - 1);
    } else if (repeatMode.value == AudioServiceRepeatMode.one) {
      nextIndex = index;
      prevIndex = index;
    } else {
      nextIndex = _realIndex(
        effectiveIndex + 1 >= _queueLength! ? 0 : effectiveIndex + 1,
      );
      prevIndex = _realIndex(
        effectiveIndex - 1 < 0 ? _queueLength! - 1 : effectiveIndex - 1,
      );
    }

    final slice = await _getQueueItems([prevIndex, index, nextIndex]);
    final current = slice.firstWhereOrNull(
      (e) => e.queueData.index == index,
    );
    final next = slice.firstWhereOrNull((e) => e.queueData.index == nextIndex);
    final prev = slice.firstWhereOrNull((e) => e.queueData.index == prevIndex);

    return QueueSlice(prev, current, next);
  }

  Future<List<QueueSourceItem>> _getQueueItems(List<int> indexes) async {
    final slice = await _db.queueInIndicies(indexes).get();
    final songs =
        await _db.songsInIds(_sourceId, slice.map((e) => e.id).toList()).get();
    final songMap = {for (var song in songs) song.id: song};

    final albumIds = songs.map((e) => e.albumId).whereNotNull().toSet();
    final albums = await _db.albumsInIds(_sourceId, albumIds.toList()).get();
    final albumArtMap = {
      for (var album in albums) album.id: _mapArtCache(album)
    };

    final queueItems = slice.map(
      (item) => _mapSong(
        songMap[item.id]!,
        MediaItemData(
          sourceId: item.sourceId,
          contextType: item.context,
          contextId: item.contextId,
          artCache: albumArtMap[songMap[item.id]!.albumId],
        ),
        item,
      ),
    );

    return queueItems.toList();
  }

  QueueSourceItem _mapSong(Song song, MediaItemData data, QueueData queueData) {
    final item = MediaItem(
      id: song.id,
      title: song.title,
      artist: song.artist,
      album: song.album,
      duration: song.duration,
      artUri: data.artCache?.thumbnailArtUri ?? _cache.placeholderThumbImageUri,
      extras: {},
    );
    item.data = data;

    return QueueSourceItem(
      mediaItem: item,
      audioSource: song.downloadFilePath != null
          ? AudioSource.file(song.downloadFilePath!, tag: queueData.index)
          : AudioSource.uri(_source.streamUri(song.id), tag: queueData.index),
      queueData: queueData,
    );
  }

  MediaItemArtCache _mapArtCache(Album album) {
    final full = _cache.albumArt(album, thumbnail: false);
    final thumbnail = _cache.albumArt(album, thumbnail: true);
    return MediaItemArtCache(
      fullArtUri: full.uri,
      fullArtCacheKey: full.cacheKey,
      thumbnailArtUri: thumbnail.uri,
      thumbnailArtCacheKey: thumbnail.cacheKey,
    );
  }

  ///
  /// AudioHandler
  ///

  @override
  Future<void> play() async {
    await _player.play();
  }

  @override
  Future<void> pause() async {
    await _player.pause();
  }

  @override
  Future<void> stop() async {
    await _player.stop();
  }

  @override
  Future<void> seek(Duration position) async {
    await _player.seek(position);
  }

  @override
  Future<void> skipToNext() => _player.seekToNext();

  @override
  Future<void> skipToPrevious() => _player.seekToPrevious();

  @override
  Future<void> skipToQueueItem(int index) async {
    if (_player.effectiveIndices == null || _player.effectiveIndices!.isEmpty) {
      return;
    }

    index = _player.effectiveIndices![index];
    if (index < 0 || index >= queue.value.length) {
      return;
    }

    await _player.seek(Duration.zero, index: index);
  }

  @override
  Future<void> setRepeatMode(AudioServiceRepeatMode repeatMode) async {
    if (queueMode.value == QueueMode.radio) {
      switch (repeatMode) {
        case AudioServiceRepeatMode.all:
        case AudioServiceRepeatMode.group:
        case AudioServiceRepeatMode.one:
          return _loop(AudioServiceRepeatMode.one);
        default:
          return _loop(AudioServiceRepeatMode.none);
      }
    }

    return _loop(repeatMode);
  }

  @override
  Future<void> setShuffleMode(AudioServiceShuffleMode shuffleMode) async {
    if (queueMode.value == QueueMode.radio) {
      return;
    }

    switch (shuffleMode) {
      case AudioServiceShuffleMode.all:
      case AudioServiceShuffleMode.group:
        return _shuffle(startIndex: _player.sequenceState?.currentSource?.tag);
      case AudioServiceShuffleMode.none:
        return _shuffle(unshuffle: true);
    }
  }
}
