import 'dart:async';

import 'package:android_intent_plus/android_intent.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:intl/intl.dart';
import 'package:kiwi/kiwi.dart';
import 'package:logging/logging.dart';
import 'package:nc_photos/account.dart';
import 'package:nc_photos/app_localizations.dart';
import 'package:nc_photos/controller/account_controller.dart';
import 'package:nc_photos/controller/pref_controller.dart';
import 'package:nc_photos/debug_util.dart';
import 'package:nc_photos/di_container.dart';
import 'package:nc_photos/entity/collection.dart';
import 'package:nc_photos/entity/collection/adapter.dart';
import 'package:nc_photos/entity/collection_item.dart';
import 'package:nc_photos/entity/exif_extension.dart';
import 'package:nc_photos/entity/file.dart';
import 'package:nc_photos/entity/file_descriptor.dart';
import 'package:nc_photos/entity/file_util.dart' as file_util;
import 'package:nc_photos/k.dart' as k;
import 'package:nc_photos/object_extension.dart';
import 'package:nc_photos/platform/features.dart' as features;
import 'package:nc_photos/set_as_handler.dart';
import 'package:nc_photos/snack_bar_manager.dart';
import 'package:nc_photos/stream_util.dart';
import 'package:nc_photos/theme.dart';
import 'package:nc_photos/use_case/inflate_file_descriptor.dart';
import 'package:nc_photos/use_case/list_file_tag.dart';
import 'package:nc_photos/use_case/update_property.dart';
import 'package:nc_photos/widget/about_geocoding_dialog.dart';
import 'package:nc_photos/widget/handler/add_selection_to_collection_handler.dart';
import 'package:nc_photos/widget/list_tile_center_leading.dart';
import 'package:nc_photos/widget/photo_date_time_edit_dialog.dart';
import 'package:np_codegen/np_codegen.dart';
import 'package:np_common/or_null.dart';
import 'package:np_geocoder/np_geocoder.dart';
import 'package:np_gps_map/np_gps_map.dart';
import 'package:np_platform_util/np_platform_util.dart';
import 'package:np_string/np_string.dart';
import 'package:np_ui/np_ui.dart';
import 'package:path/path.dart' as path_lib;
import 'package:tuple/tuple.dart';

part 'viewer_detail_pane.g.dart';

class ViewerSingleCollectionData {
  const ViewerSingleCollectionData(this.collection, this.item);

  final Collection collection;
  final CollectionItem item;
}

class ViewerDetailPane extends StatefulWidget {
  const ViewerDetailPane({
    Key? key,
    required this.account,
    required this.fd,
    this.fromCollection,
    required this.onRemoveFromCollectionPressed,
    required this.onArchivePressed,
    required this.onUnarchivePressed,
    required this.onDeletePressed,
    this.onSlideshowPressed,
  }) : super(key: key);

  @override
  createState() => _ViewerDetailPaneState();

  final Account account;
  final FileDescriptor fd;

  /// Data of the collection this file belongs to, or null
  final ViewerSingleCollectionData? fromCollection;

  final void Function(BuildContext context) onRemoveFromCollectionPressed;
  final void Function(BuildContext context) onArchivePressed;
  final void Function(BuildContext context) onUnarchivePressed;
  final void Function(BuildContext context) onDeletePressed;
  final VoidCallback? onSlideshowPressed;
}

@npLog
class _ViewerDetailPaneState extends State<ViewerDetailPane> {
  _ViewerDetailPaneState() {
    final c = KiwiContainer().resolve<DiContainer>();
    assert(require(c));
    assert(InflateFileDescriptor.require(c));
    _c = c;
  }

  static bool require(DiContainer c) =>
      DiContainer.has(c, DiType.fileRepo) &&
      DiContainer.has(c, DiType.albumRepo);

  @override
  initState() {
    _log.info("[initState] File: ${widget.fd.fdPath}");
    super.initState();
    _dateTime = widget.fd.fdDateTime.toLocal();
    _initFile();
  }

  Future<void> _initFile() async {
    _file =
        (await InflateFileDescriptor(_c)(widget.account, [widget.fd])).first;
    _log.fine("[_initFile] File inflated");
    // update file
    if (mounted) {
      setState(() {});
    } else {
      return;
    }
    if (_file!.metadata == null) {
      _log.info("[initState] Metadata missing in File");
    } else {
      _log.info("[initState] Metadata exists in File");
      if (_file!.metadata!.exif != null) {
        _initMetadata();
      }
    }
    await _initTags();
    // update tages
    if (mounted) {
      setState(() {});
    } else {
      return;
    }

    // postpone loading map to improve responsiveness
    unawaited(Future.delayed(const Duration(milliseconds: 750)).then((_) {
      if (mounted) {
        setState(() {
          _shouldBlockGpsMap = false;
        });
      }
    }));
  }

  @override
  build(BuildContext context) {
    final dateStr = DateFormat(DateFormat.YEAR_ABBR_MONTH_DAY,
            Localizations.localeOf(context).languageCode)
        .format(_dateTime);
    final timeStr = DateFormat(DateFormat.HOUR_MINUTE,
            Localizations.localeOf(context).languageCode)
        .format(_dateTime);
    final bool isShowDelete;
    if (widget.fromCollection != null) {
      final collectionAdapter = CollectionAdapter.of(KiwiContainer().resolve(),
          widget.account, widget.fromCollection!.collection);
      isShowDelete =
          collectionAdapter.isPermitted(CollectionCapability.deleteItem) &&
              collectionAdapter.isItemDeletable(widget.fromCollection!.item);
    } else {
      isShowDelete = false;
    }

    return Material(
      type: MaterialType.transparency,
      child: Column(
        crossAxisAlignment: CrossAxisAlignment.start,
        children: [
          if (_file != null) ...[
            SingleChildScrollView(
              scrollDirection: Axis.horizontal,
              child: Row(
                children: [
                  if (_canRemoveFromAlbum)
                    _DetailPaneButton(
                      icon: Icons.remove_outlined,
                      label: L10n.global().removeFromAlbumTooltip,
                      onPressed: () =>
                          widget.onRemoveFromCollectionPressed(context),
                    ),
                  if (_canSetCover)
                    _DetailPaneButton(
                      icon: Icons.photo_album_outlined,
                      label: L10n.global().useAsAlbumCoverTooltip,
                      onPressed: () => _onSetAlbumCoverPressed(context),
                    ),
                  _DetailPaneButton(
                    icon: Icons.add,
                    label: L10n.global().addItemToCollectionTooltip,
                    onPressed: () => _onAddToAlbumPressed(context),
                  ),
                  if (getRawPlatform() == NpPlatform.android &&
                      file_util.isSupportedImageFormat(_file!))
                    _DetailPaneButton(
                      icon: Icons.launch,
                      label: L10n.global().setAsTooltip,
                      onPressed: () => _onSetAsPressed(context),
                    ),
                  if (widget.fd.fdIsArchived == true)
                    _DetailPaneButton(
                      icon: Icons.unarchive_outlined,
                      label: L10n.global().unarchiveTooltip,
                      onPressed: () => widget.onUnarchivePressed(context),
                    )
                  else
                    _DetailPaneButton(
                      icon: Icons.archive_outlined,
                      label: L10n.global().archiveTooltip,
                      onPressed: () => widget.onArchivePressed(context),
                    ),
                  if (isShowDelete)
                    _DetailPaneButton(
                      icon: Icons.delete_outlined,
                      label: L10n.global().deleteTooltip,
                      onPressed: () => widget.onDeletePressed(context),
                    ),
                  _DetailPaneButton(
                    icon: Icons.slideshow_outlined,
                    label: L10n.global().slideshowTooltip,
                    onPressed: widget.onSlideshowPressed,
                  ),
                ],
              ),
            ),
            const Padding(
              padding: EdgeInsets.symmetric(horizontal: 32),
              child: Divider(),
            ),
          ],
          ListTile(
            leading: const ListTileCenterLeading(
              child: Icon(Icons.image_outlined),
            ),
            title: Text(path_lib.basenameWithoutExtension(widget.fd.fdPath)),
            subtitle: Text(widget.fd.strippedPath),
          ),
          if (_file != null) ...[
            if (!_file!.isOwned(widget.account.userId))
              ListTile(
                leading: const ListTileCenterLeading(
                  child: Icon(
                    Icons.share_outlined,
                  ),
                ),
                title:
                    Text(_file!.ownerDisplayName ?? _file!.ownerId!.toString()),
                subtitle: Text(L10n.global().fileSharedByDescription),
              ),
            if (_tags.isNotEmpty)
              ListTile(
                leading: const Icon(Icons.local_offer_outlined),
                title: SizedBox(
                  height: 40,
                  child: ListView.separated(
                    scrollDirection: Axis.horizontal,
                    itemCount: _tags.length,
                    itemBuilder: (context, index) => FilterChip(
                      elevation: 1,
                      pressElevation: 1,
                      showCheckmark: false,
                      visualDensity: VisualDensity.compact,
                      selected: true,
                      selectedColor: Theme.of(context)
                          .elevate(Theme.of(context).colorScheme.surface, 5),
                      label: Text(_tags[index]),
                      onSelected: (_) {},
                    ),
                    separatorBuilder: (context, index) =>
                        const SizedBox(width: 8),
                  ),
                ),
              ),
          ],
          ListTile(
            leading: const Icon(Icons.calendar_today_outlined),
            title: Text("$dateStr $timeStr"),
            trailing: _file == null ? null : const Icon(Icons.edit_outlined),
            onTap: _file == null ? null : () => _onDateTimeTap(context),
          ),
          if (_file != null) ...[
            if (_file!.metadata?.imageWidth != null &&
                _file!.metadata?.imageHeight != null)
              ListTile(
                leading: const ListTileCenterLeading(
                  child: Icon(Icons.aspect_ratio),
                ),
                title: Text(
                    "${_file!.metadata!.imageWidth} x ${_file!.metadata!.imageHeight}"),
                subtitle: Text(_buildSizeSubtitle()),
              )
            else
              ListTile(
                leading: const Icon(Icons.aspect_ratio),
                title: Text(_byteSizeToString(_file!.contentLength ?? 0)),
              ),
            if (_model != null)
              ListTile(
                leading: const ListTileCenterLeading(
                  child: Icon(Icons.camera_outlined),
                ),
                title: Text(_model!),
                subtitle: _buildCameraSubtitle()
                    .run((s) => s.isNotEmpty ? Text(s) : null),
              ),
            if (_location?.name != null)
              ListTile(
                leading: const ListTileCenterLeading(
                  child: Icon(Icons.location_on_outlined),
                ),
                title: Text(L10n.global().gpsPlaceText(_location!.name!)),
                subtitle: _location!.toSubtitle()?.run((obj) => Text(obj)),
                trailing: const Icon(Icons.info_outline),
                onTap: () {
                  showDialog(
                    context: context,
                    builder: (_) => const AboutGeocodingDialog(),
                  );
                },
              ),
            if (features.isSupportMapView && _gps != null)
              AnimatedVisibility(
                opacity: _shouldBlockGpsMap ? 0 : 1,
                curve: Curves.easeInOut,
                duration: k.animationDurationNormal,
                child: SizedBox(
                  height: 256,
                  child: ValueStreamBuilder<GpsMapProvider>(
                    stream: context.read<PrefController>().gpsMapProvider,
                    builder: (context, gpsMapProvider) => GpsMap(
                      providerHint: gpsMapProvider.requireData,
                      center: _gps!,
                      zoom: 16,
                      onTap: _onMapTap,
                    ),
                  ),
                ),
              ),
          ],
        ],
      ),
    );
  }

  /// Convert EXIF data to readable format
  void _initMetadata() {
    assert(_file != null);
    final exif = _file!.metadata!.exif!;
    _log.info("[_initMetadata] $exif");

    if (exif.make != null && exif.model != null) {
      _model = "${exif.make} ${exif.model}";
    }
    if (exif.fNumber != null) {
      _fNumber = exif.fNumber!.toDouble();
    }
    if (exif.exposureTime != null) {
      if (exif.exposureTime!.denominator == 1) {
        _exposureTime = exif.exposureTime!.numerator.toString();
      } else {
        _exposureTime = exif.exposureTime.toString();
      }
    }
    if (exif.focalLength != null) {
      _focalLength = exif.focalLength!.toDouble();
    }
    if (exif.isoSpeedRatings != null) {
      _isoSpeedRatings = exif.isoSpeedRatings!;
    }
    final lat = exif.gpsLatitudeDeg;
    final lng = exif.gpsLongitudeDeg;
    if (lat != null && lng != null) {
      _log.fine("GPS: ($lat, $lng)");
      _gps = Tuple2(lat, lng);
      _location = _file!.location;
    }
  }

  Future<void> _initTags() async {
    assert(_file != null);
    if (file_util.isNcAlbumFile(widget.account, _file!)) {
      // tag is not supported here
      return;
    }
    final c = KiwiContainer().resolve<DiContainer>();
    try {
      final tags = await ListFileTag(c)(widget.account, _file!);
      _tags.addAll(tags.map((t) => t.displayName));
    } catch (e, stackTrace) {
      _log.shout("[_initTags] Failed while ListFileTag", e, stackTrace);
    }
  }

  String _buildSizeSubtitle() {
    String sizeSubStr = "";
    const space = "    ";
    if (_file!.metadata?.imageWidth != null &&
        _file!.metadata?.imageHeight != null) {
      final pixelCount =
          _file!.metadata!.imageWidth! * _file!.metadata!.imageHeight!;
      if (pixelCount >= 500000) {
        final mpCount = pixelCount / 1000000.0;
        sizeSubStr += L10n.global().megapixelCount(mpCount.toStringAsFixed(1));
        sizeSubStr += space;
      }
      sizeSubStr += _byteSizeToString(_file!.contentLength ?? 0);
    }
    return sizeSubStr;
  }

  String _buildCameraSubtitle() {
    String cameraSubStr = "";
    const space = "    ";
    if (_fNumber != null) {
      cameraSubStr += "f/${_fNumber!.toStringAsFixed(1)}$space";
    }
    if (_exposureTime != null) {
      cameraSubStr += L10n.global().secondCountSymbol(_exposureTime!);
      cameraSubStr += space;
    }
    if (_focalLength != null) {
      cameraSubStr += L10n.global()
          .millimeterCountSymbol(_focalLength!.toStringAsFixedTruncated(2));
      cameraSubStr += space;
    }
    if (_isoSpeedRatings != null) {
      cameraSubStr += "ISO$_isoSpeedRatings$space";
    }
    cameraSubStr = cameraSubStr.trim();
    return cameraSubStr;
  }

  Future<void> _onSetAlbumCoverPressed(BuildContext context) async {
    assert(_file != null);
    assert(widget.fromCollection != null);
    _log.info(
        "[_onSetAlbumCoverPressed] Set '${widget.fd.fdPath}' as album cover for '${widget.fromCollection!.collection.name}'");
    try {
      await context.read<AccountController>().collectionsController.edit(
            widget.fromCollection!.collection,
            cover: OrNull(_file!),
          );
    } catch (e, stackTrace) {
      _log.shout("[_onSetAlbumCoverPressed] Failed while updating album", e,
          stackTrace);
      SnackBarManager().showSnackBar(SnackBar(
        content: Text(L10n.global().setCollectionCoverFailureNotification),
        duration: k.snackBarDurationNormal,
      ));
    }
  }

  Future<void> _onAddToAlbumPressed(BuildContext context) {
    assert(_file != null);
    return const AddSelectionToCollectionHandler()(
      context: context,
      selection: [_file!],
      clearSelection: () {},
    );
  }

  void _onSetAsPressed(BuildContext context) {
    assert(_file != null);
    final c = KiwiContainer().resolve<DiContainer>();
    SetAsHandler(c, context: context).setAsFile(widget.account, _file!);
  }

  void _onMapTap() {
    if (getRawPlatform() == NpPlatform.android) {
      final intent = AndroidIntent(
        action: "action_view",
        data: Uri.encodeFull("geo:${_gps!.item1},${_gps!.item2}?z=16"),
      );
      intent.launch();
    }
  }

  void _onDateTimeTap(BuildContext context) {
    assert(_file != null);
    showDialog(
      context: context,
      builder: (context) => PhotoDateTimeEditDialog(initialDateTime: _dateTime),
    ).then((value) async {
      if (value == null || value is! DateTime) {
        return;
      }
      try {
        await UpdateProperty(_c)
            .updateOverrideDateTime(widget.account, _file!, value);
        if (mounted) {
          setState(() {
            _dateTime = value;
          });
        }
      } catch (e, stacktrace) {
        _log.shout(
            "[_onDateTimeTap] Failed while updateOverrideDateTime: ${logFilename(widget.fd.fdPath)}",
            e,
            stacktrace);
        SnackBarManager().showSnackBar(SnackBar(
          content: Text(L10n.global().updateDateTimeFailureNotification),
          duration: k.snackBarDurationNormal,
        ));
      }
    }).catchError((e, stacktrace) {
      _log.shout("[_onDateTimeTap] Failed while showDialog", e, stacktrace);
    });
  }

  late final DiContainer _c;

  File? _file;
  late DateTime _dateTime;
  // EXIF data
  String? _model;
  double? _fNumber;
  String? _exposureTime;
  double? _focalLength;
  int? _isoSpeedRatings;
  Tuple2<double, double>? _gps;
  ImageLocation? _location;

  final _tags = <String>[];

  var _shouldBlockGpsMap = true;

  late final bool _canRemoveFromAlbum = widget.fromCollection?.run((d) =>
          CollectionAdapter.of(_c, widget.account, d.collection)
              .isItemRemovable(widget.fromCollection!.item)) ??
      false;

  late final bool _canSetCover = widget.fromCollection?.run((d) =>
          CollectionAdapter.of(_c, widget.account, d.collection)
              .isPermitted(CollectionCapability.manualCover)) ??
      false;
}

class _DetailPaneButton extends StatelessWidget {
  const _DetailPaneButton({
    Key? key,
    required this.icon,
    required this.label,
    required this.onPressed,
  }) : super(key: key);

  @override
  build(BuildContext context) {
    return TextButton(
      onPressed: onPressed,
      style: ButtonStyle(
        foregroundColor:
            MaterialStateProperty.all(Theme.of(context).colorScheme.onSurface),
      ),
      child: SizedBox(
        width: 96,
        height: 96,
        child: Padding(
          padding: const EdgeInsets.fromLTRB(8, 16, 8, 0),
          child: Column(
            children: [
              Icon(icon),
              const SizedBox(height: 4),
              Text(
                label,
                textAlign: TextAlign.center,
                style: Theme.of(context).textTheme.labelMedium,
              ),
            ],
          ),
        ),
      ),
    );
  }

  final IconData icon;
  final String label;
  final VoidCallback? onPressed;
}

String _byteSizeToString(int byteSize) {
  const units = ["B", "KB", "MB", "GB"];
  var remain = byteSize.toDouble();
  int i = 0;
  while (i < units.length) {
    final next = remain / 1024;
    if (next < 1) {
      break;
    }
    remain = next;
    ++i;
  }
  return "${remain.toStringAsFixed(2)}${units[i]}";
}

extension on ImageLocation {
  String? toSubtitle() {
    if (countryCode == null) {
      return null;
    } else if (admin1 == null) {
      return alpha2CodeToName(countryCode!);
    } else if (admin2 == null) {
      return "$admin1, ${alpha2CodeToName(countryCode!)}";
    } else {
      return "$admin2, $admin1, ${alpha2CodeToName(countryCode!)}";
    }
  }
}