//
//  HomeTimelineViewModel+LoadLatestState.swift
//  Mastodon
//
//  Created by sxiaojian on 2021/2/5.
//

import os.log
import func QuartzCore.CACurrentMediaTime
import Foundation
import CoreData
import CoreDataStack
import GameplayKit

extension HomeTimelineViewModel {
    class LoadLatestState: GKState {
        weak var viewModel: HomeTimelineViewModel?
        
        init(viewModel: HomeTimelineViewModel) {
            self.viewModel = viewModel
        }
        
        override func didEnter(from previousState: GKState?) {
            os_log("%{public}s[%{public}ld], %{public}s: enter %s, previous: %s", ((#file as NSString).lastPathComponent), #line, #function, self.debugDescription, previousState.debugDescription)
            viewModel?.loadLatestStateMachinePublisher.send(self)
        }
    }
}

extension HomeTimelineViewModel.LoadLatestState {
    class Initial: HomeTimelineViewModel.LoadLatestState {
        override func isValidNextState(_ stateClass: AnyClass) -> Bool {
            return stateClass == Loading.self
        }
    }
    
    class Loading: HomeTimelineViewModel.LoadLatestState {
        override func isValidNextState(_ stateClass: AnyClass) -> Bool {
            return stateClass == Fail.self || stateClass == Idle.self
        }
        
        override func didEnter(from previousState: GKState?) {
            super.didEnter(from: previousState)
            guard let viewModel = viewModel, let stateMachine = stateMachine else { return }
            guard let activeMastodonAuthenticationBox = viewModel.context.authenticationService.activeMastodonAuthenticationBox.value else {
                // sign out when loading will enter here
                stateMachine.enter(Fail.self)
                return
            }
            
            let predicate = viewModel.fetchedResultsController.fetchRequest.predicate
            let parentManagedObjectContext = viewModel.fetchedResultsController.managedObjectContext
            let managedObjectContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
            managedObjectContext.parent = parentManagedObjectContext

            managedObjectContext.perform {
                let start = CACurrentMediaTime()
                let latestStatusIDs: [Status.ID]
                let request = HomeTimelineIndex.sortedFetchRequest
                request.returnsObjectsAsFaults = false
                request.predicate = predicate

                do {
                    let timelineIndexes = try managedObjectContext.fetch(request)
                    let endFetch = CACurrentMediaTime()
                    os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: collect timelineIndexes cost: %.2fs", ((#file as NSString).lastPathComponent), #line, #function, endFetch - start)
                    latestStatusIDs = timelineIndexes
                        .prefix(APIService.onceRequestStatusMaxCount)        // avoid performance issue
                        .compactMap { timelineIndex in
                            timelineIndex.value(forKeyPath: #keyPath(HomeTimelineIndex.status.id)) as? Status.ID
                        }
                } catch {
                    stateMachine.enter(Fail.self)
                    return
                }

                let end = CACurrentMediaTime()
                os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: collect statuses id cost: %.2fs", ((#file as NSString).lastPathComponent), #line, #function, end - start)
                
                // TODO: only set large count when using Wi-Fi
                viewModel.context.apiService.homeTimeline(domain: activeMastodonAuthenticationBox.domain, authorizationBox: activeMastodonAuthenticationBox)
                    .receive(on: DispatchQueue.main)
                    .sink { completion in
                        viewModel.homeTimelineNavigationBarTitleViewModel.receiveLoadingStateCompletion(completion)
                        switch completion {
                        case .failure(let error):
                            // TODO: handle error
                            viewModel.isFetchingLatestTimeline.value = false
                            os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: fetch statuses failed. %s", ((#file as NSString).lastPathComponent), #line, #function, error.localizedDescription)
                        case .finished:
                            // handle isFetchingLatestTimeline in fetch controller delegate
                            break
                        }
                        
                        stateMachine.enter(Idle.self)
                        
                    } receiveValue: { response in
                        // stop refresher if no new statuses
                        let statuses = response.value
                        let newStatuses = statuses.filter { !latestStatusIDs.contains($0.id) }
                        os_log("%{public}s[%{public}ld], %{public}s: load %{public}ld new statuses", ((#file as NSString).lastPathComponent), #line, #function, newStatuses.count)
                        
                        if newStatuses.isEmpty {
                            viewModel.isFetchingLatestTimeline.value = false
                        } else {
                            if !latestStatusIDs.isEmpty {
                                viewModel.homeTimelineNavigationBarTitleViewModel.newPostsIncoming()
                            }
                        }
                        viewModel.timelineIsEmpty.value = latestStatusIDs.isEmpty && statuses.isEmpty
                    }
                    .store(in: &viewModel.disposeBag)
            }
        }
    }
    
    class Fail: HomeTimelineViewModel.LoadLatestState {
        override func isValidNextState(_ stateClass: AnyClass) -> Bool {
            return stateClass == Loading.self || stateClass == Idle.self
        }
    }
    
    class Idle: HomeTimelineViewModel.LoadLatestState {
        override func isValidNextState(_ stateClass: AnyClass) -> Bool {
            return stateClass == Loading.self
        }
    }

}