import 'package:aves/convert/convert.dart';
import 'package:aves/model/entry/entry.dart';
import 'package:aves/model/entry/extensions/multipage.dart';
import 'package:aves/model/entry/extensions/props.dart';
import 'package:aves/model/media/geotiff.dart';
import 'package:aves/model/media/panorama.dart';
import 'package:aves/model/metadata/catalog.dart';
import 'package:aves/model/metadata/overlay.dart';
import 'package:aves/model/multipage.dart';
import 'package:aves/services/common/channel.dart';
import 'package:aves/services/common/custom_exception.dart';
import 'package:aves/services/common/service_policy.dart';
import 'package:aves/services/common/services.dart';
import 'package:aves/services/metadata/xmp.dart';
import 'package:aves/utils/time_utils.dart';
import 'package:aves_model/aves_model.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';

abstract class MetadataFetchService {
  // returns Map<Map<Key, Value>> (map of directories, each directory being a map of metadata label and value description)
  Future<Map> getAllMetadata(AvesEntry entry);

  Future<CatalogMetadata?> getCatalogMetadata(AvesEntry entry, {bool background = false});

  Future<OverlayMetadata> getOverlayMetadata(AvesEntry entry, Set<MetadataSyntheticField> fields);

  Future<GeoTiffInfo?> getGeoTiffInfo(AvesEntry entry);

  Future<MultiPageInfo?> getMultiPageInfo(AvesEntry entry);

  Future<PanoramaInfo?> getPanoramaInfo(AvesEntry entry);

  Future<List<Map<String, dynamic>>?> getIptc(AvesEntry entry);

  Future<AvesXmp?> getXmp(AvesEntry entry);

  Future<bool> hasContentResolverProp(String prop);

  Future<String?> getContentResolverProp(AvesEntry entry, String prop);

  Future<DateTime?> getDate(AvesEntry entry, MetadataField field);

  Future<Map<String, dynamic>> getFields(AvesEntry entry, Set<MetadataField> fields);
}

class PlatformMetadataFetchService implements MetadataFetchService {
  static const _platform = AvesMethodChannel('deckers.thibault/aves/metadata_fetch');

  @override
  Future<Map> getAllMetadata(AvesEntry entry) async {
    if (entry.isSvg) return {};

    try {
      final result = await _platform.invokeMethod('getAllMetadata', <String, dynamic>{
        'mimeType': entry.mimeType,
        'uri': entry.uri,
        'sizeBytes': entry.sizeBytes,
      });
      if (result != null) return result as Map;
    } on PlatformException catch (e, stack) {
      await _processPlatformException(entry, e, stack);
    }
    return {};
  }

  @override
  Future<CatalogMetadata?> getCatalogMetadata(AvesEntry entry, {bool background = false}) async {
    if (entry.isSvg) return null;

    Future<CatalogMetadata?> call() async {
      try {
        // returns map with:
        // 'mimeType': MIME type as reported by metadata extractors, not Media Store (string)
        // 'dateMillis': date taken in milliseconds since Epoch (long)
        // 'isAnimated': animated gif/webp (bool)
        // 'isFlipped': flipped according to EXIF orientation (bool)
        // 'rating': rating in [-1,5] (int)
        // 'rotationDegrees': rotation degrees according to EXIF orientation or other metadata (int)
        // 'latitude': latitude (double)
        // 'longitude': longitude (double)
        // 'xmpSubjects': ';' separated XMP subjects (string)
        // 'xmpTitle': XMP title (string)
        final result =
            await _platform.invokeMethod('getCatalogMetadata', <String, dynamic>{
                  'mimeType': entry.mimeType,
                  'uri': entry.uri,
                  'path': entry.path,
                  'sizeBytes': entry.sizeBytes,
                })
                as Map;
        result['id'] = entry.id;
        AvesEntry.normalizeMimeTypeFields(result);
        return CatalogMetadata.fromMap(result);
      } on PlatformException catch (e, stack) {
        await _processPlatformException(entry, e, stack);
      }
      return null;
    }

    return background
        ? servicePolicy.call(
            call,
            priority: ServiceCallPriority.getMetadata,
          )
        : call();
  }

  @override
  Future<OverlayMetadata> getOverlayMetadata(AvesEntry entry, Set<MetadataSyntheticField> fields) async {
    if (fields.isNotEmpty && !entry.isSvg) {
      try {
        // returns fields on demand, with various value types:
        // 'aperture' (double),
        // 'description' (string)
        // 'exposureTime' (string),
        // 'focalLength' (double),
        // 'iso' (int),
        final result =
            await _platform.invokeMethod('getOverlayMetadata', <String, dynamic>{
                  'mimeType': entry.mimeType,
                  'uri': entry.uri,
                  'sizeBytes': entry.sizeBytes,
                  'fields': fields.map((v) => v.toPlatform).toList(),
                })
                as Map;
        return OverlayMetadata.fromMap(result);
      } on PlatformException catch (e, stack) {
        await _processPlatformException(entry, e, stack);
      }
    }
    return const OverlayMetadata();
  }

  @override
  Future<GeoTiffInfo?> getGeoTiffInfo(AvesEntry entry) async {
    try {
      final result =
          await _platform.invokeMethod('getGeoTiffInfo', <String, dynamic>{
                'mimeType': entry.mimeType,
                'uri': entry.uri,
                'sizeBytes': entry.sizeBytes,
              })
              as Map;
      return GeoTiffInfo.fromMap(result);
    } on PlatformException catch (e, stack) {
      await _processPlatformException(entry, e, stack);
    }
    return null;
  }

  @override
  Future<MultiPageInfo?> getMultiPageInfo(AvesEntry entry) async {
    try {
      final result = await _platform.invokeMethod('getMultiPageInfo', <String, dynamic>{
        'mimeType': entry.mimeType,
        'uri': entry.uri,
        'sizeBytes': entry.sizeBytes,
        'isMotionPhoto': entry.isMotionPhoto,
      });
      final pageMaps = ((result as List?) ?? []).cast<Map>();
      if (entry.isMotionPhoto && pageMaps.isNotEmpty) {
        final imagePage = pageMaps[0];
        imagePage['width'] = entry.width;
        imagePage['height'] = entry.height;
        imagePage['rotationDegrees'] = entry.rotationDegrees;
      }
      pageMaps.forEach(AvesEntry.normalizeMimeTypeFields);
      return MultiPageInfo.fromPageMaps(entry, pageMaps);
    } on PlatformException catch (e, stack) {
      if (e.code != 'getMultiPageInfo-empty') {
        await _processPlatformException(entry, e, stack);
      }
    }
    return null;
  }

  @override
  Future<PanoramaInfo?> getPanoramaInfo(AvesEntry entry) async {
    try {
      // returns map with values for:
      // 'croppedAreaLeft' (int), 'croppedAreaTop' (int), 'croppedAreaWidth' (int), 'croppedAreaHeight' (int),
      // 'fullPanoWidth' (int), 'fullPanoHeight' (int)
      final result =
          await _platform.invokeMethod('getPanoramaInfo', <String, dynamic>{
                'mimeType': entry.mimeType,
                'uri': entry.uri,
                'sizeBytes': entry.sizeBytes,
              })
              as Map;
      return PanoramaInfo.fromMap(result);
    } on PlatformException catch (e, stack) {
      await _processPlatformException(entry, e, stack);
    }
    return null;
  }

  @override
  Future<List<Map<String, dynamic>>?> getIptc(AvesEntry entry) async {
    try {
      final result = await _platform.invokeMethod('getIptc', <String, dynamic>{
        'mimeType': entry.mimeType,
        'uri': entry.uri,
      });
      if (result != null) return (result as List).cast<Map>().map((fields) => fields.cast<String, dynamic>()).toList();
    } on PlatformException catch (e, stack) {
      await _processPlatformException(entry, e, stack);
    }
    return null;
  }

  @override
  Future<AvesXmp?> getXmp(AvesEntry entry) async {
    try {
      final result = await _platform.invokeMethod('getXmp', <String, dynamic>{
        'mimeType': entry.mimeType,
        'uri': entry.uri,
        'sizeBytes': entry.sizeBytes,
      });
      if (result != null) return AvesXmp.fromList((result as List).cast<String>());
    } on PlatformException catch (e, stack) {
      await _processPlatformException(entry, e, stack);
    }
    return null;
  }

  final Map<String, bool> _contentResolverProps = {};

  @override
  Future<bool> hasContentResolverProp(String prop) async {
    var exists = _contentResolverProps[prop];
    if (exists != null) return SynchronousFuture(exists);

    try {
      exists = await _platform.invokeMethod('hasContentResolverProp', <String, dynamic>{
        'prop': prop,
      });
    } on PlatformException catch (e, stack) {
      await reportService.recordError(e, stack);
    }
    exists ??= false;
    _contentResolverProps[prop] = exists;
    return exists;
  }

  @override
  Future<String?> getContentResolverProp(AvesEntry entry, String prop) async {
    try {
      return await _platform.invokeMethod('getContentResolverProp', <String, dynamic>{
        'mimeType': entry.mimeType,
        'uri': entry.uri,
        'prop': prop,
      });
    } on PlatformException catch (e, stack) {
      await _processPlatformException(entry, e, stack);
    }
    return null;
  }

  @override
  Future<DateTime?> getDate(AvesEntry entry, MetadataField field) async {
    try {
      final result = await _platform.invokeMethod('getDate', <String, dynamic>{
        'mimeType': entry.mimeType,
        'uri': entry.uri,
        'sizeBytes': entry.sizeBytes,
        'field': field.toPlatform,
      });
      if (result is int) {
        return dateTimeFromMillis(result, isUtc: false);
      }
    } on PlatformException catch (e, stack) {
      await _processPlatformException(entry, e, stack);
    }
    return null;
  }

  @override
  Future<Map<String, dynamic>> getFields(AvesEntry entry, Set<MetadataField> fields) async {
    if (fields.isNotEmpty && !entry.isSvg) {
      try {
        final result = await _platform.invokeMethod('getFields', <String, dynamic>{
          'mimeType': entry.mimeType,
          'uri': entry.uri,
          'sizeBytes': entry.sizeBytes,
          'fields': fields.map((v) => v.toPlatform).toList(),
        });
        if (result != null) return (result as Map).cast<String, dynamic>();
      } on PlatformException catch (e, stack) {
        await _processPlatformException(entry, e, stack);
      }
    }
    return {};
  }

  Future<void> _processPlatformException(AvesEntry entry, PlatformException e, StackTrace stack) async {
    if (entry.isValid) {
      final code = e.code;
      final customException = CustomPlatformException.fromStandard(e);
      if (code.endsWith('filenotfound')) {
        await fileNotFound(customException);
      } else {
        await reportService.recordError(e, stack);
      }
    }
  }

  // distinct exceptions to convince Crashlytics to split reports into distinct issues
  // The distinct debug statement is there to make the body unique, so that the methods are not merged at compile time.

  Future<void> fileNotFound(CustomPlatformException e) {
    debugPrint('fileNotFound $e');
    return reportService.recordError(e);
  }
}
