/*
 Copyright 2015 OpenMarket Ltd
 Copyright 2017 Vector Creations Ltd
 
 Licensed under the Apache License, Version 2.0 (the "License");
 you may not use this file except in compliance with the License.
 You may obtain a copy of the License at
 
 http://www.apache.org/licenses/LICENSE-2.0
 
 Unless required by applicable law or agreed to in writing, software
 distributed under the License is distributed on an "AS IS" BASIS,
 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 See the License for the specific language governing permissions and
 limitations under the License.
 */

#import "EventFormatter.h"

#import "ThemeService.h"
#import "GeneratedInterface-Swift.h"

#import "WidgetManager.h"

#import "MXDecryptionResult.h"
#import "DecryptionFailureTracker.h"

#import <MatrixSDK/MatrixSDK.h>

#pragma mark - Constants definitions

NSString *const EventFormatterOnReRequestKeysLinkAction = @"EventFormatterOnReRequestKeysLinkAction";
NSString *const EventFormatterLinkActionSeparator = @"/";
NSString *const EventFormatterEditedEventLinkAction = @"EventFormatterEditedEventLinkAction";

NSString *const FunctionalMembersStateEventType = @"io.element.functional_members";
NSString *const FunctionalMembersServiceMembersKey = @"service_members";

static NSString *const kEventFormatterTimeFormat = @"HH:mm";

@interface EventFormatter ()
{
    /**
     The calendar used to retrieve the today date.
     */
    NSCalendar *calendar;
}
@end

@implementation EventFormatter

- (void)initDateTimeFormatters
{
    [super initDateTimeFormatters];
    
    timeFormatter.locale = [NSLocale localeWithLocaleIdentifier:@"en_US_POSIX"];
    [timeFormatter setDateFormat:kEventFormatterTimeFormat];
}

- (NSString *)stringFromEvent:(MXEvent *)event
                withRoomState:(MXRoomState *)roomState
           andLatestRoomState:(MXRoomState *)latestRoomState
                        error:(MXKEventFormatterError *)error
{
    NSString *stringFromEvent;
    NSAttributedString *attributedStringFromEvent = [self attributedStringFromEvent:event
                                                                      withRoomState:roomState
                                                                 andLatestRoomState:latestRoomState
                                                                       displayPills:NO
                                                                              error:error];
    if (*error == MXKEventFormatterErrorNone)
    {
        stringFromEvent = attributedStringFromEvent.string;
    }

    return stringFromEvent;
}

- (NSAttributedString *)attributedStringFromEvent:(MXEvent *)event
                                    withRoomState:(MXRoomState *)roomState
                               andLatestRoomState:(MXRoomState *)latestRoomState
                                     displayPills:(BOOL)displayPills
                                            error:(MXKEventFormatterError *)error
{
    NSAttributedString *string = [self unsafeAttributedStringFromEvent:event
                                                         withRoomState:roomState
                                                    andLatestRoomState:latestRoomState
                                                                 error:error];
    if (!string)
    {
        MXLogDebug(@"[EventFormatter]: No attributed string for event: %@, type: %@, msgtype: %@, has room state: %d, members: %lu, error: %lu",
                   event.eventId,
                   event.type,
                   event.content[@"msgtype"],
                   roomState != nil,
                   roomState.membersCount.members,
                   *error);

        // If we cannot create attributed string, but the message is nevertheless meant for display, show generic error
        // instead of a missing message on a timeline.
        if ([self shouldDisplayEvent:event]) {
            MXLogErrorDetails(@"[EventFormatter]: Missing attributed string for message event", @{
                @"event_id": event.eventId ?: @"unknown"
            });
            string = [[NSAttributedString alloc] initWithString:[VectorL10n noticeErrorUnformattableEvent] attributes:@{
                NSFontAttributeName: [self encryptedMessagesTextFont],
                NSForegroundColorAttributeName: [self encryptingTextColor]
            }];
        }
    }

    if (@available(iOS 15.0, *))
    {
        if (displayPills && roomState && [self shouldDisplayEvent:event])
        {
            string = [PillsFormatter insertPillsIn:string
                                       withSession:mxSession
                                    eventFormatter:self
                                             event:event
                                         roomState:roomState
                                andLatestRoomState:latestRoomState
                                        isEditMode:NO];
        }
    }

    return string;
}

- (NSAttributedString *)attributedStringFromEvent:(MXEvent *)event
                                    withRoomState:(MXRoomState *)roomState
                               andLatestRoomState:(MXRoomState *)latestRoomState
                                            error:(MXKEventFormatterError *)error
{
    return [self attributedStringFromEvent:event
                             withRoomState:roomState
                        andLatestRoomState:latestRoomState
                              displayPills:YES
                                     error:error];
}

- (BOOL)shouldDisplayEvent:(MXEvent *)event {
    return event.eventType == MXEventTypeRoomMessage
    && !event.isEditEvent
    && !event.isRedactedEvent;
}

// The attributed string can fail to be created for a number of reasons, and the size of the function (as well as super's implementation) makes
// it impossible to catch all the `return nil` and failure states.
// To make catching of missing strings reliable (and not place that burden on callers), we use private `unsafeAttributedStringFromEvent` method
// which is called by the public `attributedStringFromEvent`, and which also handles the catch-all missing message.
- (NSAttributedString *)unsafeAttributedStringFromEvent:(MXEvent *)event
                                          withRoomState:(MXRoomState *)roomState
                                     andLatestRoomState:(MXRoomState *)latestRoomState
                                                  error:(MXKEventFormatterError *)error
{
    if (event.isRedactedEvent)
    {
        if (event.eventType == MXEventTypeReaction)
        {
            //  do not show redacted reactions in the timeline
            return nil;
        }
        // Check whether the event is a thread root or redacted information is required
        if ((RiotSettings.shared.enableThreads && [mxSession.threadingService isEventThreadRoot:event])
            || self.settings.showRedactionsInRoomHistory)
        {
            NSAttributedString *result = [self redactedMessageReplacementAttributedString];
            
            if (error)
            {
                *error = MXKEventFormatterErrorNone;
            }
            
            return result;
        }
    }
    BOOL isEventSenderMyUser = [event.sender isEqualToString:mxSession.myUserId];
    
    if (event.eventType == MXEventTypeCustom) {
        
        // Build strings for widget events
        if ([event.type isEqualToString:kWidgetMatrixEventTypeString]
                || [event.type isEqualToString:kWidgetModularEventTypeString])
        {
            NSString *displayText;
            
            Widget *widget = [[Widget alloc] initWithWidgetEvent:event inMatrixSession:mxSession];
            if (widget)
            {
                // Prepare the display name of the sender
                NSString *senderDisplayName = roomState ? [self senderDisplayNameForEvent:event withRoomState:roomState] : event.sender;
                
                if (widget.isActive)
                {
                    if ([widget.type isEqualToString:kWidgetTypeJitsiV1]
                        || [widget.type isEqualToString:kWidgetTypeJitsiV2])
                    {
                        // This is an alive jitsi widget
                        if (isEventSenderMyUser)
                        {
                            displayText = [VectorL10n eventFormatterJitsiWidgetAddedByYou];
                        }
                        else
                        {
                            displayText = [VectorL10n eventFormatterJitsiWidgetAdded:senderDisplayName];
                        }
                    }
                    else
                    {
                        if (isEventSenderMyUser)
                        {
                            displayText = [VectorL10n eventFormatterWidgetAddedByYou:(widget.name ? widget.name : widget.type)];
                        }
                        else
                        {
                            displayText = [VectorL10n eventFormatterWidgetAdded:(widget.name ? widget.name : widget.type) :senderDisplayName];
                        }
                    }
                }
                else
                {
                    // This is a closed widget
                    // Check if it corresponds to a jitsi widget by looking at other state events for
                    // this jitsi widget (widget id = event.stateKey).
                    // Get all widgets state events in the room
                    NSMutableArray<MXEvent*> *widgetStateEvents = [NSMutableArray arrayWithArray:[roomState stateEventsWithType:kWidgetMatrixEventTypeString]];
                    [widgetStateEvents addObjectsFromArray:[roomState stateEventsWithType:kWidgetModularEventTypeString]];
                    
                    for (MXEvent *widgetStateEvent in widgetStateEvents)
                    {
                        if ([widgetStateEvent.stateKey isEqualToString:widget.widgetId])
                        {
                            Widget *activeWidget = [[Widget alloc] initWithWidgetEvent:widgetStateEvent inMatrixSession:mxSession];
                            if (activeWidget.isActive)
                            {
                                if ([activeWidget.type isEqualToString:kWidgetTypeJitsiV1]
                                    || [activeWidget.type isEqualToString:kWidgetTypeJitsiV2])
                                {
                                    // This was a jitsi widget
                                    return nil;
                                }
                                else
                                {
                                    if (isEventSenderMyUser)
                                    {
                                        displayText = [VectorL10n eventFormatterWidgetRemovedByYou:(activeWidget.name ? activeWidget.name : activeWidget.type)];
                                    }
                                    else
                                    {
                                        displayText = [VectorL10n eventFormatterWidgetRemoved:(activeWidget.name ? activeWidget.name : activeWidget.type) :senderDisplayName];
                                    }
                                }
                                break;
                            }
                        }
                    }
                }
            }
        
            if (displayText)
            {
                if (error)
                {
                    *error = MXKEventFormatterErrorNone;
                }

                // Build the attributed string with the right font and color for the events
                return [self renderString:displayText forEvent:event];
            }
        } else if ([event.type isEqualToString:VoiceBroadcastSettings.voiceBroadcastInfoContentKeyType]) {
            //  do not show voice broadcast info in the timeline
            return nil;
        }
    }
    
    switch (event.eventType)
    {
        case MXEventTypeRoomCreate:
        {
            MXRoomCreateContent *createContent = [MXRoomCreateContent modelFromJSON:event.content];
            
            NSString *roomPredecessorId = createContent.roomPredecessorInfo.roomId;
            
            if (roomPredecessorId)
            {
                return [self roomCreatePredecessorAttributedStringWithPredecessorRoomId:roomPredecessorId];
            }
            else
            {
                NSAttributedString *string = [super attributedStringFromEvent:event
                                                                withRoomState:roomState
                                                           andLatestRoomState:latestRoomState
                                                                        error:error];
                NSMutableAttributedString *result = [[NSMutableAttributedString alloc] initWithString:@"· "];
                [result appendAttributedString:string];
                return result;
            }
        }
            break;
        case MXEventTypeCallCandidates:
        case MXEventTypeCallSelectAnswer:
        case MXEventTypeCallNegotiate:
        case MXEventTypeCallReplaces:
        case MXEventTypeCallRejectReplacement:
            //  Do not show call events except invite and reject in timeline
            return nil;
        case MXEventTypeCallInvite:
        {
            MXCallInviteEventContent *content = [MXCallInviteEventContent modelFromJSON:event.content];
            MXCall *call = [mxSession.callManager callWithCallId:content.callId];
            if (call && call.isIncoming && call.state == MXCallStateRinging)
            {
                //  incoming call UI will be handled by CallKit (or incoming call screen if CallKit disabled)
                //  do not show a bubble for this case
                return nil;
            }
        }
            break;
        case MXEventTypeKeyVerificationCancel:
        case MXEventTypeKeyVerificationDone:
            // Make event types MXEventTypeKeyVerificationCancel and MXEventTypeKeyVerificationDone visible in timeline.
            // TODO: Find another way to keep them visible and avoid instantiate empty NSMutableAttributedString.
            return [NSMutableAttributedString new];
        default:
            break;
    }
    
    NSAttributedString *attributedString = [super attributedStringFromEvent:event
                                                              withRoomState:roomState
                                                         andLatestRoomState:latestRoomState
                                                                      error:error];

    if (event.sentState == MXEventSentStateSent
        && [event.decryptionError.domain isEqualToString:MXDecryptingErrorDomain])
    {
        // Track e2e failures
        dispatch_async(dispatch_get_main_queue(), ^{
            [[DecryptionFailureTracker sharedInstance] reportUnableToDecryptErrorForEvent:event withRoomState:roomState myUser:self->mxSession.myUser.userId];
        });

        if (event.decryptionError.code == MXDecryptingErrorUnknownInboundSessionIdCode)
        {
            // Append to the displayed error an attibuted string with a tappable link
            // so that the user can try to fix the UTD
            NSMutableAttributedString *attributedStringWithRerequestMessage = [attributedString mutableCopy];
            [attributedStringWithRerequestMessage appendAttributedString:[[NSAttributedString alloc] initWithString:@"\n"]];

            NSString *linkActionString = [NSString stringWithFormat:@"%@%@%@", EventFormatterOnReRequestKeysLinkAction,
                                          EventFormatterLinkActionSeparator,
                                          event.eventId];

            [attributedStringWithRerequestMessage appendAttributedString:
             [[NSAttributedString alloc] initWithString:[VectorL10n eventFormatterRerequestKeysPart1Link]
                                             attributes:@{
                                                          NSLinkAttributeName: linkActionString,
                                                          NSForegroundColorAttributeName: self.sendingTextColor,
                                                          NSFontAttributeName: self.encryptedMessagesTextFont,
                                                          NSUnderlineStyleAttributeName: [NSNumber numberWithInt:NSUnderlineStyleSingle] 
                                                          }]];

            [attributedStringWithRerequestMessage appendAttributedString:
             [[NSAttributedString alloc] initWithString:[VectorL10n eventFormatterRerequestKeysPart2]
                                             attributes:@{
                                                          NSForegroundColorAttributeName: self.sendingTextColor,
                                                          NSFontAttributeName: self.encryptedMessagesTextFont
                                                          }]];

            attributedString = attributedStringWithRerequestMessage;
        }
    }
    else if (self.showEditionMention && event.contentHasBeenEdited)
    {
        NSMutableAttributedString *attributedStringWithEditMention = [attributedString mutableCopy];
        
        NSString *linkActionString = [NSString stringWithFormat:@"%@%@%@", EventFormatterEditedEventLinkAction,
                                      EventFormatterLinkActionSeparator,
                                      event.eventId];
        
        [attributedStringWithEditMention appendAttributedString:
         [[NSAttributedString alloc] initWithString:[NSString stringWithFormat:@" %@", [VectorL10n eventFormatterMessageEditedMention]]
                                         attributes:@{
                                                      NSLinkAttributeName: linkActionString,
                                                      NSForegroundColorAttributeName: self.editionMentionTextColor,
                                                      NSFontAttributeName: self.editionMentionTextFont
                                                      }]];
        
        attributedString = attributedStringWithEditMention;
    }

    return attributedString;
}

- (NSAttributedString*)attributedStringFromEvents:(NSArray<MXEvent*>*)events
                                    withRoomState:(MXRoomState*)roomState
                               andLatestRoomState:(MXRoomState*)latestRoomState
                                            error:(MXKEventFormatterError*)error
{
    NSString *displayText;

    if (events.count)
    {
        MXEvent *roomCreateEvent = [events filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"type == %@", kMXEventTypeStringRoomCreate]].firstObject;
        
        MXEvent *callInviteEvent = [events filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"type == %@", kMXEventTypeStringCallInvite]].firstObject;
        
        if (roomCreateEvent)
        {
            MXKEventFormatterError tmpError;
            displayText = [super attributedStringFromEvent:roomCreateEvent
                                             withRoomState:roomState
                                        andLatestRoomState:latestRoomState
                                                     error:&tmpError].string;

            NSAttributedString *rendered = [self renderString:displayText forEvent:roomCreateEvent];
            NSMutableAttributedString *result = [[NSMutableAttributedString alloc] initWithString:@"· "];
            [result appendAttributedString:rendered];
            [result setAttributes:@{
                NSFontAttributeName: [UIFont systemFontOfSize:13],
                NSForegroundColorAttributeName: ThemeService.shared.theme.textSecondaryColor
            } range:NSMakeRange(0, result.length)];
            //  add one-char space
            [result appendAttributedString:[[NSAttributedString alloc] initWithString:@" "]];
            //  add more link
            NSAttributedString *linkMore = [[NSAttributedString alloc] initWithString:[VectorL10n more] attributes:@{
                NSFontAttributeName: [UIFont systemFontOfSize:13],
                NSForegroundColorAttributeName: ThemeService.shared.theme.tintColor
            }];
            [result appendAttributedString:linkMore];
            return result;
        }
        else if (callInviteEvent)
        {
            //  return a non-nil value
            return [NSMutableAttributedString new];
        }
        else if (events[0].eventType == MXEventTypeRoomMember)
        {
            // This is a series for cells tagged with RoomBubbleCellDataTagMembership
            // TODO: Build a complete summary like Riot-web
            displayText = [VectorL10n eventFormatterMemberUpdates:events.count];
        }
    }

    if (displayText)
    {
        // Build the attributed string with the right font and color for the events
        return [self renderString:displayText forEvent:events[0]];
    }

    return [super attributedStringFromEvents:events
                               withRoomState:roomState
                          andLatestRoomState:latestRoomState
                                       error:error];
}

- (instancetype)initWithMatrixSession:(MXSession *)matrixSession
{
    self = [super initWithMatrixSession:matrixSession];
    if (self)
    {
        calendar = [[NSCalendar alloc] initWithCalendarIdentifier:NSCalendarIdentifierGregorian];
        
        // Use the selected bg color to set the code block background color in the default CSS.
        NSUInteger bgColor = [MXKTools rgbValueWithColor:ThemeService.shared.theme.selectedBackgroundColor];
        self.defaultCSS = [NSString stringWithFormat:@" \
                           pre,code { \
                           background-color: #%06lX; \
                           display: inline; \
                           font-family: monospace; \
                           white-space: pre; \
                           -coretext-fontname: Menlo-Regular; \
                           font-size: small; \
                           } \
                           h1,h2 { \
                           font-size: 1.2em; \
                           }", (unsigned long)bgColor];
        
        self.defaultTextColor = ThemeService.shared.theme.textPrimaryColor;
        self.subTitleTextColor = ThemeService.shared.theme.textSecondaryColor;
        self.prefixTextColor = ThemeService.shared.theme.textSecondaryColor;
        self.bingTextColor = ThemeService.shared.theme.noticeColor;
        self.encryptingTextColor = ThemeService.shared.theme.textPrimaryColor;
        self.sendingTextColor = ThemeService.shared.theme.textPrimaryColor;
        self.linksColor = ThemeService.shared.theme.colors.links;
        self.errorTextColor = ThemeService.shared.theme.textPrimaryColor;
        self.showEditionMention = YES;
        self.editionMentionTextColor = ThemeService.shared.theme.textSecondaryColor;
        
        self.defaultTextFont = [UIFont systemFontOfSize:15];
        self.prefixTextFont = [UIFont boldSystemFontOfSize:15];
        self.bingTextFont = [UIFont systemFontOfSize:15 weight:UIFontWeightMedium];
        self.stateEventTextFont = [UIFont italicSystemFontOfSize:15];
        self.callNoticesTextFont = [UIFont italicSystemFontOfSize:15];
        self.encryptedMessagesTextFont = [UIFont italicSystemFontOfSize:15];
        self.emojiOnlyTextFont = [UIFont systemFontOfSize:48];
        self.editionMentionTextFont = [UIFont systemFontOfSize:12];
        
        // Handle space and video room types, enables their display in the room list
        defaultRoomSummaryUpdater.showRoomTypeStrings = @[
            MXRoomTypeStringSpace,
            MXRoomTypeStringVideo
        ];
    }
    return self;
}

- (NSDictionary*)stringAttributesForEventTimestamp
{
    return @{
             NSForegroundColorAttributeName : [UIColor lightGrayColor],
             NSFontAttributeName: [UIFont systemFontOfSize:10]
             };
}

#pragma mark event sender info

- (NSString*)senderAvatarUrlForEvent:(MXEvent*)event withRoomState:(MXRoomState*)roomState
{
    // Override this method to ignore the identicons defined by default in matrix kit.
    
    // Consider first the avatar url defined in provided room state (Note: this room state is supposed to not take the new event into account)
    NSString *senderAvatarUrl = [roomState.members memberWithUserId:event.sender].avatarUrl;
    
    // Check whether this avatar url is updated by the current event (This happens in case of new joined member)
    NSString* membership = event.content[@"membership"];
    NSString* eventAvatarUrl = event.content[@"avatar_url"];
    NSString* prevEventAvatarUrl = event.prevContent[@"avatar_url"];
    if (membership && [membership isEqualToString:@"join"] && [eventAvatarUrl length] && ![eventAvatarUrl isEqualToString:prevEventAvatarUrl])
    {
        // Use the actual avatar
        senderAvatarUrl = eventAvatarUrl;
    }
    
    // We ignore non mxc avatar url (The identicons are removed here).
    if (senderAvatarUrl && [senderAvatarUrl hasPrefix:kMXContentUriScheme] == NO)
    {
        senderAvatarUrl = nil;
    }
    
    return senderAvatarUrl;
}

#pragma mark - MXRoomSummaryUpdating
- (BOOL)session:(MXSession *)session updateRoomSummary:(MXRoomSummary *)summary withLastEvent:(MXEvent *)event eventState:(MXRoomState *)eventState roomState:(MXRoomState *)roomState
{
    // Do not display voice broadcast chunk in last message.
    if (event.eventType == MXEventTypeRoomMessage && event.content[VoiceBroadcastSettings.voiceBroadcastContentKeyChunkType])
    {
        return NO;
    }
    
    // Update last message if we have a voice broadcast in the room.
    MXEvent *lastVoiceBroadcastInfoEvent = [self lastVoiceBroadcastInfoEventWithEvent:event roomState:roomState];
    if (lastVoiceBroadcastInfoEvent != nil)
    {
        MXEvent *voiceBroadcastInfoStartedEvent = [self voiceBroadcastInfoStartedEventWithEvent:lastVoiceBroadcastInfoEvent
                                                                                         roomId:summary.roomId
                                                                                        session:session];
        if (voiceBroadcastInfoStartedEvent != nil
            && !(voiceBroadcastInfoStartedEvent.isRedactedEvent || [voiceBroadcastInfoStartedEvent.eventId isEqualToString:event.redacts]))
        {
            return [self session:session
               updateRoomSummary:summary
withVoiceBroadcastInfoStateEvent:lastVoiceBroadcastInfoEvent
  voiceBroadcastInfoStartedEvent:voiceBroadcastInfoStartedEvent
                       roomState:roomState];
        }
    }
    
    BOOL updated = [super session:session updateRoomSummary:summary withLastEvent:event eventState:eventState roomState:roomState];
    
    if (updated)
    {
        // Force the default text color for the last message (cancel highlighted message color)
        NSMutableAttributedString *lastEventDescription = [[NSMutableAttributedString alloc] initWithAttributedString:summary.lastMessage.attributedText];
        NSRange range = NSMakeRange(0, lastEventDescription.length);
        [lastEventDescription addAttribute:NSForegroundColorAttributeName
                                     value:ThemeService.shared.theme.colors.secondaryContent
                                     range:range];
        [lastEventDescription addThemeColorNameAttribute:@"secondaryContent" range:range];
        [lastEventDescription addThemeIdentifierAttribute];
        
        summary.lastMessage.attributedText = lastEventDescription;
    }
    
    return updated;
}


- (MXEvent *)lastVoiceBroadcastInfoEventWithEvent:(MXEvent *)event roomState:(MXRoomState *)roomState
{
    MXEvent *voiceBroadcastInfoEvent = nil;
    VoiceBroadcastInfo *info = nil;
    if ([event.type isEqualToString:VoiceBroadcastSettings.voiceBroadcastInfoContentKeyType])
    {
        info = [VoiceBroadcastInfo modelFromJSON: event.content];
        
        if (info != nil)
        {
            voiceBroadcastInfoEvent = event;
        }
    }
    else
    {
        MXEvent *stateEvent = [roomState stateEventsWithType:VoiceBroadcastSettings.voiceBroadcastInfoContentKeyType].lastObject;
        if (stateEvent != nil)
        {
            info = [VoiceBroadcastInfo modelFromJSON: stateEvent.content];
            if (info != nil && ![VoiceBroadcastInfo isStoppedFor:info.state])
            {
                voiceBroadcastInfoEvent = stateEvent;
            }
        }
    }
    
    return voiceBroadcastInfoEvent;
}

- (MXEvent *)voiceBroadcastInfoStartedEventWithEvent:(MXEvent *)voiceBroadcastInfoEvent roomId:(NSString *)roomId session:(MXSession *)session
{
    VoiceBroadcastInfo *voiceBroadcastInfo = [VoiceBroadcastInfo modelFromJSON: voiceBroadcastInfoEvent.content];
    if ([VoiceBroadcastInfo isStartedFor:voiceBroadcastInfo.state])
    {
        return voiceBroadcastInfoEvent;
    }
    else
    {
        // Search for the event only in the store to avoid network calls while updating the room summary (this a synchronous process and we cannot delay it).
        return [mxSession.store eventWithEventId:voiceBroadcastInfo.voiceBroadcastId inRoom:roomId];
    }
}

- (BOOL)session:(MXSession *)session updateRoomSummary:(MXRoomSummary *)summary withStateEvents:(NSArray<MXEvent *> *)stateEvents roomState:(MXRoomState *)roomState
{
    BOOL updated = [super session:session updateRoomSummary:summary withStateEvents:stateEvents roomState:roomState];
    
    // Customisation for EMS Functional Members in direct rooms
    if (BuildSettings.supportFunctionalMembers && summary.room.isDirect)
    {
        if ([self functionalMembersEventFromStateEvents:stateEvents])
        {
            MXLogDebug(@"[EventFormatter] The functional members event has been updated.")
            
            // The stateEvents parameter contains state events that may change the room summary. If service members are found,
            // it's likely that something changed. As they aren't stored, the only reliable check would be to compute the
            // room name which we'll do twice more in updateRoomSummary:withServerRoomSummary:roomState: anyway.
            //
            // So return YES and let that happen there.
            return YES;
        }
    }
    
    return updated;
}

- (BOOL)session:(MXSession *)session updateRoomSummary:(MXRoomSummary *)summary withVoiceBroadcastInfoStateEvent:(MXEvent *)stateEvent voiceBroadcastInfoStartedEvent:(MXEvent *)voiceBroadcastInfoStartedEvent roomState:(MXRoomState *)roomState
{
    BOOL isStoppedVoiceBroadcast = [VoiceBroadcastInfo isStoppedFor:[VoiceBroadcastInfo modelFromJSON: stateEvent.content].state];
    
    if ([summary.lastMessage.eventId isEqualToString:voiceBroadcastInfoStartedEvent.eventId])
    {
        if (!isStoppedVoiceBroadcast)
        {
            return NO;
        }
    }
    else
    {
        [summary updateLastMessage:[[MXRoomLastMessage alloc] initWithEvent:voiceBroadcastInfoStartedEvent]];
        if (summary.lastMessage.others == nil)
        {
            summary.lastMessage.others = [NSMutableDictionary dictionary];
        }
    }
    
    NSAttributedString *attachmentString = nil;
    UIColor *textColor;
    NSString *colorIdentifier;
    if (isStoppedVoiceBroadcast)
    {
        textColor = ThemeService.shared.theme.colors.secondaryContent;
        colorIdentifier = @"secondaryContent";
        NSString *senderDisplayName;
        if ([stateEvent.stateKey isEqualToString:session.myUser.userId])
        {
            summary.lastMessage.text = VectorL10n.noticeVoiceBroadcastEndedByYou;
        }
        else
        {
            senderDisplayName = [self senderDisplayNameForEvent:stateEvent withRoomState:roomState];
            summary.lastMessage.text = [VectorL10n noticeVoiceBroadcastEnded:senderDisplayName];
        }
        summary.lastMessage.others[@"lastEventDate"] = [self dateStringFromEvent:stateEvent withTime:YES];
    }
    else
    {
        textColor = ThemeService.shared.theme.colors.alert;
        colorIdentifier = @"alert";
        UIImage *liveImage = AssetImages.voiceBroadcastLive.image;
        
        NSTextAttachment *attachment = [[NSTextAttachment alloc] init];
        attachment.image = [liveImage imageWithTintColor:textColor renderingMode:UIImageRenderingModeAlwaysTemplate];
        attachmentString = [NSAttributedString attributedStringWithAttachment:attachment];
        
        summary.lastMessage.text = VectorL10n.noticeVoiceBroadcastLive;
        summary.lastMessage.others[@"lastEventDate"] = [self dateStringFromEvent:voiceBroadcastInfoStartedEvent withTime:YES];
    }
    
    // Compute the attribute text message
    NSMutableAttributedString *lastMessage;
    if (attachmentString)
    {
        lastMessage = [[NSMutableAttributedString alloc] initWithAttributedString:attachmentString];
        // Change base line
        [lastMessage addAttribute:NSBaselineOffsetAttributeName value:@(-3.0f) range:NSMakeRange(0, attachmentString.length)];
        
        NSAttributedString *attributedText = [[NSAttributedString alloc] initWithString:[NSString stringWithFormat:@" %@", summary.lastMessage.text]];
        [lastMessage appendAttributedString:attributedText];
        [lastMessage addAttribute:NSFontAttributeName value:self.defaultTextFont range:NSMakeRange(0, lastMessage.length)];
    }
    else
    {
        NSAttributedString *attributedText = [self renderString:summary.lastMessage.text forEvent:stateEvent];
        lastMessage = [[NSMutableAttributedString alloc] initWithAttributedString:attributedText];
    }
    
    [lastMessage addAttribute:NSForegroundColorAttributeName value:textColor range:NSMakeRange(0, lastMessage.length)];
    if (colorIdentifier)
    {
        [lastMessage addThemeColorNameAttribute:colorIdentifier range:NSMakeRange(0, lastMessage.length)];
        [lastMessage addThemeIdentifierAttribute];
    }

    summary.lastMessage.attributedText = lastMessage;
    
    return YES;
}

- (NSAttributedString *)redactedMessageReplacementAttributedString
{
    UIFont *font = self.defaultTextFont;
    UIColor *color = ThemeService.shared.theme.colors.secondaryContent;
    NSString *string = [NSString stringWithFormat:@" %@", VectorL10n.eventFormatterMessageDeleted];
    NSAttributedString *attrString = [[NSAttributedString alloc] initWithString:string
                                                                     attributes:@{
                                                                         NSFontAttributeName: font,
                                                                         NSForegroundColorAttributeName: color
                                                                     }];

    CGSize imageSize = CGSizeMake(20, 20);
    NSTextAttachment *attachment = [[NSTextAttachment alloc] init];
    attachment.image = [[AssetImages.roomContextMenuDelete.image vc_resizedWith:imageSize] vc_tintedImageUsingColor:color];
    attachment.bounds = CGRectMake(0, font.descender, imageSize.width, imageSize.height);
    NSAttributedString *imageString = [NSAttributedString attributedStringWithAttachment:attachment];

    NSMutableAttributedString *result = [[NSMutableAttributedString alloc] initWithAttributedString:imageString];
    [result appendAttributedString:attrString];

    return result;
}

- (BOOL)session:(MXSession *)session updateRoomSummary:(MXRoomSummary *)summary withServerRoomSummary:(MXRoomSyncSummary *)serverRoomSummary roomState:(MXRoomState *)roomState
{
    BOOL updated = [super session:session updateRoomSummary:summary withServerRoomSummary:serverRoomSummary roomState:roomState];
    
    // Customisation for EMS Functional Members in direct rooms
    if (BuildSettings.supportFunctionalMembers && summary.room.isDirect)
    {
        MXEvent *functionalMembersEvent = [self functionalMembersEventFromStateEvents:roomState.stateEvents];
        
        if (functionalMembersEvent)
        {
            MXLogDebug(@"[EventFormatter] Computing the room name and avatar excluding functional members.")
            
            NSArray<NSString*> *serviceMemberIDs = functionalMembersEvent.content[FunctionalMembersServiceMembersKey] ?: @[];
            
            updated |= [defaultRoomSummaryUpdater updateSummaryDisplayname:summary
                                                                   session:session
                                                     withServerRoomSummary:serverRoomSummary
                                                                 roomState:roomState
                                                          excludingUserIDs:serviceMemberIDs];
            
            updated |= [defaultRoomSummaryUpdater updateSummaryAvatar:summary
                                                              session:session
                                                withServerRoomSummary:serverRoomSummary
                                                            roomState:roomState
                                                     excludingUserIDs:serviceMemberIDs];
        }
    }

    return updated;
}

/**
 Gets the latest state event of type `io.element.functional_members` from the supplied array of state events.
 Note: This function will be expensive on big rooms, recommended for use only on DMs.
 @return An event of type `io.element.functional_members`, or nil if the event wasn't found.
 */
- (MXEvent *)functionalMembersEventFromStateEvents:(NSArray<MXEvent *> *)stateEvents
{
    NSPredicate *functionalMembersPredicate = [NSPredicate predicateWithFormat:@"type == %@", FunctionalMembersStateEventType];
    return [stateEvents filteredArrayUsingPredicate:functionalMembersPredicate].lastObject;
}

#pragma mark - Timestamp formatting

- (NSString*)dateStringFromDate:(NSDate *)date withTime:(BOOL)time
{
    // Check the provided date
    if (!date)
    {
        return nil;
    }
    
    // Retrieve today date at midnight
    NSDate *today = [calendar startOfDayForDate:[NSDate date]];
    
    NSTimeInterval interval = -[date timeIntervalSinceDate:today];
    
    if (interval > 60*60*24*364)
    {
        [dateFormatter setDateFormat:@"MMM dd yyyy"];
        
        // Ignore time information here
        return [super dateStringFromDate:date withTime:NO];
    }
    else if (interval > 60*60*24*6)
    {
        [dateFormatter setDateFormat:@"MMM dd"];
        
        // Ignore time information here
        return [super dateStringFromDate:date withTime:NO];
    }
    else if (interval > 60*60*24)
    {
        if (time)
        {
            [dateFormatter setDateFormat:@"EEE"];
        }
        else
        {
            [dateFormatter setDateFormat:@"EEEE"];
        }
        
        return [super dateStringFromDate:date withTime:time];
    }
    else if (interval > 0)
    {
        if (time)
        {
            [dateFormatter setDateFormat:nil];
            return [NSString stringWithFormat:@"%@ %@", [VectorL10n yesterday], [super dateStringFromDate:date withTime:YES]];
        }
        return [VectorL10n yesterday];
    }
    else if (interval > - 60*60*24)
    {
        if (time)
        {
            [dateFormatter setDateFormat:nil];
            return [NSString stringWithFormat:@"%@", [super dateStringFromDate:date withTime:YES]];
        }
        return [VectorL10n today];
    }
    else
    {
        // Date in future
        [dateFormatter setDateFormat:@"EEE MMM dd yyyy"];
        return [super dateStringFromDate:date withTime:time];
    }
}

#pragma mark - Room create predecessor

- (NSAttributedString*)roomCreatePredecessorAttributedStringWithPredecessorRoomId:(NSString*)predecessorRoomId
{
    NSDictionary *roomPredecessorReasonAttributes = @{
                                                      NSFontAttributeName : self.defaultTextFont
                                                      };
    
    NSDictionary *roomLinkAttributes = @{
                                         NSFontAttributeName : self.defaultTextFont,
                                         NSUnderlineStyleAttributeName : @(NSUnderlineStyleSingle)
                                         };
    
    NSMutableAttributedString *roomPredecessorAttributedString = [NSMutableAttributedString new];
    
    NSString *roomPredecessorReasonString = [NSString stringWithFormat:@"%@\n", [VectorL10n roomPredecessorInformation]];
    NSAttributedString *roomPredecessorReasonAttributedString = [[NSAttributedString alloc] initWithString:roomPredecessorReasonString attributes:roomPredecessorReasonAttributes];
    
    NSString *predecessorRoomLinkString = [VectorL10n roomPredecessorLink];
    NSAttributedString *predecessorRoomLinkAttributedString = [[NSAttributedString alloc] initWithString:predecessorRoomLinkString attributes:roomLinkAttributes];
    
    [roomPredecessorAttributedString appendAttributedString:roomPredecessorReasonAttributedString];
    [roomPredecessorAttributedString appendAttributedString:predecessorRoomLinkAttributedString];
    
    NSRange wholeStringRange = NSMakeRange(0, roomPredecessorAttributedString.length);
    [roomPredecessorAttributedString addAttribute:NSForegroundColorAttributeName value:self.defaultTextColor range:wholeStringRange];
    
    return roomPredecessorAttributedString;
}

@end
