//
//  TimelineLikeCollectionViewController.swift
//  Tusker
//
//  Created by Shadowfacts on 9/24/22.
//  Copyright © 2022 Shadowfacts. All rights reserved.
//

import UIKit
import Combine

@MainActor
protocol TimelineLikeCollectionViewController: UIViewController, TimelineLikeControllerDelegate, TuskerNavigationDelegate {
    associatedtype Section: TimelineLikeCollectionViewSection
    associatedtype Item: TimelineLikeCollectionViewItem where Item.TimelineItem == Self.TimelineItem
    associatedtype Error: TimelineLikeCollectionViewError
    
    // this needs to be an IUO because it can't be set until after the super init is called, so that self can be passed as the delegate param
    var controller: TimelineLikeController<TimelineItem>! { get }
    var confirmLoadMore: PassthroughSubject<Void, Never> { get }
    
    var collectionView: UICollectionView! { get }
    var dataSource: UICollectionViewDiffableDataSource<Section, Item>! { get }
    
    var reconfigureVisibleItemsOnEndDecelerating: Bool { get set }
}

protocol TimelineLikeCollectionViewSection: Hashable, Sendable {
    static var entries: Self { get }
    static var footer: Self { get }
}

protocol TimelineLikeCollectionViewItem: Hashable, Sendable {
    associatedtype TimelineItem
    
    static var loadingIndicator: Self { get }
    static var confirmLoadMore: Self { get }
    
    @MainActor
    static func fromTimelineItem(_ item: TimelineItem) -> Self
}

// TODO: equatable might not be the best for this?
protocol TimelineLikeCollectionViewError: Error, Equatable {
    static var allCaughtUp: Self { get }
}

// MARK: TimelineLikeControllerDelegate
extension TimelineLikeCollectionViewController {
    func canLoadOlder() async -> Bool {
        if Preferences.shared.disableInfiniteScrolling {
            var snapshot = dataSource.snapshot()
            if !snapshot.itemIdentifiers.contains(.confirmLoadMore) {
                if !snapshot.sectionIdentifiers.contains(.footer) {
                    snapshot.appendSections([.footer])
                }
                snapshot.appendItems([.confirmLoadMore], toSection: .footer)
                await apply(snapshot, animatingDifferences: false)
            }
            for await _ in confirmLoadMore.values {
                return true
            }
            fatalError("unreachable")
        } else {
            return true
        }
    }
    
    func handleAddLoadingIndicator() async {
        if case .loadingInitial(_, _) = controller.state,
           let refreshControl = collectionView.refreshControl,
           refreshControl.isRefreshing {
            refreshControl.beginRefreshing()
            // if we're loading initial and the refresh control is already going, we don't need to add another indicator
            return
        }
        
        var snapshot = dataSource.snapshot()
        if !snapshot.sectionIdentifiers.contains(.footer) {
            snapshot.appendSections([.footer])
        }
        if snapshot.itemIdentifiers.contains(.confirmLoadMore) {
            snapshot.reconfigureItems([.confirmLoadMore])
        } else {
            snapshot.appendItems([.loadingIndicator], toSection: .footer)
        }
        await apply(snapshot, animatingDifferences: false)
    }
    
    func handleRemoveLoadingIndicator() async {
        if case .loadingInitial(_, _) = controller.state,
           let refreshControl = collectionView.refreshControl,
           refreshControl.isRefreshing {
            refreshControl.endRefreshing()
            return
        }
        
        let oldContentOffset = collectionView.contentOffset
        var snapshot = dataSource.snapshot()
        snapshot.deleteSections([.footer])
        await apply(snapshot, animatingDifferences: false)
        // prevent the collection view from scrolling as we remove the loading indicator and add the timeline items
        collectionView.contentOffset = oldContentOffset
    }
    
    func handleLoadAllError(_ error: Swift.Error) async {
        let config = ToastConfiguration(from: error, with: "Error Loading", in: self) { [weak self] toast in
            toast.dismissToast(animated: true)
            Task {
                await self?.controller.loadInitial()
            }
        }
        self.showToast(configuration: config, animated: true)
    }
    
    func handleReplaceAllItems(_ timelineItems: [TimelineItem]) async {
        var snapshot = dataSource.snapshot()
        if snapshot.sectionIdentifiers.contains(.entries) {
            snapshot.deleteSections([.entries])
        }
        snapshot.appendSections([.entries])
        snapshot.appendItems(timelineItems.map { .fromTimelineItem($0) }, toSection: .entries)
        await apply(snapshot, animatingDifferences: false)
    }
    
    func handleLoadNewerError(_ error: Swift.Error) async {
        var config: ToastConfiguration
        if let error = error as? Self.Error,
           error == .allCaughtUp {
            // Reconfigure visible items to update timestamps.
            #if targetEnvironment(macCatalyst)
            let isRefreshing = false
            #else
            let isRefreshing = collectionView.refreshControl?.isRefreshing ?? false
            #endif
            if isRefreshing {
                reconfigureVisibleItemsOnEndDecelerating = true
            } else {
                reconfigureVisibleCells()
            }
            
            config = ToastConfiguration(title: "You're all caught up")
            config.edge = .top
            config.dismissAutomaticallyAfter = 2
            config.action = { toast in
                toast.dismissToast(animated: true)
            }
        } else {
            config = ToastConfiguration(from: error, with: "Error Loading Newer", in: self) { [weak self] toast in
                toast.dismissToast(animated: true)
                Task {
                    await self?.controller.loadNewer()
                }
            }
        }
        self.showToast(configuration: config, animated: true)
    }
    
    func handlePrependItems(_ timelineItems: [TimelineItem]) async {
        let items = timelineItems.map { Item.fromTimelineItem($0) }
        var snapshot = dataSource.snapshot()
        let first = snapshot.itemIdentifiers(inSection: .entries).first
        if let first {
            snapshot.insertItems(items, beforeItem: first)
        } else {
            snapshot.appendItems(items, toSection: .entries)
        }
        await apply(snapshot, animatingDifferences: false)
        
        // todo: this won't work for cmd+r when not at top
        if let first,
           let indexPath = dataSource.indexPath(for: first) {
            // TODO: i can't tell if this actually works or not
            // maintain the current scroll position in the list (don't scroll to top)
            collectionView.scrollToItem(at: indexPath, at: .top, animated: false)
        }
    }
    
    func handleLoadOlderError(_ error: Swift.Error) async {
        let config = ToastConfiguration(from: error, with: "Error Loading Older", in: self) { [weak self] toast in
            toast.dismissToast(animated: true)
            Task {
                await self?.controller.loadOlder()
            }
        }
        self.showToast(configuration: config, animated: true)
    }
    
    func handleAppendItems(_ timelineItems: [TimelineItem]) async {
        var snapshot = dataSource.snapshot()
        // TODO: this might not be necessary, isn't the confirm item removed separately?
        if snapshot.itemIdentifiers.contains(.confirmLoadMore) {
            snapshot.deleteItems([.confirmLoadMore])
        }
        snapshot.appendItems(timelineItems.map { .fromTimelineItem($0) }, toSection: .entries)
        await apply(snapshot, animatingDifferences: false)
    }
    
    func loadGap(in direction: TimelineGapDirection) async throws -> [TimelineItem] {
        fatalError("not supported by \(String(describing: type(of: self)))")
    }
    
    func handleLoadGapError(_ error: Swift.Error, direction: TimelineGapDirection) async {
    }
    
    func handleFillGap(_ timelineItems: [TimelineItem], direction: TimelineGapDirection) async {
        fatalError("not supported by \(String(describing: type(of: self)))")
    }
}

extension TimelineLikeCollectionViewController {
    // apply(_:animatingDifferences:) is marked as nonisolated, so just awaiting it doesn't dispatch to the main thread, unlike other async @MainActor methods
    // but we always want to update the data source on the main thread for consistency, so this method does that
    func apply(_ snapshot: NSDiffableDataSourceSnapshot<Section, Item>, animatingDifferences: Bool) async {
        await MainActor.run {
            dataSource?.apply(snapshot, animatingDifferences: animatingDifferences)
        }
    }
    
    @MainActor
    func reconfigureVisibleCells() {
        let items = collectionView.indexPathsForVisibleItems.compactMap { dataSource.itemIdentifier(for: $0) }
        if !items.isEmpty {
            var snapshot = dataSource.snapshot()
            snapshot.reconfigureItems(items)
            dataSource.apply(snapshot, animatingDifferences: false)
        }
    }
    
    func registerTimelineLikeCells() {
        collectionView.register(LoadingCollectionViewCell.self, forCellWithReuseIdentifier: "loadingIndicator")
        collectionView.register(ConfirmLoadMoreCollectionViewCell.self, forCellWithReuseIdentifier: "confirmLoadMore")
    }
    
    func loadingIndicatorCell(for indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "loadingIndicator", for: indexPath) as! LoadingCollectionViewCell
        cell.indicator.startAnimating()
        return cell
    }
    
    func confirmLoadMoreCell(for indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "confirmLoadMore", for: indexPath) as! ConfirmLoadMoreCollectionViewCell
        cell.confirmLoadMore = self.confirmLoadMore
        Task {
            if case .loadingOlder(_, _) = controller.state {
                cell.isLoading = true
            } else {
                cell.isLoading = false
            }
        }
        return cell
    }
}
