import 'package:flutter/services.dart';
import 'package:nut_player/src/provider/common_provider.dart';
import 'package:nut_player/src/provider/provider.dart';

import '../legacy/video_player_value.dart';
import 'package:flutter/material.dart';
import 'dart:async';
import 'package:screen_brightness/screen_brightness.dart';
import 'package:nut_player_platform_interface/nut_player_platform_interface.dart'
    show PlayerId, VideoEvent, VideoEventType, PlatformHTTPMethod, PlatformSubtitleType;
import '../model/content_type.dart';
import '../model/platform_player_content_impl.dart';
import '../model/player_statistic_record.dart';
import '../model/player_subtitle_record.dart';
import '../platform/nut_player_platform.dart';

class VideoPlayerController extends ValueNotifier<VideoPlayerValue> {

  // MARK: - Private properties

  Provider _provider;
  PlayerId? _playerId;

  Timer? _timer;

  bool _isDisposed = false;
  bool get _isDisposedOrNotInitialized => _isDisposed || !value.isInitialized;

  Completer<void>? _creationHandler;

  _VideoAppLifeCycleObserver? _lifeCycleObserver;
  StreamSubscription<VideoEvent>? _eventSubscription;

  // MARK: - Constructors

  // Private constructor
  VideoPlayerController._(this._provider, super.value);

  @override
  Future<void> dispose() async {
    if (_isDisposed) {
      return;
    }

    final playerId = _playerId;
    if (playerId != null) {

      final completionHandler = _creationHandler;
      if (completionHandler != null) {
        await completionHandler.future;
      }

      _timer?.cancel();
      _timer = null;

      await _eventSubscription?.cancel();
      _eventSubscription = null;

      await nutPlayerPlatform.dispose(playerId);

      _lifeCycleObserver?.dispose();
      _lifeCycleObserver = null;
    }

    _isDisposed = true;
    super.dispose();
  }

  // MARK: - Factories

  factory VideoPlayerController.network(String urlPath, {bool isLive = false}) {
    return VideoPlayerController._(
        CommonProvider.url(urlPath, isLive: isLive),
        const VideoPlayerValue(duration: Duration.zero)
    );
  }

  factory VideoPlayerController.content({
    required ContentType content,
    List<PlayerStatisticRecord> statistics = const [],
    List<PlayerSubtitleRecord> subtitles = const []
  }) {
    return VideoPlayerController._(CommonProvider.content(
        content: content, statistics: statistics, subtitles: subtitles),
        const VideoPlayerValue(duration: Duration.zero)
    );
  }

  factory VideoPlayerController.provider(Provider provider) {
    return VideoPlayerController._(provider, const VideoPlayerValue(duration: Duration.zero));
  }

  // MARK: - Public properties

  PlayerId? get playerId => _playerId;

  Stream<String> get logStream => nutPlayerPlatform.logStream();
  Stream<Object> get skinActionStream => nutPlayerPlatform.skinActionStream();

  /// The position in the current video.
  Future<Duration?> get position async {
    final playerId = _playerId;
    if (_isDisposed || playerId == null) {
      return null;
    }

    return nutPlayerPlatform.getPosition(playerId);
  }

  // MARK: - Public methods

  @override
  void removeListener(VoidCallback listener) {
    // Prevent VideoPlayer from causing an exception to be thrown when attempting to
    // remove its own listener after the controller has already been disposed.
    if (!_isDisposed) {
      super.removeListener(listener);
    }
  }

  Future<void> initialize({Map<String, Object>? params}) async {
    final bool allowBackgroundPlayback;
    if (params != null && params["enablePip"] is bool) {
      allowBackgroundPlayback = params["enablePip"] as bool;
    } else {
      allowBackgroundPlayback = false;
    }
    if (!allowBackgroundPlayback && _lifeCycleObserver == null) {
      _lifeCycleObserver = _VideoAppLifeCycleObserver(this);
      _lifeCycleObserver?.initialize();
    }

    final playerContent = await _provider.retrieveContent();

    final platformContent = PlatformPlayerContentImpl(
        content: createPlatformContent(playerContent.content),
        statistics: playerContent.statistics.map((record) => PlatformStatisticRecordImpl(
            name: record.name,
            urlTemplate: record.urlTemplate,
            start: record.start,
            delay: record.delay,
            count: record.count,
            method: record.method == HTTPMethod.get ? PlatformHTTPMethod.get : PlatformHTTPMethod.post,
            body: record.body
        )).toList(),
        subtitles: playerContent.subtitles.map((record) => PlatformSubtitleRecordImpl(
            title: record.title,
            type: PlatformSubtitleType.srt,
            url: record.url,
            language: record.language,
        )).toList()
    );

    final playerId = await nutPlayerPlatform.create(content: platformContent, params: params);
    _playerId = playerId;
    _creationHandler?.complete(null);
    _creationHandler = null;

    final Completer<void> initializingCompleter = Completer<void>();

    void eventListener(VideoEvent event) {
      if (_isDisposed) {
        return;
      }
      switch (event.eventType) {
        case VideoEventType.initialized:
          value = value.copyWith(
            isInitialized: true,
            isCompleted: false,
          );
          initializingCompleter.complete(null);
          break;
        case VideoEventType.ready:
          value = value.copyWith(
            duration: event.duration,
            size: event.size,
            subtitles: event.subtitles,
            rotationCorrection: event.rotationCorrection,
            errorDescription: null,
          );
          _applyVolume();
          break;
        case VideoEventType.completed:
          pause().then((void pauseResult) => seek(value.duration));
          value = value.copyWith(isCompleted: true);
          break;
        case VideoEventType.bufferingUpdate:
          value = value.copyWith(buffered: event.buffered);
          break;
        case VideoEventType.bufferingStart:
          value = value.copyWith(isBuffering: true);
          break;
        case VideoEventType.bufferingEnd:
          value = value.copyWith(isBuffering: false);
          break;
        case VideoEventType.isPlayingStateUpdate:
          if (event.isPlaying ?? false) {
            value = value.copyWith(isPlaying: event.isPlaying, isCompleted: false);
          } else {
            value = value.copyWith(isPlaying: event.isPlaying);
          }
          break;
        case VideoEventType.didFetchQualities:
          value = value.copyWith(qualities: event.qualities);
          break;
        case VideoEventType.unknown:
          break;
      }
    }

    void errorListener(Object obj) {
      final PlatformException e = obj as PlatformException;
      value = VideoPlayerValue.erroneous(e.message!);
      _timer?.cancel();
      if (!initializingCompleter.isCompleted) {
        initializingCompleter.completeError(obj);
      }
    }

    _eventSubscription = nutPlayerPlatform
        .videoEventsFor(playerId)
        .listen(eventListener, onError: errorListener);

    return initializingCompleter.future;
  }

  /// Play video.
  Future<void> play() async {
    if (value.position == value.duration) {
      await seek(Duration.zero);
    }

    value = value.copyWith(isPlaying: true);
    await _applyPlayPause();
  }

  /// Pauses the video.
  Future<void> pause() async {
    value = value.copyWith(isPlaying: false);
    await _applyPlayPause();
  }

  /// Завершение видео.
  Future<void> end() async {
    final playerId = _playerId;
    if (_isDisposedOrNotInitialized || playerId == null) {
      return;
    }
    await nutPlayerPlatform.end(playerId);
  }

  /// Seek video.
  Future<void> seek(Duration position) async {
    final playerId = _playerId;
    if (_isDisposedOrNotInitialized || playerId == null) {
      return;
    }

    if (position > value.duration) {
      position = value.duration;
    } else if (position < Duration.zero) {
      position = Duration.zero;
    }
    await nutPlayerPlatform.seek(playerId, position);
    _updatePosition(position);
  }

  Future<void> setVolume(double volume) async {
    value = value.copyWith(volume: volume.clamp(0.0, 1.0));
    await _applyVolume();
  }

  /// Sets subtitle by the id.
  Future<void> setSubtitles(String subtitleID) async {
    final playerId = _playerId;
    if (_isDisposedOrNotInitialized || playerId == null) {
      return;
    }
    await nutPlayerPlatform.setSubtitle(playerId, subtitleID);
  }

  // Установить качество по идентификатору
  Future<void> setQuality(String qualityID) async {
    final playerId = _playerId;
    if (_isDisposedOrNotInitialized || playerId == null) {
      return;
    }
    await nutPlayerPlatform.setQuality(playerId, qualityID);
  }

  Future<void> setLog(String limitOutputLevel) async {
    await nutPlayerPlatform.setLimitOutputLevel(limitOutputLevel);
  }

  Future<void> setBrightness(double brightness) async {
    await ScreenBrightness().setScreenBrightness(brightness);
  }

  Future<void> setPlaybackSpeed(double speed) async {
    if (speed < 0) {
      throw ArgumentError.value(
        speed,
        'Negative playback speeds are generally unsupported.',
      );
    } else if (speed == 0) {
      throw ArgumentError.value(
        speed,
        'Zero playback speed is generally unsupported. Consider using [pause].',
      );
    }

    value = value.copyWith(playbackSpeed: speed);
    await _applyPlaybackSpeed();
  }

  // MARK: - Private methods

  Future<void> _applyPlayPause() async {
    final playerId = _playerId;
    if (_isDisposedOrNotInitialized || playerId == null) {
      return;
    }

    if (value.isPlaying) {
      await nutPlayerPlatform.play(playerId);

      // Cancel previous timer.
      _timer?.cancel();
      _timer = Timer.periodic(
        const Duration(milliseconds: 500),
            (Timer timer) async {
          if (_isDisposed) {
            return;
          }
          final Duration? newPosition = await position;
          if (newPosition == null) {
            return;
          }
          _updatePosition(newPosition);
        },
      );

      // This ensures that the correct playback speed is always applied when
      // playing back. This is necessary because we do not set playback speed
      // when paused.
      await _applyPlaybackSpeed();
    } else {
      _timer?.cancel();
      await nutPlayerPlatform.pause(playerId);
    }
  }

  Future<void> _applyPlaybackSpeed() async {
    final playerId = _playerId;
    if (_isDisposedOrNotInitialized || playerId == null) {
      return;
    }

    // Setting the playback speed on iOS will trigger the video to play. We
    // prevent this from happening by not applying the playback speed until
    // the video is manually played from Flutter.
    if (!value.isPlaying) {
      return;
    }

    await nutPlayerPlatform.setPlaybackSpeed(playerId, value.playbackSpeed);
  }

  Future<void> _applyVolume() async {
    final playerId = _playerId;
    final volume = value.volume;
    if (_isDisposedOrNotInitialized ||
        playerId == null ||
        volume == null) {
      return;
    }

    await nutPlayerPlatform.setVolume(playerId, volume);
  }

  void _updatePosition(Duration position) {
    value = value.copyWith(
      position: position,
      isCompleted: position == value.duration,
    );
  }

}

class _VideoAppLifeCycleObserver extends Object with WidgetsBindingObserver {
  _VideoAppLifeCycleObserver(this._controller);

  bool _wasPlayingBeforePause = false;
  final VideoPlayerController _controller;

  void initialize() {
    final value = _ambiguate(WidgetsBinding.instance);
    if (value != null) {
      value.addObserver(this);
    }
  }

  @override
  void didChangeAppLifecycleState(AppLifecycleState state) {
    if (state == AppLifecycleState.paused) {
      _wasPlayingBeforePause = _controller.value.isPlaying;
      _controller.pause();
    } else if (state == AppLifecycleState.resumed) {
      if (_wasPlayingBeforePause) {
        _controller.play();
      }
    }
  }

  void dispose() {
    final value = _ambiguate(WidgetsBinding.instance);
    if (value != null) {
      value.removeObserver(this);
    }
  }
}

T? _ambiguate<T>(T? value) => value;