//
//  ExploreViewController.swift
//  Tusker
//
//  Created by Shadowfacts on 12/14/19.
//  Copyright © 2019 Shadowfacts. All rights reserved.
//

import UIKit
import Combine
import Pachyderm
import CoreData
import WebURLFoundationExtras

class ExploreViewController: UIViewController, UICollectionViewDelegate, CollectionViewController {

    weak var mastodonController: MastodonController!
    
    var collectionView: UICollectionView!
    private var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
    
    private(set) var resultsController: SearchResultsViewController!
    private(set) var searchController: UISearchController!
    
    var searchControllerStatusOnAppearance: Bool? = nil
    
    private var cancellables = Set<AnyCancellable>()
    
    init(mastodonController: MastodonController) {
        self.mastodonController = mastodonController
        
        super.init(nibName: nil, bundle: nil)
        
        title = NSLocalizedString("Explore", comment: "explore tab title")
        tabBarItem.image = UIImage(systemName: "magnifyingglass")
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()

        var configuration = UICollectionLayoutListConfiguration(appearance: .insetGrouped)
        configuration.backgroundColor = .appGroupedBackground
        configuration.trailingSwipeActionsConfigurationProvider = { [unowned self] in self.trailingSwipeActionsForCell(at: $0) }
        configuration.headerMode = .supplementary
        let layout = UICollectionViewCompositionalLayout.list(using: configuration)
        collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: layout)
        collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
        collectionView.delegate = self
        collectionView.dragDelegate = self
        collectionView.allowsFocus = true
        view.addSubview(collectionView)
        
        dataSource = createDataSource()
        applyInitialSnapshot()
        
        resultsController = SearchResultsViewController(mastodonController: mastodonController)
        resultsController.exploreNavigationController = self.navigationController!
        searchController = MastodonSearchController(searchResultsController: resultsController)
        definesPresentationContext = true
        
        navigationItem.searchController = searchController
        navigationItem.hidesSearchBarWhenScrolling = false
        
        NotificationCenter.default.addObserver(self, selector: #selector(savedInstancesChanged), name: .savedInstancesChanged, object: nil)
        NotificationCenter.default.addObserver(self, selector: #selector(preferencesChanged), name: .preferencesChanged, object: nil)
        
        mastodonController.instanceFeatures.featuresUpdated
            .sink { [unowned self] in self.instanceFeaturesChanged() }
            .store(in: &cancellables)
        mastodonController.$lists
            .sink { [unowned self] in self.reloadLists($0) }
            .store(in: &cancellables)
        mastodonController.$followedHashtags
            .merge(with:
                    NotificationCenter.default.publisher(for: .savedHashtagsChanged)
                        .map { [unowned self] _ in self.mastodonController.followedHashtags  }
            )
            .sink { [unowned self] in self.updateHashtagsSection(followed: $0) }
            .store(in: &cancellables)
    }
    
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        
        // UISearchController exists outside of the normal VC hierarchy,
        // so we manually propagate this down to the results controller
        // so that it can deselect on appear
        if searchController.isActive {
            resultsController.viewWillAppear(animated)
        }
        
        clearSelectionOnAppear(animated: animated)
    }
    
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        
        if searchController.isActive {
            resultsController.viewDidAppear(animated)
        }
        
        // this is a workaround for the issue that setting isActive on a search controller that is not visible
        // does not cause it to automatically become active once it becomes visible
        // see FB7814561
        if let active = searchControllerStatusOnAppearance {
            searchController.isActive = active
            searchControllerStatusOnAppearance = nil
        }
    }
    
    private func createDataSource() -> UICollectionViewDiffableDataSource<Section, Item> {
        let sectionHeaderCell = UICollectionView.SupplementaryRegistration<UICollectionViewListCell>(elementKind: UICollectionView.elementKindSectionHeader) { (headerView, collectionView, indexPath) in
            let section = self.dataSource.snapshot().sectionIdentifiers[indexPath.section]
            
            var config = headerView.defaultContentConfiguration()
            config.text = section.label
            headerView.contentConfiguration = config
        }
        
        let listCell = UICollectionView.CellRegistration<UICollectionViewListCell, Item> { (cell, indexPath, item) in
            var config = cell.defaultContentConfiguration()
            config.text = item.label
            config.image = item.image
            cell.contentConfiguration = config
            
            cell.configurationUpdateHandler = { cell, state in
                cell.backgroundConfiguration = .appListGroupedCell(for: state)
            }

            switch item {
            case .addList, .addSavedHashtag, .findInstance:
                cell.accessories = []
            default:
                cell.accessories = [.disclosureIndicator()]
            }
        }
        
        let dataSource = UICollectionViewDiffableDataSource<Section, Item>(collectionView: collectionView) { (collectionView, indexPath, item) in
            return collectionView.dequeueConfiguredReusableCell(using: listCell, for: indexPath, item: item)
        }
        dataSource.supplementaryViewProvider = { (collectionView, elementKind, indexPath) in
            if elementKind == UICollectionView.elementKindSectionHeader {
                return collectionView.dequeueConfiguredReusableSupplementary(using: sectionHeaderCell, for: indexPath)
            } else {
                return nil
            }
        }
        return dataSource
    }

    private func applyInitialSnapshot() {
        var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
        snapshot.appendSections(Section.allCases.filter { $0 != .discover })
        snapshot.appendItems([.bookmarks, .favorites], toSection: .bookmarks)
        if mastodonController.instanceFeatures.trends,
           !Preferences.shared.hideTrends {
            addDiscoverSection(to: &snapshot)
        }
        snapshot.appendItems([.addList], toSection: .lists)
        let hashtags = fetchHashtagItems(followed: mastodonController.followedHashtags)
        snapshot.appendItems(hashtags, toSection: .savedHashtags)
        snapshot.appendItems([.addSavedHashtag], toSection: .savedHashtags)
        let instances = fetchSavedInstances().map {
            Item.savedInstance($0.url)
        }
        snapshot.appendItems(instances, toSection: .savedInstances)
        snapshot.appendItems([.findInstance], toSection: .savedInstances)
        dataSource.apply(snapshot, animatingDifferences: false)
        
        reloadLists(mastodonController.lists)
    }
    
    private func addDiscoverSection(to snapshot: inout NSDiffableDataSourceSnapshot<Section, Item>) {
        snapshot.insertSections([.discover], afterSection: .bookmarks)
        snapshot.appendItems([.trends], toSection: .discover)
    }
    
    private func instanceFeaturesChanged() {
        var snapshot = self.dataSource.snapshot()
        if mastodonController.instanceFeatures.trends,
           !snapshot.sectionIdentifiers.contains(.discover) {
            addDiscoverSection(to: &snapshot)
        } else if !mastodonController.instanceFeatures.trends,
                  snapshot.sectionIdentifiers.contains(.discover) {
            snapshot.deleteSections([.discover])
        }
        self.dataSource.apply(snapshot)
    }
    
    private func reloadLists(_ lists: [List]) {
        var snapshot = self.dataSource.snapshot()
        snapshot.deleteItems(snapshot.itemIdentifiers(inSection: .lists))
        snapshot.appendItems(lists.map { .list($0) }, toSection: .lists)
        snapshot.appendItems([.addList], toSection: .lists)
        
        self.dataSource.apply(snapshot)
    }
    
    @MainActor
    private func fetchHashtagItems(followed: [FollowedHashtag]) -> [Item] {
        let req = SavedHashtag.fetchRequest(account: mastodonController.accountInfo!)
        let saved = (try? mastodonController.persistentContainer.viewContext.fetch(req)) ?? []
        var items = saved.map {
            Item.savedHashtag(Hashtag(name: $0.name, url: $0.url))
        }
        for followed in followed where !saved.contains(where: { $0.name == followed.name }) {
            items.append(.savedHashtag(Hashtag(name: followed.name, url: followed.url)))
        }
        items = items.uniques()
        items.sort(using: SemiCaseSensitiveComparator.keyPath(\.label))
        return items
    }
    
    @MainActor
    private func fetchSavedInstances() -> [SavedInstance] {
        let req = SavedInstance.fetchRequest(account: mastodonController.accountInfo!)
        req.sortDescriptors = [NSSortDescriptor(key: "url.host", ascending: true)]
        do {
            return try mastodonController.persistentContainer.viewContext.fetch(req).uniques(by: \.url)
        } catch {
            return []
        }
    }
    
    private func updateHashtagsSection(followed: [FollowedHashtag]) {
        var snapshot = dataSource.snapshot()
        snapshot.deleteItems(snapshot.itemIdentifiers(inSection: .savedHashtags))
        let hashtags = fetchHashtagItems(followed: followed)
        snapshot.appendItems(hashtags, toSection: .savedHashtags)
        snapshot.appendItems([.addSavedHashtag], toSection: .savedHashtags)
        dataSource.apply(snapshot)
    }
    
    @objc private func savedInstancesChanged() {
        var snapshot = dataSource.snapshot()
        snapshot.deleteItems(snapshot.itemIdentifiers(inSection: .savedInstances))
        let instances = fetchSavedInstances().map {
            Item.savedInstance($0.url)
        }
        snapshot.appendItems(instances, toSection: .savedInstances)
        snapshot.appendItems([.findInstance], toSection: .savedInstances)
        dataSource.apply(snapshot)
    }
    
    @objc private func preferencesChanged() {
        var snapshot = dataSource.snapshot()
        let hasSection = snapshot.sectionIdentifiers.contains(.discover)
        let hide = Preferences.shared.hideTrends
        if hasSection && hide {
            snapshot.deleteSections([.discover])
        } else if !hasSection && !hide {
            addDiscoverSection(to: &snapshot)
        } else {
            return
        }
        dataSource.apply(snapshot)
    }
    
    private func deleteList(_ list: List, completion: @escaping (Bool) -> Void) {
        Task { @MainActor in
            let service = DeleteListService(list: list, mastodonController: mastodonController, present: { self.present($0, animated: true) })
            if await service.run() {
                var snapshot = dataSource.snapshot()
                snapshot.deleteItems([.list(list)])
                await dataSource.apply(snapshot)
                completion(true)
            } else {
                completion(false)
            }
        }
    }
    
    func removeSavedInstance(_ instanceURL: URL) {
        let context = mastodonController.persistentContainer.viewContext
        let req = SavedInstance.fetchRequest(url: instanceURL, account: mastodonController.accountInfo!)
        if let instance = try? context.fetch(req).first {
            context.delete(instance)
            try! context.save()
        }
    }
    
    private func trailingSwipeActionsForCell(at indexPath: IndexPath) -> UISwipeActionsConfiguration? {
        var actions = [UIContextualAction]()
        switch dataSource.itemIdentifier(for: indexPath) {
        case let .list(list):
            actions.append(UIContextualAction(style: .destructive, title: "Delete", handler: { _, _, completion in
                self.deleteList(list, completion: completion)
            }))
            
        case let .savedHashtag(hashtag):
            let name = hashtag.name.lowercased()
            let context = mastodonController.persistentContainer.viewContext
            let existing = try? context.fetch(SavedHashtag.fetchRequest(name: name, account: mastodonController.accountInfo!)).first
            if let existing {
                actions.append(UIContextualAction(style: .destructive, title: "Unsave", handler: { _, _, completion in
                    context.delete(existing)
                    try! context.save()
                }))
            }
            if mastodonController.instanceFeatures.canFollowHashtags,
               mastodonController.followedHashtags.contains(where: { $0.name.lowercased() == name }) {
                actions.append(UIContextualAction(style: .destructive, title: "Unfollow", handler: { _, _, completion in
                    Task {
                        let success =
                            await ToggleFollowHashtagService(hashtagName: hashtag.name, presenter: self)
                                .toggleFollow()
                        completion(success)
                    }
                }))
            }
            
        case let .savedInstance(url):
            actions.append(UIContextualAction(style: .destructive, title: "Unsave", handler: { _, _, completion in
                self.removeSavedInstance(url)
                completion(true)
            }))
        
        default:
            return nil
        }
        return UISwipeActionsConfiguration(actions: actions)
    }
    
    // MARK: - Collection View Delegate
    
    func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
        switch dataSource.itemIdentifier(for: indexPath) {
        case nil:
            return
            
        case .bookmarks:
            show(BookmarksViewController(mastodonController: mastodonController), sender: nil)
            
        case .favorites:
            show(FavoritesViewController(mastodonController: mastodonController), sender: nil)
            
        case .trends:
            show(TrendsViewController(mastodonController: mastodonController), sender: nil)
            
        case let .list(list):
            show(ListTimelineViewController(for: list, mastodonController: mastodonController), sender: nil)
            
        case .addList:
            collectionView.deselectItem(at: indexPath, animated: true)
            let service = CreateListService(mastodonController: mastodonController, present: { self.present($0, animated: true) }) { list in
                let listTimelineController = ListTimelineViewController(for: list, mastodonController: self.mastodonController)
                listTimelineController.presentEditOnAppear = true
                self.show(listTimelineController, sender: nil)
            }
            service.run()
            
        case let .savedHashtag(hashtag):
            show(HashtagTimelineViewController(for: hashtag, mastodonController: mastodonController), sender: nil)
            
        case .addSavedHashtag:
            collectionView.deselectItem(at: indexPath, animated: true)
            let navController = UINavigationController(rootViewController: AddSavedHashtagViewController(mastodonController: mastodonController))
            present(navController, animated: true)
            
        case let .savedInstance(url):
            show(InstanceTimelineViewController(for: url, parentMastodonController: mastodonController), sender: nil)
            
        case .findInstance:
            collectionView.deselectItem(at: indexPath, animated: true)
            let findController = FindInstanceViewController(parentMastodonController: mastodonController)
            findController.instanceTimelineDelegate = self
            let navController = UINavigationController(rootViewController: findController)
            present(navController, animated: true)
        }
    }
    
}

extension ExploreViewController {
    enum Section: CaseIterable {
        case bookmarks
        case discover
        case lists
        case savedHashtags
        case savedInstances
        
        var label: String? {
            switch self {
            case .bookmarks:
                return  nil
            case .discover:
                return nil
            case .lists:
                return NSLocalizedString("Lists", comment: "explore lists section title")
            case .savedHashtags:
                return NSLocalizedString("Hashtags", comment: "explore saved hashtags section title")
            case .savedInstances:
                return NSLocalizedString("Instance Timelines", comment: "explore instance timelines section title")
            }
        }
    }
    
    enum Item: Hashable {
        case bookmarks
        case favorites
        case trends
        case list(List)
        case addList
        case savedHashtag(Hashtag)
        case addSavedHashtag
        case savedInstance(URL)
        case findInstance
        
        var label: String {
            switch self {
            case .bookmarks:
                return NSLocalizedString("Bookmarks", comment: "bookmarks nav item title")
            case .favorites:
                return NSLocalizedString("Favorites", comment: "favorites nav item title")
            case .trends:
                return NSLocalizedString("Trends", comment: "trends nav item title")
            case let .list(list):
                return list.title
            case .addList:
                return NSLocalizedString("New List...", comment: "new list nav item title")
            case let .savedHashtag(hashtag):
                return hashtag.name
            case .addSavedHashtag:
                return NSLocalizedString("Add Hashtag...", comment: "save hashtag nav item title")
            case let .savedInstance(url):
                return url.host!
            case .findInstance:
                return NSLocalizedString("Find An Instance...", comment: "find instance nav item title")
            }
        }
        
        var image: UIImage {
            let name: String
            switch self {
            case .bookmarks:
                name = "bookmark.fill"
            case .favorites:
                name = "star.fill"
            case .trends:
                name = "chart.line.uptrend.xyaxis"
            case .list(_):
                name = "list.bullet"
            case .addList, .addSavedHashtag:
                name = "plus"
            case .savedHashtag(_):
                name = "number"
            case .savedInstance(_):
                name = "globe"
            case .findInstance:
                name = "magnifyingglass"
            }
            return UIImage(systemName: name)!
        }
        
        static func == (lhs: Item, rhs: Item) -> Bool {
            switch (lhs, rhs) {
            case (.bookmarks, .bookmarks):
                return true
            case (.favorites, .favorites):
                return true
            case (.trends, .trends):
                return true
            case let (.list(a), .list(b)):
                return a.id == b.id && a.title == b.title
            case (.addList, .addList):
                return true
            case let (.savedHashtag(a), .savedHashtag(b)):
                return a == b
            case (.addSavedHashtag, .addSavedHashtag):
                return true
            case let (.savedInstance(a), .savedInstance(b)):
                return a == b
            case (.findInstance, .findInstance):
                return true
            default:
                return false
            }
        }
        
        func hash(into hasher: inout Hasher) {
            switch self {
            case .bookmarks:
                hasher.combine("bookmarks")
            case .favorites:
                hasher.combine("favorites")
            case .trends:
                hasher.combine("trends")
            case let .list(list):
                hasher.combine("list")
                hasher.combine(list.id)
                hasher.combine(list.title)
            case .addList:
                hasher.combine("addList")
            case let .savedHashtag(hashtag):
                hasher.combine("savedHashtag")
                hasher.combine(hashtag.name)
            case .addSavedHashtag:
                hasher.combine("addSavedHashtag")
            case let .savedInstance(url):
                hasher.combine("savedInstance")
                hasher.combine(url)
            case .findInstance:
                hasher.combine("findInstance")
            }
        }
    }
}

extension ExploreViewController: StateRestorableViewController {
    func stateRestorationActivity() -> NSUserActivity? {
        if searchController.isActive {
            return UserActivityManager.searchActivity(query: searchController.searchBar.text, accountID: mastodonController.accountInfo!.id)
        } else {
            return nil
        }
    }
}

extension ExploreViewController: InstanceTimelineViewControllerDelegate {
    func didSaveInstance(url: URL) {
        dismiss(animated: true) {
            self.show(InstanceTimelineViewController(for: url, parentMastodonController: self.mastodonController), sender: nil)
        }
    }
    
    func didUnsaveInstance(url: URL) {
        dismiss(animated: true)
    }
}

extension ExploreViewController: UICollectionViewDragDelegate {
    func collectionView(_ collectionView: UICollectionView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] {
        guard let item = dataSource.itemIdentifier(for: indexPath),
              let accountID = mastodonController.accountInfo?.id else {
            return []
        }
        
        let provider: NSItemProvider
        switch item {
        case .bookmarks:
            let activity = UserActivityManager.bookmarksActivity(accountID: accountID)
            activity.displaysAuxiliaryScene = true
            provider = NSItemProvider(object: activity)
        case let .list(list):
            guard let activity = UserActivityManager.showTimelineActivity(timeline: .list(id: list.id), accountID: accountID) else { return [] }
            activity.displaysAuxiliaryScene = true
            provider = NSItemProvider(object: activity)
        case let .savedHashtag(hashtag):
            guard let url = URL(hashtag.url) else {
                return []
            }
            provider = NSItemProvider(object: url as NSURL)
            if let activity = UserActivityManager.showTimelineActivity(timeline: .tag(hashtag: hashtag.name), accountID: accountID) {
                activity.displaysAuxiliaryScene = true
                provider.registerObject(activity, visibility: .all)
            }
        case let .savedInstance(url):
            provider = NSItemProvider(object: url as NSURL)
            // todo: should dragging public timelines into new windows be supported?
        default:
            return []
        }
        return [UIDragItem(itemProvider: provider)]
    }
}

extension ExploreViewController: TuskerNavigationDelegate {
    var apiController: MastodonController! { mastodonController }
}
