//
//  ComposeViewModel+Diffable.swift
//  Mastodon
//
//  Created by MainasuK Cirno on 2021-3-11.
//

import os.log
import UIKit
import Combine
import CoreDataStack
import MastodonSDK
import MastodonMeta
import MetaTextKit
import MastodonAsset
import MastodonLocalization

extension ComposeViewModel {

    func setupDataSource(
        tableView: UITableView,
        metaTextDelegate: MetaTextDelegate,
        metaTextViewDelegate: UITextViewDelegate,
        customEmojiPickerInputViewModel: CustomEmojiPickerInputViewModel,
        composeStatusAttachmentCollectionViewCellDelegate: ComposeStatusAttachmentCollectionViewCellDelegate,
        composeStatusPollOptionCollectionViewCellDelegate: ComposeStatusPollOptionCollectionViewCellDelegate,
        composeStatusPollOptionAppendEntryCollectionViewCellDelegate: ComposeStatusPollOptionAppendEntryCollectionViewCellDelegate,
        composeStatusPollExpiresOptionCollectionViewCellDelegate: ComposeStatusPollExpiresOptionCollectionViewCellDelegate
    ) {
        // UI
        bind()
        
        // content
        bind(cell: composeStatusContentTableViewCell, tableView: tableView)
        composeStatusContentTableViewCell.metaText.delegate = metaTextDelegate
        composeStatusContentTableViewCell.metaText.textView.delegate = metaTextViewDelegate
        
        // attachment
        bind(cell: composeStatusAttachmentTableViewCell, tableView: tableView)
        composeStatusAttachmentTableViewCell.composeStatusAttachmentCollectionViewCellDelegate = composeStatusAttachmentCollectionViewCellDelegate
        
        // poll
        bind(cell: composeStatusPollTableViewCell, tableView: tableView)
        composeStatusPollTableViewCell.delegate = self
        composeStatusPollTableViewCell.customEmojiPickerInputViewModel = customEmojiPickerInputViewModel
        composeStatusPollTableViewCell.composeStatusPollOptionCollectionViewCellDelegate = composeStatusPollOptionCollectionViewCellDelegate
        composeStatusPollTableViewCell.composeStatusPollOptionAppendEntryCollectionViewCellDelegate = composeStatusPollOptionAppendEntryCollectionViewCellDelegate
        composeStatusPollTableViewCell.composeStatusPollExpiresOptionCollectionViewCellDelegate = composeStatusPollExpiresOptionCollectionViewCellDelegate

        // setup data source
        tableView.dataSource = self 
    }
    
    func setupCustomEmojiPickerDiffableDataSource(
        for collectionView: UICollectionView,
        dependency: NeedsDependency
    ) {
        let diffableDataSource = CustomEmojiPickerSection.collectionViewDiffableDataSource(
            for: collectionView,
            dependency: dependency
        )
        self.customEmojiPickerDiffableDataSource = diffableDataSource
        
        let _domain = customEmojiViewModel?.domain
        customEmojiViewModel?.emojis
            .receive(on: DispatchQueue.main)
            .sink { [weak self, weak diffableDataSource] emojis in
                guard let _ = self else { return }
                guard let diffableDataSource = diffableDataSource else { return }
                
                var snapshot = NSDiffableDataSourceSnapshot<CustomEmojiPickerSection, CustomEmojiPickerItem>()
                let domain = _domain?.uppercased() ?? " "
                let customEmojiSection = CustomEmojiPickerSection.emoji(name: domain)
                snapshot.appendSections([customEmojiSection])
                let items: [CustomEmojiPickerItem] = {
                    var items = [CustomEmojiPickerItem]()
                    for emoji in emojis where emoji.visibleInPicker {
                        let attribute = CustomEmojiPickerItem.CustomEmojiAttribute(emoji: emoji)
                        let item = CustomEmojiPickerItem.emoji(attribute: attribute)
                        items.append(item)
                    }
                    return items
                }()
                snapshot.appendItems(items, toSection: customEmojiSection)
                
                diffableDataSource.apply(snapshot)
            }
            .store(in: &disposeBag)
    }
    
}

// MARK: - UITableViewDataSource
extension ComposeViewModel: UITableViewDataSource {

    enum Section: CaseIterable {
        case repliedTo
        case status
        case attachment
        case poll
    }

    func numberOfSections(in tableView: UITableView) -> Int {
        return Section.allCases.count
    }

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        switch Section.allCases[section] {
        case .repliedTo:
            switch composeKind {
            case .reply:        return 1
            default:            return 0
            }
        case .status:           return 1
        case .attachment:       return 1
        case .poll:             return 1
        }
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        switch Section.allCases[indexPath.section] {
        case .repliedTo:
            let cell = tableView.dequeueReusableCell(withIdentifier: String(describing: ComposeRepliedToStatusContentTableViewCell.self), for: indexPath) as! ComposeRepliedToStatusContentTableViewCell
            guard case let .reply(record) = composeKind else { return cell }

            // bind frame publisher
            cell.framePublisher
                .receive(on: DispatchQueue.main)
                .assign(to: \.repliedToCellFrame, on: self)
                .store(in: &cell.disposeBag)
            
            // set initial width
            if cell.statusView.frame.width == .zero {
                cell.statusView.frame.size.width = tableView.frame.width
            }

            // configure status
            context.managedObjectContext.performAndWait {
                guard let replyTo = record.object(in: context.managedObjectContext) else { return }
                cell.statusView.configure(status: replyTo)
            }
            
            return cell
        case .status:
            return composeStatusContentTableViewCell
        case .attachment:
            return composeStatusAttachmentTableViewCell
        case .poll:
            return composeStatusPollTableViewCell
        }
    }
}

// MARK: - ComposeStatusPollTableViewCellDelegate
extension ComposeViewModel: ComposeStatusPollTableViewCellDelegate {
    func composeStatusPollTableViewCell(_ cell: ComposeStatusPollTableViewCell, pollOptionAttributesDidReorder options: [ComposeStatusPollItem.PollOptionAttribute]) {
        os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)

        self.pollOptionAttributes = options
    }
}

extension ComposeViewModel {
    private func bind() {
        $isCustomEmojiComposing
            .assign(to: \.value, on: customEmojiPickerInputViewModel.isCustomEmojiComposing)
            .store(in: &disposeBag)
            
        $isContentWarningComposing
            .assign(to: \.isContentWarningComposing, on: composeStatusAttribute)
            .store(in: &disposeBag)
        
        // bind compose toolbar UI state
        Publishers.CombineLatest(
            $isPollComposing,
            $attachmentServices
        )
        .receive(on: DispatchQueue.main)
        .sink(receiveValue: { [weak self] isPollComposing, attachmentServices in
            guard let self = self else { return }
            let shouldMediaDisable = isPollComposing || attachmentServices.count >= self.maxMediaAttachments
            let shouldPollDisable = attachmentServices.count > 0

            self.isMediaToolbarButtonEnabled = !shouldMediaDisable
            self.isPollToolbarButtonEnabled = !shouldPollDisable
        })
        .store(in: &disposeBag)
        
        // calculate `Idempotency-Key`
        let content = Publishers.CombineLatest3(
            composeStatusAttribute.$isContentWarningComposing,
            composeStatusAttribute.$contentWarningContent,
            composeStatusAttribute.$composeContent
        )
        .map { isContentWarningComposing, contentWarningContent, composeContent -> String in
            if isContentWarningComposing {
                return contentWarningContent + (composeContent ?? "")
            } else {
                return composeContent ?? ""
            }
        }
        let attachmentIDs = $attachmentServices.map { attachments -> String in
            let attachmentIDs = attachments.compactMap { $0.attachment.value?.id }
            return attachmentIDs.joined(separator: ",")
        }
        let pollOptionsAndDuration = Publishers.CombineLatest3(
            $isPollComposing,
            $pollOptionAttributes,
            pollExpiresOptionAttribute.expiresOption
        )
        .map { isPollComposing, pollOptionAttributes, expiresOption -> String in
            guard isPollComposing else {
                return ""
            }
            
            let pollOptions = pollOptionAttributes.map { $0.option.value }.joined(separator: ",")
            return pollOptions + expiresOption.rawValue
        }
        
        Publishers.CombineLatest4(
            content,
            attachmentIDs,
            pollOptionsAndDuration,
            $selectedStatusVisibility
        )
            .map { content, attachmentIDs, pollOptionsAndDuration, selectedStatusVisibility -> String in
                var hasher = Hasher()
                hasher.combine(content)
                hasher.combine(attachmentIDs)
                hasher.combine(pollOptionsAndDuration)
                hasher.combine(selectedStatusVisibility.visibility.rawValue)
                let hashValue = hasher.finalize()
                return "\(hashValue)"
            }
            .assign(to: \.value, on: idempotencyKey)
            .store(in: &disposeBag)
        
        // bind modal dismiss state
        composeStatusAttribute.$composeContent
            .receive(on: DispatchQueue.main)
            .map { [weak self] content in
                let content = content ?? ""
                if content.isEmpty {
                    return true
                }
                // if preInsertedContent plus a space is equal to the content, simply dismiss the modal
                if let preInsertedContent = self?.preInsertedContent {
                    return content == preInsertedContent
                }
                return false
            }
            .assign(to: &$shouldDismiss)
        
        // bind compose bar button item UI state
        let isComposeContentEmpty = composeStatusAttribute.$composeContent
            .map { ($0 ?? "").isEmpty }
        let isComposeContentValid = $characterCount
            .compactMap { [weak self] characterCount -> Bool in
                guard let self = self else { return characterCount <= 500 }
                return characterCount <= self.composeContentLimit
            }
        let isMediaEmpty = $attachmentServices
            .map { $0.isEmpty }
        let isMediaUploadAllSuccess = $attachmentServices
            .map { services in
                services.allSatisfy { $0.uploadStateMachineSubject.value is MastodonAttachmentService.UploadState.Finish }
            }
        let isPollAttributeAllValid = $pollOptionAttributes
            .map { pollAttributes in
                pollAttributes.allSatisfy { attribute -> Bool in
                    !attribute.option.value.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
                }
            }
        
        let isPublishBarButtonItemEnabledPrecondition1 = Publishers.CombineLatest4(
            isComposeContentEmpty,
            isComposeContentValid,
            isMediaEmpty,
            isMediaUploadAllSuccess
        )
        .map { isComposeContentEmpty, isComposeContentValid, isMediaEmpty, isMediaUploadAllSuccess -> Bool in
            if isMediaEmpty {
                return isComposeContentValid && !isComposeContentEmpty
            } else {
                return isComposeContentValid && isMediaUploadAllSuccess
            }
        }
        .eraseToAnyPublisher()
        
        let isPublishBarButtonItemEnabledPrecondition2 = Publishers.CombineLatest4(
            isComposeContentEmpty,
            isComposeContentValid,
            $isPollComposing,
            isPollAttributeAllValid
        )
        .map { isComposeContentEmpty, isComposeContentValid, isPollComposing, isPollAttributeAllValid -> Bool in
            if isPollComposing {
                return isComposeContentValid && !isComposeContentEmpty && isPollAttributeAllValid
            } else {
                return isComposeContentValid && !isComposeContentEmpty
            }
        }
        .eraseToAnyPublisher()
        
        Publishers.CombineLatest(
            isPublishBarButtonItemEnabledPrecondition1,
            isPublishBarButtonItemEnabledPrecondition2
        )
        .map { $0 && $1 }
        .assign(to: &$isPublishBarButtonItemEnabled)
    }
}

extension ComposeViewModel {
    private func bind(
        cell: ComposeStatusContentTableViewCell,
        tableView: UITableView
    ) {
        // bind status content character count
        Publishers.CombineLatest3(
            composeStatusAttribute.$composeContent,
            composeStatusAttribute.$isContentWarningComposing,
            composeStatusAttribute.$contentWarningContent
        )
        .map { composeContent, isContentWarningComposing, contentWarningContent -> Int in
            let composeContent = composeContent ?? ""
            var count = composeContent.count
            if isContentWarningComposing {
                count += contentWarningContent.count
            }
            return count
        }
        .assign(to: &$characterCount)
        
        // bind content warning
        composeStatusAttribute.$isContentWarningComposing
            .receive(on: DispatchQueue.main)
            .sink { [weak cell, weak tableView] isContentWarningComposing in
                guard let cell = cell else { return }
                guard let tableView = tableView else { return }
                
                // self size input cell
                cell.statusContentWarningEditorView.isHidden = !isContentWarningComposing
                cell.statusContentWarningEditorView.alpha = 0
                UIView.animate(withDuration: 0.33, delay: 0, options: [.curveEaseOut]) {
                    cell.statusContentWarningEditorView.alpha = 1
                    tableView.beginUpdates()
                    tableView.endUpdates()
                } completion: { _ in
                    // do nothing
                }
            }
            .store(in: &disposeBag)
        
        cell.contentWarningContent
            .removeDuplicates()
            .receive(on: DispatchQueue.main)
            .sink { [weak tableView, weak self] text in
                guard let self = self else { return }
                // bind input data
                self.composeStatusAttribute.contentWarningContent = text

                // self size input cell
                guard let tableView = tableView else { return }
                UIView.performWithoutAnimation {
                    tableView.beginUpdates()
                    tableView.endUpdates()
                }
            }
            .store(in: &cell.disposeBag)
        
        // configure custom emoji picker
        ComposeStatusSection.configureCustomEmojiPicker(
            viewModel: customEmojiPickerInputViewModel,
            customEmojiReplaceableTextInput: cell.metaText.textView,
            disposeBag: &disposeBag
        )
        ComposeStatusSection.configureCustomEmojiPicker(
            viewModel: customEmojiPickerInputViewModel,
            customEmojiReplaceableTextInput: cell.statusContentWarningEditorView.textView,
            disposeBag: &disposeBag
        )
    }
}

extension ComposeViewModel {
    private func bind(
        cell: ComposeStatusPollTableViewCell,
        tableView: UITableView
    ) {
        Publishers.CombineLatest(
            $isPollComposing,
            $pollOptionAttributes
        )
        .receive(on: DispatchQueue.main)
        .sink { [weak self] isPollComposing, pollOptionAttributes in
            guard let self = self else { return }
            guard self.isViewAppeared else { return }

            let cell = self.composeStatusPollTableViewCell
            guard let dataSource = cell.dataSource else { return }

            var snapshot = NSDiffableDataSourceSnapshot<ComposeStatusPollSection, ComposeStatusPollItem>()
            snapshot.appendSections([.main])
            var items: [ComposeStatusPollItem] = []
            if isPollComposing {
                for attribute in pollOptionAttributes {
                    items.append(.pollOption(attribute: attribute))
                }
                if pollOptionAttributes.count < self.maxPollOptions {
                    items.append(.pollOptionAppendEntry)
                }
                items.append(.pollExpiresOption(attribute: self.pollExpiresOptionAttribute))
            }
            snapshot.appendItems(items, toSection: .main)

            tableView.performBatchUpdates {
                if #available(iOS 15.0, *) {
                    dataSource.apply(snapshot, animatingDifferences: false)
                } else {
                    dataSource.apply(snapshot, animatingDifferences: true)
                }
            }
        }
        .store(in: &disposeBag)
        
        // bind delegate
        $pollOptionAttributes
            .sink { [weak self] pollAttributes in
                guard let self = self else { return }
                pollAttributes.forEach { $0.delegate = self }
            }
            .store(in: &disposeBag)
    }
}

extension ComposeViewModel {
    private func bind(
        cell: ComposeStatusAttachmentTableViewCell,
        tableView: UITableView
    ) {
        cell.collectionViewHeightDidUpdate
            .receive(on: DispatchQueue.main)
            .sink { [weak self] _ in
                guard let _ = self else { return }
                tableView.beginUpdates()
                tableView.endUpdates()
            }
            .store(in: &disposeBag)

        $attachmentServices
            .removeDuplicates()
            .receive(on: DispatchQueue.main)
            .sink { [weak self] attachmentServices in
                guard let self = self else { return }
                guard self.isViewAppeared else { return }

                let cell = self.composeStatusAttachmentTableViewCell
                guard let dataSource = cell.dataSource else { return }

                var snapshot = NSDiffableDataSourceSnapshot<ComposeStatusAttachmentSection, ComposeStatusAttachmentItem>()
                snapshot.appendSections([.main])
                let items = attachmentServices.map { ComposeStatusAttachmentItem.attachment(attachmentService: $0) }
                snapshot.appendItems(items, toSection: .main)

                if #available(iOS 15.0, *) {
                    dataSource.applySnapshotUsingReloadData(snapshot)
                } else {
                    dataSource.apply(snapshot, animatingDifferences: false)
                }
            }
            .store(in: &disposeBag)
        
        // setup attribute updater
        $attachmentServices
            .receive(on: DispatchQueue.main)
            .debounce(for: 0.3, scheduler: DispatchQueue.main)
            .sink { attachmentServices in
                // drive service upload state
                // make image upload in the queue
                for attachmentService in attachmentServices {
                    // skip when prefix N task when task finish OR fail OR uploading
                    guard let currentState = attachmentService.uploadStateMachine.currentState else { break }
                    if currentState is MastodonAttachmentService.UploadState.Fail {
                        continue
                    }
                    if currentState is MastodonAttachmentService.UploadState.Finish {
                        continue
                    }
                    if currentState is MastodonAttachmentService.UploadState.Processing {
                        continue
                    }
                    if currentState is MastodonAttachmentService.UploadState.Uploading {
                        break
                    }
                    // trigger uploading one by one
                    if currentState is MastodonAttachmentService.UploadState.Initial {
                        attachmentService.uploadStateMachine.enter(MastodonAttachmentService.UploadState.Uploading.self)
                        break
                    }
                }
            }
            .store(in: &disposeBag)
        
        // bind delegate
        $attachmentServices
            .sink { [weak self] attachmentServices in
                guard let self = self else { return }
                attachmentServices.forEach { $0.delegate = self }
            }
            .store(in: &disposeBag)
    }
}
