import 'package:bloc/bloc.dart';
import 'package:collection/collection.dart';
import 'package:flutter/foundation.dart';
import 'package:logging/logging.dart';
import 'package:nc_photos/ci_string.dart';
import 'package:nc_photos/iterable_extension.dart';
import 'package:tuple/tuple.dart';
import 'package:woozy_search/woozy_search.dart';

abstract class SearchSuggestionBlocEvent<T> {
  const SearchSuggestionBlocEvent();
}

class SearchSuggestionBlocUpdateItemsEvent<T>
    extends SearchSuggestionBlocEvent<T> {
  const SearchSuggestionBlocUpdateItemsEvent(this.items);

  @override
  toString() {
    return "$runtimeType {"
        "items: List {legth: ${items.length}}, "
        "}";
  }

  final List<T> items;
}

class SearchSuggestionBlocSearchEvent<T> extends SearchSuggestionBlocEvent<T> {
  const SearchSuggestionBlocSearchEvent(this.phrase);

  @override
  toString() {
    return "$runtimeType {"
        "phrase: '$phrase', "
        "}";
  }

  final CiString phrase;
}

abstract class SearchSuggestionBlocState<T> {
  const SearchSuggestionBlocState(this.results);

  @override
  toString() {
    return "$runtimeType {"
        "results: List {legth: ${results.length}}, "
        "}";
  }

  final List<T> results;
}

class SearchSuggestionBlocInit<T> extends SearchSuggestionBlocState<T> {
  const SearchSuggestionBlocInit() : super(const []);
}

class SearchSuggestionBlocLoading<T> extends SearchSuggestionBlocState<T> {
  const SearchSuggestionBlocLoading(List<T> results) : super(results);
}

class SearchSuggestionBlocSuccess<T> extends SearchSuggestionBlocState<T> {
  const SearchSuggestionBlocSuccess(List<T> results) : super(results);
}

class SearchSuggestionBloc<T>
    extends Bloc<SearchSuggestionBlocEvent, SearchSuggestionBlocState<T>> {
  SearchSuggestionBloc({
    required this.itemToKeywords,
  }) : super(SearchSuggestionBlocInit<T>()) {
    on<SearchSuggestionBlocEvent>(_onEvent);
  }

  Future<void> _onEvent(SearchSuggestionBlocEvent event,
      Emitter<SearchSuggestionBlocState<T>> emit) async {
    _log.info("[_onEvent] $event");
    if (event is SearchSuggestionBlocSearchEvent) {
      await _onEventSearch(event, emit);
    } else if (event is SearchSuggestionBlocUpdateItemsEvent<T>) {
      await _onEventUpdateItems(event, emit);
    }
  }

  Future<void> _onEventSearch(SearchSuggestionBlocSearchEvent ev,
      Emitter<SearchSuggestionBlocState<T>> emit) async {
    emit(SearchSuggestionBlocLoading(state.results));
    // doesn't work with upper case
    final results = _search.search(ev.phrase.toCaseInsensitiveString());
    if (kDebugMode) {
      final str = results.map((e) => "${e.score}: ${e.text}").join("\n");
      _log.info("[_onEventSearch] Search '${ev.phrase}':\n$str");
    }
    final matches = results
        .where((element) => element.score > 0)
        .map((e) {
          if (itemToKeywords(e.value as T)
              .any((k) => k.startsWith(ev.phrase))) {
            // prefer names that start exactly with the search phrase
            return Tuple2(e.score + 1, e.value as T);
          } else {
            return Tuple2(e.score, e.value as T);
          }
        })
        .sorted((a, b) => a.item1.compareTo(b.item1))
        .reversed
        .distinctIf(
          (a, b) => identical(a.item2, b.item2),
          (a) => a.item2.hashCode,
        )
        .map((e) => e.item2)
        .toList();
    emit(SearchSuggestionBlocSuccess(matches));
    _lastSearch = ev;
  }

  Future<void> _onEventUpdateItems(SearchSuggestionBlocUpdateItemsEvent<T> ev,
      Emitter<SearchSuggestionBlocState<T>> emit) async {
    _search.setEntries([]);
    for (final a in ev.items) {
      for (final k in itemToKeywords(a)) {
        _search.addEntry(k.toCaseInsensitiveString(), value: a);
      }
    }
    if (_lastSearch != null) {
      // search again
      await _onEventSearch(_lastSearch!, emit);
    }
  }

  final List<CiString> Function(T item) itemToKeywords;

  final _search = Woozy(limit: 5);
  SearchSuggestionBlocSearchEvent? _lastSearch;

  static final _log =
      Logger("bloc.album_search_suggestion.SearchSuggestionBloc");
}