//
//  Copyright (c) 2017 Open Whisper Systems. All rights reserved.
//

#import "ConversationInputToolbar.h"
#import "ConversationInputTextView.h"
#import "OWSMath.h"
#import "Signal-Swift.h"
#import "UIColor+OWS.h"
#import "UIFont+OWS.h"
#import "UIView+OWS.h"
#import "ViewControllerUtils.h"
#import <SignalMessaging/OWSFormat.h>
#import <SignalServiceKit/NSTimer+OWS.h>

NS_ASSUME_NONNULL_BEGIN

static void *kConversationInputTextViewObservingContext = &kConversationInputTextViewObservingContext;

@interface ConversationInputToolbar () <UIGestureRecognizerDelegate, ConversationTextViewToolbarDelegate>

@property (nonatomic, readonly) UIView *contentView;
@property (nonatomic, readonly) ConversationInputTextView *inputTextView;
@property (nonatomic, readonly) UIButton *attachmentButton;
@property (nonatomic, readonly) UIButton *sendButton;
@property (nonatomic, readonly) UIButton *voiceMemoButton;
@property (nonatomic, readonly) UIView *leftButtonWrapper;
@property (nonatomic, readonly) UIView *rightButtonWrapper;

@property (nonatomic) BOOL shouldShowVoiceMemoButton;

@property (nonatomic) NSArray<NSLayoutConstraint *> *contentContraints;
@property (nonatomic) NSValue *lastTextContentSize;

#pragma mark - Voice Memo Recording UI

@property (nonatomic, nullable) UIView *voiceMemoUI;
@property (nonatomic, nullable) UIView *voiceMemoContentView;
@property (nonatomic) NSDate *voiceMemoStartTime;
@property (nonatomic, nullable) NSTimer *voiceMemoUpdateTimer;
@property (nonatomic, nullable) UILabel *recordingLabel;
@property (nonatomic) BOOL isRecordingVoiceMemo;
@property (nonatomic) CGPoint voiceMemoGestureStartLocation;

#pragma mark - Attachment Approval

@property (nonatomic, nullable) MediaMessageView *attachmentView;
@property (nonatomic, nullable) UIView *cancelAttachmentWrapper;
@property (nonatomic, nullable) SignalAttachment *attachmentToApprove;

@end

#pragma mark -

@implementation ConversationInputToolbar

- (instancetype)init
{
    self = [super init];
    if (self) {
        [self createContents];
    }

    return self;
}

- (void)dealloc
{
    [self removeKVOObservers];
}

- (void)createContents
{
    self.layoutMargins = UIEdgeInsetsZero;

    self.backgroundColor = [UIColor ows_inputToolbarBackgroundColor];

    UIView *borderView = [UIView new];
    borderView.backgroundColor = [UIColor colorWithWhite:238 / 255.f alpha:1.f];
    [self addSubview:borderView];
    [borderView autoPinWidthToSuperview];
    [borderView autoPinEdgeToSuperviewEdge:ALEdgeTop];
    [borderView autoSetDimension:ALDimensionHeight toSize:0.5f];

    _contentView = [UIView containerView];
    [self addSubview:self.contentView];
    [self.contentView autoPinEdgesToSuperviewEdges];

    _inputTextView = [ConversationInputTextView new];
    self.inputTextView.textViewToolbarDelegate = self;
    self.inputTextView.font = [UIFont ows_dynamicTypeBodyFont];
    [self.contentView addSubview:self.inputTextView];

    // We want to be permissive about taps on the send and attachment buttons,
    // so we use wrapper views that capture nearby taps.  This is a lot easier
    // than trying to manipulate the size of the buttons themselves, as you
    // can't coordinate the layout of the button content (e.g. image or text)
    // using iOS auto layout.
    _leftButtonWrapper = [UIView containerView];
    [self.leftButtonWrapper
        addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(leftButtonTapped:)]];
    [self.contentView addSubview:self.leftButtonWrapper];
    _rightButtonWrapper = [UIView containerView];
    [self.rightButtonWrapper
        addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(rightButtonTapped:)]];
    [self.contentView addSubview:self.rightButtonWrapper];

    _attachmentButton = [[UIButton alloc] init];
    self.attachmentButton.accessibilityLabel
        = NSLocalizedString(@"ATTACHMENT_LABEL", @"Accessibility label for attaching photos");
    self.attachmentButton.accessibilityHint = NSLocalizedString(
        @"ATTACHMENT_HINT", @"Accessibility hint describing what you can do with the attachment button");
    [self.attachmentButton addTarget:self
                              action:@selector(attachmentButtonPressed)
                    forControlEvents:UIControlEventTouchUpInside];
    [self.attachmentButton setImage:[UIImage imageNamed:@"btnAttachments--blue"] forState:UIControlStateNormal];
    self.attachmentButton.contentEdgeInsets = UIEdgeInsetsMake(0, 3, 0, 3);
    [self.leftButtonWrapper addSubview:self.attachmentButton];

    // TODO: Fix layout in this class.
    _sendButton = [UIButton buttonWithType:UIButtonTypeCustom];
    [self.sendButton
        setTitle:NSLocalizedString(@"SEND_BUTTON_TITLE", @"Label for the send button in the conversation view.")
        forState:UIControlStateNormal];
    [self.sendButton setTitleColor:[UIColor ows_materialBlueColor] forState:UIControlStateNormal];
    self.sendButton.titleLabel.font = [UIFont ows_regularFontWithSize:17.0f];
    self.sendButton.titleLabel.textAlignment = NSTextAlignmentCenter;
    self.sendButton.titleLabel.font = [UIFont ows_mediumFontWithSize:16.f];
    [self.sendButton addTarget:self action:@selector(sendButtonPressed) forControlEvents:UIControlEventTouchUpInside];
    [self.rightButtonWrapper addSubview:self.sendButton];

    UIImage *voiceMemoIcon = [UIImage imageNamed:@"voice-memo-button"];
    OWSAssert(voiceMemoIcon);
    _voiceMemoButton = [UIButton buttonWithType:UIButtonTypeCustom];
    [self.voiceMemoButton setImage:[voiceMemoIcon imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]
                          forState:UIControlStateNormal];
    self.voiceMemoButton.imageView.tintColor = [UIColor ows_materialBlueColor];
    [self.rightButtonWrapper addSubview:self.voiceMemoButton];

    // We want to be permissive about the voice message gesture, so we hang
    // the long press GR on the button's wrapper, not the button itself.
    UILongPressGestureRecognizer *longPressGestureRecognizer =
        [[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(handleLongPress:)];
    longPressGestureRecognizer.minimumPressDuration = 0;
    longPressGestureRecognizer.delegate = self;
    [self.rightButtonWrapper addGestureRecognizer:longPressGestureRecognizer];

    self.userInteractionEnabled = YES;

    [self addKVOObservers];

    [self ensureShouldShowVoiceMemoButton];

    [self ensureContentConstraints];
}

- (void)updateFontSizes
{
    self.inputTextView.font = [UIFont ows_dynamicTypeBodyFont];

    [self ensureContentConstraints];
}

- (void)setInputTextViewDelegate:(id<ConversationInputTextViewDelegate>)value
{
    OWSAssert(self.inputTextView);
    OWSAssert(value);

    self.inputTextView.inputTextViewDelegate = value;
}

- (NSString *)messageText
{
    OWSAssert(self.inputTextView);

    return self.inputTextView.trimmedText;
}

- (void)setMessageText:(NSString *_Nullable)value
{
    OWSAssert(self.inputTextView);

    self.inputTextView.text = value;

    [self ensureShouldShowVoiceMemoButton];
}

- (void)clearTextMessage
{
    [self setMessageText:nil];
    [self.inputTextView.undoManager removeAllActions];
}

- (void)setShouldShowVoiceMemoButton:(BOOL)shouldShowVoiceMemoButton
{
    if (_shouldShowVoiceMemoButton == shouldShowVoiceMemoButton) {
        return;
    }

    _shouldShowVoiceMemoButton = shouldShowVoiceMemoButton;

    [self ensureContentConstraints];
}

- (void)beginEditingTextMessage
{
    [self.inputTextView becomeFirstResponder];
}

- (void)endEditingTextMessage
{
    [self.inputTextView resignFirstResponder];
}

- (void)ensureContentConstraints
{
    [NSLayoutConstraint deactivateConstraints:self.contentContraints];

    const int textViewVInset = 5;
    const int contentHInset = 6;
    const int contentHSpacing = 6;

    // We want to grow the text input area to fit its content within reason.
    const CGFloat kMinTextViewHeight = ceil(self.inputTextView.font.lineHeight
        + self.inputTextView.textContainerInset.top + self.inputTextView.textContainerInset.bottom
        + self.inputTextView.contentInset.top + self.inputTextView.contentInset.bottom);
    const CGFloat kMaxTextViewHeight = 100.f;
    const CGFloat textViewDesiredHeight = (self.inputTextView.contentSize.height + self.inputTextView.contentInset.top
        + self.inputTextView.contentInset.bottom);
    const CGFloat textViewHeight = ceil(Clamp(textViewDesiredHeight, kMinTextViewHeight, kMaxTextViewHeight));
    const CGFloat kMinContentHeight = kMinTextViewHeight + textViewVInset * 2;

    if (self.attachmentToApprove) {
        OWSAssert(self.attachmentView);

        self.leftButtonWrapper.hidden = YES;
        self.inputTextView.hidden = YES;
        self.voiceMemoButton.hidden = YES;
        UIButton *rightButton = self.sendButton;
        rightButton.enabled = YES;
        rightButton.hidden = NO;

        [rightButton setContentHuggingHigh];
        [rightButton setCompressionResistanceHigh];
        [self.attachmentView setContentHuggingLow];

        OWSAssert(rightButton.superview == self.rightButtonWrapper);

        self.contentContraints = @[
            [self.attachmentView autoPinEdgeToSuperviewEdge:ALEdgeTop withInset:textViewVInset],
            [self.attachmentView autoPinEdgeToSuperviewEdge:ALEdgeBottom withInset:textViewVInset],
            [self.attachmentView autoPinEdgeToSuperviewEdge:ALEdgeLeft withInset:contentHInset],
            [self.attachmentView autoSetDimension:ALDimensionHeight toSize:150.f],

            [self.rightButtonWrapper autoPinEdge:ALEdgeLeft toEdge:ALEdgeRight ofView:self.attachmentView],
            [self.rightButtonWrapper autoPinEdgeToSuperviewEdge:ALEdgeRight],
            [self.rightButtonWrapper autoPinEdgeToSuperviewEdge:ALEdgeTop],
            [self.rightButtonWrapper autoPinEdgeToSuperviewEdge:ALEdgeBottom],

            [rightButton autoSetDimension:ALDimensionHeight toSize:kMinContentHeight],
            [rightButton autoPinLeadingToSuperviewWithMargin:contentHSpacing],
            [rightButton autoPinTrailingToSuperviewWithMargin:contentHInset],
            [rightButton autoPinEdgeToSuperviewEdge:ALEdgeBottom],
        ];

        [self setNeedsLayout];
        [self layoutIfNeeded];

        // Ensure the keyboard is dismissed.
        //
        // NOTE: We need to do this _last_ or the layout changes in the input toolbar
        //       will be inadvertently animated.
        [self.inputTextView resignFirstResponder];

        return;
    }

    self.leftButtonWrapper.hidden = NO;
    self.inputTextView.hidden = NO;
    self.voiceMemoButton.hidden = NO;
    [self.attachmentView removeFromSuperview];
    self.attachmentView = nil;
    [self.cancelAttachmentWrapper removeFromSuperview];
    self.cancelAttachmentWrapper = nil;

    UIButton *leftButton = self.attachmentButton;
    UIButton *rightButton = (self.shouldShowVoiceMemoButton ? self.voiceMemoButton : self.sendButton);
    UIButton *inactiveRightButton = (self.shouldShowVoiceMemoButton ? self.sendButton : self.voiceMemoButton);
    leftButton.enabled = YES;
    rightButton.enabled = YES;
    inactiveRightButton.enabled = NO;
    leftButton.hidden = NO;
    rightButton.hidden = NO;
    inactiveRightButton.hidden = YES;

    [leftButton setContentHuggingHigh];
    [rightButton setContentHuggingHigh];
    [leftButton setCompressionResistanceHigh];
    [rightButton setCompressionResistanceHigh];
    [self.inputTextView setCompressionResistanceLow];
    [self.inputTextView setContentHuggingLow];

    OWSAssert(leftButton.superview == self.leftButtonWrapper);
    OWSAssert(rightButton.superview == self.rightButtonWrapper);

    // The leading and trailing buttons should be center-aligned with the
    // inputTextView when the inputTextView is at its minimum size.
    //
    // We want the leading and trailing buttons to hug the bottom of the input
    // toolbar as the inputTextView expands.
    //
    // Therefore we fix the button heights to the size of the toolbar when
    // inputTextView is at its minimum size.
    //
    // Additionally, we use "wrapper" views around the leading and trailing
    // buttons to expand their hot area.
    self.contentContraints = @[
        [self.leftButtonWrapper autoPinEdgeToSuperviewEdge:ALEdgeLeft],
        [self.leftButtonWrapper autoPinEdgeToSuperviewEdge:ALEdgeTop],
        [self.leftButtonWrapper autoPinEdgeToSuperviewEdge:ALEdgeBottom],

        [leftButton autoSetDimension:ALDimensionHeight toSize:kMinContentHeight],
        [leftButton autoPinLeadingToSuperviewWithMargin:contentHInset],
        [leftButton autoPinTrailingToSuperviewWithMargin:contentHSpacing],
        [leftButton autoPinEdgeToSuperviewEdge:ALEdgeBottom],

        [self.inputTextView autoPinEdge:ALEdgeLeft toEdge:ALEdgeRight ofView:self.leftButtonWrapper],
        [self.inputTextView autoPinEdgeToSuperviewEdge:ALEdgeTop withInset:textViewVInset],
        [self.inputTextView autoPinEdgeToSuperviewEdge:ALEdgeBottom withInset:textViewVInset],
        [self.inputTextView autoSetDimension:ALDimensionHeight toSize:textViewHeight],

        [self.rightButtonWrapper autoPinEdge:ALEdgeLeft toEdge:ALEdgeRight ofView:self.inputTextView],
        [self.rightButtonWrapper autoPinEdgeToSuperviewEdge:ALEdgeRight],
        [self.rightButtonWrapper autoPinEdgeToSuperviewEdge:ALEdgeTop],
        [self.rightButtonWrapper autoPinEdgeToSuperviewEdge:ALEdgeBottom],

        [rightButton autoSetDimension:ALDimensionHeight toSize:kMinContentHeight],
        [rightButton autoPinLeadingToSuperviewWithMargin:contentHSpacing],
        [rightButton autoPinTrailingToSuperviewWithMargin:contentHInset],
        [rightButton autoPinEdgeToSuperviewEdge:ALEdgeBottom],
    ];

    // Layout immediately, unless the input toolbar hasn't even been laid out yet.
    if (self.bounds.size.width > 0 && self.bounds.size.height > 0) {
        [self layoutIfNeeded];
    }
}

- (void)ensureShouldShowVoiceMemoButton
{
    self.shouldShowVoiceMemoButton = (self.attachmentToApprove == nil && self.inputTextView.trimmedText.length < 1);
}

- (void)handleLongPress:(UIGestureRecognizer *)sender
{
    if (!self.shouldShowVoiceMemoButton) {
        return;
    }

    switch (sender.state) {
        case UIGestureRecognizerStatePossible:
        case UIGestureRecognizerStateCancelled:
        case UIGestureRecognizerStateFailed:
            if (self.isRecordingVoiceMemo) {
                // Cancel voice message if necessary.
                self.isRecordingVoiceMemo = NO;
                [self.inputToolbarDelegate voiceMemoGestureDidCancel];
            }
            break;
        case UIGestureRecognizerStateBegan:
            if (self.isRecordingVoiceMemo) {
                // Cancel voice message if necessary.
                self.isRecordingVoiceMemo = NO;
                [self.inputToolbarDelegate voiceMemoGestureDidCancel];
            }
            // Start voice message.
            self.isRecordingVoiceMemo = YES;
            self.voiceMemoGestureStartLocation = [sender locationInView:self];
            [self.inputToolbarDelegate voiceMemoGestureDidStart];
            break;
        case UIGestureRecognizerStateChanged:
            if (self.isRecordingVoiceMemo) {
                // Check for "slide to cancel" gesture.
                CGPoint location = [sender locationInView:self];
                // For LTR/RTL, swiping in either direction will cancel.
                // This is okay because there's only space on screen to perform the
                // gesture in one direction.
                CGFloat offset = fabs(self.voiceMemoGestureStartLocation.x - location.x);
                // The lower this value, the easier it is to cancel by accident.
                // The higher this value, the harder it is to cancel.
                const CGFloat kCancelOffsetPoints = 100.f;
                CGFloat cancelAlpha = offset / kCancelOffsetPoints;
                BOOL isCancelled = cancelAlpha >= 1.f;
                if (isCancelled) {
                    self.isRecordingVoiceMemo = NO;
                    [self.inputToolbarDelegate voiceMemoGestureDidCancel];
                } else {
                    [self.inputToolbarDelegate voiceMemoGestureDidChange:cancelAlpha];
                }
            }
            break;
        case UIGestureRecognizerStateEnded:
            if (self.isRecordingVoiceMemo) {
                // End voice message.
                self.isRecordingVoiceMemo = NO;
                [self.inputToolbarDelegate voiceMemoGestureDidEnd];
            }
            break;
    }
}

#pragma mark - UIGestureRecognizerDelegate

- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch
{
    if ([gestureRecognizer isKindOfClass:[UILongPressGestureRecognizer class]]) {
        return self.shouldShowVoiceMemoButton;
    } else {
        return YES;
    }
}

#pragma mark - Voice Memo

- (void)showVoiceMemoUI
{
    OWSAssert([NSThread isMainThread]);

    self.voiceMemoStartTime = [NSDate date];

    [self.voiceMemoUI removeFromSuperview];

    self.voiceMemoUI = [UIView new];
    self.voiceMemoUI.userInteractionEnabled = NO;
    self.voiceMemoUI.backgroundColor = [UIColor whiteColor];
    [self addSubview:self.voiceMemoUI];
    self.voiceMemoUI.frame = CGRectMake(0, 0, self.bounds.size.width, self.bounds.size.height);

    self.voiceMemoContentView = [UIView new];
    [self.voiceMemoUI addSubview:self.voiceMemoContentView];
    [self.voiceMemoContentView autoPinToSuperviewEdges];

    self.recordingLabel = [UILabel new];
    self.recordingLabel.textColor = [UIColor ows_destructiveRedColor];
    self.recordingLabel.font = [UIFont ows_mediumFontWithSize:14.f];
    [self.voiceMemoContentView addSubview:self.recordingLabel];
    [self updateVoiceMemo];

    UIImage *icon = [UIImage imageNamed:@"voice-memo-button"];
    OWSAssert(icon);
    UIImageView *imageView =
        [[UIImageView alloc] initWithImage:[icon imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]];
    imageView.tintColor = [UIColor ows_destructiveRedColor];
    [self.voiceMemoContentView addSubview:imageView];

    NSMutableAttributedString *cancelString = [NSMutableAttributedString new];
    const CGFloat cancelArrowFontSize = ScaleFromIPhone5To7Plus(18.4, 20.f);
    const CGFloat cancelFontSize = ScaleFromIPhone5To7Plus(14.f, 16.f);
    NSString *arrowHead = (self.isRTL ? @"\uf105" : @"\uf104");
    [cancelString
        appendAttributedString:[[NSAttributedString alloc]
                                   initWithString:arrowHead
                                       attributes:@{
                                           NSFontAttributeName : [UIFont ows_fontAwesomeFont:cancelArrowFontSize],
                                           NSForegroundColorAttributeName : [UIColor ows_destructiveRedColor],
                                           NSBaselineOffsetAttributeName : @(-1.f),
                                       }]];
    [cancelString
        appendAttributedString:[[NSAttributedString alloc]
                                   initWithString:@"  "
                                       attributes:@{
                                           NSFontAttributeName : [UIFont ows_fontAwesomeFont:cancelArrowFontSize],
                                           NSForegroundColorAttributeName : [UIColor ows_destructiveRedColor],
                                           NSBaselineOffsetAttributeName : @(-1.f),
                                       }]];
    [cancelString
        appendAttributedString:[[NSAttributedString alloc]
                                   initWithString:NSLocalizedString(@"VOICE_MESSAGE_CANCEL_INSTRUCTIONS",
                                                      @"Indicates how to cancel a voice message.")
                                       attributes:@{
                                           NSFontAttributeName : [UIFont ows_mediumFontWithSize:cancelFontSize],
                                           NSForegroundColorAttributeName : [UIColor ows_destructiveRedColor],
                                       }]];
    [cancelString
        appendAttributedString:[[NSAttributedString alloc]
                                   initWithString:@"  "
                                       attributes:@{
                                           NSFontAttributeName : [UIFont ows_fontAwesomeFont:cancelArrowFontSize],
                                           NSForegroundColorAttributeName : [UIColor ows_destructiveRedColor],
                                           NSBaselineOffsetAttributeName : @(-1.f),
                                       }]];
    [cancelString
        appendAttributedString:[[NSAttributedString alloc]
                                   initWithString:arrowHead
                                       attributes:@{
                                           NSFontAttributeName : [UIFont ows_fontAwesomeFont:cancelArrowFontSize],
                                           NSForegroundColorAttributeName : [UIColor ows_destructiveRedColor],
                                           NSBaselineOffsetAttributeName : @(-1.f),
                                       }]];
    UILabel *cancelLabel = [UILabel new];
    cancelLabel.attributedText = cancelString;
    [self.voiceMemoContentView addSubview:cancelLabel];

    const CGFloat kRedCircleSize = 100.f;
    UIView *redCircleView = [UIView new];
    redCircleView.backgroundColor = [UIColor ows_destructiveRedColor];
    redCircleView.layer.cornerRadius = kRedCircleSize * 0.5f;
    [redCircleView autoSetDimension:ALDimensionWidth toSize:kRedCircleSize];
    [redCircleView autoSetDimension:ALDimensionHeight toSize:kRedCircleSize];
    [self.voiceMemoContentView addSubview:redCircleView];
    [redCircleView autoAlignAxis:ALAxisHorizontal toSameAxisOfView:self.voiceMemoButton];
    [redCircleView autoAlignAxis:ALAxisVertical toSameAxisOfView:self.voiceMemoButton];

    UIImage *whiteIcon = [UIImage imageNamed:@"voice-message-large-white"];
    OWSAssert(whiteIcon);
    UIImageView *whiteIconView = [[UIImageView alloc] initWithImage:whiteIcon];
    [redCircleView addSubview:whiteIconView];
    [whiteIconView autoCenterInSuperview];

    [imageView autoVCenterInSuperview];
    [imageView autoPinLeadingToSuperviewWithMargin:10.f];
    [self.recordingLabel autoVCenterInSuperview];
    [self.recordingLabel autoPinLeadingToTrailingOfView:imageView margin:5.f];
    [cancelLabel autoVCenterInSuperview];
    [cancelLabel autoHCenterInSuperview];
    [self.voiceMemoUI setNeedsLayout];
    [self.voiceMemoUI layoutSubviews];

    // Slide in the "slide to cancel" label.
    CGRect cancelLabelStartFrame = cancelLabel.frame;
    CGRect cancelLabelEndFrame = cancelLabel.frame;
    cancelLabelStartFrame.origin.x
        = (self.isRTL ? -self.voiceMemoUI.bounds.size.width : self.voiceMemoUI.bounds.size.width);
    cancelLabel.frame = cancelLabelStartFrame;
    [UIView animateWithDuration:0.35f
                          delay:0.f
                        options:UIViewAnimationOptionCurveEaseOut
                     animations:^{
                         cancelLabel.frame = cancelLabelEndFrame;
                     }
                     completion:nil];

    // Pulse the icon.
    imageView.layer.opacity = 1.f;
    [UIView animateWithDuration:0.5f
                          delay:0.2f
                        options:UIViewAnimationOptionRepeat | UIViewAnimationOptionAutoreverse
                        | UIViewAnimationOptionCurveEaseIn
                     animations:^{
                         imageView.layer.opacity = 0.f;
                     }
                     completion:nil];

    // Fade in the view.
    self.voiceMemoUI.layer.opacity = 0.f;
    [UIView animateWithDuration:0.2f
        animations:^{
            self.voiceMemoUI.layer.opacity = 1.f;
        }
        completion:^(BOOL finished) {
            if (finished) {
                self.voiceMemoUI.layer.opacity = 1.f;
            }
        }];

    [self.voiceMemoUpdateTimer invalidate];
    self.voiceMemoUpdateTimer = [NSTimer weakScheduledTimerWithTimeInterval:0.1f
                                                                     target:self
                                                                   selector:@selector(updateVoiceMemo)
                                                                   userInfo:nil
                                                                    repeats:YES];
}

- (void)hideVoiceMemoUI:(BOOL)animated
{
    OWSAssert([NSThread isMainThread]);

    UIView *oldVoiceMemoUI = self.voiceMemoUI;
    self.voiceMemoUI = nil;
    self.voiceMemoContentView = nil;
    self.recordingLabel = nil;
    NSTimer *voiceMemoUpdateTimer = self.voiceMemoUpdateTimer;
    self.voiceMemoUpdateTimer = nil;

    [oldVoiceMemoUI.layer removeAllAnimations];

    if (animated) {
        [UIView animateWithDuration:0.35f
            animations:^{
                oldVoiceMemoUI.layer.opacity = 0.f;
            }
            completion:^(BOOL finished) {
                [oldVoiceMemoUI removeFromSuperview];
                [voiceMemoUpdateTimer invalidate];
            }];
    } else {
        [oldVoiceMemoUI removeFromSuperview];
        [voiceMemoUpdateTimer invalidate];
    }
}

- (void)setVoiceMemoUICancelAlpha:(CGFloat)cancelAlpha
{
    OWSAssert([NSThread isMainThread]);

    // Fade out the voice message views as the cancel gesture
    // proceeds as feedback.
    self.voiceMemoContentView.layer.opacity = MAX(0.f, MIN(1.f, 1.f - (float)cancelAlpha));
}

- (void)updateVoiceMemo
{
    OWSAssert([NSThread isMainThread]);

    NSTimeInterval durationSeconds = fabs([self.voiceMemoStartTime timeIntervalSinceNow]);
    self.recordingLabel.text = [OWSFormat formatDurationSeconds:(long)round(durationSeconds)];
    [self.recordingLabel sizeToFit];
}

- (void)cancelVoiceMemoIfNecessary
{
    if (self.isRecordingVoiceMemo) {
        self.isRecordingVoiceMemo = NO;
    }
}

#pragma mark - Event Handlers

- (void)leftButtonTapped:(UIGestureRecognizer *)sender
{
    if (sender.state == UIGestureRecognizerStateRecognized) {
        [self attachmentButtonPressed];
    }
}

- (void)rightButtonTapped:(UIGestureRecognizer *)sender
{
    if (sender.state == UIGestureRecognizerStateRecognized) {
        if (!self.shouldShowVoiceMemoButton) {
            [self sendButtonPressed];
        }
    }
}

- (void)sendButtonPressed
{
    OWSAssert(self.inputToolbarDelegate);

    if (self.attachmentToApprove) {
        [self attachmentApprovalSendPressed];
    } else {
        [self.inputToolbarDelegate sendButtonPressed];
    }
}

- (void)attachmentButtonPressed
{
    OWSAssert(self.inputToolbarDelegate);

    [self.inputToolbarDelegate attachmentButtonPressed];
}

#pragma mark - ConversationTextViewToolbarDelegate

- (void)textViewDidChange
{
    OWSAssert(self.inputToolbarDelegate);

    [self ensureShouldShowVoiceMemoButton];
}

#pragma mark - Text Input Sizing

- (void)addKVOObservers
{
    [self.inputTextView addObserver:self
                         forKeyPath:NSStringFromSelector(@selector(contentSize))
                            options:NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew
                            context:kConversationInputTextViewObservingContext];
}

- (void)removeKVOObservers
{
    @try {
        [self.inputTextView removeObserver:self
                                forKeyPath:NSStringFromSelector(@selector(contentSize))
                                   context:kConversationInputTextViewObservingContext];
    } @catch (NSException *__unused exception) {
        // TODO: This try/catch can probably be safely removed.
        OWSFail(@"%@ removeKVOObservers failed.", self.logTag);
    }
}

- (void)observeValueForKeyPath:(nullable NSString *)keyPath
                      ofObject:(nullable id)object
                        change:(nullable NSDictionary<NSKeyValueChangeKey, id> *)change
                       context:(nullable void *)context
{
    if (context == kConversationInputTextViewObservingContext) {

        if (object == self.inputTextView && [keyPath isEqualToString:NSStringFromSelector(@selector(contentSize))]) {
            CGSize textContentSize = self.inputTextView.contentSize;
            NSValue *_Nullable lastTextContentSize = self.lastTextContentSize;
            self.lastTextContentSize = [NSValue valueWithCGSize:textContentSize];

            // Update view constraints, but only when text content size changes.
            //
            // NOTE: We use a "fuzzy equals" comparison to avoid infinite recursion,
            //       since ensureContentConstraints can affect the text content size.
            if (!lastTextContentSize || fabs(lastTextContentSize.CGSizeValue.width - textContentSize.width) > 0.1f
                || fabs(lastTextContentSize.CGSizeValue.height - textContentSize.height) > 0.1f) {
                [self ensureContentConstraints];
            }
        }
    }
}

#pragma mark - Attachment Approval

- (void)showApprovalUIForAttachment:(SignalAttachment *)attachment
{
    OWSAssert(attachment);

    self.attachmentToApprove = attachment;

    MediaMessageView *attachmentView =
        [[MediaMessageView alloc] initWithAttachment:attachment mode:MediaMessageViewModeSmall];
    self.attachmentView = attachmentView;
    [self.contentView addSubview:attachmentView];

    UIView *cancelAttachmentWrapper = [UIView containerView];
    self.cancelAttachmentWrapper = cancelAttachmentWrapper;
    [cancelAttachmentWrapper
        addGestureRecognizer:[[UITapGestureRecognizer alloc] initWithTarget:self
                                                                     action:@selector(cancelAttachmentWrapperTapped:)]];
    UIView *_Nullable attachmentContentView = [self.attachmentView contentView];
    // Place the cancel button inside the attachment view's content area,
    // if possible.  If not, just place it inside the attachment view.
    UIView *cancelButtonReferenceView = attachmentContentView;
    if (attachmentContentView) {
        attachmentContentView.layer.borderColor = self.inputTextView.layer.borderColor;
        attachmentContentView.layer.borderWidth = self.inputTextView.layer.borderWidth;
        attachmentContentView.layer.cornerRadius = self.inputTextView.layer.cornerRadius;
        attachmentContentView.clipsToBounds = YES;
    } else {
        cancelButtonReferenceView = self.attachmentView;
    }
    [self.contentView addSubview:cancelAttachmentWrapper];
    [cancelAttachmentWrapper autoPinEdge:ALEdgeTop toEdge:ALEdgeTop ofView:cancelButtonReferenceView];
    [cancelAttachmentWrapper autoPinEdge:ALEdgeRight toEdge:ALEdgeRight ofView:cancelButtonReferenceView];

    UIImage *cancelIcon = [UIImage imageNamed:@"cancel-cross-white"];
    OWSAssert(cancelIcon);
    UIButton *cancelButton = [UIButton buttonWithType:UIButtonTypeCustom];
    [cancelButton setImage:cancelIcon forState:UIControlStateNormal];
    [cancelButton setBackgroundColor:[UIColor ows_materialBlueColor]];
    OWSAssert(cancelIcon.size.width == cancelIcon.size.height);
    CGFloat cancelIconSize = MIN(cancelIcon.size.width, cancelIcon.size.height);
    CGFloat cancelIconInset = round(cancelIconSize * 0.35f);
    [cancelButton
        setContentEdgeInsets:UIEdgeInsetsMake(cancelIconInset, cancelIconInset, cancelIconInset, cancelIconInset)];
    CGFloat cancelButtonRadius = cancelIconInset + cancelIconSize * 0.5f;
    cancelButton.layer.cornerRadius = cancelButtonRadius;
    CGFloat cancelButtonInset = 10.f;
    [cancelButton addTarget:self
                     action:@selector(attachmentApprovalCancelPressed)
           forControlEvents:UIControlEventTouchUpInside];
    [cancelAttachmentWrapper addSubview:cancelButton];
    [cancelButton autoPinWidthToSuperviewWithMargin:cancelButtonInset];
    [cancelButton autoPinHeightToSuperviewWithMargin:cancelButtonInset];
    CGFloat cancelButtonSize = cancelIconSize + 2 * cancelIconInset;
    [cancelButton autoSetDimension:ALDimensionWidth toSize:cancelButtonSize];
    [cancelButton autoSetDimension:ALDimensionHeight toSize:cancelButtonSize];

    [self ensureContentConstraints];
    [self ensureShouldShowVoiceMemoButton];
}

- (void)cancelAttachmentWrapperTapped:(UIGestureRecognizer *)sender
{
    if (sender.state == UIGestureRecognizerStateRecognized) {
        [self attachmentApprovalCancelPressed];
    }
}

- (void)attachmentApprovalCancelPressed
{
    self.attachmentToApprove = nil;

    [self ensureContentConstraints];
    [self ensureShouldShowVoiceMemoButton];
}

- (void)attachmentApprovalSendPressed
{
    SignalAttachment *attachment = self.attachmentToApprove;
    self.attachmentToApprove = nil;

    if (attachment) {
        [self.inputToolbarDelegate didApproveAttachment:attachment];
    }

    [self ensureContentConstraints];
    [self ensureShouldShowVoiceMemoButton];
}

- (void)viewWillAppear:(BOOL)animated
{
    [self.attachmentView viewWillAppear:animated];
}

- (void)viewWillDisappear:(BOOL)animated
{
    [self.attachmentView viewWillDisappear:animated];

    [self endEditingTextMessage];
}

- (nullable NSString *)textInputPrimaryLanguage
{
    return self.inputTextView.textInputMode.primaryLanguage;
}

@end

NS_ASSUME_NONNULL_END
