import 'package:flutter/foundation.dart';
import 'package:result_monad/result_monad.dart';

import '../friendica_client/friendica_client.dart';
import '../friendica_client/paging_data.dart';
import '../models/auth/profile.dart';
import '../models/exec_error.dart';
import '../models/gallery_data.dart';
import '../models/image_entry.dart';

class GalleryService extends ChangeNotifier {
  static const IMAGES_PER_PAGE = 50;
  final _galleries = <String, GalleryData>{};
  final _galleryPages = <String, List<PagingData>>{};
  final _images = <String, List<ImageEntry>>{};
  var _loaded = false;

  final Profile profile;

  GalleryService(this.profile);

  bool get loaded => _loaded;

  void clear() {
    _galleries.clear();
    _galleryPages.clear();
    _images.clear();
    _loaded = false;
    notifyListeners();
  }

  List<GalleryData> getGalleries() {
    if (_galleries.isEmpty) {
      updateGalleries();
    }

    return _galleries.values.toList(growable: false);
  }

  Result<GalleryData, ExecError> getGallery(String galleryName) {
    final gallery = _galleries[galleryName];

    if (gallery == null) {
      return buildErrorResult(
        type: ErrorType.notFound,
        message: 'Gallery $galleryName not found',
      );
    }

    return Result.ok(gallery);
  }

  FutureResult<List<GalleryData>, ExecError> updateGalleries() async {
    final result = await GalleryClient(profile).getGalleryData();
    if (result.isFailure) {
      return result.errorCast();
    }

    final galleriesReturned = result.value.map((g) => g.name).toList();
    _galleries.clear();
    _galleryPages.removeWhere((key, value) => !galleriesReturned.contains(key));
    _images.removeWhere((key, value) => !galleriesReturned.contains(key));

    for (final gallery in result.value) {
      _galleries[gallery.name] = gallery;
    }

    _loaded = true;
    notifyListeners();
    return Result.ok(_galleries.values.toList(growable: false));
  }

  Result<List<ImageEntry>, ExecError> getGalleryImageList(String galleryName) {
    if (!_galleries.containsKey(galleryName)) {
      return Result.error(
        ExecError(
          type: ErrorType.localError,
          message: 'Unknown Gallery: $galleryName',
        ),
      );
    }

    if (!_images.containsKey(galleryName)) {
      updateGalleryImageList(galleryName: galleryName, withNextPage: true);
      return Result.ok([]);
    } else {
      return Result.ok(_images[galleryName]!.toList(growable: false));
    }
  }

  FutureResult<GalleryData, ExecError> renameGallery(
      GalleryData gallery, String newName) async {
    if (!_galleries.containsKey(gallery.name)) {
      return buildErrorResult(
          type: ErrorType.notFound,
          message: 'Unknown gallery: ${gallery.name}');
    }
    final result = await GalleryClient(profile)
        .renameGallery(gallery.name, newName)
        .transform((_) => gallery.copy(name: newName))
        .withResultAsync((_) async => await updateGalleries());

    return result.execErrorCast();
  }

  //TODO Paging
  FutureResult<List<ImageEntry>, ExecError> updateGalleryImageList(
      {required String galleryName,
      required bool withNextPage,
      bool nextPageOnly = true}) async {
    final pages = _galleryPages.putIfAbsent(galleryName, () => []);
    if (pages.isEmpty) {
      pages.add(PagingData(offset: 0, limit: IMAGES_PER_PAGE));
    } else if (withNextPage) {
      final offset = pages.last.offset! + IMAGES_PER_PAGE;
      pages.add(PagingData(offset: offset, limit: IMAGES_PER_PAGE));
    }

    final imageSet = nextPageOnly
        ? _images.putIfAbsent(galleryName, () => []).toSet()
        : <ImageEntry>{};

    final pagesToUse = nextPageOnly ? [pages.last] : pages;
    for (final page in pagesToUse) {
      final result =
          await GalleryClient(profile).getGalleryImages(galleryName, page);
      if (result.isFailure) {
        return result.errorCast();
      }

      imageSet.addAll(result.value);
    }

    _images[galleryName] = imageSet.toList();
    notifyListeners();
    return Result.ok(imageSet.toList(growable: false));
  }

  Result<ImageEntry, ExecError> getImage(String galleryName, String id) {
    if (!_images.containsKey(galleryName)) {
      return buildErrorResult(
        type: ErrorType.notFound,
        message: 'Image gallery $galleryName not known.',
      );
    }

    final potentialImages =
        _images[galleryName]?.where((i) => i.id == id).toList() ?? [];

    if (potentialImages.isEmpty) {
      return buildErrorResult(
        type: ErrorType.notFound,
        message: 'Image $id not found in gallery $galleryName',
      );
    }

    return Result.ok(potentialImages.first);
  }

  FutureResult<ImageEntry, ExecError> updateImage(ImageEntry image) async {
    final images = _images[image.album];
    if (images == null) {
      buildErrorResult(
          type: ErrorType.notFound, message: 'Album not found ${image.album}');
    }

    final index = _images[image.album]!.indexOf(image);
    if (index < 0) {
      return buildErrorResult(
          type: ErrorType.notFound,
          message: 'Image ${image.id} does not exist for ${image.album}');
    }
    final result =
        await ImageClient(profile).editImageData(image).withResult((_) {
      images!.removeAt(index);
      images.insert(index, image);
    });
    notifyListeners();
    return result.execErrorCast();
  }

  FutureResult<ImageEntry, ExecError> deleteImage(ImageEntry image) async {
    final images = _images[image.album];
    if (images == null) {
      buildErrorResult(
          type: ErrorType.notFound, message: 'Album not found ${image.album}');
    }

    final index = _images[image.album]!.indexOf(image);
    if (index < 0) {
      return buildErrorResult(
          type: ErrorType.notFound,
          message: 'Image ${image.id} does not exist for ${image.album}');
    }
    final result = await ImageClient(profile)
        .deleteImage(image)
        .withResultAsync((_) async {
      images!.removeAt(index);
      await updateGalleries();
      notifyListeners();
    });

    return result.execErrorCast();
  }
}
