import 'dart:async';
import 'dart:io';

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

import '../media/media_server_client.dart';
import '../services/device_performance.dart';
import '../utils/media_image_helper.dart';
import 'optimized_media_image.dart';

Future<bool> _defaultLocalFileExists(File file) => file.exists();

/// Displays server artwork and rotates through multiple backdrops in order.
///
/// The settled image remains visible until the incoming provider produces a
/// frame. Failed candidates are skipped without flashing an empty frame.
class CyclingMediaBackdrop extends StatefulWidget {
  const CyclingMediaBackdrop({
    super.key,
    required this.mediaKey,
    required this.imagePaths,
    required this.client,
    required this.width,
    required this.height,
    required this.fallbackColor,
    this.fallbackImagePaths = const [],
    this.localArtworkPathResolver,
    this.imageProviderResolver,
    this.allowNetwork = true,
    this.localFileExists = _defaultLocalFileExists,
    this.active = true,
    this.fit = BoxFit.cover,
    this.alignment = Alignment.center,
    this.rotationInterval = const Duration(seconds: 10),
    this.fadeDuration = const Duration(milliseconds: 280),
  });

  final Object? mediaKey;
  final List<String> imagePaths;
  final List<String> fallbackImagePaths;
  final MediaServerClient? client;
  final String? Function(String artworkPath)? localArtworkPathResolver;

  /// Overrides provider construction for deterministic widget tests.
  @visibleForTesting
  final ImageProvider? Function(String artworkPath)? imageProviderResolver;

  /// Overrides local file checks for deterministic widget tests.
  @visibleForTesting
  final Future<bool> Function(File file) localFileExists;
  final bool allowNetwork;
  final bool active;
  final double width;
  final double height;
  final BoxFit fit;
  final Alignment alignment;
  final Color fallbackColor;
  final Duration rotationInterval;
  final Duration fadeDuration;

  @override
  State<CyclingMediaBackdrop> createState() => _CyclingMediaBackdropState();
}

class _CyclingMediaBackdropState extends State<CyclingMediaBackdrop> with WidgetsBindingObserver {
  Timer? _rotationTimer;
  late List<String> _rotationPaths;
  late List<String> _fallbackPaths;
  final Set<String> _failedPaths = <String>{};
  final Set<String> _pendingProviderFailures = <String>{};
  int _rotationIndex = 0;
  int _fallbackIndex = 0;
  bool _lifecycleResumed = true;
  bool _tickerEnabled = true;
  bool _disableAnimations = false;

  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addObserver(this);
    _lifecycleResumed = switch (WidgetsBinding.instance.lifecycleState) {
      null || AppLifecycleState.resumed => true,
      _ => false,
    };
    _replacePaths();
  }

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    _tickerEnabled = TickerMode.valuesOf(context).enabled;
    final disableAnimations = MediaQuery.maybeOf(context)?.disableAnimations ?? false;
    if (_disableAnimations != disableAnimations) {
      _disableAnimations = disableAnimations;
    }
    _restartRotationTimer();
  }

  @override
  void didUpdateWidget(covariant CyclingMediaBackdrop oldWidget) {
    super.didUpdateWidget(oldWidget);
    final pathsChanged =
        widget.mediaKey != oldWidget.mediaKey ||
        !listEquals(widget.imagePaths, oldWidget.imagePaths) ||
        !listEquals(widget.fallbackImagePaths, oldWidget.fallbackImagePaths);
    if (pathsChanged) {
      _replacePaths();
    }
    if (pathsChanged || widget.active != oldWidget.active || widget.rotationInterval != oldWidget.rotationInterval) {
      _restartRotationTimer();
    }
  }

  @override
  void didChangeAppLifecycleState(AppLifecycleState state) {
    _lifecycleResumed = state == AppLifecycleState.resumed;
    if (_lifecycleResumed) {
      _restartRotationTimer();
    } else {
      _rotationTimer?.cancel();
      _rotationTimer = null;
    }
  }

  @override
  void dispose() {
    WidgetsBinding.instance.removeObserver(this);
    _rotationTimer?.cancel();
    super.dispose();
  }

  void _replacePaths() {
    _rotationPaths = _uniquePaths(widget.imagePaths);
    final rotating = _rotationPaths.toSet();
    _fallbackPaths = _uniquePaths(widget.fallbackImagePaths).where((path) => !rotating.contains(path)).toList();
    _failedPaths.clear();
    _pendingProviderFailures.clear();
    _rotationIndex = 0;
    _fallbackIndex = 0;
  }

  static List<String> _uniquePaths(List<String> paths) {
    if (paths.isEmpty) return const [];
    final unique = <String>[];
    for (final path in paths) {
      if (path.isEmpty || unique.contains(path)) continue;
      unique.add(path);
    }
    return unique;
  }

  int get _usableRotationCount => _rotationPaths.where((path) => !_failedPaths.contains(path)).length;

  bool get _canRotate =>
      widget.active && _lifecycleResumed && _tickerEnabled && !_disableAnimations && _usableRotationCount > 1;

  void _restartRotationTimer() {
    _rotationTimer?.cancel();
    _rotationTimer = null;
    if (!_canRotate) return;
    _rotationTimer = Timer(widget.rotationInterval, _handleRotationTimer);
  }

  void _handleRotationTimer() {
    _rotationTimer = null;
    if (!mounted) return;
    if (!_canRotate) return;
    _advanceRotation();
    _restartRotationTimer();
  }

  void _advanceRotation() {
    if (_rotationPaths.isEmpty) return;
    for (var offset = 1; offset <= _rotationPaths.length; offset++) {
      final next = (_rotationIndex + offset) % _rotationPaths.length;
      if (_failedPaths.contains(_rotationPaths[next])) continue;
      if (next == _rotationIndex) return;
      setState(() => _rotationIndex = next);
      return;
    }
  }

  String? get _currentPath {
    if (_rotationPaths.isNotEmpty && !_failedPaths.contains(_rotationPaths[_rotationIndex])) {
      return _rotationPaths[_rotationIndex];
    }
    for (var offset = 0; offset < _rotationPaths.length; offset++) {
      final index = (_rotationIndex + offset) % _rotationPaths.length;
      if (!_failedPaths.contains(_rotationPaths[index])) return _rotationPaths[index];
    }
    if (_fallbackPaths.isNotEmpty && !_failedPaths.contains(_fallbackPaths[_fallbackIndex])) {
      return _fallbackPaths[_fallbackIndex];
    }
    for (var offset = 0; offset < _fallbackPaths.length; offset++) {
      final index = (_fallbackIndex + offset) % _fallbackPaths.length;
      if (!_failedPaths.contains(_fallbackPaths[index])) return _fallbackPaths[index];
    }
    return null;
  }

  void _handleImageError(Object? key) {
    final path = key is String ? key : null;
    if (!mounted || path == null || _failedPaths.contains(path)) return;
    setState(() {
      _failedPaths.add(path);
      _pendingProviderFailures.remove(path);
      final rotationPosition = _rotationPaths.indexOf(path);
      if (rotationPosition >= 0) {
        for (var offset = 1; offset <= _rotationPaths.length; offset++) {
          final next = (rotationPosition + offset) % _rotationPaths.length;
          if (_failedPaths.contains(_rotationPaths[next])) continue;
          _rotationIndex = next;
          break;
        }
      } else {
        final fallbackPosition = _fallbackPaths.indexOf(path);
        if (fallbackPosition >= 0) {
          for (var offset = 1; offset <= _fallbackPaths.length; offset++) {
            final next = (fallbackPosition + offset) % _fallbackPaths.length;
            if (_failedPaths.contains(_fallbackPaths[next])) continue;
            _fallbackIndex = next;
            break;
          }
        }
      }
    });
    _restartRotationTimer();
  }

  ImageProvider? _providerFor(BuildContext context, String path, File? localFile) {
    final providerOverride = widget.imageProviderResolver;
    if (providerOverride != null) return providerOverride(path);

    final size = MediaQuery.sizeOf(context);
    final width = widget.width.isFinite && widget.width > 0 ? widget.width : size.width;
    final height = widget.height.isFinite && widget.height > 0 ? widget.height : size.height;
    final dpr = MediaImageHelper.effectiveDevicePixelRatio(context);
    final (memWidth, memHeight) = MediaImageHelper.getMemCacheDimensions(
      displayWidth: (width * dpr).round(),
      displayHeight: (height * dpr).round(),
      imageType: ImageType.art,
    );

    if (localFile != null) {
      return MediaImageHelper.boundedDecode(FileImage(localFile), memWidth: memWidth, memHeight: memHeight);
    }
    if (!widget.allowNetwork) return null;

    final imageUrl = MediaImageHelper.getOptimizedImageUrl(
      client: widget.client,
      thumbPath: path,
      maxWidth: width,
      maxHeight: height,
      devicePixelRatio: dpr,
      imageType: ImageType.art,
    );
    if (imageUrl.isEmpty) return null;
    return MediaImageHelper.serverArtworkProvider(imageUrl: imageUrl, memWidth: memWidth, memHeight: memHeight);
  }

  void _reportMissingProvider(String path) {
    if (!_pendingProviderFailures.add(path)) return;
    WidgetsBinding.instance.addPostFrameCallback((_) {
      if (mounted) _handleImageError(path);
    });
  }

  Widget _buildCrossfade(
    BuildContext context,
    String? path, {
    required LocalFileResolution localResolution,
    File? localFile,
  }) {
    final pending = localResolution == LocalFileResolution.pending;
    final provider = path == null || pending ? null : _providerFor(context, path, localFile);
    if (path != null && provider == null && !pending) _reportMissingProvider(path);
    final fadeDuration = _disableAnimations ? Duration.zero : DevicePerformance.reducedDuration(widget.fadeDuration);

    return _BackdropArtworkCrossfade(
      artworkKey: (widget.mediaKey, path),
      imageErrorKey: path,
      image: provider,
      pending: pending,
      duration: fadeDuration,
      fit: widget.fit,
      alignment: widget.alignment,
      fallbackColor: widget.fallbackColor,
      onImageError: _handleImageError,
    );
  }

  @override
  Widget build(BuildContext context) {
    final path = _currentPath;
    if (path == null) {
      return _buildCrossfade(context, null, localResolution: LocalFileResolution.missing);
    }

    final localPath = widget.localArtworkPathResolver?.call(path);
    if (localPath == null) {
      return _buildCrossfade(context, path, localResolution: LocalFileResolution.missing);
    }

    return ResolvedLocalFile(
      path: localPath,
      cacheMissing: true,
      fileExists: widget.localFileExists,
      builder: (context, resolution, file) =>
          _buildCrossfade(context, path, localResolution: resolution, localFile: file),
    );
  }
}

class _BackdropArtworkCrossfade extends StatefulWidget {
  const _BackdropArtworkCrossfade({
    required this.artworkKey,
    required this.imageErrorKey,
    required this.image,
    required this.pending,
    required this.duration,
    required this.fit,
    required this.alignment,
    required this.fallbackColor,
    required this.onImageError,
  });

  final Object? artworkKey;
  final Object? imageErrorKey;
  final ImageProvider? image;
  final bool pending;
  final Duration duration;
  final BoxFit fit;
  final Alignment alignment;
  final Color fallbackColor;
  final ValueChanged<Object?> onImageError;

  @override
  State<_BackdropArtworkCrossfade> createState() => _BackdropArtworkCrossfadeState();
}

class _BackdropArtworkCrossfadeState extends State<_BackdropArtworkCrossfade> with SingleTickerProviderStateMixin {
  late final AnimationController _fade;
  late Object? _currentKey = widget.artworkKey;
  late ImageProvider? _base = widget.pending ? null : widget.image;
  late Object? _baseErrorKey = widget.imageErrorKey;
  ImageProvider? _incoming;
  Object? _incomingErrorKey;
  bool _incomingIsColor = false;
  bool _fadeStarted = false;

  @override
  void initState() {
    super.initState();
    _fade = AnimationController(vsync: this, duration: widget.duration);
  }

  @override
  void didUpdateWidget(covariant _BackdropArtworkCrossfade oldWidget) {
    super.didUpdateWidget(oldWidget);
    _fade.duration = widget.duration;
    if (widget.artworkKey == _currentKey) {
      if (widget.pending) return;
      if (oldWidget.pending) {
        _transitionToIncoming();
        return;
      }
      if (widget.image != null && widget.image != _base && _incoming == null) {
        _base = widget.image;
        _baseErrorKey = widget.imageErrorKey;
      }
      return;
    }

    _currentKey = widget.artworkKey;
    if (widget.pending) {
      if (_base == null && _incoming != null && _fade.value == 1) {
        _base = _incoming;
        _baseErrorKey = _incomingErrorKey;
      }
      setState(() {
        _fade.stop();
        _dropIncoming();
      });
      return;
    }
    if (widget.image != null && widget.image == _base) {
      _baseErrorKey = widget.imageErrorKey;
      _dropIncoming();
      return;
    }

    _transitionToIncoming();
  }

  void _transitionToIncoming() {
    setState(() {
      _fade.stop();
      _fade.value = 0;
      _fadeStarted = false;
      _incoming = widget.image;
      _incomingErrorKey = widget.imageErrorKey;
      _incomingIsColor = widget.image == null;
      if (_incoming == null) _startFade();
    });
  }

  @override
  void dispose() {
    _fade.dispose();
    super.dispose();
  }

  void _startFade() {
    if (_fadeStarted) return;
    _fadeStarted = true;
    _fade.forward().whenComplete(_promoteIncoming);
  }

  void _promoteIncoming() {
    if (!mounted) return;
    setState(() {
      _base = _incoming;
      _baseErrorKey = _incomingErrorKey;
      _dropIncoming();
    });
  }

  void _dropIncoming() {
    _incoming = null;
    _incomingErrorKey = null;
    _incomingIsColor = false;
    _fadeStarted = false;
    _fade.value = 0;
  }

  void _reportError(Object? imageErrorKey) {
    WidgetsBinding.instance.addPostFrameCallback((_) {
      if (mounted) widget.onImageError(imageErrorKey);
    });
  }

  Widget _image(ImageProvider provider, Object? imageErrorKey, {Animation<double>? opacity}) {
    final incoming = opacity != null;
    return Image(
      key: incoming ? ValueKey<ImageProvider>(provider) : null,
      image: provider,
      fit: widget.fit,
      alignment: widget.alignment,
      excludeFromSemantics: true,
      gaplessPlayback: true,
      opacity: opacity,
      frameBuilder: !incoming
          ? null
          : (context, child, frame, wasSynchronouslyLoaded) {
              if (frame != null || wasSynchronouslyLoaded) {
                _startFade();
              }
              return child;
            },
      errorBuilder: (context, error, stackTrace) {
        _reportError(imageErrorKey);
        return incoming ? const SizedBox.shrink() : ColoredBox(color: widget.fallbackColor);
      },
    );
  }

  @override
  Widget build(BuildContext context) {
    return Stack(
      fit: StackFit.expand,
      children: [
        if (_base != null) _image(_base!, _baseErrorKey) else ColoredBox(color: widget.fallbackColor),
        if (_incoming != null) _image(_incoming!, _incomingErrorKey, opacity: _fade),
        if (_incomingIsColor)
          AnimatedBuilder(
            animation: _fade,
            builder: (context, _) =>
                ColoredBox(color: widget.fallbackColor.withValues(alpha: widget.fallbackColor.a * _fade.value)),
          ),
      ],
    );
  }
}
