import 'dart:async';

import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:logging/logging.dart';

/*
 * PaginationModel load data in page 
 * and listen to document change based on 'update_time' and 'delete_time' fields
 * of the document
 */
class PaginationModel<T> {
  final log = Logger('PaginationModel');

  List<String> ids = [];
  DocumentSnapshot prev;
  int rowPerLoad = 10;
  bool ended = false;

  StreamSubscription<QuerySnapshot> listener;
  CollectionReference listeningCol;
  Query pageQuery;

  PaginationModel(CollectionReference listeningCol, Query pageQuery,
      {this.rowPerLoad = 10}) {
    this.listeningCol = listeningCol;
    this.pageQuery = pageQuery;
    initData();
  }

  void initData() async {
    _clearState();
    _initListener();
    load();
  }

  void _clearState() {
    prev = null;
    ids = [];
    ended = false;
    if (listener != null) listener.cancel();
    listener = null;
    if (controller != null) controller.close();
  }

  StreamController<Result> controller;
  Stream<Result> listen() {
    if (controller != null) {
      controller.close();
    }
    controller = StreamController<Result>(onCancel: _clearState);
    return controller.stream;
  }

  void close() {
    _clearState();
  }

  final String updateTimeField = 'update_time';
  final String deleteTimeField = 'delete_time';
  void _initListener() {
    Query _query =
        listeningCol.orderBy(updateTimeField, descending: true).limit(1);
    _query.getDocuments(source: Source.server).then((QuerySnapshot snapshot) {
      int count = snapshot.documents.length;
      int updateTime = 0;
      if (count == 1) {
        updateTime = snapshot.documents[0].data[updateTimeField];
      }

      Query _queryListener = listeningCol
          .where(updateTimeField, isGreaterThan: updateTime)
          .orderBy(updateTimeField, descending: true);

      listener =
          _queryListener.snapshots(includeMetadataChanges: true).listen((qs) {
        qs.documentChanges.forEach((c) {
          switch (c.type) {
            case DocumentChangeType.added:
              log.info("added!! $c");
              _update(c.document.documentID, c.document.data);
              break;
            case DocumentChangeType.modified:
              log.info("modified!! $c");
              _update(c.document.documentID, c.document.data);
              break;
            default:
          }
        });
      });
    });
  }

  void _update(String id, Map<String, dynamic> data) {
    if (ids.contains(id)) {
      var deleted = data[deleteTimeField];
      if (deleted > 0) {
        ids.remove(id);
        controller.add(Result(
            id: id,
            data: data,
            documentChangeType: DocumentChangeType.removed));
      } else {
        controller.add(Result(
            id: id,
            data: data,
            documentChangeType: DocumentChangeType.modified));
      }
    } else {
      ids.add(id);
      controller.add(Result(
          id: id, data: data, documentChangeType: DocumentChangeType.added));
    }
  }

  Future<bool> load() async {
    Query _query =
        prev != null ? pageQuery.startAfterDocument(prev) : pageQuery;
    try {
      await _query
          .where(deleteTimeField, isEqualTo: 0)
          .limit(rowPerLoad)
          .getDocuments(source: Source.server)
          .then((QuerySnapshot snapshot) {
        int count = snapshot.documents.length;
        ended = count < rowPerLoad;
        prev = count > 0 ? snapshot.documents[count - 1] : prev;
        snapshot.documents.forEach((e) {
          if (!ids.contains(e.documentID)) log.shout("load!! $e");
          ids.add(e.documentID);
          controller.add(Result(
              id: e.documentID,
              data: e.data,
              documentChangeType: DocumentChangeType.added));
        });
        if (ended) {
          controller.add(Result(isEnded: true));
        }
      });
    } catch (e) {
      log.warning("Error!! $e");
    }
    return ended;
  }
}

class Result {
  String id;
  Map<String, dynamic> data;
  DocumentChangeType documentChangeType;
  bool isEnded;
  Result({this.id, this.data, this.documentChangeType, this.isEnded = false});
}
