//
//  TimelineStatusCollectionViewCell.swift
//  Tusker
//
//  Created by Shadowfacts on 10/1/22.
//  Copyright © 2022 Shadowfacts. All rights reserved.
//

import UIKit
import Pachyderm
import Combine

private let reblogIcon = UIImage(systemName: "repeat")
private let hashtagIcon = UIImage(systemName: "number")

class TimelineStatusCollectionViewCell: UICollectionViewListCell, StatusCollectionViewCell {
    
    static let separatorInsets = NSDirectionalEdgeInsets(top: 0, leading: 16, bottom: 0, trailing: 0)
    static let contentFont = UIFontMetrics.default.scaledFont(for: .systemFont(ofSize: 16))
    static let monospaceFont = UIFontMetrics.default.scaledFont(for: .monospacedSystemFont(ofSize: 16, weight: .regular))
    static let contentParagraphStyle = HTMLConverter.defaultParagraphStyle
    
    private static let timelineReasonIconSize: CGFloat = 25
    
    // MARK: Subviews
    
    private lazy var timelineReasonLabel = EmojiLabel().configure {
        $0.textColor = .secondaryLabel
        $0.font = .preferredFont(forTextStyle: .body)
        $0.adjustsFontForContentSizeCategory = true
    }
    private let timelineReasonIcon = CachedImageView(cache: .avatars).configure {
        $0.image = reblogIcon
        $0.contentMode = .scaleAspectFit
        $0.layer.masksToBounds = true
        $0.layer.cornerCurve = .continuous
        $0.tintColor = .secondaryLabel
        let heightConstraint = $0.heightAnchor.constraint(equalToConstant: TimelineStatusCollectionViewCell.timelineReasonIconSize)
        heightConstraint.identifier = "TimelineReason-Height"
        NSLayoutConstraint.activate([
            // this needs to be lessThanOrEqualTo not just equalTo b/c otherwise intermediate layouts are broken
            heightConstraint,
            $0.widthAnchor.constraint(equalTo: $0.heightAnchor),
        ])
    }
    private lazy var timelineReasonHStack = UIStackView(arrangedSubviews: [
        timelineReasonIcon,
        timelineReasonLabel,
    ]).configure {
        $0.axis = .horizontal
        $0.spacing = 8
        // this needs to have a higher priorty than the content container's zero height constraint
        $0.setContentHuggingPriority(.defaultHigh, for: .vertical)
        $0.isUserInteractionEnabled = true
        $0.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(reblogLabelPressed)))
    }
    
    private lazy var mainContainer = UIView().configure {
        avatarImageView.translatesAutoresizingMaskIntoConstraints = false
        $0.addSubview(avatarImageView)
        contentVStack.translatesAutoresizingMaskIntoConstraints = false
        $0.addSubview(contentVStack)
        metaIndicatorsView.translatesAutoresizingMaskIntoConstraints = false
        $0.addSubview(metaIndicatorsView)
        let avatarTopConstraint = avatarImageView.topAnchor.constraint(equalTo: $0.topAnchor)
        avatarTopConstraint.identifier = "Avatar-Top"
        let metaIndicatorsTopConstraint = metaIndicatorsView.topAnchor.constraint(equalTo: avatarImageView.bottomAnchor, constant: 4)
        metaIndicatorsTopConstraint.identifier = "MetaIndicators-Top"
        NSLayoutConstraint.activate([
            avatarImageView.leadingAnchor.constraint(equalTo: $0.leadingAnchor),
            avatarTopConstraint,
            contentVStack.leadingAnchor.constraint(equalTo: avatarImageView.trailingAnchor, constant: 8),
            contentVStack.trailingAnchor.constraint(equalTo: $0.trailingAnchor),
            contentVStack.topAnchor.constraint(equalTo: $0.topAnchor),
            contentVStack.bottomAnchor.constraint(equalTo: $0.bottomAnchor),
            metaIndicatorsView.leadingAnchor.constraint(greaterThanOrEqualTo: $0.leadingAnchor),
            metaIndicatorsView.trailingAnchor.constraint(equalTo: avatarImageView.trailingAnchor),
            metaIndicatorsTopConstraint,
        ])
    }
    
    private static let avatarImageViewSize: CGFloat = 50
    private(set) lazy var avatarImageView = CachedImageView(cache: .avatars).configure {
        $0.contentMode = .scaleAspectFill
        $0.layer.masksToBounds = true
        $0.layer.cornerCurve = .continuous
        NSLayoutConstraint.activate([
            $0.heightAnchor.constraint(equalToConstant: TimelineStatusCollectionViewCell.avatarImageViewSize),
            $0.widthAnchor.constraint(equalToConstant: TimelineStatusCollectionViewCell.avatarImageViewSize),
        ])
        $0.isUserInteractionEnabled = true
        $0.addInteraction(UIContextMenuInteraction(delegate: self))
        $0.addInteraction(UIDragInteraction(delegate: self))
        $0.addInteraction(UIPointerInteraction(delegate: self))
        $0.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(accountPressed)))
    }
    
    private let metaIndicatorsView = StatusMetaIndicatorsView().configure {
        $0.primaryAxis = .vertical
        $0.secondaryAxisAlignment = .trailing
    }
    
    private lazy var contentVStack = UIStackView(arrangedSubviews: [
        nameHStack,
        contentWarningLabel,
        collapseButton,
        contentContainer,
    ]).configure {
        $0.axis = .vertical
        $0.spacing = 4
        $0.alignment = .fill
    }
    
    private lazy var nameHStack = UIStackView(arrangedSubviews: [
        displayNameLabel,
        usernameLabel,
        pinImageView,
        timestampLabel,
    ]).configure {
        $0.axis = .horizontal
        $0.spacing = 4
    }
    
    let displayNameLabel = AccountDisplayNameLabel().configure {
        $0.font = UIFont(descriptor: .preferredFontDescriptor(withTextStyle: .body).addingAttributes([
            .traits: [
                UIFontDescriptor.TraitKey.weight: UIFont.Weight.semibold.rawValue,
            ]
        ]), size: 0)
        $0.adjustsFontForContentSizeCategory = true
        $0.setContentHuggingPriority(.init(251), for: .horizontal)
        $0.setContentCompressionResistancePriority(.init(749), for: .horizontal)
    }
    
    let usernameLabel = UILabel().configure {
        $0.textColor = .secondaryLabel
        $0.font = UIFont(descriptor: .preferredFontDescriptor(withTextStyle: .body).addingAttributes([
            .traits: [
                UIFontDescriptor.TraitKey.weight: UIFont.Weight.light.rawValue,
            ]
        ]), size: 0)
        $0.adjustsFontForContentSizeCategory = true
        $0.setContentHuggingPriority(.init(249), for: .horizontal)
        $0.setContentCompressionResistancePriority(.init(748), for: .horizontal)
    }
    
    private let pinImageView = UIImageView(image: UIImage(systemName: "pin.fill")).configure {
        $0.tintColor = .secondaryLabel
        $0.setContentHuggingPriority(.init(251), for: .horizontal)
    }
    
    private let timestampLabel = UILabel().configure {
        $0.textColor = .secondaryLabel
        $0.font = UIFont(descriptor: .preferredFontDescriptor(withTextStyle: .body).addingAttributes([
            .traits: [
                UIFontDescriptor.TraitKey.weight: UIFont.Weight.light.rawValue,
            ]
        ]), size: 0)
        $0.adjustsFontForContentSizeCategory = true
    }
    
    private(set) lazy var contentWarningLabel = EmojiLabel().configure {
        $0.numberOfLines = 0
        $0.textColor = .secondaryLabel
        $0.font = UIFont(descriptor: .preferredFontDescriptor(withTextStyle: .body).addingAttributes([
            .traits: [
                UIFontDescriptor.TraitKey.weight: UIFont.Weight.bold.rawValue,
            ]
        ]), size: 0)
        $0.adjustsFontForContentSizeCategory = true
        // this needs to have a higher priorty than the content container's zero height constraint
        $0.setContentHuggingPriority(.defaultHigh, for: .vertical)
        $0.isUserInteractionEnabled = true
        $0.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(collapseButtonPressed)))
    }
    
    private(set) lazy var collapseButton = StatusCollapseButton(configuration: {
        var config = UIButton.Configuration.filled()
        config.image = UIImage(systemName: "chevron.down")
        return config
    }()).configure {
        // this button is so big that dimming its background color is visually distracting
        $0.tintAdjustmentMode = .normal
        $0.setContentHuggingPriority(.defaultHigh, for: .vertical)
        $0.addTarget(self, action: #selector(collapseButtonPressed), for: .touchUpInside)
        $0.setContentCompressionResistancePriority(.required, for: .vertical)
    }
    
    let contentContainer = StatusContentContainer<StatusContentTextView, StatusPollView>(useTopSpacer: false).configure {
        $0.contentTextView.defaultFont = TimelineStatusCollectionViewCell.contentFont
        $0.contentTextView.monospaceFont = TimelineStatusCollectionViewCell.monospaceFont
        $0.contentTextView.paragraphStyle = TimelineStatusCollectionViewCell.contentParagraphStyle
        $0.setContentHuggingPriority(.defaultLow, for: .vertical)
    }
    private var contentTextView: StatusContentTextView {
        contentContainer.contentTextView
    }
    private var cardView: StatusCardView {
        contentContainer.cardView
    }
    private var attachmentsView: AttachmentsContainerView {
        contentContainer.attachmentsView
    }
    private var pollView: StatusPollView {
        contentContainer.pollView
    }
    
    private var placeholderReplyButtonLeadingConstraint: NSLayoutConstraint!
    private lazy var actionsContainer = UIView().configure {
        replyButton.translatesAutoresizingMaskIntoConstraints = false
        $0.addSubview(replyButton)
        favoriteButton.translatesAutoresizingMaskIntoConstraints = false
        $0.addSubview(favoriteButton)
        reblogButton.translatesAutoresizingMaskIntoConstraints = false
        $0.addSubview(reblogButton)
        moreButton.translatesAutoresizingMaskIntoConstraints = false
        $0.addSubview(moreButton)
        placeholderReplyButtonLeadingConstraint = replyButton.leadingAnchor.constraint(equalTo: $0.leadingAnchor)
        NSLayoutConstraint.activate([
            favoriteButton.widthAnchor.constraint(equalTo: replyButton.widthAnchor),
            reblogButton.widthAnchor.constraint(equalTo: replyButton.widthAnchor),
            moreButton.widthAnchor.constraint(equalTo: replyButton.widthAnchor),
            
            placeholderReplyButtonLeadingConstraint,
            replyButton.topAnchor.constraint(equalTo: $0.topAnchor),
            replyButton.bottomAnchor.constraint(equalTo: $0.bottomAnchor),
            
            reblogButton.leadingAnchor.constraint(equalTo: replyButton.trailingAnchor),
            reblogButton.topAnchor.constraint(equalTo: $0.topAnchor),
            reblogButton.bottomAnchor.constraint(equalTo: $0.bottomAnchor),
            
            favoriteButton.leadingAnchor.constraint(equalTo: reblogButton.trailingAnchor),
            favoriteButton.topAnchor.constraint(equalTo: $0.topAnchor),
            favoriteButton.bottomAnchor.constraint(equalTo: $0.bottomAnchor),
            
            moreButton.leadingAnchor.constraint(equalTo: favoriteButton.trailingAnchor),
            moreButton.trailingAnchor.constraint(equalTo: $0.trailingAnchor),
            moreButton.topAnchor.constraint(equalTo: $0.topAnchor),
            moreButton.bottomAnchor.constraint(equalTo: $0.bottomAnchor),
        ])
    }
    
    private(set) lazy var replyButton = UIButton().configure {
        $0.setImage(UIImage(systemName: "arrowshape.turn.up.left.fill"), for: .normal)
        $0.addTarget(self, action: #selector(replyPressed), for: .touchUpInside)
        $0.addInteraction(UIPointerInteraction(delegate: self))
    }
    
    private(set) lazy var favoriteButton = ToggleableButton(activeColor: UIColor(displayP3Red: 1, green: 0.8, blue: 0, alpha: 1)).configure {
        $0.setImage(UIImage(systemName: "star.fill"), for: .normal)
        $0.addTarget(self, action: #selector(favoritePressed), for: .touchUpInside)
        $0.addInteraction(UIPointerInteraction(delegate: self))
    }
    
    private(set) lazy var reblogButton = ToggleableButton(activeColor: UIColor(displayP3Red: 1, green: 0.8, blue: 0, alpha: 1)).configure {
        $0.setImage(UIImage(systemName: "repeat"), for: .normal)
        $0.addTarget(self, action: #selector(reblogPressed), for: .touchUpInside)
        $0.addInteraction(UIPointerInteraction(delegate: self))
    }
    
    private(set) lazy var moreButton = UIButton().configure {
        $0.setImage(UIImage(systemName: "ellipsis"), for: .normal)
        $0.showsMenuAsPrimaryAction = true
        $0.addInteraction(UIPointerInteraction(delegate: self))
    }
    
    private var actionButtons: [UIButton] {
        [replyButton, favoriteButton, reblogButton, moreButton]
    }
    
    private var contentViewMode: ContentViewMode!
    private let statusContainer = UIView().configure {
        $0.translatesAutoresizingMaskIntoConstraints = false
    }
    private let filteredLabel = UILabel().configure {
        $0.textAlignment = .center
        $0.textColor = .secondaryLabel
        $0.font = UIFont(descriptor: UIFontDescriptor.preferredFontDescriptor(withTextStyle: .body).withSymbolicTraits(.traitItalic)!, size: 0)
        $0.adjustsFontForContentSizeCategory = true
        $0.translatesAutoresizingMaskIntoConstraints = false
    }
    
    var prevThreadLinkView: UIView?
    var nextThreadLinkView: UIView?
    
    // MARK: Cell state
    
    private var mainContainerTopToReblogLabelConstraint: NSLayoutConstraint!
    private var mainContainerTopToSelfConstraint: NSLayoutConstraint!
    private var mainContainerBottomToActionsConstraint: NSLayoutConstraint!
    private var mainContainerBottomToSelfConstraint: NSLayoutConstraint!
    
    weak var overrideMastodonController: MastodonController?
    var mastodonController: MastodonController! { overrideMastodonController ?? delegate?.apiController }
    weak var delegate: StatusCollectionViewCellDelegate?
    var showStatusAutomatically = false
    var showReplyIndicator = true
    var showPinned: Bool = false
    var showFollowedHashtags: Bool = false
    
    // alas these need to be internal so they're accessible from the protocol extensions
    var statusID: String!
    var statusState: CollapseState!
    var accountID: String!
    private var reblogStatusID: String?
    private var rebloggerID: String?
    private var filterReason: String?
    
    private var firstLayout = true
    var isGrayscale = false
    private var updateTimestampWorkItem: DispatchWorkItem?
    private var hasCreatedObservers = false
    var cancellables = Set<AnyCancellable>()
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        
        setContentViewMode(.status)
        
        for subview in [timelineReasonHStack, mainContainer, actionsContainer] {
            subview.translatesAutoresizingMaskIntoConstraints = false
            statusContainer.addSubview(subview)
        }
        
        mainContainerTopToReblogLabelConstraint = mainContainer.topAnchor.constraint(equalTo: timelineReasonHStack.bottomAnchor, constant: 4)
        mainContainerTopToReblogLabelConstraint.identifier = "MainContainerTopToReblog"
        mainContainerTopToSelfConstraint = mainContainer.topAnchor.constraint(equalTo: statusContainer.topAnchor, constant: 8)
        mainContainerTopToSelfConstraint.identifier = "MainContainerTopToSelf"
        mainContainerBottomToActionsConstraint = mainContainer.bottomAnchor.constraint(equalTo: actionsContainer.topAnchor, constant: -4)
        mainContainerBottomToSelfConstraint = mainContainer.bottomAnchor.constraint(equalTo: statusContainer.bottomAnchor, constant: -6)
        
        NSLayoutConstraint.activate([
            // why is this 4 but the mainContainerTopSelfConstraint constant 8? because this looks more balanced
            timelineReasonHStack.topAnchor.constraint(equalTo: statusContainer.topAnchor, constant: 4),
            timelineReasonLabel.leadingAnchor.constraint(equalTo: contentVStack.leadingAnchor),
            timelineReasonHStack.trailingAnchor.constraint(lessThanOrEqualTo: statusContainer.trailingAnchor, constant: -16),
            
            mainContainer.leadingAnchor.constraint(equalTo: statusContainer.leadingAnchor, constant: 16),
            mainContainer.trailingAnchor.constraint(equalTo: statusContainer.trailingAnchor, constant: -16),
            mainContainerBottomToActionsConstraint,
            
            actionsContainer.leadingAnchor.constraint(equalTo: statusContainer.leadingAnchor, constant: 16),
            actionsContainer.trailingAnchor.constraint(equalTo: statusContainer.trailingAnchor, constant: -16),
            // yes, this is deliberately 6. 4 looks to cramped, 8 looks uneven
            actionsContainer.bottomAnchor.constraint(equalTo: statusContainer.bottomAnchor, constant: -6),
            
            metaIndicatorsView.bottomAnchor.constraint(lessThanOrEqualTo: statusContainer.bottomAnchor, constant: -6),
        ])
        
        updateActionsVisibility()
        
        NotificationCenter.default.addObserver(self, selector: #selector(preferencesChanged), name: .preferencesChanged, object: nil)
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override func layoutSubviews() {
        super.layoutSubviews()
        
        if firstLayout {
            firstLayout = false
            
            // the button's image view doesn't exist until after the first layout
            // accessing it before that cause the button to layoutIfNeeded which generates a broken, intermediate layout and prints a bunch of unhelpful autolayout warnings
            // so we wait until after the first layout pass to setup the reply button's real constraint
            placeholderReplyButtonLeadingConstraint.isActive = false
            replyButton.imageView!.leadingAnchor.constraint(equalTo: contentTextView.leadingAnchor).isActive = true
        }
    }
    
    override func updateConfiguration(using state: UICellConfigurationState) {
        backgroundConfiguration = .appListPlainCell(for: state)
    }
    
    // MARK: Accessibility
    
    override var isAccessibilityElement: Bool {
        get { true }
        set {}
    }
    
    override var accessibilityAttributedLabel: NSAttributedString? {
        get {
            if contentViewMode == .filtered,
               let filterReason {
                return NSAttributedString(string: "Filtered: \(filterReason)")
            }
            
            guard let status = mastodonController.persistentContainer.status(for: statusID) else {
                return nil
            }
            var str: AttributedString = ""
            if let rebloggerID,
               let reblogger = mastodonController.persistentContainer.account(for: rebloggerID) {
                str += AttributedString("Reblogged by \(reblogger.displayNameWithoutCustomEmoji): ")
            }
            str += AttributedString(status.account.displayNameWithoutCustomEmoji)
            str += ", "
            if statusState.collapsed ?? false {
                if !status.spoilerText.isEmpty {
                    str += AttributedString(status.spoilerText)
                    str += ", "
                }
                str += "collapsed"
            } else {
                str += AttributedString(contentTextView.attributedText)

                if status.attachments.count > 0 {
                    let includeDescriptions: Bool
                    switch Preferences.shared.attachmentBlurMode {
                    case .useStatusSetting:
                        includeDescriptions = !Preferences.shared.blurMediaBehindContentWarning || status.spoilerText.isEmpty
                    case .always:
                        includeDescriptions = true
                    case .never:
                        includeDescriptions = false
                    }
                    if includeDescriptions {
                        if status.attachments.count  == 1 {
                            let attachment = status.attachments[0]
                            let desc = attachment.description?.isEmpty == false ? attachment.description! : "no description"
                            str += AttributedString(", attachment: \(desc)")
                        } else {
                            for (index, attachment) in status.attachments.enumerated() {
                                let desc = attachment.description?.isEmpty == false ? attachment.description! : "no description"
                                str += AttributedString(", attachment \(index + 1): \(desc)")
                            }
                        }
                    } else {
                        str += AttributedString(", \(status.attachments.count) attachment\(status.attachments.count == 1 ? "" : "s")")
                    }
                }
                if status.poll != nil {
                    str += ", poll"
                }
            }
            
            str += AttributedString(", \(status.createdAt.formatted(.relative(presentation: .numeric)))")
            if status.visibility < .unlisted {
                str += AttributedString(", \(status.visibility.displayName)")
            }
            if status.localOnly {
                str += ", local"
            }
            return NSAttributedString(str)
        }
        set {}
    }
    
    override var accessibilityHint: String? {
        get {
            if contentViewMode == .filtered {
                return "Double tap to show the post."
            } else if statusState.collapsed ?? false {
                return "Double tap to expand the post."
            } else {
                return nil
            }
        }
        set {}
    }
    
    override func accessibilityActivate() -> Bool {
        if contentViewMode == .filtered {
            delegate?.statusCellShowFiltered(self)
        } else if statusState.collapsed ?? false {
            toggleCollapse()
        } else {
            delegate?.selected(status: statusID, state: statusState.copy())
        }
        return true
    }
    
    override var accessibilityCustomActions: [UIAccessibilityCustomAction]? {
        get {
            guard let text = contentTextView.attributedText,
                  let status = mastodonController.persistentContainer.status(for: statusID) else {
                return nil
            }
            var actions = [
                UIAccessibilityCustomAction(name: "Show \(status.account.displayNameWithoutCustomEmoji)", actionHandler: { [unowned self] _ in
                    self.delegate?.selected(account: status.account.id)
                    return true
                })
            ]
            text.enumerateAttribute(.link, in: NSRange(location: 0, length: text.length)) { value, range, stop in
                guard let value = value as? URL else {
                    return
                }
                let text = text.attributedSubstring(from: range).string
                actions.append(UIAccessibilityCustomAction(name: text) { [unowned self] _ in
                    self.contentTextView.handleLinkTapped(url: value, text: text)
                    return true
                })
            }
            return actions
        }
        set {}
    }
    
    // MARK: Configure UI
    
    private func setContentViewMode(_ mode: ContentViewMode) {
        guard mode != contentViewMode else {
            return
        }
        contentViewMode = mode
        switch mode {
        case .status:
            // make the offscreen view transparent so the filtered -> status animation looks better
            statusContainer.layer.opacity = 1
            filteredLabel.layer.opacity = 0
            filteredLabel.removeFromSuperview()
            contentView.addSubview(statusContainer)
            NSLayoutConstraint.activate([
                statusContainer.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
                statusContainer.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
                statusContainer.topAnchor.constraint(equalTo: contentView.topAnchor),
                statusContainer.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
            ])
        case .filtered:
            statusContainer.layer.opacity = 0
            filteredLabel.layer.opacity = 1
            statusContainer.removeFromSuperview()
            contentView.addSubview(filteredLabel)
            NSLayoutConstraint.activate([
                filteredLabel.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
                filteredLabel.trailingAnchor.constraint(equalTo: contentView.trailingAnchor),
                filteredLabel.topAnchor.constraint(equalToSystemSpacingBelow: contentView.topAnchor, multiplier: 1),
                contentView.bottomAnchor.constraint(equalToSystemSpacingBelow: filteredLabel.bottomAnchor, multiplier: 1),
            ])
        }
    }
    
    func updateUI(statusID: String, state: CollapseState, filterResult: Filterer.Result, precomputedContent: NSAttributedString?) {
        guard var status = mastodonController.persistentContainer.status(for: statusID) else {
            fatalError()
        }
        
        self.statusState = state
        
        let reblogStatus: StatusMO?
        if let rebloggedStatus = status.reblog {
            reblogStatus = status
            reblogStatusID = statusID
            rebloggerID = status.account.id
            
            status = rebloggedStatus
        } else {
            reblogStatus = nil
            reblogStatusID = nil
            rebloggerID = nil
        }
        
        switch filterResult {
        case .allow:
            setContentViewMode(.status)
        case .warn(let filterTitle):
            filterReason = filterTitle
            let attrStr = NSMutableAttributedString(string: "Filtered: \(filterTitle) ")
            let showStr = NSAttributedString(string: "Show", attributes: [.foregroundColor: UIColor.tintColor])
            attrStr.append(showStr)
            filteredLabel.attributedText = attrStr
            setContentViewMode(.filtered)
            
            // still update id properties, so that info for other methods (e.g., context menus) is correct
            self.statusID = status.id
            self.accountID = status.account.id
            return
        case .hide:
            fatalError("unreachable")
        }
        
        createObservers()
        
        var hideTimelineReason = true
        
        if let reblogStatus {
            hideTimelineReason = false
            updateRebloggerLabel(reblogger: reblogStatus.account)
        }
        
        if showFollowedHashtags {
            let hashtags = mastodonController.followedHashtags.filter({ followed in status.hashtags.contains(where: { followed.name == $0.name }) })
            if !hashtags.isEmpty {
                hideTimelineReason = false
                timelineReasonIcon.image = hashtagIcon
                timelineReasonLabel.text = hashtags.map(\.name).formatted(.list(type: .and, width: .narrow))
                timelineReasonLabel.removeEmojis()
            }
        }
        
        timelineReasonHStack.isHidden = hideTimelineReason
        // do this to make sure the currently active constraint is deactivated first
        if hideTimelineReason {
            mainContainerTopToReblogLabelConstraint.isActive = false
            mainContainerTopToSelfConstraint.isActive = true
        } else {
            mainContainerTopToSelfConstraint.isActive = false
            mainContainerTopToReblogLabelConstraint.isActive = true
        }
        
        doUpdateUI(status: status, precomputedContent: precomputedContent)
        
        doUpdateTimestamp(status: status)
        timestampLabel.isHidden = showPinned
        pinImageView.isHidden = !showPinned
    }
    
    private func createObservers() {
        guard !hasCreatedObservers else {
            return
        }
        hasCreatedObservers = true
        baseCreateObservers()
        
        mastodonController.persistentContainer.accountSubject
            .receive(on: DispatchQueue.main)
            .filter { [unowned self] in $0 == self.rebloggerID }
            .sink { [unowned self] in
                if let mastodonController = self.mastodonController,
                   let reblogger = mastodonController.persistentContainer.account(for: $0) {
                    self.updateRebloggerLabel(reblogger: reblogger)
                }
            }
            .store(in: &cancellables)
    }
    
    func updateUIForPreferences(status: StatusMO) {
        baseUpdateUIForPreferences(status: status)
        
        if showReplyIndicator {
            metaIndicatorsView.allowedIndicators = .all
        } else {
            metaIndicatorsView.allowedIndicators = .all.subtracting(.reply)
        }
        metaIndicatorsView.updateUI(status: status)
        
        timelineReasonIcon.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadiusFraction * Self.timelineReasonIconSize
    }
    
    func updateStatusState(status: StatusMO) {
        baseUpdateStatusState(status: status)
    }
    
    func estimateContentHeight() -> CGFloat {
        let width = bounds.width /* leading spacing: */ - 16 /* avatar: */ - 50 /* spacing: 8 */ - 8 /* trailing spacing: */ - 16
        return contentContainer.estimateVisibleSubviewHeight(effectiveWidth: width)
    }
    
    private func updateTimestamp() {
        guard let mastodonController,
              let status = mastodonController.persistentContainer.status(for: statusID) else {
            return
        }
        doUpdateTimestamp(status: status)
    }
    
    private func doUpdateTimestamp(status: StatusMO) {
        // if there's a pending update timestamp work item, cancel it
        updateTimestampWorkItem?.cancel()
        
        let timeAgo = status.createdAt.timeAgoString()
        timestampLabel.text = "\(timeAgo)\(status.editedAt != nil ? "*" : "")"
        
        let delay: DispatchTimeInterval?
        switch status.createdAt.timeAgo().1 {
        case .second:
            delay = .seconds(10)
        case .minute:
            delay = .seconds(60)
        default:
            delay = nil
        }
        if let delay {
            if updateTimestampWorkItem == nil {
                updateTimestampWorkItem = DispatchWorkItem { [weak self] in
                    self?.updateTimestamp()
                }
            }
            DispatchQueue.main.asyncAfter(deadline: .now() + delay, execute: updateTimestampWorkItem!)
        } else {
            updateTimestampWorkItem = nil
        }
    }
    
    private func updateRebloggerLabel(reblogger: AccountMO) {
        timelineReasonIcon.update(for: reblogger.avatar, placeholder: reblogIcon)
        
        if Preferences.shared.hideCustomEmojiInUsernames {
            timelineReasonLabel.text = "\(reblogger.displayNameWithoutCustomEmoji) reblogged"
            timelineReasonLabel.removeEmojis()
        } else {
            timelineReasonLabel.text = "\(reblogger.displayOrUserName) reblogged"
            timelineReasonLabel.setEmojis(reblogger.emojis, identifier: reblogger.id)
        }
    }
    
    private func updateActionsVisibility() {
        if Preferences.shared.hideActionsInTimeline && !actionsContainer.isHidden {
            actionsContainer.isHidden = true
            mainContainerBottomToActionsConstraint.isActive = false
            mainContainerBottomToSelfConstraint.isActive = true
        } else if !Preferences.shared.hideActionsInTimeline && actionsContainer.isHidden {
            actionsContainer.isHidden = false
            mainContainerBottomToSelfConstraint.isActive = false
            mainContainerBottomToActionsConstraint.isActive = true
        }
    }
    
    @objc private func preferencesChanged() {
        guard let mastodonController,
              let statusID,
              let status = mastodonController.persistentContainer.status(for: statusID) else {
            return
        }
        
        updateUIForPreferences(status: status)
        
        if let rebloggerID,
           let reblogger = mastodonController.persistentContainer.account(for: rebloggerID) {
            updateRebloggerLabel(reblogger: reblogger)
        }
        
        if isGrayscale != Preferences.shared.grayscaleImages {
            updateGrayscaleableUI(status: status)
        }
        
        // only needs to happen when prefs change, rather than in updateUIForPrefs b/c this is setup correctly during init
        let oldState = actionsContainer.isHidden
        if oldState != Preferences.shared.hideActionsInTimeline {
            updateActionsVisibility()
            delegate?.statusCellNeedsReconfigure(self, animated: true, completion: nil)
        }
    }
    
    // MARK: Interaction
    
    @objc private func reblogLabelPressed() {
        if let rebloggerID {
            delegate?.selected(account: rebloggerID)
        } else if showFollowedHashtags,
                  let status = mastodonController.persistentContainer.status(for: statusID),
                  let hashtag = mastodonController.followedHashtags.first(where: { followed in status.hashtags.contains(where: { followed.name == $0.name }) }) {
            delegate?.selected(tag: Hashtag(name: hashtag.name, url: hashtag.url))
        }
    }
    
    @objc private func accountPressed() {
        delegate?.selected(account: accountID)
    }
    
    @objc private func collapseButtonPressed() {
        toggleCollapse()
    }
    
    @objc private func replyPressed() {
        if Preferences.shared.mentionReblogger,
            let rebloggerID = rebloggerID,
            let rebloggerAccount = mastodonController.persistentContainer.account(for: rebloggerID) {
            delegate?.compose(inReplyToID: statusID, mentioningAcct: rebloggerAccount.acct)
        } else {
            delegate?.compose(inReplyToID: statusID)
        }
    }
    
    @objc private func favoritePressed() {
        toggleFavorite()
    }
    
    @objc private func reblogPressed() {
        toggleReblog()
    }
    
    func leadingSwipeActions() -> UISwipeActionsConfiguration? {
        guard let status = mastodonController.persistentContainer.status(for: statusID) else {
            return nil
        }
        return UISwipeActionsConfiguration(actions: Preferences.shared.leadingStatusSwipeActions.compactMap { $0.createAction(status: status, container: self) })
    }
    
    func trailingSwipeActions() -> UISwipeActionsConfiguration? {
        guard let status = mastodonController.persistentContainer.status(for: statusID) else {
            return nil
        }
        return UISwipeActionsConfiguration(actions: Preferences.shared.trailingStatusSwipeActions.compactMap { $0.createAction(status: status, container: self) })
    }
    
    func dragItemsForBeginning(session: UIDragSession) -> [UIDragItem] {
        // the poll options view is tracking while the user is dragging between options
        // while that's happening, don't initiate a drag
        guard !pollView.isTracking,
              let status = mastodonController.persistentContainer.status(for: statusID),
              let accountID = mastodonController.accountInfo?.id else {
            return []
        }
        let provider = NSItemProvider(object: status.url! as NSURL)
        let activity = UserActivityManager.showConversationActivity(mainStatusID: status.id, accountID: accountID)
        activity.displaysAuxiliaryScene = true
        provider.registerObject(activity, visibility: .all)
        return [UIDragItem(itemProvider: provider)]
    }
    
    func contextMenuConfiguration() -> UIContextMenuConfiguration? {
        guard let mastodonController,
              let status = mastodonController.persistentContainer.status(for: statusID) else {
            return nil
        }
        return UIContextMenuConfiguration {
            ConversationViewController(for: self.statusID, state: self.statusState.copy(), mastodonController: self.mastodonController)
        } actionProvider: { _ in
            guard let delegate = self.delegate else {
                return nil
            }
            return UIMenu(children: delegate.actionsForStatus(status, source: .view(self)))
        }
    }
    
}

extension TimelineStatusCollectionViewCell {
    private enum ContentViewMode {
        case status
        case filtered
    }
}

extension TimelineStatusCollectionViewCell: UIContextMenuInteractionDelegate {
    func contextMenuInteraction(_ interaction: UIContextMenuInteraction, configurationForMenuAtLocation location: CGPoint) -> UIContextMenuConfiguration? {
        return contextMenuConfigurationForAccount(sourceView: interaction.view!)
    }
    
    func contextMenuInteraction(_ interaction: UIContextMenuInteraction, willPerformPreviewActionForMenuWith configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionCommitAnimating) {
        if let delegate {
            MenuPreviewHelper.willPerformPreviewAction(animator: animator, presenter: delegate)
        }
    }
}

extension TimelineStatusCollectionViewCell: UIDragInteractionDelegate {
    func dragInteraction(_ interaction: UIDragInteraction, itemsForBeginning session: UIDragSession) -> [UIDragItem] {
        return dragItemsForAccount()
    }
}

extension TimelineStatusCollectionViewCell: UIPointerInteractionDelegate {
    func pointerInteraction(_ interaction: UIPointerInteraction, regionFor request: UIPointerRegionRequest, defaultRegion: UIPointerRegion) -> UIPointerRegion? {
        if interaction.view === avatarImageView {
            return defaultRegion
        } else if let button = interaction.view as? UIButton,
                  actionButtons.contains(button) {
            var rect = button.convert(button.imageView!.bounds, from: button.imageView!)
            rect = rect.insetBy(dx: -24, dy: -24)
            return UIPointerRegion(rect: rect)
        }
        return nil
    }
    
    func pointerInteraction(_ interaction: UIPointerInteraction, styleFor region: UIPointerRegion) -> UIPointerStyle? {
        if interaction.view === avatarImageView {
            let preview = UITargetedPreview(view: avatarImageView)
            return UIPointerStyle(effect: .lift(preview))
        } else if let button = interaction.view as? UIButton,
                  actionButtons.contains(button) {
            let preview = UITargetedPreview(view: button.imageView!)
            var rect = button.convert(button.imageView!.bounds, from: button.imageView!)
            rect = rect.insetBy(dx: -8, dy: -8)
            return UIPointerStyle(effect: .highlight(preview), shape: .roundedRect(rect))
        }
        return nil
    }
}

extension TimelineStatusCollectionViewCell: StatusSwipeActionContainer {
    var navigationDelegate: TuskerNavigationDelegate { delegate! }
    var toastableViewController: ToastableViewController? { delegate }
    
    var canReblog: Bool {
        reblogButton.isEnabled
    }
    
    func performReplyAction() {
        self.replyPressed()
    }
}
