import '../utils/isolate_helper.dart';
import 'dart:typed_data';

import 'plex_client.dart';
import 'scrub_preview_source.dart';
import '../utils/app_logger.dart';

/// A single BIF thumbnail entry: timestamp in milliseconds + JPEG bytes.
typedef BifEntry = ({int timestampMs, Uint8List imageBytes});

/// Parse raw BIF file bytes into a list of thumbnail entries.
///
/// BIF format:
///   - 0..7   : magic bytes (0x89 "BIF" 0x0D 0x0A 0x1A 0x0A)
///   - 8..11  : version (uint32 LE)
///   - 12..15 : image count (uint32 LE)
///   - 16..19 : timestamp multiplier (uint32 LE, ms per unit; 0 = 1000)
///   - 20..63 : reserved
///   - 64..   : index table — (imageCount + 1) entries of 8 bytes each:
///              [timestamp (uint32 LE), offset (uint32 LE)]
///              The last entry is a sentinel (timestamp = 0xFFFFFFFF).
///
/// Top-level function so it can be passed to [Isolate.run].
List<BifEntry> _parseBifBytes(Uint8List bytes) {
  if (bytes.length < 64) return [];

  final data = ByteData.sublistView(bytes);

  // Validate magic: 0x89 B I F 0x0D 0x0A 0x1A 0x0A
  const magic = [0x89, 0x42, 0x49, 0x46, 0x0D, 0x0A, 0x1A, 0x0A];
  for (var i = 0; i < magic.length; i++) {
    if (bytes[i] != magic[i]) return [];
  }

  final imageCount = data.getUint32(12, Endian.little);
  var timestampMultiplier = data.getUint32(16, Endian.little);
  if (timestampMultiplier == 0) timestampMultiplier = 1000;

  // Index table starts at byte 64; each entry is 8 bytes.
  // There are (imageCount + 1) entries (last is sentinel).
  final indexTableSize = (imageCount + 1) * 8;
  if (bytes.length < 64 + indexTableSize) return [];

  final entries = <BifEntry>[];
  for (var i = 0; i < imageCount; i++) {
    final entryOffset = 64 + i * 8;
    final timestamp = data.getUint32(entryOffset, Endian.little);
    final imgOffset = data.getUint32(entryOffset + 4, Endian.little);

    // Next entry's offset gives us the end of this image's data.
    final nextEntryOffset = 64 + (i + 1) * 8;
    final nextImgOffset = data.getUint32(nextEntryOffset + 4, Endian.little);

    if (nextImgOffset <= imgOffset || nextImgOffset > bytes.length) continue;

    entries.add((
      timestampMs: timestamp * timestampMultiplier,
      imageBytes: Uint8List.sublistView(bytes, imgOffset, nextImgOffset),
    ));
  }

  return entries;
}

/// Caches a full BIF file in memory and serves thumbnails by timestamp.
class BifThumbnailService implements ScrubPreviewSource {
  List<BifEntry>? _entries;

  /// Download and parse the BIF file for [partId].
  /// Returns silently on failure (thumbnails simply won't be available).
  Future<void> load(PlexClient client, int partId) async {
    _entries = null;
    try {
      final bytes = await client.downloadBifFile(partId);
      if (bytes == null || bytes.isEmpty) return;
      if (bytes.length > 50 * 1024 * 1024) {
        appLogger.w('BIF file too large (${bytes.length} bytes), skipping');
        return;
      }
      _entries = await tryIsolateRun(() => _parseBifBytes(bytes));
    } catch (e) {
      appLogger.w('BIF download/parse failed', error: e);
    }
  }

  /// Whether thumbnails have been loaded successfully.
  @override
  bool get isAvailable => _entries != null && _entries!.isNotEmpty;

  @override
  ScrubFrame? getFrame(Duration time) {
    final bytes = getThumbnail(time);
    if (bytes == null) return null;
    return BytesScrubFrame(bytes);
  }

  /// Return the JPEG bytes for the thumbnail nearest to [time].
  /// Uses binary search for O(log n) lookup.
  Uint8List? getThumbnail(Duration time) {
    final entries = _entries;
    if (entries == null || entries.isEmpty) return null;

    final ms = time.inMilliseconds;

    // Binary search for the largest timestamp <= ms.
    var lo = 0;
    var hi = entries.length - 1;
    while (lo < hi) {
      final mid = (lo + hi + 1) ~/ 2; // bias right
      if (entries[mid].timestampMs <= ms) {
        lo = mid;
      } else {
        hi = mid - 1;
      }
    }

    return entries[lo].imageBytes;
  }

  /// Release cached data.
  @override
  void dispose() {
    _entries = null;
  }
}
