//
//  MainSidebarViewController.swift
//  Tusker
//
//  Created by Shadowfacts on 6/24/20.
//  Copyright © 2020 Shadowfacts. All rights reserved.
//

import UIKit
import Pachyderm
import Combine

protocol MainSidebarViewControllerDelegate: AnyObject {
    func sidebarRequestPresentCompose(_ sidebarViewController: MainSidebarViewController)
    func sidebar(_ sidebarViewController: MainSidebarViewController, didSelectItem item: MainSidebarViewController.Item)
    func sidebar(_ sidebarViewController: MainSidebarViewController, showViewController viewController: UIViewController)
}

class MainSidebarViewController: UIViewController {
    
    private weak var mastodonController: MastodonController!
    
    weak var sidebarDelegate: MainSidebarViewControllerDelegate?
    var onViewDidLoad: (() -> Void)? = nil {
        willSet {
            precondition(onViewDidLoad == nil)
        }
    }
    
    private var collectionView: UICollectionView!
    private var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
    
    private var cancellables = Set<AnyCancellable>()
    
    var allItems: [Item] {
        [
            .tab(.timelines),
            .tab(.notifications),
            .tab(.myProfile),
        ] + exploreTabItems
    }
    
    var exploreTabItems: [Item] {
        var items: [Item] = [.explore, .bookmarks, .favorites]
        let snapshot = dataSource.snapshot()
        for case let .list(list) in snapshot.itemIdentifiers(inSection: .lists) {
            items.append(.list(list))
        }
        for case let .savedHashtag(hashtag) in snapshot.itemIdentifiers(inSection: .savedHashtags) {
            items.append(.savedHashtag(hashtag))
        }
        for case let .savedInstance(instance) in snapshot.itemIdentifiers(inSection: .savedInstances) {
            items.append(.savedInstance(instance))
        }
        return items
    }
    
    private(set) var previouslySelectedItem: Item?
    var selectedItem: Item? {
        guard let indexPath = collectionView?.indexPathsForSelectedItems?.first else {
            return nil
        }
        return dataSource.itemIdentifier(for: indexPath)
    }
    
    private(set) var itemLastSelectedTimestamps = [Item: Date]()

    init(mastodonController: MastodonController) {
        self.mastodonController = mastodonController
        
        super.init(nibName: nil, bundle: nil)
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()

        navigationItem.title = "Tusker"
        navigationItem.largeTitleDisplayMode = .always
        navigationController!.navigationBar.prefersLargeTitles = true
        
        let layout = UICollectionViewCompositionalLayout.list(using: .init(appearance: .sidebar))
        collectionView = UICollectionView(frame: view.bounds, collectionViewLayout: layout)
        collectionView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
        collectionView.backgroundColor = .clear
        collectionView.delegate = self
        collectionView.dragDelegate = self
        collectionView.isSpringLoaded = true
        // TODO: allow focusing sidebar once there's a workaround for keyboard shortcuts from main split content not being accessible when not in the responder chain
        collectionView.allowsFocus = false
        view.addSubview(collectionView)
        
        dataSource = createDataSource()
        
        applyInitialSnapshot()
        
        select(item: .tab(.timelines), animated: false)
        
        NotificationCenter.default.addObserver(self, selector: #selector(reloadSavedInstances), name: .savedInstancesChanged, object: nil)
        
        mastodonController.$lists
            .sink { [unowned self] in self.reloadLists($0, animated: true) }
            .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)
        
        onViewDidLoad?()
    }
    
    func select(item: Item, animated: Bool) {
        // ensure view is loaded, since dataSource is created in viewDidLoad
        loadViewIfNeeded()
        guard let indexPath = dataSource.indexPath(for: item) else { return }
        collectionView.selectItem(at: indexPath, animated: animated, scrollPosition: .top)
        itemLastSelectedTimestamps[item] = Date()
    }
    
    func hasItem(_ item: Item) -> Bool {
        loadViewIfNeeded()
        return dataSource.snapshot().itemIdentifiers.contains(item)
    }
    
    private func createDataSource() -> UICollectionViewDiffableDataSource<Section, Item> {
        let listCell = UICollectionView.CellRegistration<UICollectionViewListCell, Item> { (cell, indexPath, item) in
            var config = cell.defaultContentConfiguration()
            config.text = item.title
            if let imageName = item.imageName {
                config.image = UIImage(systemName: imageName)
            }
            cell.contentConfiguration = config
        }
        
        let myProfileCell = UICollectionView.CellRegistration<MainSidebarMyProfileCollectionViewCell, Item> { [unowned self] cell, indexPath, item in
            cell.updateUI(item: item, account: self.mastodonController.accountInfo!)
        }
        
        let outlineHeaderCell = UICollectionView.CellRegistration<UICollectionViewListCell, Item> { (cell, indexPath, item) in
            var config = cell.defaultContentConfiguration()
            config.attributedText = NSAttributedString(string: item.title, attributes: [
                .font: UIFont.boldSystemFont(ofSize: 21)
            ])
            cell.contentConfiguration = config
            cell.accessories = [.outlineDisclosure(options: .init(style: .header))]
        }
        
        return UICollectionViewDiffableDataSource<Section, Item>(collectionView: collectionView, cellProvider: { (collectionView, indexPath, item) -> UICollectionViewCell? in
            if case .tab(.myProfile) = item {
                return collectionView.dequeueConfiguredReusableCell(using: myProfileCell, for: indexPath, item: item)
            } else if item.hasChildren {
                return collectionView.dequeueConfiguredReusableCell(using: outlineHeaderCell, for: indexPath, item: item)
            } else {
                return collectionView.dequeueConfiguredReusableCell(using: listCell, for: indexPath, item: item)
            }
        })
    }

    private func applyInitialSnapshot() {
        var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
        snapshot.appendSections(Section.allCases)
        snapshot.appendItems([
            .tab(.timelines),
            .tab(.notifications),
            .explore,
            .bookmarks,
            .favorites,
            .tab(.myProfile)
        ], toSection: .tabs)
        snapshot.appendItems([
            .tab(.compose)
        ], toSection: .compose)
        dataSource.apply(snapshot, animatingDifferences: false)
        
        reloadLists(mastodonController.lists, animated: false)
        updateHashtagsSection(followed: mastodonController.followedHashtags)
        reloadSavedInstances()
    }
    
    private func reloadLists(_ lists: [List], animated: Bool) {
        if let selectedItem,
           case .list(let list) = selectedItem,
           !lists.contains(where: { $0.id == list.id }) {
            returnToPreviousItem()
        }
        
        var exploreSnapshot = NSDiffableDataSourceSectionSnapshot<Item>()
        exploreSnapshot.append([.listsHeader])
        exploreSnapshot.expand([.listsHeader])
        exploreSnapshot.append(lists.map { .list($0) }, to: .listsHeader)
        exploreSnapshot.append([.addList], to: .listsHeader)
        self.dataSource.apply(exploreSnapshot, to: .lists, animatingDifferences: animated)
    }
    
    @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($0.name)
        }
        for followed in followed where !saved.contains(where: { $0.name == followed.name }) {
            items.append(.savedHashtag(followed.name))
        }
        items = items.uniques()
        items.sort(using: SemiCaseSensitiveComparator.keyPath(\.title))
        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]) {
        let hashtags = fetchHashtagItems(followed: followed)
        if let selectedItem,
           case .savedHashtag(_) = selectedItem,
           !hashtags.contains(selectedItem) {
            returnToPreviousItem()
        }
        
        var hashtagsSnapshot = NSDiffableDataSourceSectionSnapshot<Item>()
        hashtagsSnapshot.append([.savedHashtagsHeader])
        hashtagsSnapshot.expand([.savedHashtagsHeader])
        hashtagsSnapshot.append(hashtags, to: .savedHashtagsHeader)
        hashtagsSnapshot.append([.addSavedHashtag], to: .savedHashtagsHeader)
        self.dataSource.apply(hashtagsSnapshot, to: .savedHashtags)
    }
    
    @objc private func reloadSavedInstances() {
        let instances = fetchSavedInstances().map {
            Item.savedInstance($0.url)
        }
        if let selectedItem,
           case .savedInstance(_) = selectedItem,
           !instances.contains(selectedItem) {
            returnToPreviousItem()
        }

        var instancesSnapshot = NSDiffableDataSourceSectionSnapshot<Item>()
        instancesSnapshot.append([.savedInstancesHeader])
        instancesSnapshot.expand([.savedInstancesHeader])
        instancesSnapshot.append(instances, to: .savedInstancesHeader)
        instancesSnapshot.append([.addSavedInstance], to: .savedInstancesHeader)
        self.dataSource.apply(instancesSnapshot, to: .savedInstances)
    }
    
    private func returnToPreviousItem() {
        let item = previouslySelectedItem ?? .tab(.timelines)
        previouslySelectedItem = nil
        select(item: item, animated: true)
        sidebarDelegate?.sidebar(self, didSelectItem: item)
    }

    private func showAddList() {
        let service = CreateListService(mastodonController: mastodonController, present: { self.present($0, animated: true
        ) }) { list in
            self.select(item: .list(list), animated: false)
            let list = ListTimelineViewController(for: list, mastodonController: self.mastodonController)
            list.presentEditOnAppear = true
            self.sidebarDelegate?.sidebar(self, showViewController: list)
        }
        service.run()
    }
    
    // todo: deduplicate with ExploreViewController
    private func showAddSavedHashtag() {
        let navController = EnhancedNavigationViewController(rootViewController: AddSavedHashtagViewController(mastodonController: mastodonController))
        present(navController, animated: true)
    }
    
    // todo: deduplicate with ExploreViewController
    private func showAddSavedInstance() {
        let findController = FindInstanceViewController(parentMastodonController: mastodonController)
        findController.instanceTimelineDelegate = self
        let navController = EnhancedNavigationViewController(rootViewController: findController)
        present(navController, animated: true)
    }
    
    private func userActivityForItem(_ item: Item) -> NSUserActivity? {
        guard let id = mastodonController.accountInfo?.id else { return nil }
        
        switch item {
        case .tab(.notifications):
            return UserActivityManager.checkNotificationsActivity(mode: Preferences.shared.defaultNotificationsMode, accountID: id)
        case .tab(.compose):
            return UserActivityManager.newPostActivity(accountID: id)
        case .explore:
            return UserActivityManager.searchActivity(query: nil, accountID: id)
        case .bookmarks:
            return UserActivityManager.bookmarksActivity(accountID: id)
        case .tab(.myProfile):
            return UserActivityManager.myProfileActivity(accountID: id)
        case let .list(list):
            return UserActivityManager.showTimelineActivity(timeline: .list(id: list.id), accountID: id)
        case let .savedHashtag(name):
            return UserActivityManager.showTimelineActivity(timeline: .tag(hashtag: name), accountID: id)
        case .savedInstance(_):
            // todo: show timeline activity doesn't work for public timelines
            return nil
        default:
            return nil
        }
    }
    
    func myProfileCell() -> UICollectionViewCell? {
        guard let indexPath = dataSource.indexPath(for: .tab(.myProfile)),
              let item = collectionView.cellForItem(at: indexPath) else {
            return nil
        }
        return item
    }
    
}

extension MainSidebarViewController {
    enum Section: Int, Hashable, CaseIterable {
        case tabs
        case compose
        case lists
        case savedHashtags
        case savedInstances
    }
    enum Item: Hashable {
        case tab(MainTabBarViewController.Tab)
        case explore, bookmarks, favorites
        case listsHeader, list(List), addList
        case savedHashtagsHeader, savedHashtag(String), addSavedHashtag
        case savedInstancesHeader, savedInstance(URL), addSavedInstance
        
        var title: String {
            switch self {
            case let .tab(tab):
                return tab.title
            case .explore:
                return "Explore"
            case .bookmarks:
                return "Bookmarks"
            case .favorites:
                return "Favorites"
            case .listsHeader:
                return "Lists"
            case let .list(list):
                return list.title
            case .addList:
                return "New List..."
            case .savedHashtagsHeader:
                return "Hashtags"
            case let .savedHashtag(name):
                return name
            case .addSavedHashtag:
                return "Add Hashtag..."
            case .savedInstancesHeader:
                return "Instance Timelines"
            case let .savedInstance(url):
                return url.host!
            case .addSavedInstance:
                return "Find An Instance..."
            }
        }
        
        var imageName: String? {
            switch self {
            case let .tab(tab):
                return tab.imageName
            case .explore:
                return "magnifyingglass"
            case .bookmarks:
                return "bookmark"
            case .favorites:
                return "star"
            case .list(_):
                return "list.bullet"
            case .savedHashtag(_):
                return "number"
            case .savedInstance(_):
                return "globe"
            case .listsHeader, .savedHashtagsHeader, .savedInstancesHeader:
                return nil
            case .addList, .addSavedHashtag, .addSavedInstance:
                return "plus"
            }
        }
        
        var hasChildren: Bool {
            switch self {
            case .listsHeader, .savedHashtagsHeader, .savedInstancesHeader:
                return true
            default:
                return false
            }
        }
    }
}

fileprivate extension MainTabBarViewController.Tab {
    var title: String {
        switch self {
        case .timelines:
            return "Home"
        case .notifications:
            return "Notifications"
        case .compose:
            return "Compose"
        case .explore:
            return "Explore"
        case .myProfile:
            return "My Profile"
        }
    }
    
    var imageName: String? {
        switch self {
        case .timelines:
            return "house"
        case .notifications:
            return "bell"
        case .compose:
            return "pencil"
        case .explore:
            return "magnifyingglass"
        case .myProfile:
            return "person"
        }
    }
}

extension MainSidebarViewController: UICollectionViewDelegate {
    func collectionView(_ collectionView: UICollectionView, shouldSelectItemAt indexPath: IndexPath) -> Bool {
        previouslySelectedItem = selectedItem
        return true
    }
    
    func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
        guard let item = dataSource.itemIdentifier(for: indexPath) else {
            collectionView.deselectItem(at: indexPath, animated: true)
            return
        }
        itemLastSelectedTimestamps[item] = Date()
        if [MainSidebarViewController.Item.tab(.compose), .addList, .addSavedHashtag, .addSavedInstance].contains(item) {
            if let previous = previouslySelectedItem, let indexPath = dataSource.indexPath(for: previous) {
                collectionView.selectItem(at: indexPath, animated: false, scrollPosition: .centeredVertically)
            }
            switch item {
            case .tab(.compose):
                sidebarDelegate?.sidebarRequestPresentCompose(self)
            case .addList:
                showAddList()
            case .addSavedHashtag:
                showAddSavedHashtag()
            case .addSavedInstance:
                showAddSavedInstance()
            default:
                fatalError("unreachable")
            }
        } else {
            sidebarDelegate?.sidebar(self, didSelectItem: item)
        }
    }
    
    @available(iOS 15.0, *)
    func collectionView(_ collectionView: UICollectionView, selectionFollowsFocusForItemAt indexPath: IndexPath) -> Bool {
        guard let item = dataSource.itemIdentifier(for: indexPath) else {
            return true
        }
        // don't immediately select items that present VCs when the they're focused, only when deliberately selected
        switch item {
        case .tab(.compose), .addList, .addSavedHashtag, .addSavedInstance:
            return false
        default:
            return true
        }
    }
    
    func collectionView(_ collectionView: UICollectionView, contextMenuConfigurationForItemAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? {
        guard let item = dataSource.itemIdentifier(for: indexPath),
              let activity = userActivityForItem(item) else {
            return nil
        }
        if case .tab(.myProfile) = item,
           // only disable context menu on long-press, to allow fast account switching
           collectionView.contextMenuInteraction?.menuAppearance == .rich {
            return nil
        }
        activity.displaysAuxiliaryScene = true
        return UIContextMenuConfiguration(identifier: nil, previewProvider: nil) { (_) in
            var actions: [UIAction] = [
                UIWindowScene.ActivationAction({ action in
                    return UIWindowScene.ActivationConfiguration(userActivity: activity)
                }),
            ]
            
            if case .list(let list) = item {
                actions.append(UIAction(title: "Delete List", image: UIImage(systemName: "trash"), attributes: .destructive, handler: { [unowned self] _ in
                    Task {
                        let service = DeleteListService(list: list, mastodonController: self.mastodonController, present: { self.present($0, animated: true) })
                        await service.run()
                    }
                }))
            }
            
            return UIMenu(children: actions)
        }
    }
}

extension MainSidebarViewController: UICollectionViewDragDelegate {
    func collectionView(_ collectionView: UICollectionView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] {
        guard let item = dataSource.itemIdentifier(for: indexPath),
              let activity = userActivityForItem(item) else {
            return []
        }
        if case .tab(.myProfile) = item {
            return []
        }
        
        activity.displaysAuxiliaryScene = true
        let provider = NSItemProvider(object: activity)
        return [UIDragItem(itemProvider: provider)]
    }
}

extension MainSidebarViewController: InstanceTimelineViewControllerDelegate {
    func didSaveInstance(url: URL) {
        dismiss(animated: true) {
            self.select(item: .savedInstance(url), animated: true)
            self.sidebarDelegate?.sidebar(self, didSelectItem: .savedInstance(url))
        }
    }
    
    func didUnsaveInstance(url: URL) {
        dismiss(animated: true)
    }
}
