// Copyright (C) 2022 WYATT GROUP
// Please see the AUTHORS file for details.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.

import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:wyatt_crud_bloc/src/core/enums/where_query_type.dart';
import 'package:wyatt_crud_bloc/src/domain/data_sources/crud_data_source.dart';
import 'package:wyatt_crud_bloc/src/domain/entities/object_model.dart';
import 'package:wyatt_crud_bloc/src/domain/entities/query.dart';

class CrudFirestoreDataSourceImpl<Model extends ObjectModel, Entity>
    extends CrudDataSource<Model> {

  CrudFirestoreDataSourceImpl(
    String collection, {
    required Model Function(
      DocumentSnapshot<Map<String, dynamic>>,
      SnapshotOptions?,
    )
        fromFirestore,
    required Map<String, Object?> Function(Model, SetOptions?) toFirestore,
    FirebaseFirestore? firestore,
  })  : _firestore = firestore ?? FirebaseFirestore.instance,
        _toFirestore = toFirestore {
    _collectionReference =
        _firestore.collection(collection).withConverter<Model>(
              fromFirestore: fromFirestore,
              toFirestore: toFirestore,
            );
  }
  final FirebaseFirestore _firestore;

  final Map<String, Object?> Function(Model, SetOptions?) _toFirestore;
  late CollectionReference<Model> _collectionReference;

  @override
  Future<void> create(Model object, {String? id}) {
    if (id != null) {
      return _collectionReference.doc(id).set(object);
    } else {
      if (object.id != null) {
        return _collectionReference.doc(object.id).set(object);
      }
      return _collectionReference.add(object);
    }
  }

  @override
  Future<void> delete(String id) => _collectionReference.doc(id).delete();

  @override
  Future<void> deleteAll() async {
    final batch = _firestore.batch();
    final QuerySnapshot snapshots = await _collectionReference.get();
    for (final DocumentSnapshot snapshot in snapshots.docs) {
      batch.delete(snapshot.reference);
    }
    return batch.commit();
  }

  @override
  Future<Model?> get(String id) async {
    final DocumentSnapshot<Model> snapshot =
        await _collectionReference.doc(id).get();
    return snapshot.data();
  }

  @override
  Future<List<Model?>> getAll() async {
    final QuerySnapshot<Model> snapshots = await _collectionReference.get();
    return snapshots.docs.map((snapshot) => snapshot.data()).toList();
  }

  @override
  Future<List<Model?>> query(List<QueryInterface> conditions) async {
    Query<Model> query = _collectionReference;
    for (final condition in conditions) {
      query = _queryParser(condition, query);
    }
    final QuerySnapshot<Model> snapshots = await query.get();
    return snapshots.docs.map((snapshot) => snapshot.data()).toList();
  }

  @override
  Stream<List<Model?>> stream({
    String? id,
    List<QueryInterface>? conditions,
    bool includeMetadataChanges = false,
  }) {
    if (id != null) {
      return _collectionReference
          .doc(id)
          .snapshots(
            includeMetadataChanges: includeMetadataChanges,
          )
          .map<List<Model?>>(
            (snapshot) => [snapshot.data()],
          );
    } else {
      if (conditions != null) {
        Query<Model> query = _collectionReference;
        for (final condition in conditions) {
          query = _queryParser(condition, query);
        }
        return query
            .snapshots(
              includeMetadataChanges: includeMetadataChanges,
            )
            .map(
              (querySnapshot) => querySnapshot.docs
                  .map((snapshot) => snapshot.data())
                  .toList(),
            );
      } else {
        return _collectionReference
            .snapshots(
              includeMetadataChanges: includeMetadataChanges,
            )
            .map(
              (querySnapshot) => querySnapshot.docs
                  .map((snapshot) => snapshot.data())
                  .toList(),
            );
      }
    }
  }

  @override
  Future<void> update(
    String id, {
    Model? object,
    Map<String, dynamic>? raw,
  }) {
    if (object != null) {
      return _collectionReference
          .doc(id)
          .update(_toFirestore.call(object, null));
    } else {
      if (raw != null) {
        return _collectionReference.doc(id).update(raw);
      } else {
        throw Exception('You must provide an object or a raw map');
      }
    }
  }

  @override
  Future<void> updateAll(Map<String, Object?>? data) async {
    if (data == null) {
      throw Exception('You must provide data to update');
    }
    final batch = _firestore.batch();
    final QuerySnapshot<Model> snapshots = await _collectionReference.get();
    for (final DocumentSnapshot snapshot in snapshots.docs) {
      batch.update(snapshot.reference, data);
    }
    return batch.commit();
  }

  Query<Model> _queryParser(QueryInterface condition, Object query) {
    query as Query<Model>;
    if (condition is WhereQuery) {
      switch (condition.type) {
        case WhereQueryType.isEqualTo:
          return query.where(condition.field, isEqualTo: condition.value);
        case WhereQueryType.isNotEqualTo:
          return query.where(condition.field, isNotEqualTo: condition.value);
        case WhereQueryType.isLessThan:
          return query.where(condition.field, isLessThan: condition.value);
        case WhereQueryType.isLessThanOrEqualTo:
          return query.where(
            condition.field,
            isLessThanOrEqualTo: condition.value,
          );
        case WhereQueryType.isGreaterThan:
          return query.where(condition.field, isGreaterThan: condition.value);
        case WhereQueryType.isGreaterThanOrEqualTo:
          return query.where(
            condition.field,
            isGreaterThanOrEqualTo: condition.value,
          );
        case WhereQueryType.arrayContains:
          return query.where(condition.field, arrayContains: condition.value);
        case WhereQueryType.arrayContainsAny:
          return query.where(
            condition.field,
            arrayContainsAny: condition.value as List<Object>,
          );
        case WhereQueryType.whereIn:
          return query.where(
            condition.field,
            whereIn: condition.value as List<Object>,
          );
        case WhereQueryType.whereNotIn:
          return query.where(
            condition.field,
            whereNotIn: condition.value as List<Object>,
          );
        case WhereQueryType.isNull:
          return query.where(condition.field, isNull: condition.value as bool);
      }
    } else if (condition is LimitQuery) {
      return query.limit(condition.limit);
    } else if (condition is OrderByQuery) {
      return query.orderBy(
        condition.field,
        descending: !condition.ascending,
      );
    }
    return query;
  }
}
