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

import 'package:namida/class/media_info.dart';
import 'package:namida/class/replay_gain_data.dart';
import 'package:namida/class/taglib_res.dart';
import 'package:namida/core/extensions.dart';

class FArtwork {
  /// if specified directory to save in.
  /// or to save a new artwork when writing tags.
  File? file;

  /// if no directory to save in was specified.
  Uint8List? bytes;

  int? size;

  bool get hasArtwork => file != null || bytes != null;

  FutureOr<int?> get sizeActual => bytes?.length ?? file?.fileSize();

  FArtwork({
    this.file,
    this.bytes,
    this.size,
  });

  FArtwork.dummy() : file = null, bytes = null, size = null;

  factory FArtwork.fromMap(Map<String, dynamic> map) {
    final art = map["artwork"];
    File? file;
    Uint8List? bytes;
    if (art is String) {
      file = File(art);
    } else if (art is Uint8List) {
      bytes = art;
    }
    return FArtwork(
      file: file,
      bytes: bytes,
      size: map["artworkLength"],
    );
  }

  dynamic toMapValue() => file?.path ?? bytes;

  @override
  String toString() {
    return file?.toString() ?? bytes?.length.toString() ?? 'null';
  }
}

class FTags {
  bool get isValid =>
      title?.isNotEmpty == true || //
      album?.isNotEmpty == true ||
      artist?.isNotEmpty == true ||
      albumArtist?.isNotEmpty == true;

  String? get label => recordLabel;
  String? get date => year;
  String? get style => null; // TODO: add style

  /// Used for bulk extractions.
  final String path;
  final FArtwork artwork;
  final String? title;
  final String? album;
  final String? albumArtist;
  final String? artist;
  final String? composer;
  final String? genre;
  final String? trackNumber;
  final String? trackTotal;
  final String? discNumber;
  final String? discTotal;
  final String? lyrics;
  final String? comment;
  final String? description;
  final String? synopsis;
  final String? year;
  final String? language;
  final String? lyricist;
  final String? djmixer;
  final String? mixer;
  final String? mood;
  final String? rating;
  final String? remixer;
  final String? tags;
  final String? tempo;
  final String? country;
  final String? recordLabel;
  final int? bpm;

  final double? ratingPercentage;
  final ReplayGainData? gainData;
  final FTagsSortInfo? sortInfo;

  const FTags({
    required this.path,
    required this.artwork,
    required this.title,
    required this.album,
    required this.albumArtist,
    required this.artist,
    required this.composer,
    required this.genre,
    required this.trackNumber,
    required this.trackTotal,
    required this.discNumber,
    required this.discTotal,
    required this.lyrics,
    required this.comment,
    required this.description,
    required this.synopsis,
    required this.year,
    required this.language,
    required this.lyricist,
    required this.djmixer,
    required this.mixer,
    required this.mood,
    required this.rating,
    required this.remixer,
    required this.tags,
    required this.tempo,
    required this.country,
    required this.recordLabel,
    required this.bpm,
    required this.ratingPercentage,
    required this.gainData,
    required this.sortInfo,
  });

  const FTags.edit({
    required this.path,
    required this.artwork,
    this.title,
    this.album,
    this.albumArtist,
    this.artist,
    this.composer,
    this.genre,
    this.trackNumber,
    this.trackTotal,
    this.discNumber,
    this.discTotal,
    this.lyrics,
    this.comment,
    this.description,
    this.synopsis,
    this.year,
    this.language,
    this.lyricist,
    this.djmixer,
    this.mixer,
    this.mood,
    this.rating,
    this.remixer,
    this.tags,
    this.tempo,
    this.country,
    this.recordLabel,
    this.bpm,
    this.ratingPercentage,
    this.gainData,
    this.sortInfo,
  });

  static String? _listToString(dynamic list) {
    if (list is! List || list.isEmpty) return null;
    if (list.length == 1) return list[0];
    return list.join('; ');
  }

  static double? ratingToPercentage(String? rating) {
    if (rating == null) return null;
    final value = num.tryParse(rating);
    if (value is double && value <= 1.0) return value;
    if (value == null || value == 0) return null;
    if (value <= 5) return value / 5.0; // 0-5
    return value / 100.0; // 0-100
  }

  // static int ratingPercentageToUnsignedInt(double ratingPercentage) {
  //   return (ratingPercentage * 255).round();
  // }

  // -- upper cased are the ones extracted manually.
  factory FTags.fromMap(Map<String, dynamic> map) {
    var lyricsList = map["lyrics"] as List?;
    if (map["LYRICS"] is String) {
      // -- recreating bcz its fixed length.
      lyricsList = [
        map["LYRICS"] as String,
        ...?lyricsList,
      ];
    }

    final ratingString = map["FMPS_Rating"] ?? map["rating"] ?? map["RATING"];

    return FTags(
      path: map["path"],
      artwork: FArtwork.fromMap(map),
      title: _listToString(map["title"]) ?? map["TITLE"],
      album: map["album"] ?? map["ALBUM"],
      albumArtist: map["albumArtist"] ?? map["ALBUMARTIST"],
      artist: _listToString(map["artist"]) ?? map["ARTIST"],
      composer: _listToString(map["composer"]) ?? map["COMPOSER"],
      genre: _listToString(map["genre"]) ?? map["GENRE"],
      trackNumber: map["trackNumber"] ?? map["TRACKNUMBER"],
      trackTotal: map["trackTotal"] ?? map["TRACKTOTAL"],
      discNumber: map["discNumber"] ?? map["DISCNUMBER"],
      discTotal: map["discTotal"] ?? map["DISCTOTAL"],
      lyrics: lyricsList?.firstWhereEff((e) => e is String ? e.isValidLRC() : false) ?? lyricsList?.firstOrNull,
      comment: _listToString(map["comment"]) ?? map["COMMENT"],
      description: map["description"] ?? map["desc"] ?? map["DESCRIPTION"] ?? map["DESC"],
      synopsis: _listToString(map["synopsis"]) ?? map["synopsis"] ?? map["SYNOPSIS"],
      year: map["year"] ?? map["YEAR"],
      language: _listToString(map["language"]) ?? map["LANGUAGE"],
      lyricist: _listToString(map["lyricist"]) ?? map["LYRICIST"],
      djmixer: _listToString(map["djmixer"]) ?? map["DJMIXER"],
      mixer: _listToString(map["mixer"]) ?? map["MIXER"],
      mood: _listToString(map["mood"]) ?? map["MOOD"],
      rating: ratingString,
      remixer: _listToString(map["remixer"]) ?? map["REMIXER"],
      tags: _listToString(map["tags"]) ?? map["TAGS"],
      tempo: _listToString(map["tempo"]) ?? map["TEMPO"],
      country: _listToString(map["country"]) ?? map["COUNTRY"],
      recordLabel: _listToString(map["recordLabel"]) ?? map["RECORDLABEL"] ?? map["label"] ?? map["LABEL"],
      bpm: MediaInfo.extractInt(map["bpm"]),
      ratingPercentage: ratingToPercentage(ratingString),
      gainData: ReplayGainData.fromPropertiesMap(map),
      sortInfo: FTagsSortInfo.fromAndroidMap(map),
    );
  }

  Map<String, dynamic> toMap() {
    return <String, dynamic>{
      "path": path,
      "artwork": artwork.toMapValue(),
      "title": title,
      "album": album,
      "albumArtist": albumArtist,
      "artist": artist,
      "composer": composer,
      "genre": genre,
      "comment": comment,
      "description": description,
      "synopsis": synopsis,
      "year": year,
      "trackNumber": trackNumber,
      "trackTotal": trackTotal,
      "discNumber": discNumber,
      "discTotal": discTotal,
      "lyrics": lyrics,
      "lyricist": lyricist,
      "djmixer": djmixer,
      "mixer": mixer,
      "mood": mood,
      "rating": ratingPercentage ?? rating,
      "remixer": remixer,
      "tags": tags,
      "tempo": tempo,
      "country": country,
      "recordLabel": recordLabel,
      "bpm": bpm,
      "language": language,
      "gainData": gainData?.toMap(),
      "sortInfo": sortInfo?.toMap(),
    };
  }

  List<String>? _createList(String? value) => value == null ? null : List<String>.filled(1, value, growable: false);

  Map<String, List<String>> toTagLibMap() {
    return <String, List<String>>{
      TagLibField.title: ?_createList(title),
      TagLibField.album: ?_createList(album),
      TagLibField.albumArtist: ?_createList(albumArtist),
      TagLibField.artist: ?_createList(artist),
      TagLibField.composer: ?_createList(composer),
      TagLibField.genre: ?_createList(genre),
      TagLibField.style: ?_createList(style),
      TagLibField.trackNumber: ?_createList(trackNumber),
      TagLibField.trackTotal: ?_createList(trackTotal),
      TagLibField.discNumber: ?_createList(discNumber),
      TagLibField.discTotal: ?_createList(discTotal),
      TagLibField.lyrics: ?_createList(lyrics),
      TagLibField.comment: ?_createList(comment),
      TagLibField.description: ?_createList(description),
      TagLibField.synopsis: ?_createList(synopsis),
      TagLibField.date: ?_createList(date),
      TagLibField.language: ?_createList(language),
      TagLibField.lyricist: ?_createList(lyricist),
      TagLibField.remixer: ?_createList(remixer),
      TagLibField.rating: ?_createList(ratingPercentage?.toString()),
      TagLibField.mood: ?_createList(mood),
      TagLibField.tags: ?_createList(tags),
      TagLibField.country: ?_createList(country),
      TagLibField.label: ?_createList(label),
      TagLibField.tempo: ?_createList(tempo),
      TagLibField.mixer: ?_createList(mixer),
      TagLibField.djmixer: ?_createList(djmixer),
      TagLibField.titleSort: ?_createList(sortInfo?.title),
      TagLibField.albumSort: ?_createList(sortInfo?.album),
      TagLibField.albumArtistSort: ?_createList(sortInfo?.albumArtist),
      TagLibField.artistSort: ?_createList(sortInfo?.artist),
      TagLibField.composerSort: ?_createList(sortInfo?.composer),
    };
  }
}

class FTagsSortInfo {
  final String? title, album, albumArtist, artist, composer;

  const FTagsSortInfo._({
    required this.title,
    required this.album,
    required this.albumArtist,
    required this.artist,
    required this.composer,
  });

  static FTagsSortInfo? orNull({
    String? title,
    String? album,
    String? albumArtist,
    String? artist,
    String? composer,
  }) {
    if (title == null && album == null && albumArtist == null && artist == null && composer == null) {
      return null;
    }
    return FTagsSortInfo._(
      title: title,
      album: album,
      albumArtist: albumArtist,
      artist: artist,
      composer: composer,
    );
  }

  static String? _sortKeyJoinerValue(Map tags, String part1, String part2, [String? part3]) {
    final keys = [
      '$part1$part2${part3 ?? ''}',
      '$part1-$part2${part3 == null ? '' : '-$part3'}',
      '${part1.toUpperCase()}${part2.toUpperCase()}${part3?.toUpperCase() ?? ''}',
      '${part1}_$part2${part3 == null ? '' : '_$part3'}',
    ];
    for (final key in keys) {
      final val = tags[key];
      if (val != null) return val;
    }
    return null;
  }

  static FTagsSortInfo? fromFFmpegMap(Map? map) {
    final tags = map?["tags"] as Map? ?? map;
    if (tags == null) return null;

    return FTagsSortInfo.orNull(
      title: _sortKeyJoinerValue(tags, 'title', 'sort'),
      album: _sortKeyJoinerValue(tags, 'album', 'sort'),
      albumArtist: _sortKeyJoinerValue(tags, 'album', 'artist', 'sort') ?? _sortKeyJoinerValue(tags, 'album', 'artists', 'sort'),
      artist: _sortKeyJoinerValue(tags, 'artist', 'sort') ?? _sortKeyJoinerValue(tags, 'artists', 'sort'),
      composer: _sortKeyJoinerValue(tags, 'composer', 'sort'),
    );
  }

  static FTagsSortInfo? fromAndroidMap(Map? map) {
    final sort = map?["sortInfo"] as Map?;
    if (sort == null) return null;
    return FTagsSortInfo.orNull(
      title: sort["title"],
      album: sort["album"],
      albumArtist: sort["albumArtist"],
      artist: sort["artist"],
      composer: sort["composer"],
    );
  }

  static FTagsSortInfo? fromTagLibMap(TagLibPropertiesWrapper properties) {
    return FTagsSortInfo.orNull(
      title: properties.titleSort,
      album: properties.albumSort,
      albumArtist: properties.albumArtistSort,
      artist: properties.artistSort,
      composer: properties.composerSort,
    );
  }

  static FTagsSortInfo? fromMap(Map map) {
    return FTagsSortInfo.orNull(
      title: map["title"],
      album: map["album"],
      albumArtist: map["albumArtist"],
      artist: map["artist"],
      composer: map["composer"],
    );
  }

  Map<String, dynamic> toMap() {
    return <String, dynamic>{
      'title': ?title,
      'album': ?album,
      'albumArtist': ?albumArtist,
      'artist': ?artist,
      'composer': ?composer,
    };
  }
}

class FAudioModel {
  final FTags tags;
  final int? durationMS;
  final int? bitRate;
  final String? channels;
  final String? encodingType;
  final String? format;
  final int? sampleRate;
  final int? bits;
  final bool? isVariableBitRate;
  final bool? isLossless;
  final bool hasError;
  final Map<String, String> errorsMap;

  const FAudioModel({
    required this.tags,
    this.durationMS,
    this.bitRate,
    this.channels,
    this.encodingType,
    this.format,
    this.sampleRate,
    this.bits,
    this.isVariableBitRate,
    this.isLossless,
    this.hasError = false,
    this.errorsMap = const {},
  });

  factory FAudioModel.dummy(String? path, FArtwork? artwork) {
    return FAudioModel(
      tags: FTags.edit(path: path ?? '', artwork: artwork ?? FArtwork(size: 0)),
      hasError: true,
    );
  }

  factory FAudioModel.fromMap(Map<String, dynamic> map) {
    return FAudioModel(
      tags: FTags.fromMap(map),
      durationMS: map["durationMS"],
      bitRate: map["bitRate"],
      channels: map["channels"],
      encodingType: map["encodingType"],
      format: map["format"],
      sampleRate: map["sampleRate"],
      bits: map["bits"],
      isVariableBitRate: map["isVariableBitRate"],
      isLossless: map["isLossless"],
      hasError: map["ERROR_FAULTY"] == true,
      errorsMap: (map["ERRORS"] as Map?)?.cast() ?? {},
    );
  }

  Map<String, dynamic> _toMapMini() {
    final tagsMap = tags.toMap();
    tagsMap.addAll(<String, dynamic>{
      "durationMS": durationMS,
      "bitRate": bitRate,
      "channels": channels,
      "encodingType": encodingType,
      "format": format,
      "sampleRate": sampleRate,
      "bits": bits,
      "isVariableBitRate": isVariableBitRate,
      "isLossless": isLossless,
    });
    return tagsMap;
  }

  Map<String, dynamic> toMap() {
    final map = _toMapMini();
    map["artwork"] = tags.artwork.toMapValue();
    return map;
  }

  @override
  String toString() {
    final map = _toMapMini();
    map["artworkDetails"] = tags.artwork.toString();
    return map.toString();
  }
}
