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

import UIKit
import Combine
import Pachyderm
import WebURLFoundationExtras

fileprivate let accountCell = "accountCell"
fileprivate let statusCell = "statusCell"
fileprivate let hashtagCell = "hashtagCell"

protocol SearchResultsViewControllerDelegate: AnyObject {
    func selectedSearchResult(account accountID: String)
    func selectedSearchResult(hashtag: Hashtag)
    func selectedSearchResult(status statusID: String)
}

extension SearchResultsViewControllerDelegate {
    func selectedSearchResult(account accountID: String) {}
    func selectedSearchResult(hashtag: Hashtag) {}
    func selectedSearchResult(status statusID: String) {}
}

class SearchResultsViewController: UIViewController, CollectionViewController {

    weak var mastodonController: MastodonController!
    
    weak var exploreNavigationController: UINavigationController?
    weak var delegate: SearchResultsViewControllerDelegate?
    var tokenHandler: ((String, SearchOperatorType) -> Void)?
    
    var collectionView: UICollectionView! { view as? UICollectionView }
    private var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
    
    /// Types of results to search for.
    var scope: Scope
    /// Whether to limit results to accounts the users is following.
    var following: Bool? = nil
    
    private let searchSubject = PassthroughSubject<String?, Never>()
    private var searchCancellable: AnyCancellable?
    private var currentQuery: String?
    
    init(mastodonController: MastodonController, scope: Scope = .all) {
        self.mastodonController = mastodonController
        self.scope = scope

        super.init(nibName: nil, bundle: nil)
        
        title = NSLocalizedString("Search", comment: "search screen title")
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override func loadView() {
        let layout = UICollectionViewCompositionalLayout { [unowned self] sectionIndex, environment in
            let sectionIdentifier = self.dataSource.sectionIdentifier(for: sectionIndex)!
            switch sectionIdentifier {
            case .tokenSuggestions(_):
                let itemSize = NSCollectionLayoutSize(widthDimension: .estimated(100), heightDimension: .absolute(30))
                let item = NSCollectionLayoutItem(layoutSize: itemSize)
                let group = NSCollectionLayoutGroup.horizontal(layoutSize: itemSize, subitems: [item])
                let section = NSCollectionLayoutSection(group: group)
                section.orthogonalScrollingBehavior = .continuousGroupLeadingBoundary
                section.interGroupSpacing = 8
                section.contentInsets = NSDirectionalEdgeInsets(top: sectionIndex == 0 ? 16 : 4, leading: 16, bottom: 4, trailing: 16)
                return section
                
            case .loadingIndicator:
                var config = UICollectionLayoutListConfiguration(appearance: .grouped)
                config.backgroundColor = .appGroupedBackground
                config.showsSeparators = false
                config.headerMode = .none
                return .list(using: config, layoutEnvironment: environment)
                
            case .accounts, .hashtags, .statuses:
                var config = UICollectionLayoutListConfiguration(appearance: .grouped)
                config.backgroundColor = .appGroupedBackground
                config.headerMode = .supplementary
                config.leadingSwipeActionsConfigurationProvider = { [unowned self] in
                    (self.collectionView.cellForItem(at: $0) as? TimelineStatusCollectionViewCell)?.leadingSwipeActions()
                }
                config.trailingSwipeActionsConfigurationProvider = { [unowned self] in
                    (self.collectionView.cellForItem(at: $0) as? TimelineStatusCollectionViewCell)?.trailingSwipeActions()
                }
                if sectionIdentifier == .statuses {
                    config.separatorConfiguration.topSeparatorInsets = TimelineStatusCollectionViewCell.separatorInsets
                    config.separatorConfiguration.bottomSeparatorInsets = TimelineStatusCollectionViewCell.separatorInsets
                }
                // we don't use the readable content inset here, because it insets the entire cell, rather than just the content
                // so the cell backgrounds not being full width looks weird
                return .list(using: config, layoutEnvironment: environment)
            }
        }
        view = UICollectionView(frame: .zero, collectionViewLayout: layout)
        collectionView.delegate = self
        collectionView.dragDelegate = self
        collectionView.allowsFocus = true
        collectionView.backgroundColor = .appGroupedBackground
        collectionView.keyboardDismissMode = .interactive
        
        dataSource = createDataSource()
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        searchCancellable = searchSubject
            .debounce(for: .seconds(1), scheduler: RunLoop.main)
            .map { $0?.trimmingCharacters(in: .whitespacesAndNewlines) }
            .sink { [unowned self] in self.performSearch(query: $0) }

        userActivity = UserActivityManager.searchActivity(query: nil, accountID: mastodonController.accountInfo!.id)
        
        NotificationCenter.default.addObserver(self, selector: #selector(handleStatusDeleted), name: .statusDeleted, object: mastodonController)
    }
    
    private func createDataSource() -> UICollectionViewDiffableDataSource<Section, Item> {
        let sectionHeader = UICollectionView.SupplementaryRegistration<UICollectionViewListCell>(elementKind: UICollectionView.elementKindSectionHeader) { [unowned self] supplementaryView, elementKind, indexPath in
            let section = self.dataSource.sectionIdentifier(for: indexPath.section)!
            var config = UIListContentConfiguration.groupedHeader()
            config.text = section.displayName
            supplementaryView.contentConfiguration = config
        }
        let tokenSuggestionCell = UICollectionView.CellRegistration<SearchTokenSuggestionCollectionViewCell, String> { cell, indexPath, itemIdentifier in
            cell.setText(itemIdentifier)
        }
        let loadingCell = UICollectionView.CellRegistration<LoadingCollectionViewCell, Void> { cell, indexPath, itemIdentifier in
            cell.indicator.startAnimating()
        }
        let accountCell = UICollectionView.CellRegistration<AccountCollectionViewCell, String> { cell, indexPath, itemIdentifier in
            cell.delegate = self
            cell.updateUI(accountID: itemIdentifier)
        }
        let hashtagCell = UICollectionView.CellRegistration<TrendingHashtagCollectionViewCell, Hashtag> { cell, indexPath, itemIdentifier in
            cell.updateUI(hashtag: itemIdentifier)
        }
        let statusCell = UICollectionView.CellRegistration<TimelineStatusCollectionViewCell, (String, CollapseState)> { [unowned self] cell, indexPath, itemIdentifier in
            cell.delegate = self
            cell.updateUI(statusID: itemIdentifier.0, state: itemIdentifier.1, filterResult: .allow, precomputedContent: nil)
        }
        let dataSource = UICollectionViewDiffableDataSource<Section, Item>(collectionView: collectionView) { collectionView, indexPath, itemIdentifier in
            let cell: UICollectionViewCell
            switch itemIdentifier {
            case .tokenSuggestion(let value):
                return collectionView.dequeueConfiguredReusableCell(using: tokenSuggestionCell, for: indexPath, item: value)
            case .loadingIndicator:
                return collectionView.dequeueConfiguredReusableCell(using: loadingCell, for: indexPath, item: ())
            case .account(let accountID):
                cell = collectionView.dequeueConfiguredReusableCell(using: accountCell, for: indexPath, item: accountID)
            case .hashtag(let hashtag):
                cell = collectionView.dequeueConfiguredReusableCell(using: hashtagCell, for: indexPath, item: hashtag)
            case .status(let id, let state):
                cell = collectionView.dequeueConfiguredReusableCell(using: statusCell, for: indexPath, item: (id, state))
            }
            cell.configurationUpdateHandler = { cell, state in
                cell.backgroundConfiguration = .appListGroupedCell(for: state)
            }
            return cell
        }
        dataSource.supplementaryViewProvider = { collectionView, elementKind, indexPath in
            if elementKind == UICollectionView.elementKindSectionHeader {
                return collectionView.dequeueConfiguredReusableSupplementary(using: sectionHeader, for: indexPath)
            } else {
                return nil
            }
        }
        return dataSource
    }
    
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        
        clearSelectionOnAppear(animated: animated)
    }
    
    override func targetViewController(forAction action: Selector, sender: Any?) -> UIViewController? {
        // if we're showing a view controller, we need to go up to the explore VC's nav controller
        // the UISearchController that is our parent is not part of the normal VC hierarchy and itself doesn't have a parent
        if action == #selector(UIViewController.show(_:sender:)),
            let exploreNavController = exploreNavigationController {
            return exploreNavController
        }
        return super.targetViewController(forAction: action, sender: sender)
    }
    
    func updateTokenSuggestions(_ suggestions: [(SearchOperatorType, [String])], animated: Bool) {
        var snapshot = dataSource.snapshot()
        var prev: Section?
        for (op, values) in suggestions {
            let section = Section.tokenSuggestions(op)
            if values.isEmpty {
                if snapshot.sectionIdentifiers.contains(section) {
                    snapshot.deleteSections([section])
                }
            } else {
                if !snapshot.sectionIdentifiers.contains(section) {
                    if let prev {
                        snapshot.insertSections([section], afterSection: prev)
                    } else if let first = snapshot.sectionIdentifiers.first {
                        snapshot.insertSections([section], beforeSection: first)
                    } else {
                        snapshot.appendSections([section])
                    }
                } else {
                    snapshot.deleteItems(snapshot.itemIdentifiers(inSection: section))
                }
                snapshot.appendItems(values.map { .tokenSuggestion($0) }, toSection: section)
                prev = section
            }
        }
        
        dataSource.apply(snapshot, animatingDifferences: animated)
    }
    
    func removeResults() {
        var snapshot = dataSource.snapshot()
        removeResults(from: &snapshot)
        dataSource.apply(snapshot, animatingDifferences: false)
    }
    
    private func removeResults(from snapshot: inout NSDiffableDataSourceSnapshot<Section, Item>) {
        snapshot.deleteSections([Section.loadingIndicator, .accounts, .hashtags, .statuses].filter { snapshot.sectionIdentifiers.contains($0) })
    }
    
    func loadResults(from source: SearchResultsViewController) {
        currentQuery = source.currentQuery
        if let sourceDataSource = source.dataSource {
            dataSource.apply(sourceDataSource.snapshot())
        }
    }
    
    func performSearch(query: String?) {
        guard isViewLoaded,
              query != currentQuery else {
            return
        }
        guard let query = query, !query.isEmpty else {
            removeResults()
            return
        }
        self.currentQuery = query

        var snapshot = dataSource.snapshot()
        removeResults(from: &snapshot)
        snapshot.appendSections([.loadingIndicator])
        snapshot.appendItems([.loadingIndicator])
        dataSource.apply(snapshot)

        let request = Client.search(query: query, types: scope.resultTypes, resolve: true, limit: 10, following: following)
        mastodonController.run(request) { (response) in
            switch response {
            case let .success(results, _):
                guard self.currentQuery == query else { return }
                self.showSearchResults(results)
            case let .failure(error):
                DispatchQueue.main.async {
                    self.showSearchError(error)
                }
            }
        }
    }
    
    private func showSearchResults(_ results: SearchResults) {
        var snapshot = dataSource.snapshot()
        snapshot.deleteSections([.loadingIndicator])
        
        self.mastodonController.persistentContainer.performBatchUpdates({ (context, addAccounts, addStatuses) in
            let resultTypes = self.scope.resultTypes
            if !results.accounts.isEmpty && resultTypes.contains(.accounts) {
                snapshot.appendSections([.accounts])
                snapshot.appendItems(results.accounts.map { .account($0.id) }, toSection: .accounts)
                addAccounts(results.accounts)
            }
            if !results.hashtags.isEmpty && resultTypes.contains(.hashtags) {
                snapshot.appendSections([.hashtags])
                snapshot.appendItems(results.hashtags.map { .hashtag($0) }, toSection: .hashtags)
            }
            if !results.statuses.isEmpty && resultTypes.contains(.statuses) {
                snapshot.appendSections([.statuses])
                snapshot.appendItems(results.statuses.map { .status($0.id, .unknown) }, toSection: .statuses)
                addStatuses(results.statuses)
            }
        }, completion: {
            DispatchQueue.main.async {
                self.dataSource.apply(snapshot)
            }
        })
    }
    
    private func showSearchError(_ error: Client.Error) {
        let snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
        dataSource.apply(snapshot)
        
        let config = ToastConfiguration(from: error, with: "Error Searching", in: self) { [unowned self] toast in
            toast.dismissToast(animated: true)
            self.performSearch(query: self.currentQuery)
        }
        showToast(configuration: config, animated: true)
    }
    
    @objc private func handleStatusDeleted(_ notification: Foundation.Notification) {
        guard let userInfo = notification.userInfo,
              let statusIDs = userInfo["statusIDs"] as? [String] else {
            return
        }
        var snapshot = self.dataSource.snapshot()
        let toDelete = statusIDs
            .map { id in
                Item.status(id, .unknown)
            }
            .filter { item in
                snapshot.itemIdentifiers.contains(item)
            }
        if !toDelete.isEmpty {
            snapshot.deleteItems(toDelete)
            self.dataSource.apply(snapshot, animatingDifferences: true)
        }
    }

}

extension SearchResultsViewController {
    enum Scope: CaseIterable {
        case all
        case people
        case hashtags
        case posts
        
        var title: String {
            switch self {
            case .all:
                return "All"
            case .people:
                return "People"
            case .hashtags:
                return "Hashtags"
            case .posts:
                return "Posts"
            }
        }
        
        var resultTypes: [SearchResultType] {
            switch self {
            case .all:
                return [.accounts, .statuses, .hashtags]
            case .people:
                return [.accounts]
            case .hashtags:
                return [.hashtags]
            case .posts:
                return [.statuses]
            }
        }
    }
}

extension SearchResultsViewController {
    enum Section: Hashable {
        case tokenSuggestions(SearchOperatorType)
        case loadingIndicator
        case accounts
        case hashtags
        case statuses

        var displayName: String? {
            switch self {
            case .tokenSuggestions:
                return nil
            case .loadingIndicator:
                return nil
            case .accounts:
                return NSLocalizedString("People", comment: "accounts search results section")
            case .hashtags:
                return NSLocalizedString("Hashtags", comment: "hashtag search results section")
            case .statuses:
                return NSLocalizedString("Posts", comment: "statuses search results section")
            }
        }
    }
    enum Item: Hashable {
        case tokenSuggestion(String)
        case loadingIndicator
        case account(String)
        case hashtag(Hashtag)
        case status(String, CollapseState)
        
        func hash(into hasher: inout Hasher) {
            switch self {
            case let .tokenSuggestion(value):
                hasher.combine("tokenSuggestion")
                hasher.combine(value)
            case .loadingIndicator:
                hasher.combine("loadingIndicator")
            case let .account(id):
                hasher.combine("account")
                hasher.combine(id)
            case let .hashtag(hashtag):
                hasher.combine("hashtag")
                hasher.combine(hashtag.url)
            case let .status(id, _):
                hasher.combine("status")
                hasher.combine(id)
            }
        }
        
        static func ==(lhs: Item, rhs: Item) -> Bool {
            switch (lhs, rhs) {
            case (.tokenSuggestion(let a), .tokenSuggestion(let b)):
                return a == b
            case (.loadingIndicator, .loadingIndicator):
                return true
            case (.account(let a), .account(let b)):
                return a == b
            case (.hashtag(let a), .hashtag(let b)):
                return a.name == b.name
            case (.status(let a, _), .status(let b, _)):
                return a == b
            default:
                return false
            }
        }
    }
}

extension SearchResultsViewController: UICollectionViewDelegate {
    func collectionView(_ collectionView: UICollectionView, shouldSelectItemAt indexPath: IndexPath) -> Bool {
        switch dataSource.itemIdentifier(for: indexPath) {
        case .loadingIndicator:
            return false
        default:
            return true
        }
    }
    
    func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
        switch dataSource.itemIdentifier(for: indexPath) {
        case nil, .loadingIndicator:
            return
        case .tokenSuggestion(let value):
            guard case .tokenSuggestions(let op) = dataSource.sectionIdentifier(for: indexPath.section) else {
                return
            }
            tokenHandler?(value, op)
            collectionView.deselectItem(at: indexPath, animated: true)
        case let .account(id):
            if let delegate {
                delegate.selectedSearchResult(account: id)
            } else {
                selected(account: id)
            }
        case let .hashtag(hashtag):
            if let delegate {
                delegate.selectedSearchResult(hashtag: hashtag)
            } else {
                selected(tag: hashtag)
            }
        case let .status(id, state):
            if let delegate {
                delegate.selectedSearchResult(status: id)
            } else {
                selected(status: id, state: state.copy())
            }
        }
    }
    
    func collectionView(_ collectionView: UICollectionView, contextMenuConfigurationForItemAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? {
        guard let item = dataSource.itemIdentifier(for: indexPath),
              let cell = collectionView.cellForItem(at: indexPath) else {
            return nil
        }
        switch item {
        case .loadingIndicator, .tokenSuggestion(_):
            return nil
        case .account(let id):
            return UIContextMenuConfiguration {
                ProfileViewController(accountID: id, mastodonController: self.mastodonController)
            } actionProvider: { _ in
                UIMenu(children: self.actionsForProfile(accountID: id, source: .view(cell)))
            }
        case .hashtag(let tag):
            return UIContextMenuConfiguration {
                HashtagTimelineViewController(for: tag, mastodonController: self.mastodonController)
            } actionProvider: { _ in
                UIMenu(children: self.actionsForHashtag(tag, source: .view(cell)))
            }
        case .status(_, _):
            return (cell as? TimelineStatusCollectionViewCell)?.contextMenuConfiguration()
        }
    }
    
    func collectionView(_ collectionView: UICollectionView, willPerformPreviewActionForMenuWith configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionCommitAnimating) {
        MenuPreviewHelper.willPerformPreviewAction(animator: animator, presenter: self)
    }
}

extension SearchResultsViewController: UICollectionViewDragDelegate {
    func collectionView(_ collectionView: UICollectionView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] {
        guard let accountInfo = mastodonController.accountInfo,
              let item = dataSource.itemIdentifier(for: indexPath) else {
            return []
        }
        let url: URL
        let activity: NSUserActivity
        switch item {
        case .loadingIndicator, .tokenSuggestion(_):
            return []
        case .account(let id):
            guard let account = mastodonController.persistentContainer.account(for: id) else {
                return []
            }
            url = account.url
            activity = UserActivityManager.showProfileActivity(id: id, accountID: accountInfo.id)
        case .hashtag(let tag):
            url = URL(tag.url)!
            activity = UserActivityManager.showTimelineActivity(timeline: .tag(hashtag: tag.name), accountID: accountInfo.id)!
        case .status(let id, _):
            guard let status = mastodonController.persistentContainer.status(for: id),
                  status.url != nil else {
                return []
            }
            url = status.url!
            activity = UserActivityManager.showConversationActivity(mainStatusID: id, accountID: accountInfo.id)
        }
        activity.displaysAuxiliaryScene = true
        let provider = NSItemProvider(object: url as NSURL)
        provider.registerObject(activity, visibility: .all)
        return [UIDragItem(itemProvider: provider)]
    }
}

extension SearchResultsViewController: UISearchResultsUpdating {
    func updateSearchResults(for searchController: UISearchController) {
        searchSubject.send(searchController.searchBar.searchQueryWithOperators)
    }
}

extension SearchResultsViewController: UISearchBarDelegate {
    func searchBarTextDidEndEditing(_ searchBar: UISearchBar) {
        var snapshot = dataSource.snapshot()
        let allSuggestionSections = snapshot.sectionIdentifiers.filter {
            if case .tokenSuggestions(_) = $0 {
                return true
            } else {
                return false
            }
        }
        snapshot.deleteSections(allSuggestionSections)
        dataSource.apply(snapshot, animatingDifferences: true)
        
        // perform a search immedaitely when the search button is pressed
        performSearch(query: searchBar.searchQueryWithOperators)
    }
    
    func searchBar(_ searchBar: UISearchBar, selectedScopeButtonIndexDidChange selectedScope: Int) {
        let newQuery = searchBar.searchQueryWithOperators
        let newScope = Scope.allCases[selectedScope]
        if self.scope == .all && currentQuery == newQuery {
            self.scope = newScope
            var snapshot = dataSource.snapshot()
            if snapshot.sectionIdentifiers.contains(.accounts) && scope != .people {
                snapshot.deleteSections([.accounts])
            }
            if snapshot.sectionIdentifiers.contains(.hashtags) && scope != .hashtags {
                snapshot.deleteSections([.hashtags])
            }
            if snapshot.sectionIdentifiers.contains(.statuses) && scope != .posts {
                snapshot.deleteSections([.statuses])
            }
            dataSource.apply(snapshot)
        } else {
            self.scope = newScope
            performSearch(query: newQuery)
        }
    }
}

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

extension SearchResultsViewController: ToastableViewController {
}

extension SearchResultsViewController: MenuActionProvider {
}

extension SearchResultsViewController: StatusCollectionViewCellDelegate {
    func statusCellNeedsReconfigure(_ cell: StatusCollectionViewCell, animated: Bool, completion: (() -> Void)?) {
        if let indexPath = collectionView.indexPath(for: cell) {
            var snapshot = dataSource.snapshot()
            snapshot.reconfigureItems([dataSource.itemIdentifier(for: indexPath)!])
            dataSource.apply(snapshot, animatingDifferences: animated, completion: completion)
        }
    }
    
    func statusCellShowFiltered(_ cell: StatusCollectionViewCell) {
        // not yet supported
    }
}
