import 'dart:async';
import 'dart:ui' as ui;

import 'package:flutter/foundation.dart';
import 'package:flutter/widgets.dart';

import '../../contracts/cancellation.dart';


int? streamOpenErrorHttpStatus(String error) {
  final match = RegExp(r'\(http (\d{3})\)').firstMatch(error.toLowerCase());
  if (match == null) return null;
  return int.tryParse(match.group(1)!);
}


bool isTerminalStreamOpenError(String error) {
  final status = streamOpenErrorHttpStatus(error);
  if (status == null) return false;
  if (status == 408 || status == 429) return false;
  return status >= 400 && status < 500;
}

abstract class PlayerEngine {
  PlayerStreams get stream;
  PlayerState get state;
  ValueListenable<int?> get textureId;
  ValueListenable<int> get textureVersion;
  ValueNotifier<ui.Size?> get videoSize;


  String get displayName;


  bool get usesPlatformSurface => false;


  Future<int?> totalRxBytes() async => null;


  bool isNetworkStreamError(String error) => false;

  void configureHardwareDecoding(String mode);


  Future<void> open(
    String url, {
    Duration start = Duration.zero,
    bool play = true,
    Map<String, String>? headers,
    CancellationToken? cancel,
  });

  Future<void> play();
  Future<void> pause();
  Future<void> stop();
  Future<void> seek(Duration position);
  Future<void> setRate(double rate);
  Future<void> setVolume(double volume);
  Future<void> toggleMute();
  Future<void> setAudioTrack(AudioTrack track);
  Future<void> setSubtitleTrack(SubtitleTrack track);


  void setNativeSubtitleRendering(bool enabled) {}

  void setSubtitleStyle({
    required double fontSize,
    required double bgOpacity,
    required int position,
  });


  Widget buildVideoView(BuildContext context, ValueListenable<BoxFit> fit);


  ValueListenable<PlaybackStats?> get debugStats => kEmptyPlaybackStats;

  Future<void> dispose();
}


final ValueNotifier<PlaybackStats?> kEmptyPlaybackStats =
    ValueNotifier<PlaybackStats?>(null);


@immutable
class PlaybackStats {
  final Duration position;


  final int bufferLeadMs;
  final double bitrateMbps;
  final double rate;


  final String decoder;
  final bool isDolbyVision;
  final String hwdec;

  const PlaybackStats({
    required this.position,
    required this.bufferLeadMs,
    required this.bitrateMbps,
    required this.rate,
    required this.decoder,
    required this.isDolbyVision,
    required this.hwdec,
  });
}

class PlayerStreams {
  final StreamController<Duration> _position =
      StreamController<Duration>.broadcast();
  final StreamController<Duration> _duration =
      StreamController<Duration>.broadcast();
  final StreamController<Duration> _buffer =
      StreamController<Duration>.broadcast();
  final StreamController<bool> _playing = StreamController<bool>.broadcast();
  final StreamController<bool> _completed = StreamController<bool>.broadcast();
  final StreamController<PlayerTracks> _tracks =
      StreamController<PlayerTracks>.broadcast();
  final StreamController<PlayerTrack> _track =
      StreamController<PlayerTrack>.broadcast();
  final StreamController<List<String>> _subtitle =
      StreamController<List<String>>.broadcast();
  final StreamController<double> _volume = StreamController<double>.broadcast();
  final StreamController<String> _error = StreamController<String>.broadcast();

  Stream<Duration> get position => _position.stream;
  Stream<Duration> get duration => _duration.stream;
  Stream<Duration> get buffer => _buffer.stream;
  Stream<bool> get playing => _playing.stream;
  Stream<bool> get completed => _completed.stream;
  Stream<PlayerTracks> get tracks => _tracks.stream;
  Stream<PlayerTrack> get track => _track.stream;
  Stream<List<String>> get subtitle => _subtitle.stream;
  Stream<double> get volume => _volume.stream;
  Stream<String> get error => _error.stream;

  void addPosition(Duration value) => _position.add(value);
  void addDuration(Duration value) => _duration.add(value);
  void addBuffer(Duration value) => _buffer.add(value);
  void addPlaying(bool value) => _playing.add(value);
  void addCompleted(bool value) => _completed.add(value);
  void addTracks(PlayerTracks value) => _tracks.add(value);
  void addTrack(PlayerTrack value) => _track.add(value);
  void addSubtitle(List<String> value) => _subtitle.add(value);
  void addVolume(double value) => _volume.add(value);
  void addError(String value) => _error.add(value);

  void dispose() {
    _position.close();
    _duration.close();
    _buffer.close();
    _playing.close();
    _completed.close();
    _tracks.close();
    _track.close();
    _subtitle.close();
    _volume.close();
    _error.close();
  }
}

class PlayerState {
  Duration position = Duration.zero;
  Duration duration = Duration.zero;
  Duration buffer = Duration.zero;
  bool playing = false;
  bool completed = false;
  double volume = 100.0;
  double rate = 1.0;
  PlayerTracks tracks = const PlayerTracks();
  PlayerTrack track = const PlayerTrack();
  List<String> subtitle = const [];
  String? error;
}

@immutable
class PlayerTracks {
  final List<AudioTrack> audio;
  final List<SubtitleTrack> subtitle;

  const PlayerTracks({
    this.audio = const <AudioTrack>[],
    this.subtitle = const <SubtitleTrack>[],
  });

  List<SubtitleTrack> get embeddedSubtitles =>
      subtitle.where((t) => t.id != 'auto' && t.id != 'no').toList();

  List<AudioTrack> get embeddedAudio =>
      audio.where((t) => t.id != 'auto' && t.id != 'no').toList();

  @override
  bool operator ==(Object other) {
    return other is PlayerTracks &&
        listEquals(other.audio, audio) &&
        listEquals(other.subtitle, subtitle);
  }

  @override
  int get hashCode =>
      Object.hash(Object.hashAll(audio), Object.hashAll(subtitle));
}

@immutable
class PlayerTrack {
  final AudioTrack audio;
  final SubtitleTrack subtitle;

  const PlayerTrack({
    this.audio = const AudioTrack.auto(),
    this.subtitle = const SubtitleTrack.no(),
  });

  PlayerTrack copyWith({AudioTrack? audio, SubtitleTrack? subtitle}) {
    return PlayerTrack(
      audio: audio ?? this.audio,
      subtitle: subtitle ?? this.subtitle,
    );
  }

  @override
  bool operator ==(Object other) {
    return other is PlayerTrack &&
        other.audio == audio &&
        other.subtitle == subtitle;
  }

  @override
  int get hashCode => Object.hash(audio, subtitle);
}

@immutable
class AudioTrack {
  final String id;
  final int? index;
  final String? title;
  final String? language;
  final bool no;
  final bool auto;
  final Object? native;

  const AudioTrack({
    required this.id,
    this.index,
    this.title,
    this.language,
    this.no = false,
    this.auto = false,
    this.native,
  });

  const AudioTrack.no()
    : id = 'no',
      index = null,
      title = null,
      language = null,
      no = true,
      auto = false,
      native = null;

  const AudioTrack.auto()
    : id = 'auto',
      index = null,
      title = null,
      language = null,
      no = false,
      auto = true,
      native = null;

  @override
  bool operator ==(Object other) {
    return other is AudioTrack &&
        other.id == id &&
        other.index == index &&
        other.title == title &&
        other.language == language &&
        other.no == no &&
        other.auto == auto;
  }

  @override
  int get hashCode => Object.hash(id, index, title, language, no, auto);
}

@immutable
class SubtitleTrack {
  final String id;
  final int? index;
  final String? title;
  final String? language;
  final bool no;
  final Object? native;

  const SubtitleTrack({
    required this.id,
    this.index,
    this.title,
    this.language,
    this.no = false,
    this.native,
  });

  const SubtitleTrack.no()
    : id = 'no',
      index = null,
      title = null,
      language = null,
      no = true,
      native = null;

  @override
  bool operator ==(Object other) {
    return other is SubtitleTrack &&
        other.id == id &&
        other.index == index &&
        other.title == title &&
        other.language == language &&
        other.no == no;
  }

  @override
  int get hashCode => Object.hash(id, index, title, language, no);
}
