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

import os.log
import UIKit
import Combine
import CoreData
import CoreDataStack
import GameplayKit
import MastodonSDK
import MastodonAsset
import MastodonLocalization
import MastodonMeta
import MastodonUI

final class ComposeViewModel: NSObject {
    
    let logger = Logger(subsystem: "ComposeViewModel", category: "ViewModel")
    
    var disposeBag = Set<AnyCancellable>()

    let id = UUID()
    
    // input
    let context: AppContext
    let composeKind: ComposeStatusSection.ComposeKind
    let authenticationBox: MastodonAuthenticationBox

    
    @Published var isPollComposing = false
    @Published var isCustomEmojiComposing = false
    @Published var isContentWarningComposing = false
    
    @Published var selectedStatusVisibility: ComposeToolbarView.VisibilitySelectionType
    @Published var repliedToCellFrame: CGRect = .zero
    @Published var autoCompleteRetryLayoutTimes = 0
    @Published var autoCompleteInfo: ComposeViewController.AutoCompleteInfo? = nil

    let traitCollectionDidChangePublisher = CurrentValueSubject<Void, Never>(Void())      // use CurrentValueSubject to make initial event emit
    var isViewAppeared = false
    
    // output
    let instanceConfiguration: Mastodon.Entity.Instance.Configuration?
    var composeContentLimit: Int {
        guard let maxCharacters = instanceConfiguration?.statuses?.maxCharacters else { return 500 }
        return max(1, maxCharacters)
    }
    var maxMediaAttachments: Int {
        guard let maxMediaAttachments = instanceConfiguration?.statuses?.maxMediaAttachments else {
            return 4
        }
        // FIXME: update timeline media preview UI
        return min(4, max(1, maxMediaAttachments))
        // return max(1, maxMediaAttachments)
    }
    var maxPollOptions: Int {
        guard let maxOptions = instanceConfiguration?.polls?.maxOptions else { return 4 }
        return max(2, maxOptions)
    }
    
    let composeStatusAttribute = ComposeStatusItem.ComposeStatusAttribute()
    let composeStatusContentTableViewCell = ComposeStatusContentTableViewCell()
    let composeStatusAttachmentTableViewCell = ComposeStatusAttachmentTableViewCell()
    let composeStatusPollTableViewCell = ComposeStatusPollTableViewCell()

    // var dataSource: UITableViewDiffableDataSource<ComposeStatusSection, ComposeStatusItem>?
    var customEmojiPickerDiffableDataSource: UICollectionViewDiffableDataSource<CustomEmojiPickerSection, CustomEmojiPickerItem>?
    private(set) lazy var publishStateMachine: GKStateMachine = {
        // exclude timeline middle fetcher state
        let stateMachine = GKStateMachine(states: [
            PublishState.Initial(viewModel: self),
            PublishState.Publishing(viewModel: self),
            PublishState.Fail(viewModel: self),
            PublishState.Discard(viewModel: self),
            PublishState.Finish(viewModel: self),
        ])
        stateMachine.enter(PublishState.Initial.self)
        return stateMachine
    }()
    private(set) lazy var publishStateMachinePublisher = CurrentValueSubject<PublishState?, Never>(nil)
    private(set) var publishDate = Date()   // update it when enter Publishing state
    
    // TODO: group post material into Hashable class
    var idempotencyKey = CurrentValueSubject<String, Never>(UUID().uuidString)
    
    // UI & UX
    @Published var title: String
    @Published var shouldDismiss = true
    @Published var isPublishBarButtonItemEnabled = false
    @Published var isMediaToolbarButtonEnabled = true
    @Published var isPollToolbarButtonEnabled = true
    @Published var characterCount = 0
    @Published var collectionViewState: CollectionViewState = .fold
    
    // for hashtag: "#<hashtag> "
    // for mention: "@<mention> "
    var preInsertedContent: String?
    
    // custom emojis
    let customEmojiViewModel: EmojiService.CustomEmojiViewModel?
    let customEmojiPickerInputViewModel = CustomEmojiPickerInputViewModel()
    @Published var isLoadingCustomEmoji = false
    
    // attachment
    @Published var attachmentServices: [MastodonAttachmentService] = []
    
    // polls
    @Published var pollOptionAttributes: [ComposeStatusPollItem.PollOptionAttribute] = []
    let pollExpiresOptionAttribute = ComposeStatusPollItem.PollExpiresOptionAttribute()
    
    init(
        context: AppContext,
        composeKind: ComposeStatusSection.ComposeKind,
        authenticationBox: MastodonAuthenticationBox
    ) {
        self.context = context
        self.composeKind = composeKind
        self.authenticationBox = authenticationBox
        self.title = {
            switch composeKind {
            case .post, .hashtag, .mention:       return L10n.Scene.Compose.Title.newPost
            case .reply:                          return L10n.Scene.Compose.Title.newReply
            }
        }()
        self.selectedStatusVisibility = {
            // default private when user locked
            var visibility: ComposeToolbarView.VisibilitySelectionType = {
                guard let authenticationBox = context.authenticationService.activeMastodonAuthenticationBox.value,
                      let author = authenticationBox.authenticationRecord.object(in: context.managedObjectContext)?.user
                else {
                    return .public
                }
                return author.locked ? .private : .public
            }()
            // set visibility for reply post
            switch composeKind {
            case .reply(let record):
                context.managedObjectContext.performAndWait {
                    guard let status = record.object(in: context.managedObjectContext) else {
                        assertionFailure()
                        return
                    }
                    let repliedStatusVisibility = status.visibility
                    switch repliedStatusVisibility {
                    case .public, .unlisted:
                        // keep default
                        break
                    case .private:
                        visibility = .private
                    case .direct:
                        visibility = .direct
                    case ._other:
                        assertionFailure()
                        break
                    }
                }
            default:
                break
            }
            return visibility
        }()
        // set limit
        self.instanceConfiguration = {
            var configuration: Mastodon.Entity.Instance.Configuration? = nil
            context.managedObjectContext.performAndWait {
                guard let authentication = authenticationBox.authenticationRecord.object(in: context.managedObjectContext)
                else {
                    return
                }
                configuration = authentication.instance?.configuration
            }
            return configuration
        }()
        self.customEmojiViewModel = context.emojiService.dequeueCustomEmojiViewModel(for: authenticationBox.domain)
        super.init()
        // end init
        
        setup(cell: composeStatusContentTableViewCell)
    }
    
    deinit {
        os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s", ((#file as NSString).lastPathComponent), #line, #function)
    }
    
}

extension ComposeViewModel {
    enum CollectionViewState {
        case fold       // snap to input
        case expand     // snap to reply
    }
}

extension ComposeViewModel {
    func createNewPollOptionIfPossible() {
        guard pollOptionAttributes.count < maxPollOptions else { return }
        
        let attribute = ComposeStatusPollItem.PollOptionAttribute()
        pollOptionAttributes = pollOptionAttributes + [attribute]
    }
    
    func updatePublishDate() {
        publishDate = Date()
    }
}

extension ComposeViewModel {
    
    enum AttachmentPrecondition: Error, LocalizedError {
        case videoAttachWithPhoto
        case moreThanOneVideo
        
        var errorDescription: String? {
            return L10n.Common.Alerts.PublishPostFailure.title
        }
        
        var failureReason: String? {
            switch self {
            case .videoAttachWithPhoto:
                return L10n.Common.Alerts.PublishPostFailure.AttachmentsMessage.videoAttachWithPhoto
            case .moreThanOneVideo:
                return L10n.Common.Alerts.PublishPostFailure.AttachmentsMessage.moreThanOneVideo
            }
        }
    }
    
    // check exclusive limit:
    // - up to 1 video
    // - up to N photos
    func checkAttachmentPrecondition() throws {
        let attachmentServices = self.attachmentServices
        guard !attachmentServices.isEmpty else { return }
        var photoAttachmentServices: [MastodonAttachmentService] = []
        var videoAttachmentServices: [MastodonAttachmentService] = []
        attachmentServices.forEach { service in
            guard let file = service.file.value else {
                assertionFailure()
                return
            }
            switch file {
            case .jpeg, .png, .gif:
                photoAttachmentServices.append(service)
            case .other:
                videoAttachmentServices.append(service)
            }
        }
        
        if !videoAttachmentServices.isEmpty {
            guard videoAttachmentServices.count == 1 else {
                throw AttachmentPrecondition.moreThanOneVideo
            }
            guard photoAttachmentServices.isEmpty else {
                throw AttachmentPrecondition.videoAttachWithPhoto
            }
        }
    }
    
}

// MARK: - MastodonAttachmentServiceDelegate
extension ComposeViewModel: MastodonAttachmentServiceDelegate {
    func mastodonAttachmentService(_ service: MastodonAttachmentService, uploadStateDidChange state: MastodonAttachmentService.UploadState?) {
        // trigger new output event
        attachmentServices = attachmentServices
    }
}

// MARK: - ComposePollAttributeDelegate
extension ComposeViewModel: ComposePollAttributeDelegate {
    func composePollAttribute(_ attribute: ComposeStatusPollItem.PollOptionAttribute, pollOptionDidChange: String?) {
        // trigger update
        pollOptionAttributes = pollOptionAttributes
    }
}

extension ComposeViewModel {
    private func setup(
        cell: ComposeStatusContentTableViewCell
    ) {
        setupStatusHeader(cell: cell)
        setupStatusAuthor(cell: cell)
        setupStatusContent(cell: cell)
    }
    
    private func setupStatusHeader(
        cell: ComposeStatusContentTableViewCell
    ) {
        // configure header
        let managedObjectContext = context.managedObjectContext
        managedObjectContext.performAndWait {
            guard case let .reply(record) = self.composeKind,
                  let replyTo = record.object(in: managedObjectContext)
            else {
                cell.statusView.viewModel.header = .none
                return
            }
            
            let info: StatusView.ViewModel.Header.ReplyInfo
            do {
                let content = MastodonContent(
                    content: replyTo.author.displayNameWithFallback,
                    emojis: replyTo.author.emojis.asDictionary
                )
                let metaContent = try MastodonMetaContent.convert(document: content)
                info = .init(header: metaContent)
            } catch {
                let metaContent = PlaintextMetaContent(string: replyTo.author.displayNameWithFallback)
                info = .init(header: metaContent)
            }
            cell.statusView.viewModel.header = .reply(info: info)
        }
    }
    
    private func setupStatusAuthor(
        cell: ComposeStatusContentTableViewCell
    ) {
        self.context.managedObjectContext.performAndWait {
            guard let author = authenticationBox.authenticationRecord.object(in: self.context.managedObjectContext)?.user else { return }
            cell.statusView.configureAuthor(author: author)
        }
    }
    
    private func setupStatusContent(
        cell: ComposeStatusContentTableViewCell
    ) {
        switch composeKind {
        case .reply(let record):
            context.managedObjectContext.performAndWait {
                guard let status = record.object(in: context.managedObjectContext) else { return }
                let author = self.authenticationBox.authenticationRecord.object(in: context.managedObjectContext)?.user
                
                var mentionAccts: [String] = []
                if author?.id != status.author.id {
                    mentionAccts.append("@" + status.author.acct)
                }
                let mentions = status.mentions
                    .filter { author?.id != $0.id }
                for mention in mentions {
                    let acct = "@" + mention.acct
                    guard !mentionAccts.contains(acct) else { continue }
                    mentionAccts.append(acct)
                }
                for acct in mentionAccts {
                    UITextChecker.learnWord(acct)
                }
                if let spoilerText = status.spoilerText, !spoilerText.isEmpty {
                    self.isContentWarningComposing = true
                    self.composeStatusAttribute.contentWarningContent = spoilerText
                }

                let initialComposeContent = mentionAccts.joined(separator: " ")
                let preInsertedContent: String? = initialComposeContent.isEmpty ? nil : initialComposeContent + " "
                self.preInsertedContent = preInsertedContent
                self.composeStatusAttribute.composeContent = preInsertedContent
            }
        case .hashtag(let hashtag):
            let initialComposeContent = "#" + hashtag
            UITextChecker.learnWord(initialComposeContent)
            let preInsertedContent = initialComposeContent + " "
            self.preInsertedContent = preInsertedContent
            self.composeStatusAttribute.composeContent = preInsertedContent
        case .mention(let record):
            context.managedObjectContext.performAndWait {
                guard let user = record.object(in: context.managedObjectContext) else { return }
                let initialComposeContent = "@" + user.acct
                UITextChecker.learnWord(initialComposeContent)
                let preInsertedContent = initialComposeContent + " "
                self.preInsertedContent = preInsertedContent
                self.composeStatusAttribute.composeContent = preInsertedContent
            }
        case .post:
            self.preInsertedContent = nil
        }
        
        // configure content warning
        if let composeContent = composeStatusAttribute.composeContent {
            cell.metaText.textView.text = composeContent
        }
        
        // configure content warning
        cell.statusContentWarningEditorView.textView.text = composeStatusAttribute.contentWarningContent
    }
}
