// 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 'dart:async';

import 'package:wyatt_crud_bloc/src/core/enums/where_query_type.dart';
import 'package:wyatt_crud_bloc/src/core/extensions/num_extension.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 CrudInMemoryDataSourceImpl<Model extends ObjectModel>
    extends CrudDataSource<Model> {

  CrudInMemoryDataSourceImpl({required this.toMap, Map<String, Model>? data})
      : _data = data ?? {};
  final Map<String, Model> _data;
  final StreamController<List<Model?>> _streamData = StreamController();

  final Map<String, Object?> Function(Model) toMap;

  @override
  Future<void> create(Model object, {String? id}) async {
    _data[id ?? object.id ?? ''] = object;
    _streamData.add(_data.values.toList());
  }

  @override
  Future<void> delete(String id) async {
    _data.remove(id);
    _streamData.add(_data.values.toList());
  }

  @override
  Future<void> deleteAll() async {
    _data.clear();
    _streamData.add(_data.values.toList());
  }

  @override
  Future<Model?> get(String id) async => _data[id];

  @override
  Future<List<Model?>> getAll() async => _data.values.toList();

  @override
  Future<List<Model?>> query(List<QueryInterface> conditions) async {
    List<Model> result = _data.values.toList();

    for (final c in conditions) {
      if (c is WhereQuery) {
        result = result.where((m) => _whereQuery(c, m)).toList();
      }
      if (c is LimitQuery) {
        final limit = result.length - c.limit;
        result = result.sublist(limit >= 0 ? limit : 0);
      }
      if (c is OrderByQuery) {
        try {
          result.sort();
        } catch (_) {}
        if (c.ascending) {
          result = result.reversed.toList();
        }
      }
    }

    result.cast<Model>();

    return result;
  }

  @override
  Stream<List<Model?>> stream({
    String? id,
    List<QueryInterface>? conditions,
    bool includeMetadataChanges = false,
  }) =>
      _streamData.stream.map((result) {
        if (conditions == null) {
          return result;
        }

        List<Model?> res = result;

        for (final c in conditions) {
          if (c is WhereQuery) {
            res = res.where((element) => _whereQuery(c, element)).toList();
          }
          if (c is LimitQuery) {
            res = res.sublist(res.length - c.limit);
          }
          if (c is OrderByQuery) {
            res.sort();
            if (c.ascending) {
              res = res.reversed.toList();
            }
          }
        }

        return res;
      }).asBroadcastStream();

  @override
  Future<void> update(
    String id, {
    Model? object,
    Map<String, dynamic>? raw,
  }) {
    // TODO(hpcl): implement update
    throw UnimplementedError();
  }

  @override
  Future<void> updateAll(Map<String, Object?>? data) {
    // TODO(hcpl): implement updateAll
    throw UnimplementedError();
  }

  bool _whereQuery(QueryInterface condition, Model? object) {
    if (object == null) {
      return false;
    }
    final raw = toMap.call(object);
    if (condition is WhereQuery) {
      switch (condition.type) {
        case WhereQueryType.isEqualTo:
          return raw[condition.field] == condition.value;
        case WhereQueryType.isNotEqualTo:
          return raw[condition.field] != condition.value;
        case WhereQueryType.isLessThan:
          return (raw[condition.field] as num?) < (condition.value as num?);
        case WhereQueryType.isLessThanOrEqualTo:
          return (raw[condition.field] as num?) <= (condition.value as num?);
        case WhereQueryType.isGreaterThan:
          return (raw[condition.field] as num?) > (condition.value as num?);
        case WhereQueryType.isGreaterThanOrEqualTo:
          return (raw[condition.field] as num?) >= (condition.value as num?);
        case WhereQueryType.arrayContains:
          return (raw[condition.field] as List<Object>?)
                  ?.contains(condition.value) ??
              false;
        case WhereQueryType.arrayContainsAny:
          bool res = false;
          for (final o in condition.value as List<Object>) {
            res = (raw[condition.field] as List<Object>?)?.contains(o) ?? false;
          }
          return res;
        case WhereQueryType.whereIn:
          return (condition.value as List<Object>)
              .contains(raw[condition.field]);
        case WhereQueryType.whereNotIn:
          return !(condition.value as List<Object>)
              .contains(raw[condition.field]);
        case WhereQueryType.isNull:
          return raw[condition.field] == null;
      }
    }
    return true;
  }
}
