//
//  ComposeViewController.swift
//  Mastodon
//
//  Created by MainasuK Cirno on 2021-3-11.
//

import os.log
import UIKit
import Combine
import PhotosUI
import MetaTextKit
import MastodonMeta
import Meta
import MastodonUI
import MastodonAsset
import MastodonLocalization
import MastodonSDK

final class ComposeViewController: UIViewController, NeedsDependency {
    
    static let minAutoCompleteVisibleHeight: CGFloat = 100
        
    weak var context: AppContext! { willSet { precondition(!isViewLoaded) } }
    weak var coordinator: SceneCoordinator! { willSet { precondition(!isViewLoaded) } }
    
    var disposeBag = Set<AnyCancellable>()
    var viewModel: ComposeViewModel!

    let logger = Logger(subsystem: "ComposeViewController", category: "logic")
    
    private(set) lazy var cancelBarButtonItem = UIBarButtonItem(title: L10n.Common.Controls.Actions.cancel, style: .plain, target: self, action: #selector(ComposeViewController.cancelBarButtonItemPressed(_:)))
    let characterCountLabel: UILabel = {
        let label = UILabel()
        label.font = .systemFont(ofSize: 15, weight: .regular)
        label.text = "500"
        label.textColor = Asset.Colors.Label.secondary.color
        label.accessibilityLabel = L10n.A11y.Plural.Count.inputLimitRemains(500)
        return label
    }()
    private(set) lazy var characterCountBarButtonItem: UIBarButtonItem = {
        let barButtonItem = UIBarButtonItem(customView: characterCountLabel)
        return barButtonItem
    }()
    
    let publishButton: UIButton = {
        let button = RoundedEdgesButton(type: .custom)
        button.cornerRadius = 10
        button.contentEdgeInsets = UIEdgeInsets(top: 6, left: 16, bottom: 5, right: 16)     // set 28pt height
        button.titleLabel?.font = .systemFont(ofSize: 14, weight: .bold)
        button.setTitle(L10n.Scene.Compose.composeAction, for: .normal)
        return button
    }()
    private(set) lazy var publishBarButtonItem: UIBarButtonItem = {
        configurePublishButtonApperance()
        let shadowBackgroundContainer = ShadowBackgroundContainer()
        publishButton.translatesAutoresizingMaskIntoConstraints = false
        shadowBackgroundContainer.addSubview(publishButton)
        NSLayoutConstraint.activate([
            publishButton.topAnchor.constraint(equalTo: shadowBackgroundContainer.topAnchor),
            publishButton.leadingAnchor.constraint(equalTo: shadowBackgroundContainer.leadingAnchor),
            publishButton.trailingAnchor.constraint(equalTo: shadowBackgroundContainer.trailingAnchor),
            publishButton.bottomAnchor.constraint(equalTo: shadowBackgroundContainer.bottomAnchor),
        ])
        let barButtonItem = UIBarButtonItem(customView: shadowBackgroundContainer)
        return barButtonItem
    }()
    
    private func configurePublishButtonApperance() {
        publishButton.adjustsImageWhenHighlighted = false
        publishButton.setBackgroundImage(.placeholder(color: Asset.Colors.Label.primary.color), for: .normal)
        publishButton.setBackgroundImage(.placeholder(color: Asset.Colors.Label.primary.color.withAlphaComponent(0.5)), for: .highlighted)
        publishButton.setBackgroundImage(.placeholder(color: Asset.Colors.Button.disabled.color), for: .disabled)
        publishButton.setTitleColor(Asset.Colors.Label.primaryReverse.color, for: .normal)
    }

    let tableView: ComposeTableView = {
        let tableView = ComposeTableView()
        tableView.register(ComposeRepliedToStatusContentTableViewCell.self, forCellReuseIdentifier: String(describing: ComposeRepliedToStatusContentTableViewCell.self))
        tableView.register(ComposeStatusContentTableViewCell.self, forCellReuseIdentifier: String(describing: ComposeStatusContentTableViewCell.self))
        tableView.register(ComposeStatusAttachmentTableViewCell.self, forCellReuseIdentifier: String(describing: ComposeStatusAttachmentTableViewCell.self))
        tableView.alwaysBounceVertical = true
        tableView.separatorStyle = .none
        tableView.tableFooterView = UIView()
        return tableView
    }()
    
    var systemKeyboardHeight: CGFloat = .zero {
        didSet {
            // note: some system AutoLayout warning here
            let height = max(300, systemKeyboardHeight)
            customEmojiPickerInputView.frame.size.height = height
        }
    }
    
    // CustomEmojiPickerView
    let customEmojiPickerInputView: CustomEmojiPickerInputView = {
        let view = CustomEmojiPickerInputView(frame: CGRect(x: 0, y: 0, width: 0, height: 300), inputViewStyle: .keyboard)
        return view
    }()
    
    let composeToolbarView = ComposeToolbarView()
    var composeToolbarViewBottomLayoutConstraint: NSLayoutConstraint!
    let composeToolbarBackgroundView = UIView()
    
    static func createPhotoLibraryPickerConfiguration(selectionLimit: Int = 4) -> PHPickerConfiguration {
        var configuration = PHPickerConfiguration()
        configuration.filter = .any(of: [.images, .videos])
        configuration.selectionLimit = selectionLimit
        return configuration
    }
    
    private(set) lazy var photoLibraryPicker: PHPickerViewController = {
        let imagePicker = PHPickerViewController(configuration: ComposeViewController.createPhotoLibraryPickerConfiguration())
        imagePicker.delegate = self
        return imagePicker
    }()
    private(set) lazy var imagePickerController: UIImagePickerController = {
        let imagePickerController = UIImagePickerController()
        imagePickerController.sourceType = .camera
        imagePickerController.delegate = self
        return imagePickerController
    }()
    
    private(set) lazy var documentPickerController: UIDocumentPickerViewController = {
        let documentPickerController = UIDocumentPickerViewController(forOpeningContentTypes: [.image, .movie])
        documentPickerController.delegate = self
        return documentPickerController
    }()
    
    private(set) lazy var autoCompleteViewController: AutoCompleteViewController = {
        let viewController = AutoCompleteViewController()
        viewController.viewModel = AutoCompleteViewModel(context: context)
        viewController.delegate = self
        viewController.viewModel.customEmojiViewModel.value = viewModel.customEmojiViewModel
        return viewController
    }()
    
    deinit {
        os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
    }
    
}

extension ComposeViewController {
    private static func createLayout() -> UICollectionViewLayout {
        let itemSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .estimated(44))
        let item = NSCollectionLayoutItem(layoutSize: itemSize)
        let groupSize = NSCollectionLayoutSize(widthDimension: .fractionalWidth(1.0), heightDimension: .estimated(44))
        let group = NSCollectionLayoutGroup.vertical(layoutSize: groupSize, subitems: [item])
        let section = NSCollectionLayoutSection(group: group)
        section.contentInsetsReference = .readableContent
        // section.interGroupSpacing = 10
        // section.contentInsets = NSDirectionalEdgeInsets(top: 10, leading: 10, bottom: 10, trailing: 10)
        return UICollectionViewCompositionalLayout(section: section)
    }
}

extension ComposeViewController {
    
    override func viewDidLoad() {
        super.viewDidLoad()

        configureNavigationBarTitleStyle()
        viewModel.traitCollectionDidChangePublisher
            .receive(on: DispatchQueue.main)
            .sink { [weak self] _ in
                guard let self = self else { return }
                self.configureNavigationBarTitleStyle()
            }
            .store(in: &disposeBag)
        
        viewModel.$title
            .receive(on: DispatchQueue.main)
            .sink { [weak self] title in
                guard let self = self else { return }
                self.title = title
            }
            .store(in: &disposeBag)
        self.setupBackgroundColor(theme: ThemeService.shared.currentTheme.value)
        ThemeService.shared.currentTheme
            .receive(on: RunLoop.main)
            .sink { [weak self] theme in
                guard let self = self else { return }
                self.setupBackgroundColor(theme: theme)
            }
            .store(in: &disposeBag)
        navigationItem.leftBarButtonItem = cancelBarButtonItem
        navigationItem.rightBarButtonItem = publishBarButtonItem
        viewModel.traitCollectionDidChangePublisher
            .receive(on: DispatchQueue.main)
            .sink { [weak self] _ in
                guard let self = self else { return }
                guard self.traitCollection.userInterfaceIdiom == .pad else { return }
                var items = [self.publishBarButtonItem]
                if self.traitCollection.horizontalSizeClass == .regular {
                    items.append(self.characterCountBarButtonItem)
                }
                self.navigationItem.rightBarButtonItems = items
            }
            .store(in: &disposeBag)
        publishButton.addTarget(self, action: #selector(ComposeViewController.publishBarButtonItemPressed(_:)), for: .touchUpInside)


        tableView.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(tableView)
        NSLayoutConstraint.activate([
            tableView.topAnchor.constraint(equalTo: view.topAnchor),
            tableView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            tableView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
        ])
        
        composeToolbarView.translatesAutoresizingMaskIntoConstraints = false
        view.addSubview(composeToolbarView)
        composeToolbarViewBottomLayoutConstraint = view.bottomAnchor.constraint(equalTo: composeToolbarView.bottomAnchor)
        NSLayoutConstraint.activate([
            composeToolbarView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            composeToolbarView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            composeToolbarViewBottomLayoutConstraint,
            composeToolbarView.heightAnchor.constraint(equalToConstant: ComposeToolbarView.toolbarHeight),
        ])
        composeToolbarView.preservesSuperviewLayoutMargins = true
        composeToolbarView.delegate = self
        
        composeToolbarBackgroundView.translatesAutoresizingMaskIntoConstraints = false
        view.insertSubview(composeToolbarBackgroundView, belowSubview: composeToolbarView)
        NSLayoutConstraint.activate([
            composeToolbarBackgroundView.topAnchor.constraint(equalTo: composeToolbarView.topAnchor),
            composeToolbarBackgroundView.leadingAnchor.constraint(equalTo: composeToolbarView.leadingAnchor),
            composeToolbarBackgroundView.trailingAnchor.constraint(equalTo: composeToolbarView.trailingAnchor),
            view.bottomAnchor.constraint(equalTo: composeToolbarBackgroundView.bottomAnchor),
        ])

        tableView.delegate = self
        viewModel.setupDataSource(
            tableView: tableView,
            metaTextDelegate: self,
            metaTextViewDelegate: self,
            customEmojiPickerInputViewModel: viewModel.customEmojiPickerInputViewModel,
            composeStatusAttachmentCollectionViewCellDelegate: self,
            composeStatusPollOptionCollectionViewCellDelegate: self,
            composeStatusPollOptionAppendEntryCollectionViewCellDelegate: self,
            composeStatusPollExpiresOptionCollectionViewCellDelegate: self
        )

        viewModel.composeStatusAttribute.$composeContent
            .removeDuplicates()
            .receive(on: DispatchQueue.main)
            .sink { [weak self] _ in
                guard let self = self else { return }
                guard self.view.window != nil else { return }
                UIView.performWithoutAnimation {
                    self.tableView.beginUpdates()
                    self.tableView.endUpdates()
                }
            }
            .store(in: &disposeBag)
        
        customEmojiPickerInputView.collectionView.delegate = self
        viewModel.customEmojiPickerInputViewModel.customEmojiPickerInputView = customEmojiPickerInputView
        viewModel.setupCustomEmojiPickerDiffableDataSource(
            for: customEmojiPickerInputView.collectionView,
            dependency: self
        )
        
        viewModel.composeStatusContentTableViewCell.delegate = self

        // update layout when keyboard show/dismiss
        view.layoutIfNeeded()
        
        let keyboardHasShortcutBar = CurrentValueSubject<Bool, Never>(traitCollection.userInterfaceIdiom == .pad)       // update default value later
        let keyboardEventPublishers = Publishers.CombineLatest3(
            KeyboardResponderService.shared.isShow,
            KeyboardResponderService.shared.state,
            KeyboardResponderService.shared.endFrame
        )
        Publishers.CombineLatest3(
            keyboardEventPublishers,
            viewModel.$isCustomEmojiComposing,
            viewModel.$autoCompleteInfo
        )
        .sink(receiveValue: { [weak self] keyboardEvents, isCustomEmojiComposing, autoCompleteInfo in
            guard let self = self else { return }
            
            let (isShow, state, endFrame) = keyboardEvents
            
            switch self.traitCollection.userInterfaceIdiom {
            case .pad:
                keyboardHasShortcutBar.value = state != .floating
            default:
                keyboardHasShortcutBar.value = false
            }
            
            let extraMargin: CGFloat = {
                var margin = self.composeToolbarView.frame.height
                if autoCompleteInfo != nil {
                    margin += ComposeViewController.minAutoCompleteVisibleHeight
                }
                return margin
            }()

            guard isShow, state == .dock else {
                self.tableView.contentInset.bottom = extraMargin
                self.tableView.verticalScrollIndicatorInsets.bottom = extraMargin
            
                if let superView = self.autoCompleteViewController.tableView.superview {
                    let autoCompleteTableViewBottomInset: CGFloat = {
                        let tableViewFrameInWindow = superView.convert(self.autoCompleteViewController.tableView.frame, to: nil)
                        let padding = tableViewFrameInWindow.maxY + self.composeToolbarView.frame.height + AutoCompleteViewController.chevronViewHeight - self.view.frame.maxY
                        return max(0, padding)
                    }()
                    self.autoCompleteViewController.tableView.contentInset.bottom = autoCompleteTableViewBottomInset
                    self.autoCompleteViewController.tableView.verticalScrollIndicatorInsets.bottom = autoCompleteTableViewBottomInset
                }
                
                UIView.animate(withDuration: 0.3) {
                    self.composeToolbarViewBottomLayoutConstraint.constant = self.view.safeAreaInsets.bottom
                    if self.view.window != nil {
                        self.view.layoutIfNeeded()
                    }
                }
                return
            }
            // isShow AND dock state
            self.systemKeyboardHeight = endFrame.height
            
            // adjust inset for auto-complete
            let autoCompleteTableViewBottomInset: CGFloat = {
                guard let superview = self.autoCompleteViewController.tableView.superview else { return .zero }
                let tableViewFrameInWindow = superview.convert(self.autoCompleteViewController.tableView.frame, to: nil)
                let padding = tableViewFrameInWindow.maxY + self.composeToolbarView.frame.height + AutoCompleteViewController.chevronViewHeight - endFrame.minY
                return max(0, padding)
            }()
            self.autoCompleteViewController.tableView.contentInset.bottom = autoCompleteTableViewBottomInset
            self.autoCompleteViewController.tableView.verticalScrollIndicatorInsets.bottom = autoCompleteTableViewBottomInset
            
            // adjust inset for tableView
            let contentFrame = self.view.convert(self.tableView.frame, to: nil)
            let padding = contentFrame.maxY + extraMargin - endFrame.minY
            guard padding > 0 else {
                self.tableView.contentInset.bottom = self.view.safeAreaInsets.bottom + extraMargin
                self.tableView.verticalScrollIndicatorInsets.bottom = self.view.safeAreaInsets.bottom + extraMargin
                return
            }

            self.tableView.contentInset.bottom = padding - self.view.safeAreaInsets.bottom
            self.tableView.verticalScrollIndicatorInsets.bottom = padding - self.view.safeAreaInsets.bottom
            UIView.animate(withDuration: 0.3) {
                self.composeToolbarViewBottomLayoutConstraint.constant = endFrame.height
                self.view.layoutIfNeeded()
            }
        })
        .store(in: &disposeBag)
        
        // bind auto-complete
        viewModel.$autoCompleteInfo
            .receive(on: DispatchQueue.main)
            .sink { [weak self] info in
                guard let self = self else { return }
                let textEditorView = self.textEditorView
                if self.autoCompleteViewController.view.superview == nil {
                    self.autoCompleteViewController.view.frame = self.view.bounds
                    // add to container view. seealso: `viewDidLayoutSubviews()`
                    self.viewModel.composeStatusContentTableViewCell.textEditorViewContainerView.addSubview(self.autoCompleteViewController.view)
                    self.addChild(self.autoCompleteViewController)
                    self.autoCompleteViewController.didMove(toParent: self)
                    self.autoCompleteViewController.view.isHidden = true
                    self.tableView.autoCompleteViewController = self.autoCompleteViewController
                }
                self.updateAutoCompleteViewControllerLayout()
                self.autoCompleteViewController.view.isHidden = info == nil
                guard let info = info else { return }
                let symbolBoundingRectInContainer = textEditorView.textView.convert(info.symbolBoundingRect, to: self.autoCompleteViewController.chevronView)
                self.autoCompleteViewController.view.frame.origin.y = info.textBoundingRect.maxY
                self.autoCompleteViewController.viewModel.symbolBoundingRect.value = symbolBoundingRectInContainer
                self.autoCompleteViewController.viewModel.inputText.value = String(info.inputText)
            }
            .store(in: &disposeBag)

        // bind publish bar button state
        viewModel.$isPublishBarButtonItemEnabled
            .receive(on: DispatchQueue.main)
            .assign(to: \.isEnabled, on: publishButton)
            .store(in: &disposeBag)
        
        // bind media button toolbar state
        viewModel.$isMediaToolbarButtonEnabled
            .receive(on: DispatchQueue.main)
            .sink { [weak self] isMediaToolbarButtonEnabled in
                guard let self = self else { return }
                self.composeToolbarView.mediaBarButtonItem.isEnabled = isMediaToolbarButtonEnabled
                self.composeToolbarView.mediaButton.isEnabled = isMediaToolbarButtonEnabled
            }
            .store(in: &disposeBag)
        
        // bind poll button toolbar state
        viewModel.$isPollToolbarButtonEnabled
            .receive(on: DispatchQueue.main)
            .sink { [weak self] isPollToolbarButtonEnabled in
                guard let self = self else { return }
                self.composeToolbarView.pollBarButtonItem.isEnabled = isPollToolbarButtonEnabled
                self.composeToolbarView.pollButton.isEnabled = isPollToolbarButtonEnabled
            }
            .store(in: &disposeBag)
        
        Publishers.CombineLatest(
            viewModel.$isPollComposing,
            viewModel.$isPollToolbarButtonEnabled
        )
        .receive(on: DispatchQueue.main)
        .sink { [weak self] isPollComposing, isPollToolbarButtonEnabled in
            guard let self = self else { return }
            guard isPollToolbarButtonEnabled else {
                let accessibilityLabel = L10n.Scene.Compose.Accessibility.appendPoll
                self.composeToolbarView.pollBarButtonItem.accessibilityLabel = accessibilityLabel
                self.composeToolbarView.pollButton.accessibilityLabel = accessibilityLabel
                return
            }
            let accessibilityLabel = isPollComposing ? L10n.Scene.Compose.Accessibility.removePoll : L10n.Scene.Compose.Accessibility.appendPoll
            self.composeToolbarView.pollBarButtonItem.accessibilityLabel = accessibilityLabel
            self.composeToolbarView.pollButton.accessibilityLabel = accessibilityLabel
        }
        .store(in: &disposeBag)

        // bind image picker toolbar state
        viewModel.$attachmentServices
            .receive(on: DispatchQueue.main)
            .sink { [weak self] attachmentServices in
                guard let self = self else { return }
                let isEnabled = attachmentServices.count < self.viewModel.maxMediaAttachments
                self.composeToolbarView.mediaBarButtonItem.isEnabled = isEnabled
                self.composeToolbarView.mediaButton.isEnabled = isEnabled
                self.resetImagePicker()
            }
            .store(in: &disposeBag)
        
        // bind content warning button state
        viewModel.$isContentWarningComposing
            .receive(on: DispatchQueue.main)
            .sink { [weak self] isContentWarningComposing in
                guard let self = self else { return }
                let accessibilityLabel = isContentWarningComposing ? L10n.Scene.Compose.Accessibility.disableContentWarning : L10n.Scene.Compose.Accessibility.enableContentWarning
                self.composeToolbarView.contentWarningBarButtonItem.accessibilityLabel = accessibilityLabel
                self.composeToolbarView.contentWarningButton.accessibilityLabel = accessibilityLabel
            }
            .store(in: &disposeBag)
        
        // bind visibility toolbar UI
        Publishers.CombineLatest(
            viewModel.$selectedStatusVisibility,
            viewModel.traitCollectionDidChangePublisher
        )
        .receive(on: DispatchQueue.main)
        .sink { [weak self] type, _ in
            guard let self = self else { return }
            let image = type.image(interfaceStyle: self.traitCollection.userInterfaceStyle)
            self.composeToolbarView.visibilityBarButtonItem.image = image
            self.composeToolbarView.visibilityButton.setImage(image, for: .normal)
            self.composeToolbarView.activeVisibilityType.value = type
        }
        .store(in: &disposeBag)
        
        viewModel.$characterCount
            .receive(on: DispatchQueue.main)
            .sink { [weak self] characterCount in
                guard let self = self else { return }
                let count = self.viewModel.composeContentLimit - characterCount
                self.composeToolbarView.characterCountLabel.text = "\(count)"
                self.characterCountLabel.text = "\(count)"
                let font: UIFont
                let textColor: UIColor
                let accessibilityLabel: String
                switch count {
                case _ where count < 0:
                    font = .monospacedDigitSystemFont(ofSize: 24, weight: .bold)
                    textColor = Asset.Colors.danger.color
                    accessibilityLabel = L10n.A11y.Plural.Count.inputLimitExceeds(abs(count))
                default:
                    font = .monospacedDigitSystemFont(ofSize: 15, weight: .regular)
                    textColor = Asset.Colors.Label.secondary.color
                    accessibilityLabel = L10n.A11y.Plural.Count.inputLimitRemains(count)
                }
                self.composeToolbarView.characterCountLabel.font = font
                self.composeToolbarView.characterCountLabel.textColor = textColor
                self.composeToolbarView.characterCountLabel.accessibilityLabel = accessibilityLabel
                self.characterCountLabel.font = font
                self.characterCountLabel.textColor = textColor
                self.characterCountLabel.accessibilityLabel = accessibilityLabel
                self.characterCountLabel.sizeToFit()
            }
            .store(in: &disposeBag)

        // bind custom emoji picker UI
        viewModel.customEmojiViewModel?.emojis
            .receive(on: DispatchQueue.main)
            .sink(receiveValue: { [weak self] emojis in
                guard let self = self else { return }
                if emojis.isEmpty {
                    self.customEmojiPickerInputView.activityIndicatorView.startAnimating()
                } else {
                    self.customEmojiPickerInputView.activityIndicatorView.stopAnimating()
                }
            })
            .store(in: &disposeBag)
        
        // setup snap behavior
        Publishers.CombineLatest(
            viewModel.$repliedToCellFrame,
            viewModel.$collectionViewState
        )
        .receive(on: DispatchQueue.main)
        .sink { [weak self] repliedToCellFrame, collectionViewState in
            guard let self = self else { return }
            guard repliedToCellFrame != .zero else { return }
            switch collectionViewState {
            case .fold:
                self.tableView.contentInset.top = -repliedToCellFrame.height
                os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: set contentInset.top: -%s", ((#file as NSString).lastPathComponent), #line, #function, repliedToCellFrame.height.description)

            case .expand:
                self.tableView.contentInset.top = 0
            }
        }
        .store(in: &disposeBag)
        
        configureToolbarDisplay(keyboardHasShortcutBar: keyboardHasShortcutBar.value)
        Publishers.CombineLatest(
            keyboardHasShortcutBar,
            viewModel.traitCollectionDidChangePublisher
        )
        .receive(on: DispatchQueue.main)
        .sink { [weak self] keyboardHasShortcutBar, _ in
            guard let self = self else { return }
            self.configureToolbarDisplay(keyboardHasShortcutBar: keyboardHasShortcutBar)
        }
        .store(in: &disposeBag)
    }
    
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        
        // update MetaText without trigger call underlaying `UITextStorage.processEditing`
        _ = textEditorView.processEditing(textEditorView.textStorage)
        
        markTextEditorViewBecomeFirstResponser()
    }

    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)

        viewModel.isViewAppeared = true
    }
    
    override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
        super.traitCollectionDidChange(previousTraitCollection)
        
        configurePublishButtonApperance()
        viewModel.traitCollectionDidChangePublisher.send()
    }
    
    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
        
        updateAutoCompleteViewControllerLayout()
    }

    private func updateAutoCompleteViewControllerLayout() {
        // pin autoCompleteViewController frame to current view
        if let containerView = autoCompleteViewController.view.superview {
            let viewFrameInWindow = containerView.convert(autoCompleteViewController.view.frame, to: view)
            if viewFrameInWindow.origin.x != 0 {
                autoCompleteViewController.view.frame.origin.x = -viewFrameInWindow.origin.x
            }
            autoCompleteViewController.view.frame.size.width = view.frame.width
        }
    }
    
}

extension ComposeViewController {
    
    private var textEditorView: MetaText {
        return viewModel.composeStatusContentTableViewCell.metaText
    }
    
    private func markTextEditorViewBecomeFirstResponser() {
        textEditorView.textView.becomeFirstResponder()
    }
    
    private func contentWarningEditorTextView() -> UITextView? {
        viewModel.composeStatusContentTableViewCell.statusContentWarningEditorView.textView
    }
    
    private func pollOptionCollectionViewCell(of item: ComposeStatusPollItem) -> ComposeStatusPollOptionCollectionViewCell? {
        guard case .pollOption = item else { return nil }
        guard let dataSource = viewModel.composeStatusPollTableViewCell.dataSource else { return nil }
        guard let indexPath = dataSource.indexPath(for: item),
              let cell = viewModel.composeStatusPollTableViewCell.collectionView.cellForItem(at: indexPath) as? ComposeStatusPollOptionCollectionViewCell else {
            return nil
        }

        return cell
    }
    
    private func firstPollOptionCollectionViewCell() -> ComposeStatusPollOptionCollectionViewCell? {
        guard let dataSource = viewModel.composeStatusPollTableViewCell.dataSource else { return nil }
        let items = dataSource.snapshot().itemIdentifiers(inSection: .main)
        let firstPollItem = items.first { item -> Bool in
            guard case .pollOption = item else { return false }
            return true
        }

        guard let item = firstPollItem else {
            return nil
        }

        return pollOptionCollectionViewCell(of: item)
    }
    
    private func lastPollOptionCollectionViewCell() -> ComposeStatusPollOptionCollectionViewCell? {
        guard let dataSource = viewModel.composeStatusPollTableViewCell.dataSource else { return nil }
        let items = dataSource.snapshot().itemIdentifiers(inSection: .main)
        let lastPollItem = items.last { item -> Bool in
            guard case .pollOption = item else { return false }
            return true
        }

        guard let item = lastPollItem else {
            return nil
        }

        return pollOptionCollectionViewCell(of: item)
    }
    
    private func markFirstPollOptionCollectionViewCellBecomeFirstResponser() {
        guard let cell = firstPollOptionCollectionViewCell() else { return }
        cell.pollOptionView.optionTextField.becomeFirstResponder()
    }

    private func markLastPollOptionCollectionViewCellBecomeFirstResponser() {
        guard let cell = lastPollOptionCollectionViewCell() else { return }
        cell.pollOptionView.optionTextField.becomeFirstResponder()
    }
    
    private func showDismissConfirmAlertController() {
        let alertController = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
        let discardAction = UIAlertAction(title: L10n.Common.Controls.Actions.discard, style: .destructive) { [weak self] _ in
            guard let self = self else { return }
            self.dismiss(animated: true, completion: nil)
        }
        alertController.addAction(discardAction)
        let cancelAction = UIAlertAction(title: L10n.Common.Controls.Actions.cancel, style: .cancel)
        alertController.addAction(cancelAction)
        alertController.popoverPresentationController?.barButtonItem = cancelBarButtonItem
        present(alertController, animated: true, completion: nil)
    }
    
    private func resetImagePicker() {
        let selectionLimit = max(1, viewModel.maxMediaAttachments - viewModel.attachmentServices.count)
        let configuration = ComposeViewController.createPhotoLibraryPickerConfiguration(selectionLimit: selectionLimit)
        photoLibraryPicker = createImagePicker(configuration: configuration)
    }
    
    private func createImagePicker(configuration: PHPickerConfiguration) -> PHPickerViewController {
        let imagePicker = PHPickerViewController(configuration: configuration)
        imagePicker.delegate = self
        return imagePicker
    }

    private func setupBackgroundColor(theme: Theme) {
        let backgroundColor = UIColor(dynamicProvider: { traitCollection in
            switch traitCollection.userInterfaceStyle {
            case .light:
                return .systemBackground
            default:
                return theme.systemElevatedBackgroundColor
            }
        })
        view.backgroundColor = backgroundColor
        tableView.backgroundColor = backgroundColor
        composeToolbarBackgroundView.backgroundColor = theme.composeToolbarBackgroundColor
    }
    
    // keyboard shortcutBar
    private func setupInputAssistantItem(item: UITextInputAssistantItem) {
        let barButtonItems = [
            composeToolbarView.mediaBarButtonItem,
            composeToolbarView.pollBarButtonItem,
            composeToolbarView.contentWarningBarButtonItem,
            composeToolbarView.visibilityBarButtonItem,
        ]
        let group = UIBarButtonItemGroup(barButtonItems: barButtonItems, representativeItem: nil)
        
        item.trailingBarButtonGroups = [group]
    }
    
    private func configureToolbarDisplay(keyboardHasShortcutBar: Bool) {
        switch self.traitCollection.userInterfaceIdiom {
        case .pad:
            let shouldHideToolbar = keyboardHasShortcutBar && self.traitCollection.horizontalSizeClass == .regular
            self.composeToolbarView.alpha = shouldHideToolbar ? 0 : 1
            self.composeToolbarBackgroundView.alpha = shouldHideToolbar ? 0 : 1
        default:
            break
        }
    }
    
    private func configureNavigationBarTitleStyle() {
        switch traitCollection.userInterfaceIdiom {
        case .pad:
            navigationController?.navigationBar.prefersLargeTitles = traitCollection.horizontalSizeClass == .regular
        default:
            break
        }
    }

}

extension ComposeViewController {

    @objc private func cancelBarButtonItemPressed(_ sender: UIBarButtonItem) {
        os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
        guard viewModel.shouldDismiss else {
            showDismissConfirmAlertController()
            return
        }
        dismiss(animated: true, completion: nil)
    }
    
    @objc private func publishBarButtonItemPressed(_ sender: UIBarButtonItem) {
        os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
        do {
            try viewModel.checkAttachmentPrecondition()
        } catch {
            let alertController = UIAlertController(for: error, title: nil, preferredStyle: .alert)
            let okAction = UIAlertAction(title: L10n.Common.Controls.Actions.ok, style: .default, handler: nil)
            alertController.addAction(okAction)
            coordinator.present(scene: .alertController(alertController: alertController), from: nil, transition: .alertController(animated: true, completion: nil))
            return
        }
        
        guard viewModel.publishStateMachine.enter(ComposeViewModel.PublishState.Publishing.self) else {
            // TODO: handle error
            return
        }
        context.statusPublishService.publish(composeViewModel: viewModel)
        dismiss(animated: true, completion: nil)
    }
    
}

// MARK: - MetaTextDelegate
extension ComposeViewController: MetaTextDelegate {
    func metaText(_ metaText: MetaText, processEditing textStorage: MetaTextStorage) -> MetaContent? {
        let string = metaText.textStorage.string
        let content = MastodonContent(
            content: string,
            emojis: viewModel.customEmojiViewModel?.emojiMapping.value ?? [:]
        )
        let metaContent = MastodonMetaContent.convert(text: content)
        return metaContent
    }
}

// MARK: - UITextViewDelegate
extension ComposeViewController: UITextViewDelegate {
    
    func textViewShouldBeginEditing(_ textView: UITextView) -> Bool {
        setupInputAssistantItem(item: textView.inputAssistantItem)
        return true
    }

    func textViewDidChange(_ textView: UITextView) {
        switch textView {
        case textEditorView.textView:
            // update model
            let metaText = self.textEditorView
            let backedString = metaText.backedString
            viewModel.composeStatusAttribute.composeContent = backedString
            logger.debug("\((#file as NSString).lastPathComponent, privacy: .public)[\(#line, privacy: .public)], \(#function, privacy: .public): \(backedString)")
            
            // configure auto completion
            setupAutoComplete(for: textView)
        default:
            assertionFailure()
        }
    }

    struct AutoCompleteInfo {
        // model
        let inputText: Substring
        // range
        let symbolRange: Range<String.Index>
        let symbolString: Substring
        let toCursorRange: Range<String.Index>
        let toCursorString: Substring
        let toHighlightEndRange: Range<String.Index>
        let toHighlightEndString: Substring
        // geometry
        var textBoundingRect: CGRect = .zero
        var symbolBoundingRect: CGRect = .zero
    }

    private func setupAutoComplete(for textView: UITextView) {
        guard var autoCompletion = ComposeViewController.scanAutoCompleteInfo(textView: textView) else {
            viewModel.autoCompleteInfo = nil
            return
        }
        os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: auto complete %s (%s)", ((#file as NSString).lastPathComponent), #line, #function, String(autoCompletion.toHighlightEndString), String(autoCompletion.toCursorString))

        // get layout text bounding rect
        var glyphRange = NSRange()
        textView.layoutManager.characterRange(forGlyphRange: NSRange(autoCompletion.toCursorRange, in: textView.text), actualGlyphRange: &glyphRange)
        let textContainer = textView.layoutManager.textContainers[0]
        let textBoundingRect = textView.layoutManager.boundingRect(forGlyphRange: glyphRange, in: textContainer)

        let retryLayoutTimes = viewModel.autoCompleteRetryLayoutTimes
        guard textBoundingRect.size != .zero else {
            viewModel.autoCompleteRetryLayoutTimes += 1
            // avoid infinite loop
            guard retryLayoutTimes < 3 else { return }
            // needs retry calculate layout when the rect position changing
            DispatchQueue.main.async {
                self.setupAutoComplete(for: textView)
            }
            return
        }
        viewModel.autoCompleteRetryLayoutTimes = 0

        // get symbol bounding rect
        textView.layoutManager.characterRange(forGlyphRange: NSRange(autoCompletion.symbolRange, in: textView.text), actualGlyphRange: &glyphRange)
        let symbolBoundingRect = textView.layoutManager.boundingRect(forGlyphRange: glyphRange, in: textContainer)

        // set bounding rect and trigger layout
        autoCompletion.textBoundingRect = textBoundingRect
        autoCompletion.symbolBoundingRect = symbolBoundingRect
        viewModel.autoCompleteInfo = autoCompletion
    }

    private static func scanAutoCompleteInfo(textView: UITextView) -> AutoCompleteInfo? {
        guard let text = textView.text,
              textView.selectedRange.location > 0, !text.isEmpty,
              let selectedRange = Range(textView.selectedRange, in: text) else {
            return nil
        }
        let cursorIndex = selectedRange.upperBound
        let _highlightStartIndex: String.Index? = {
            var index = text.index(before: cursorIndex)
            while index > text.startIndex {
                let char = text[index]
                if char == "@" || char == "#" || char == ":" {
                    return index
                }
                index = text.index(before: index)
            }
            assert(index == text.startIndex)
            let char = text[index]
            if char == "@" || char == "#" || char == ":" {
                return index
            } else {
                return nil
            }
        }()

        guard let highlightStartIndex = _highlightStartIndex else { return nil }
        let scanRange = NSRange(highlightStartIndex..<text.endIndex, in: text)

        guard let match = text.firstMatch(pattern: MastodonRegex.autoCompletePattern, options: [], range: scanRange) else { return nil }
        guard let matchRange = Range(match.range(at: 0), in: text) else { return nil }
        let matchStartIndex = matchRange.lowerBound
        let matchEndIndex = matchRange.upperBound

        guard matchStartIndex == highlightStartIndex, matchEndIndex >= cursorIndex else { return nil }
        let symbolRange = highlightStartIndex..<text.index(after: highlightStartIndex)
        let symbolString = text[symbolRange]
        let toCursorRange = highlightStartIndex..<cursorIndex
        let toCursorString = text[toCursorRange]
        let toHighlightEndRange = matchStartIndex..<matchEndIndex
        let toHighlightEndString = text[toHighlightEndRange]

        let inputText = toHighlightEndString
        let autoCompleteInfo = AutoCompleteInfo(
            inputText: inputText,
            symbolRange: symbolRange,
            symbolString: symbolString,
            toCursorRange: toCursorRange,
            toCursorString: toCursorString,
            toHighlightEndRange: toHighlightEndRange,
            toHighlightEndString: toHighlightEndString
        )
        return autoCompleteInfo
    }

    func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool {
        switch textView {
        case textEditorView.textView:
            return false
        default:
            return true
        }
    }

    func textView(_ textView: UITextView, shouldInteractWith textAttachment: NSTextAttachment, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool {
        switch textView {
        case textEditorView.textView:
            return false
        default:
            return true
        }
    }

}

// MARK: - ComposeToolbarViewDelegate
extension ComposeViewController: ComposeToolbarViewDelegate {
    
    func composeToolbarView(_ composeToolbarView: ComposeToolbarView, mediaButtonDidPressed sender: Any, mediaSelectionType type: ComposeToolbarView.MediaSelectionType) {
        switch type {
        case .photoLibrary:
            present(photoLibraryPicker, animated: true, completion: nil)
        case .camera:
            present(imagePickerController, animated: true, completion: nil)
        case .browse:
            #if SNAPSHOT
            guard let image = UIImage(named: "Athens") else { return }

            let attachmentService = MastodonAttachmentService(
                context: context,
                image: image,
                initialAuthenticationBox: viewModel.authenticationBox
            )
            viewModel.attachmentServices = viewModel.attachmentServices + [attachmentService]
            #else
            present(documentPickerController, animated: true, completion: nil)
            #endif
        }
    }
    
    func composeToolbarView(_ composeToolbarView: ComposeToolbarView, pollButtonDidPressed sender: Any) {
        // toggle poll composing state
        viewModel.isPollComposing.toggle()

        // cancel custom picker input
        viewModel.isCustomEmojiComposing = false
        
        // setup initial poll option if needs
        if viewModel.isPollComposing, viewModel.pollOptionAttributes.isEmpty {
            viewModel.pollOptionAttributes = [ComposeStatusPollItem.PollOptionAttribute(), ComposeStatusPollItem.PollOptionAttribute()]
        }
        
        if viewModel.isPollComposing {
            // Magic RunLoop
            DispatchQueue.main.async {
                self.markFirstPollOptionCollectionViewCellBecomeFirstResponser()
            }
        } else {
            markTextEditorViewBecomeFirstResponser()
        }
    }
    
    func composeToolbarView(_ composeToolbarView: ComposeToolbarView, emojiButtonDidPressed sender: Any) {
        viewModel.isCustomEmojiComposing.toggle()
    }
    
    func composeToolbarView(_ composeToolbarView: ComposeToolbarView, contentWarningButtonDidPressed sender: Any) {
        // cancel custom picker input
        viewModel.isCustomEmojiComposing = false

        // restore first responder for text editor when content warning dismiss
        if viewModel.isContentWarningComposing {
            if contentWarningEditorTextView()?.isFirstResponder == true {
                markTextEditorViewBecomeFirstResponser()
            }
        }
        
        // toggle composing status
        viewModel.isContentWarningComposing.toggle()
        
        // active content warning after toggled
        if viewModel.isContentWarningComposing {
            contentWarningEditorTextView()?.becomeFirstResponder()
        }
    }
    
    func composeToolbarView(_ composeToolbarView: ComposeToolbarView, visibilityButtonDidPressed sender: Any, visibilitySelectionType type: ComposeToolbarView.VisibilitySelectionType) {
        viewModel.selectedStatusVisibility = type
    }
    
}

// MARK: - UIScrollViewDelegate
extension ComposeViewController {
    func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
        guard scrollView === tableView else { return }

        let repliedToCellFrame = viewModel.repliedToCellFrame
        guard repliedToCellFrame != .zero else { return }

         // try to find some patterns:
         // print("""
         // repliedToCellFrame: \(viewModel.repliedToCellFrame.value.height)
         // scrollView.contentOffset.y: \(scrollView.contentOffset.y)
         // scrollView.contentSize.height: \(scrollView.contentSize.height)
         // scrollView.frame: \(scrollView.frame)
         // scrollView.adjustedContentInset.top: \(scrollView.adjustedContentInset.top)
         // scrollView.adjustedContentInset.bottom: \(scrollView.adjustedContentInset.bottom)
         // """)

        switch viewModel.collectionViewState {
        case .fold:
            os_log("%{public}s[%{public}ld], %{public}s: fold", ((#file as NSString).lastPathComponent), #line, #function)
            guard velocity.y < 0 else { return }
            let offsetY = scrollView.contentOffset.y + scrollView.adjustedContentInset.top
            if offsetY < -44 {
                tableView.contentInset.top = 0
                targetContentOffset.pointee = CGPoint(x: 0, y: -scrollView.adjustedContentInset.top)
                viewModel.collectionViewState = .expand
            }

        case .expand:
            os_log("%{public}s[%{public}ld], %{public}s: expand", ((#file as NSString).lastPathComponent), #line, #function)
            guard velocity.y > 0 else { return }
            // check if top across
            let topOffset = (scrollView.contentOffset.y + scrollView.adjustedContentInset.top) - repliedToCellFrame.height

            // check if bottom bounce
            let bottomOffsetY = scrollView.contentOffset.y + (scrollView.frame.height - scrollView.adjustedContentInset.bottom)
            let bottomOffset = bottomOffsetY - scrollView.contentSize.height

            if topOffset > 44 {
                // do not interrupt user scrolling
                viewModel.collectionViewState = .fold
            } else if bottomOffset > 44 {
                tableView.contentInset.top = -repliedToCellFrame.height
                targetContentOffset.pointee = CGPoint(x: 0, y: -repliedToCellFrame.height)
                viewModel.collectionViewState = .fold
            }
        }
    }
}

// MARK: - UITableViewDelegate
extension ComposeViewController: UITableViewDelegate { }

// MARK: - UICollectionViewDelegate
extension ComposeViewController: UICollectionViewDelegate {
    
    func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
        os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: select %s", ((#file as NSString).lastPathComponent), #line, #function, indexPath.debugDescription)

        if collectionView === customEmojiPickerInputView.collectionView {
            guard let diffableDataSource = viewModel.customEmojiPickerDiffableDataSource else { return }
            let item = diffableDataSource.itemIdentifier(for: indexPath)
            guard case let .emoji(attribute) = item else { return }
            let emoji = attribute.emoji

            // make click sound
            UIDevice.current.playInputClick()

            // retrieve active text input and insert emoji
            // the trailing space is REQUIRED to make regex happy
            _ = viewModel.customEmojiPickerInputViewModel.insertText(":\(emoji.shortcode): ")
        } else {
            // do nothing
        }
    }
}

// MARK: - UIAdaptivePresentationControllerDelegate
extension ComposeViewController: UIAdaptivePresentationControllerDelegate {
    
    func adaptivePresentationStyle(for controller: UIPresentationController, traitCollection: UITraitCollection) -> UIModalPresentationStyle {
        switch traitCollection.horizontalSizeClass {
        case .compact:
            return .overFullScreen
        default:
            return .pageSheet
        }
    }

    func presentationControllerShouldDismiss(_ presentationController: UIPresentationController) -> Bool {
        return viewModel.shouldDismiss
    }
    
    func presentationControllerDidAttemptToDismiss(_ presentationController: UIPresentationController) {
        os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
        showDismissConfirmAlertController()

    }
    
    func presentationControllerDidDismiss(_ presentationController: UIPresentationController) {
        os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
    }
    
}

// MARK: - PHPickerViewControllerDelegate
extension ComposeViewController: PHPickerViewControllerDelegate {
    func picker(_ picker: PHPickerViewController, didFinishPicking results: [PHPickerResult]) {
        picker.dismiss(animated: true, completion: nil)
        
        let attachmentServices: [MastodonAttachmentService] = results.map { result in
            let service = MastodonAttachmentService(
                context: context,
                pickerResult: result,
                initialAuthenticationBox: viewModel.authenticationBox
            )
            return service
        }
        viewModel.attachmentServices = viewModel.attachmentServices + attachmentServices
    }
}

// MARK: - UIImagePickerControllerDelegate
extension ComposeViewController: UIImagePickerControllerDelegate & UINavigationControllerDelegate {
    
    func imagePickerController(_ picker: UIImagePickerController, didFinishPickingMediaWithInfo info: [UIImagePickerController.InfoKey : Any]) {
        picker.dismiss(animated: true, completion: nil)

        guard let image = info[.originalImage] as? UIImage else { return }
        
        let attachmentService = MastodonAttachmentService(
            context: context,
            image: image,
            initialAuthenticationBox: viewModel.authenticationBox
        )
        viewModel.attachmentServices = viewModel.attachmentServices + [attachmentService]
    }
    
    func imagePickerControllerDidCancel(_ picker: UIImagePickerController) {
        os_log("%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
        picker.dismiss(animated: true, completion: nil)
    }
}

// MARK: - UIDocumentPickerDelegate
extension ComposeViewController: UIDocumentPickerDelegate {
    func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
        guard let url = urls.first else { return }

        let attachmentService = MastodonAttachmentService(
            context: context,
            documentURL: url,
            initialAuthenticationBox: viewModel.authenticationBox
        )
        viewModel.attachmentServices = viewModel.attachmentServices + [attachmentService]
    }
}

// MARK: - ComposeStatusAttachmentTableViewCellDelegate
extension ComposeViewController: ComposeStatusAttachmentCollectionViewCellDelegate {
    
    func composeStatusAttachmentCollectionViewCell(_ cell: ComposeStatusAttachmentCollectionViewCell, removeButtonDidPressed button: UIButton) {
        guard let diffableDataSource = viewModel.composeStatusAttachmentTableViewCell.dataSource else { return }
        guard let indexPath = viewModel.composeStatusAttachmentTableViewCell.collectionView.indexPath(for: cell) else { return }
        guard let item = diffableDataSource.itemIdentifier(for: indexPath) else { return }
        guard case let .attachment(attachmentService) = item else { return }

        var attachmentServices = viewModel.attachmentServices
        guard let index = attachmentServices.firstIndex(of: attachmentService) else { return }
        let removedItem = attachmentServices[index]
        attachmentServices.remove(at: index)
        viewModel.attachmentServices = attachmentServices

        // cancel task
        removedItem.disposeBag.removeAll()
    }
    
}

// MARK: - ComposeStatusPollOptionCollectionViewCellDelegate
extension ComposeViewController: ComposeStatusPollOptionCollectionViewCellDelegate {
    
    func composeStatusPollOptionCollectionViewCell(_ cell: ComposeStatusPollOptionCollectionViewCell, textFieldDidBeginEditing textField: UITextField) {
        
        setupInputAssistantItem(item: textField.inputAssistantItem)
        
        // FIXME: make poll section visible
        // DispatchQueue.main.async {
        //     self.collectionView.scroll(to: .bottom, animated: true)
        // }
    }

    
    // handle delete backward event for poll option input
    func composeStatusPollOptionCollectionViewCell(_ cell: ComposeStatusPollOptionCollectionViewCell, textBeforeDeleteBackward text: String?) {
        guard (text ?? "").isEmpty else { return }
        guard let dataSource = viewModel.composeStatusPollTableViewCell.dataSource else { return }
        guard let indexPath = viewModel.composeStatusPollTableViewCell.collectionView.indexPath(for: cell) else { return }
        guard let item = dataSource.itemIdentifier(for: indexPath) else { return }
        guard case let .pollOption(attribute) = item else { return }

        var pollAttributes = viewModel.pollOptionAttributes
        guard let index = pollAttributes.firstIndex(of: attribute) else { return }

        // mark previous (fallback to next) item of removed middle poll option become first responder
        let pollItems = dataSource.snapshot().itemIdentifiers(inSection: .main)
        if let indexOfItem = pollItems.firstIndex(of: item), index > 0 {
            func cellBeforeRemoved() -> ComposeStatusPollOptionCollectionViewCell? {
                guard index > 0 else { return nil }
                let indexBeforeRemoved = pollItems.index(before: indexOfItem)
                let itemBeforeRemoved = pollItems[indexBeforeRemoved]
                return pollOptionCollectionViewCell(of: itemBeforeRemoved)
            }

            func cellAfterRemoved() -> ComposeStatusPollOptionCollectionViewCell? {
                guard index < pollItems.count - 1 else { return nil }
                let indexAfterRemoved = pollItems.index(after: index)
                let itemAfterRemoved = pollItems[indexAfterRemoved]
                return pollOptionCollectionViewCell(of: itemAfterRemoved)
            }

            var cell: ComposeStatusPollOptionCollectionViewCell? = cellBeforeRemoved()
            if cell == nil {
                cell = cellAfterRemoved()
            }
            cell?.pollOptionView.optionTextField.becomeFirstResponder()
        }

        guard pollAttributes.count > 2 else {
            return
        }
        pollAttributes.remove(at: index)

        // update data source
        viewModel.pollOptionAttributes = pollAttributes
    }
    
    // handle keyboard return event for poll option input
    func composeStatusPollOptionCollectionViewCell(_ cell: ComposeStatusPollOptionCollectionViewCell, pollOptionTextFieldDidReturn: UITextField) {
        guard let dataSource = viewModel.composeStatusPollTableViewCell.dataSource else { return }
        guard let indexPath = viewModel.composeStatusPollTableViewCell.collectionView.indexPath(for: cell) else { return }
        let pollItems = dataSource.snapshot().itemIdentifiers(inSection: .main).filter { item in
            guard case .pollOption = item else { return false }
            return true
        }
        guard let item = dataSource.itemIdentifier(for: indexPath) else { return }
        guard let index = pollItems.firstIndex(of: item) else { return }

        if index == pollItems.count - 1 {
            // is the last
            viewModel.createNewPollOptionIfPossible()
            DispatchQueue.main.async {
                self.markLastPollOptionCollectionViewCellBecomeFirstResponser()
            }
        } else {
            // not the last
            let indexAfter = pollItems.index(after: index)
            let itemAfter = pollItems[indexAfter]
            let cell = pollOptionCollectionViewCell(of: itemAfter)
            cell?.pollOptionView.optionTextField.becomeFirstResponder()
        }
    }
    
}

// MARK: - ComposeStatusPollOptionAppendEntryCollectionViewCellDelegate
extension ComposeViewController: ComposeStatusPollOptionAppendEntryCollectionViewCellDelegate {
    func composeStatusPollOptionAppendEntryCollectionViewCellDidPressed(_ cell: ComposeStatusPollOptionAppendEntryCollectionViewCell) {
        viewModel.createNewPollOptionIfPossible()
        DispatchQueue.main.async {
            self.markLastPollOptionCollectionViewCellBecomeFirstResponser()
        }
    }
}

// MARK: - ComposeStatusPollExpiresOptionCollectionViewCellDelegate
extension ComposeViewController: ComposeStatusPollExpiresOptionCollectionViewCellDelegate {
    func composeStatusPollExpiresOptionCollectionViewCell(_ cell: ComposeStatusPollExpiresOptionCollectionViewCell, didSelectExpiresOption expiresOption: ComposeStatusPollItem.PollExpiresOptionAttribute.ExpiresOption) {
        viewModel.pollExpiresOptionAttribute.expiresOption.value = expiresOption
    }
}

// MARK: - ComposeStatusContentTableViewCellDelegate
extension ComposeViewController: ComposeStatusContentTableViewCellDelegate {
    func composeStatusContentTableViewCell(_ cell: ComposeStatusContentTableViewCell, textViewShouldBeginEditing textView: UITextView) -> Bool {
        setupInputAssistantItem(item: textView.inputAssistantItem)
        return true
    }
}

// MARK: - AutoCompleteViewControllerDelegate
extension ComposeViewController: AutoCompleteViewControllerDelegate {
    func autoCompleteViewController(_ viewController: AutoCompleteViewController, didSelectItem item: AutoCompleteItem) {
        guard let info = viewModel.autoCompleteInfo else { return }
        let _replacedText: String? = {
            var text: String
            switch item {
            case .hashtag(let hashtag):
                text = "#" + hashtag.name
            case .hashtagV1(let hashtagName):
                text = "#" + hashtagName
            case .account(let account):
                text = "@" + account.acct
            case .emoji(let emoji):
                text = ":" + emoji.shortcode + ":"
            case .bottomLoader:
                return nil
            }
            return text
        }()
        guard let replacedText = _replacedText else { return }
        guard let text = textEditorView.textView.text else { return }

        let range = NSRange(info.toHighlightEndRange, in: text)
        textEditorView.textStorage.replaceCharacters(in: range, with: replacedText)
        DispatchQueue.main.async {
            self.textEditorView.textView.insertText(" ") // trigger textView delegate update
        }
        viewModel.autoCompleteInfo = nil

        switch item {
        case .emoji, .bottomLoader:
            break
        default:
            // set selected range except emoji
            let newRange = NSRange(location: range.location + (replacedText as NSString).length, length: 0)
            guard textEditorView.textStorage.length <= newRange.location else { return }
            textEditorView.textView.selectedRange = newRange
        }
    }
}

extension ComposeViewController {
    override var keyCommands: [UIKeyCommand]? {
        composeKeyCommands
    }
}

extension ComposeViewController {
    
    enum ComposeKeyCommand: String, CaseIterable {
        case discardPost
        case publishPost
        case mediaBrowse
        case mediaPhotoLibrary
        case mediaCamera
        case togglePoll
        case toggleContentWarning
        case selectVisibilityPublic
        // TODO: remove selectVisibilityUnlisted from codebase
        // case selectVisibilityUnlisted
        case selectVisibilityPrivate
        case selectVisibilityDirect

        var title: String {
            switch self {
            case .discardPost:              return L10n.Scene.Compose.Keyboard.discardPost
            case .publishPost:              return L10n.Scene.Compose.Keyboard.publishPost
            case .mediaBrowse:              return L10n.Scene.Compose.Keyboard.appendAttachmentEntry(L10n.Scene.Compose.MediaSelection.browse)
            case .mediaPhotoLibrary:        return L10n.Scene.Compose.Keyboard.appendAttachmentEntry(L10n.Scene.Compose.MediaSelection.photoLibrary)
            case .mediaCamera:              return L10n.Scene.Compose.Keyboard.appendAttachmentEntry(L10n.Scene.Compose.MediaSelection.camera)
            case .togglePoll:               return L10n.Scene.Compose.Keyboard.togglePoll
            case .toggleContentWarning:     return L10n.Scene.Compose.Keyboard.toggleContentWarning
            case .selectVisibilityPublic:   return L10n.Scene.Compose.Keyboard.selectVisibilityEntry(L10n.Scene.Compose.Visibility.public)
            // case .selectVisibilityUnlisted: return L10n.Scene.Compose.Keyboard.selectVisibilityEntry(L10n.Scene.Compose.Visibility.unlisted)
            case .selectVisibilityPrivate:  return L10n.Scene.Compose.Keyboard.selectVisibilityEntry(L10n.Scene.Compose.Visibility.private)
            case .selectVisibilityDirect:   return L10n.Scene.Compose.Keyboard.selectVisibilityEntry(L10n.Scene.Compose.Visibility.direct)
            }
        }
        
        // UIKeyCommand input
        var input: String {
            switch self {
            case .discardPost:              return "w"      // + command
            case .publishPost:              return "\r"     // (enter) + command
            case .mediaBrowse:              return "b"      // + option + command
            case .mediaPhotoLibrary:        return "p"      // + option + command
            case .mediaCamera:              return "c"      // + option + command
            case .togglePoll:               return "p"      // + shift + command
            case .toggleContentWarning:     return "c"      // + shift + command
            case .selectVisibilityPublic:   return "1"      // + command
            // case .selectVisibilityUnlisted: return "2"      // + command
            case .selectVisibilityPrivate:  return "2"      // + command
            case .selectVisibilityDirect:   return "3"      // + command
            }
        }
        
        var modifierFlags: UIKeyModifierFlags {
            switch self {
            case .discardPost:              return [.command]
            case .publishPost:              return [.command]
            case .mediaBrowse:              return [.alternate, .command]
            case .mediaPhotoLibrary:        return [.alternate, .command]
            case .mediaCamera:              return [.alternate, .command]
            case .togglePoll:               return [.shift, .command]
            case .toggleContentWarning:     return [.shift, .command]
            case .selectVisibilityPublic:   return [.command]
            // case .selectVisibilityUnlisted: return [.command]
            case .selectVisibilityPrivate:  return [.command]
            case .selectVisibilityDirect:   return [.command]
            }
        }
        
        var propertyList: Any {
            return rawValue
        }
    }
    
    var composeKeyCommands: [UIKeyCommand]? {
        ComposeKeyCommand.allCases.map { command in
            UIKeyCommand(
                title: command.title,
                image: nil,
                action: #selector(Self.composeKeyCommandHandler(_:)),
                input: command.input,
                modifierFlags: command.modifierFlags,
                propertyList: command.propertyList,
                alternates: [],
                discoverabilityTitle: nil,
                attributes: [],
                state: .off
            )
        }
    }
    
    @objc private func composeKeyCommandHandler(_ sender: UIKeyCommand) {
        guard let rawValue = sender.propertyList as? String,
              let command = ComposeKeyCommand(rawValue: rawValue) else { return }
        
        switch command {
        case .discardPost:
            cancelBarButtonItemPressed(cancelBarButtonItem)
        case .publishPost:
            publishBarButtonItemPressed(publishBarButtonItem)
        case .mediaBrowse:
            present(documentPickerController, animated: true, completion: nil)
        case .mediaPhotoLibrary:
            present(photoLibraryPicker, animated: true, completion: nil)
        case .mediaCamera:
            guard UIImagePickerController.isSourceTypeAvailable(.camera) else {
                return
            }
            present(imagePickerController, animated: true, completion: nil)
        case .togglePoll:
            composeToolbarView.pollButton.sendActions(for: .touchUpInside)
        case .toggleContentWarning:
            composeToolbarView.contentWarningButton.sendActions(for: .touchUpInside)
        case .selectVisibilityPublic:
            viewModel.selectedStatusVisibility = .public
        // case .selectVisibilityUnlisted:
        //     viewModel.selectedStatusVisibility.value = .unlisted
        case .selectVisibilityPrivate:
            viewModel.selectedStatusVisibility = .private
        case .selectVisibilityDirect:
            viewModel.selectedStatusVisibility = .direct
        }
    }
    
}