import 'dart:async';
import 'dart:convert';
import 'dart:io';
import 'dart:math';

enum FileKind { image, svg, pdf, text, video, audio, unknown }

const Set<String> _imageExtensions = {
  'png',
  'jpg',
  'jpeg',
  'webp',
  'gif',
  'bmp',
  'wbmp',
};

const Set<String> _videoExtensions = {
  'mp4',
  'm4v',
  'mov',
  'webm',
  'mkv',
  '3gp',
};

/// ogg/opus/flac are Android-only; iOS init errors fall through to the
/// "format not supported" message.
const Set<String> _audioExtensions = {
  'mp3',
  'm4a',
  'aac',
  'wav',
  'flac',
  'ogg',
  'oga',
  'opus',
};

/// Unknown extensions still get a content sniff via [_looksLikeText].
const Set<String> _textExtensions = {
  'txt', 'md', 'markdown', 'rst', 'log',
  'json', 'json5', 'xml', 'yaml', 'yml', 'toml',
  'csv', 'tsv', 'tab',
  'ini', 'conf', 'cfg', 'env', 'properties',
  'html', 'htm', 'xhtml',
  'css', 'scss', 'sass', 'less',
  'js', 'mjs', 'cjs', 'ts', 'jsx', 'tsx',
  'dart', 'java', 'kt', 'kts', 'groovy', 'scala', 'swift',
  'py', 'rb', 'pl', 'lua', 'r',
  'go', 'rs', 'zig',
  'c', 'cpp', 'cc', 'cxx', 'h', 'hpp', 'cs', 'm', 'mm',
  'php', 'sh', 'bash', 'zsh', 'fish', 'ps1', 'bat', 'cmd',
  'sql', 'graphql', 'gql',
  'gitignore', 'gitattributes', 'editorconfig', 'dockerignore',
  'dockerfile', 'makefile', 'cmake',
  'tex', 'bib',
  'srt', 'vtt',
};

/// Detects the [FileKind] of the file at [path] from its extension, falling
/// back to an 8 KB content sniff ([_looksLikeText]) for unknown extensions.
Future<FileKind> detectFileKind(String path) async {
  final ext = path.split('.').last.toLowerCase();
  if (_imageExtensions.contains(ext)) return FileKind.image;
  if (ext == 'svg') return FileKind.svg;
  if (ext == 'pdf') return FileKind.pdf;
  if (_videoExtensions.contains(ext)) return FileKind.video;
  if (_audioExtensions.contains(ext)) return FileKind.audio;
  if (_textExtensions.contains(ext)) return FileKind.text;
  if (await _looksLikeText(path)) return FileKind.text;
  return FileKind.unknown;
}

/// 8 KB sniff: NUL bytes or non-UTF-8 sequences disqualify.
Future<bool> _looksLikeText(String path) async {
  final file = File(path);
  RandomAccessFile? raf;
  try {
    final length = await file.length();
    if (length == 0) return true;
    raf = await file.open();
    final sample = await raf.read(min(length, 8192));
    if (sample.contains(0)) return false;
    utf8.decode(sample);
    return true;
  } on Object {
    return false;
  } finally {
    await raf?.close();
  }
}
