/*
 *   Famedly Matrix SDK
 *   Copyright (C) 2019, 2020 Famedly GmbH
 *
 *   This program is free software: you can redistribute it and/or modify
 *   it under the terms of the GNU Affero General Public License as
 *   published by the Free Software Foundation, either version 3 of the
 *   License, or (at your option) 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 Affero General Public License for more details.
 *
 *   You should have received a copy of the GNU Affero General Public License
 *   along with this program.  If not, see .
 */
import 'dart:async';
import 'package:famedlysdk/matrix_api.dart';
import 'package:famedlysdk/encryption.dart';
import 'event.dart';
import 'room.dart';
import 'utils/event_update.dart';
import 'utils/room_update.dart';
typedef onTimelineUpdateCallback = void Function();
typedef onTimelineInsertCallback = void Function(int insertID);
/// Represents the timeline of a room. The callbacks [onUpdate], [onDelete],
/// [onInsert] and [onResort] will be triggered automatically. The initial
/// event list will be retreived when created by the [room.getTimeline] method.
class Timeline {
  final Room room;
  List events = [];
  final onTimelineUpdateCallback onUpdate;
  final onTimelineInsertCallback onInsert;
  StreamSubscription sub;
  StreamSubscription roomSub;
  StreamSubscription sessionIdReceivedSub;
  bool _requestingHistoryLock = false;
  final Map _eventCache = {};
  /// Searches for the event in this timeline. If not
  /// found, requests from the server. Requested events
  /// are cached.
  Future getEventById(String id) async {
    for (var i = 0; i < events.length; i++) {
      if (events[i].eventId == id) return events[i];
    }
    if (_eventCache.containsKey(id)) return _eventCache[id];
    final requestedEvent = await room.getEventById(id);
    if (requestedEvent == null) return null;
    _eventCache[id] = requestedEvent;
    return _eventCache[id];
  }
  Future requestHistory(
      {int historyCount = Room.DefaultHistoryCount}) async {
    if (!_requestingHistoryLock) {
      _requestingHistoryLock = true;
      await room.requestHistory(
        historyCount: historyCount,
        onHistoryReceived: () {
          if (room.prev_batch.isEmpty || room.prev_batch == null) events = [];
        },
      );
      await Future.delayed(const Duration(seconds: 2));
      _requestingHistoryLock = false;
    }
  }
  Timeline({this.room, this.events, this.onUpdate, this.onInsert}) {
    sub ??= room.client.onEvent.stream.listen(_handleEventUpdate);
    // if the timeline is limited we want to clear our events cache
    // as r.limitedTimeline can be "null" sometimes, we need to check for == true
    // as after receiving a limited timeline room update new events are expected
    // to be received via the onEvent stream, it is unneeded to call sortAndUpdate
    roomSub ??= room.client.onRoomUpdate.stream
        .where((r) => r.id == room.id && r.limitedTimeline == true)
        .listen((r) => events.clear());
    sessionIdReceivedSub ??=
        room.onSessionKeyReceived.stream.listen(_sessionKeyReceived);
  }
  /// Don't forget to call this before you dismiss this object!
  void cancelSubscriptions() {
    sub?.cancel();
    roomSub?.cancel();
    sessionIdReceivedSub?.cancel();
  }
  void _sessionKeyReceived(String sessionId) async {
    var decryptAtLeastOneEvent = false;
    final decryptFn = () async {
      if (!room.client.encryptionEnabled) {
        return;
      }
      for (var i = 0; i < events.length; i++) {
        if (events[i].type == EventTypes.Encrypted &&
            events[i].messageType == MessageTypes.BadEncrypted &&
            events[i].content['body'] == DecryptError.UNKNOWN_SESSION &&
            events[i].content['session_id'] == sessionId) {
          events[i] = await room.client.encryption
              .decryptRoomEvent(room.id, events[i], store: true);
          if (events[i].type != EventTypes.Encrypted) {
            decryptAtLeastOneEvent = true;
          }
        }
      }
    };
    if (room.client.database != null) {
      await room.client.database.transaction(decryptFn);
    } else {
      await decryptFn();
    }
    if (decryptAtLeastOneEvent) onUpdate();
  }
  int _findEvent({String event_id, String unsigned_txid}) {
    int i;
    for (i = 0; i < events.length; i++) {
      if (events[i].eventId == event_id ||
          (unsigned_txid != null && events[i].eventId == unsigned_txid)) break;
    }
    return i;
  }
  void _handleEventUpdate(EventUpdate eventUpdate) async {
    try {
      if (eventUpdate.roomID != room.id) return;
      if (eventUpdate.type == 'timeline' || eventUpdate.type == 'history') {
        // Redaction events are handled as modification for existing events.
        if (eventUpdate.eventType == EventTypes.Redaction) {
          final eventId = _findEvent(event_id: eventUpdate.content['redacts']);
          if (eventId != null) {
            events[eventId].setRedactionEvent(Event.fromJson(
                eventUpdate.content, room, eventUpdate.sortOrder));
          }
        } else if (eventUpdate.content['status'] == -2) {
          var i = _findEvent(event_id: eventUpdate.content['event_id']);
          if (i < events.length) events.removeAt(i);
        }
        // Is this event already in the timeline?
        else if (eventUpdate.content.containsKey('unsigned') &&
            eventUpdate.content['unsigned']['transaction_id'] is String) {
          var i = _findEvent(
              event_id: eventUpdate.content['event_id'],
              unsigned_txid: eventUpdate.content.containsKey('unsigned')
                  ? eventUpdate.content['unsigned']['transaction_id']
                  : null);
          if (i < events.length) {
            events[i] = Event.fromJson(
                eventUpdate.content, room, eventUpdate.sortOrder);
          }
        } else {
          Event newEvent;
          var senderUser = room
                  .getState(
                      EventTypes.RoomMember, eventUpdate.content['sender'])
                  ?.asUser ??
              await room.client.database?.getUser(
                  room.client.id, eventUpdate.content['sender'], room);
          if (senderUser != null) {
            eventUpdate.content['displayname'] = senderUser.displayName;
            eventUpdate.content['avatar_url'] = senderUser.avatarUrl.toString();
          }
          newEvent =
              Event.fromJson(eventUpdate.content, room, eventUpdate.sortOrder);
          if (eventUpdate.type == 'history' &&
              events.indexWhere(
                      (e) => e.eventId == eventUpdate.content['event_id']) !=
                  -1) return;
          events.insert(0, newEvent);
          if (onInsert != null) onInsert(0);
        }
      }
      sortAndUpdate();
    } catch (e) {
      if (room.client.debug) {
        print('[WARNING] (_handleEventUpdate) ${e.toString()}');
      }
    }
  }
  bool sortLock = false;
  void sort() {
    if (sortLock || events.length < 2) return;
    sortLock = true;
    events?.sort((a, b) => b.sortOrder - a.sortOrder > 0 ? 1 : -1);
    sortLock = false;
  }
  void sortAndUpdate() async {
    sort();
    if (onUpdate != null) onUpdate();
  }
}