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

import UIKit
import Pachyderm
import Combine

@MainActor
protocol StatusCollectionViewCellDelegate: AnyObject, TuskerNavigationDelegate, MenuActionProvider {
    func statusCellNeedsReconfigure(_ cell: StatusCollectionViewCell, animated: Bool, completion: (() -> Void)?)
    func statusCellShowFiltered(_ cell: StatusCollectionViewCell)
}

@MainActor
protocol StatusCollectionViewCell: UICollectionViewCell, AttachmentViewDelegate {
    // MARK: Subviews
    var avatarImageView: CachedImageView { get }
    var displayNameLabel: AccountDisplayNameLabel { get }
    var usernameLabel: UILabel { get }
    var contentWarningLabel: EmojiLabel { get }
    var collapseButton: StatusCollapseButton { get }
    var contentContainer: StatusContentContainer<StatusContentTextView, StatusPollView> { get }
    var replyButton: UIButton { get }
    var favoriteButton: ToggleableButton { get }
    var reblogButton: ToggleableButton { get }
    var moreButton: UIButton { get }
    var prevThreadLinkView: UIView? { get set }
    var nextThreadLinkView: UIView? { get set }
    
    var delegate: StatusCollectionViewCellDelegate? { get }
    var mastodonController: MastodonController! { get }
    
    var showStatusAutomatically: Bool { get }
    
    var statusID: String! { get set }
    var statusState: CollapseState! { get set }
    var accountID: String! { get set }
    
    var isGrayscale: Bool { get set }
    var cancellables: Set<AnyCancellable> { get set }
    
    func updateUIForPreferences(status: StatusMO)
    func updateStatusState(status: StatusMO)
    func estimateContentHeight() -> CGFloat
}

// MARK: UI Configuration
extension StatusCollectionViewCell {
    static var avatarImageViewSize: CGFloat { 50 }
    
    var mastodonController: MastodonController! { delegate?.apiController }
    
    func baseCreateObservers() {
        mastodonController.persistentContainer.statusSubject
            .receive(on: DispatchQueue.main)
            .filter { [unowned self] in $0 == self.statusID }
            .sink { [unowned self] _ in
                if let mastodonController = self.mastodonController,
                   let status = mastodonController.persistentContainer.status(for: self.statusID) {
                    // update immediately w/o animation
                    self.favoriteButton.active = status.favourited
                    self.reblogButton.active = status.reblogged
                    
                    self.delegate?.statusCellNeedsReconfigure(self, animated: true, completion: nil)
                }
            }
            .store(in: &cancellables)
        
        mastodonController.persistentContainer.accountSubject
            .receive(on: DispatchQueue.main)
            .filter { [unowned self] in $0 == self.accountID }
            .sink { [unowned self] in
                if let mastodonController = self.mastodonController,
                   let account = mastodonController.persistentContainer.account(for: $0) {
                    self.updateAccountUI(account: account)
                }
            }
            .store(in: &cancellables)
    }
    
    func doUpdateUI(status: StatusMO, precomputedContent: NSAttributedString? = nil) {
        statusID = status.id
        accountID = status.account.id
        
        updateAccountUI(account: status.account)
        
        contentContainer.contentTextView.setTextFrom(status: status, precomputed: precomputedContent)
        contentContainer.contentTextView.navigationDelegate = delegate
        contentContainer.attachmentsView.delegate = self
        contentContainer.attachmentsView.updateUI(attachments: status.attachments)
        contentContainer.pollView.isHidden = status.poll == nil
        contentContainer.pollView.mastodonController = mastodonController
        contentContainer.pollView.delegate = delegate
        contentContainer.pollView.updateUI(status: status, poll: status.poll)
        if Preferences.shared.showLinkPreviews {
            contentContainer.cardView.updateUI(status: status)
            contentContainer.cardView.isHidden = status.card == nil
            contentContainer.cardView.navigationDelegate = delegate
            contentContainer.cardView.actionProvider = delegate
        } else {
            contentContainer.cardView.isHidden = true
        }
        
        updateUIForPreferences(status: status)
        updateStatusState(status: status)
        
        contentWarningLabel.text = status.spoilerText
        contentWarningLabel.isHidden = status.spoilerText.isEmpty
        if !contentWarningLabel.isHidden {
            contentWarningLabel.setEmojis(status.emojis, identifier: statusID)
        }
        
        reblogButton.isEnabled = reblogEnabled(status: status)
        replyButton.isEnabled = mastodonController.loggedIn
        favoriteButton.isEnabled = mastodonController.loggedIn
        
        let didResolve = statusState.resolveFor(status: status, height: self.estimateContentHeight)
        if didResolve {
            if statusState.collapsible! && showStatusAutomatically {
                statusState.collapsed = false
            }
        }
        let expected = !statusState.collapsible!
        // Very very rarely, setting isHidden to false only seems to partially take effect:
        // the button will be rendered, but isHidden will still return true, and the
        // containing stack view won't have updated constraints for it and so the cell
        // layout will be wrong and the button will overlap other views in the stack.
        // So, as a truly cursed workaround, just try a few times in a row until reading
        // back isHidden returns the correct value.
        for _ in 0..<5 {
            collapseButton.isHidden = expected
            if collapseButton.isHidden == expected {
                break
            }
        }
        contentContainer.setCollapsed(statusState.collapsed!)
        if statusState.collapsed! {
            contentContainer.alpha = 0
            // TODO: is this accessing the image view before the button's been laid out?
            collapseButton.imageView!.transform = CGAffineTransform(rotationAngle: 0)
            collapseButton.accessibilityLabel = NSLocalizedString("Expand Status", comment: "expand status button accessibility label")
        } else {
            contentContainer.alpha = 1
            collapseButton.imageView!.transform = CGAffineTransform(rotationAngle: .pi)
            collapseButton.accessibilityLabel = NSLocalizedString("Collapse Status", comment: "collapse status button accessibility label")
        }
    }
    
    private func reblogEnabled(status: StatusMO) -> Bool {
        guard mastodonController.loggedIn else {
            return false
        }
        if status.visibility == .direct {
            return false
        } else if status.visibility == .private {
            if mastodonController.instanceFeatures.boostToOriginalAudience,
               status.account.id == mastodonController.account?.id {
                return true
            }
            return false
        }
        return true
    }
    
    func updateAccountUI(account: AccountMO) {
        avatarImageView.update(for: account.avatar)
        displayNameLabel.updateForAccountDisplayName(account: account)
        usernameLabel.text = "@\(account.acct)"
    }
    
    func baseUpdateUIForPreferences(status: StatusMO) {
        avatarImageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadiusFraction * Self.avatarImageViewSize
        
        let newCardHidden = !Preferences.shared.showLinkPreviews || status.card == nil
        if contentContainer.cardView.isHidden != newCardHidden {
            delegate?.statusCellNeedsReconfigure(self, animated: false, completion: nil)
        }
        
        switch Preferences.shared.attachmentBlurMode {
        case .never:
            contentContainer.attachmentsView.contentHidden = false
        case .always:
            contentContainer.attachmentsView.contentHidden = true
        default:
            if status.sensitive {
                contentContainer.attachmentsView.contentHidden = status.spoilerText.isEmpty || Preferences.shared.blurMediaBehindContentWarning
            } else {
                contentContainer.attachmentsView.contentHidden = false
            }
        }
        
        let reblogButtonImage: UIImage
        if Preferences.shared.alwaysShowStatusVisibilityIcon || reblogEnabled(status: status) {
            reblogButtonImage = UIImage(systemName: "repeat")!
        } else {
            reblogButtonImage = UIImage(systemName: status.visibility.imageName)!
        }
        reblogButton.setImage(reblogButtonImage, for: .normal)
    }
    
    // only called when isGrayscale does not match the pref
    func updateGrayscaleableUI(status: StatusMO) {
        isGrayscale = Preferences.shared.grayscaleImages
        if contentContainer.contentTextView.hasEmojis {
            contentContainer.contentTextView.setEmojis(status.emojis, identifier: status.id)
        }
        displayNameLabel.updateForAccountDisplayName(account: status.account)
    }
    
    func baseUpdateStatusState(status: StatusMO) {
        favoriteButton.active = status.favourited
        if status.favourited {
            favoriteButton.accessibilityLabel = NSLocalizedString("Undo Favorite", comment: "undo favorite button accessibility label")
        } else {
            favoriteButton.accessibilityLabel = NSLocalizedString("Favorite", comment: "favorite button accessibility label")
        }
        reblogButton.active = status.reblogged
        if status.reblogged {
            reblogButton.accessibilityLabel = NSLocalizedString("Undo Reblog", comment: "undo reblog button accessibility label")
        } else {
            reblogButton.accessibilityLabel = NSLocalizedString("Reblog", comment: "reblog button accessibility label")
        }
        
        // keep menu in sync with changed states e.g. bookmarked, muted
        // do not include reply action here, because the cell already contains a button for it
        moreButton.menu = UIMenu(title: "", image: nil, identifier: nil, options: [], children: delegate?.actionsForStatus(status, source: .view(moreButton), includeStatusButtonActions: false) ?? [])
        
        contentContainer.pollView.isHidden = status.poll == nil
        contentContainer.pollView.mastodonController = mastodonController
        contentContainer.pollView.delegate = delegate
        contentContainer.pollView.updateUI(status: status, poll: status.poll)
    }
    
    func setShowThreadLinks(prev: Bool, next: Bool) {
        if prev {
            if let prevThreadLinkView {
                prevThreadLinkView.isHidden = false
            } else {
                let view = UIView()
                view.translatesAutoresizingMaskIntoConstraints = false
                view.backgroundColor = .tintColor.withAlphaComponent(0.5)
                view.layer.cornerRadius = 2.5
                view.layer.maskedCorners = [.layerMinXMaxYCorner, .layerMaxXMaxYCorner]
                prevThreadLinkView = view
                contentView.addSubview(view)
                NSLayoutConstraint.activate([
                    view.widthAnchor.constraint(equalToConstant: 5),
                    view.centerXAnchor.constraint(equalTo: avatarImageView.centerXAnchor),
                    view.topAnchor.constraint(equalTo: contentView.topAnchor),
                    view.bottomAnchor.constraint(equalTo: avatarImageView.topAnchor, constant: -2),
                ])
            }
        } else {
            prevThreadLinkView?.isHidden = true
        }
        
        if next {
            if let nextThreadLinkView {
                nextThreadLinkView.isHidden = false
            } else {
                let view = UIView()
                view.translatesAutoresizingMaskIntoConstraints = false
                view.backgroundColor = .tintColor.withAlphaComponent(0.5)
                view.layer.cornerRadius = 2.5
                view.layer.maskedCorners = [.layerMinXMinYCorner, .layerMaxXMinYCorner]
                nextThreadLinkView = view
                contentView.addSubview(view)
                let bottomConstraint = view.bottomAnchor.constraint(equalTo: contentView.bottomAnchor)
                // let this constraint get broken during intermediate layouts to avoid a bunch of spurious 'unable to simultaneously satisfy constraints' messages
                bottomConstraint.priority = .init(999)
                NSLayoutConstraint.activate([
                    view.widthAnchor.constraint(equalToConstant: 5),
                    view.centerXAnchor.constraint(equalTo: avatarImageView.centerXAnchor),
                    view.topAnchor.constraint(equalTo: avatarImageView.bottomAnchor, constant: 2),
                    bottomConstraint,
                ])
            }
        } else {
            nextThreadLinkView?.isHidden = true
        }
    }
}

// MARK: Interaction
extension StatusCollectionViewCell {
    func toggleCollapse() {
        statusState.collapsed!.toggle()
        // mask so that the content appears to expand with the container during the animation
        // but only while the animation is taking place, otherwise the mask interferes with context menu presentation animation
        contentContainer.layer.masksToBounds = true
        // this delegate call causes the collection view to reconfigure this cell, at which point (and inside of the collection view's animation handling) we'll update the contentContainer
        delegate?.statusCellNeedsReconfigure(self, animated: true) {
            self.contentContainer.layer.masksToBounds = false
        }
    }
    
    func toggleFavorite() {
        guard let status = mastodonController.persistentContainer.status(for: statusID) else {
            fatalError()
        }
        Task {
            await FavoriteService(status: status, mastodonController: mastodonController, presenter: delegate!).toggleFavorite()
        }
    }
    
    func toggleReblog() {
        guard let status = mastodonController.persistentContainer.status(for: statusID) else {
            fatalError()
        }
        Task {
            await ReblogService(status: status, mastodonController: mastodonController, presenter: delegate!).toggleReblog()
        }
    }
}

extension StatusCollectionViewCell {
    func attachmentViewGallery(startingAt index: Int) -> GalleryViewController? {
        guard let delegate = delegate,
              let status = mastodonController.persistentContainer.status(for: statusID) else { return nil }
        let sourceViews = status.attachments.map(contentContainer.attachmentsView.getAttachmentView(for:))
        let gallery = delegate.gallery(attachments: status.attachments, sourceViews: sourceViews, startIndex: index)
        // TODO: PiP
//        gallery.avPlayerViewControllerDelegate = self
        return gallery
    }
    
    func attachmentViewPresent(_ vc: UIViewController, animated: Bool) {
        delegate?.present(vc, animated: animated)
    }
}

extension StatusCollectionViewCell {
    func contextMenuConfigurationForAccount(sourceView: UIView) -> UIContextMenuConfiguration? {
        return UIContextMenuConfiguration() {
            ProfileViewController(accountID: self.accountID, mastodonController: self.mastodonController)
        } actionProvider: { _ in
            return UIMenu(children: self.delegate?.actionsForProfile(accountID: self.accountID, source: .view(sourceView)) ?? [])
        }
    }
}

extension StatusCollectionViewCell {
    func dragItemsForAccount() -> [UIDragItem] {
        guard let currentAccountID = mastodonController.accountInfo?.id,
              let account = mastodonController.persistentContainer.account(for: accountID) else {
            return []
        }
        let provider = NSItemProvider(object: account.url as NSURL)
        let activity = UserActivityManager.showProfileActivity(id: accountID, accountID: currentAccountID)
        activity.displaysAuxiliaryScene = true
        provider.registerObject(activity, visibility: .all)
        return [UIDragItem(itemProvider: provider)]
    }
}
