// 
// Copyright 2022 New Vector Ltd
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

// swiftlint:disable file_length

import UIKit
import Reusable

protocol AllChatsViewControllerDelegate: AnyObject {
    func allChatsViewControllerDidCompleteAuthentication(_ allChatsViewController: AllChatsViewController)
    func allChatsViewController(_ allChatsViewController: AllChatsViewController, didSelectRoomWithParameters roomNavigationParameters: RoomNavigationParameters, completion: @escaping () -> Void)
    func allChatsViewController(_ allChatsViewController: AllChatsViewController, didSelectRoomPreviewWithParameters roomPreviewNavigationParameters: RoomPreviewNavigationParameters, completion: (() -> Void)?)
    func allChatsViewController(_ allChatsViewController: AllChatsViewController, didSelectContact contact: MXKContact, with presentationParameters: ScreenPresentationParameters)
}

class AllChatsViewController: HomeViewController {
    // MARK: - Class methods
    
    static override func nib() -> UINib! {
        return UINib(nibName: String(describing: self), bundle: Bundle(for: self.classForCoder()))
    }
    
    static override func instantiate() -> Self {
        let storyboard = UIStoryboard(name: "Main", bundle: .main)
        guard let viewController = storyboard.instantiateViewController(withIdentifier: "AllChatsViewController") as? Self else {
            fatalError("No view controller of type \(self) in the main storyboard")
        }
        return viewController
    }
    
    // MARK: - Properties
    
    weak var allChatsDelegate: AllChatsViewControllerDelegate?
    
    // MARK: - Private
    
    private let searchController = UISearchController(searchResultsController: nil)
    
    private let spaceActionProvider = AllChatsSpaceActionProvider()
    
    private let editActionProvider = AllChatsEditActionProvider()

    private var spaceSelectorBridgePresenter: SpaceSelectorBottomSheetCoordinatorBridgePresenter?
    
    private var childCoordinators: [Coordinator] = []
    
    private let tableViewPaginationThrottler = MXThrottler(minimumDelay: 0.1)
    
    private let reviewSessionAlertSnoozeController = ReviewSessionAlertSnoozeController()
    
    private var bannerView: UIView? {
        didSet {
            bannerView?.translatesAutoresizingMaskIntoConstraints = false
            set(tableHeadeView: bannerView)
        }
    }
    
    private var isOnboardingCoordinatorPreparing: Bool = false

    private var theme: Theme {
        ThemeService.shared().theme
    }

    @IBOutlet private var toolbar: UIToolbar!
    private var isToolbarHidden: Bool = false {
        didSet {
            if isViewLoaded {
                toolbar.transform = isToolbarHidden ? CGAffineTransform(translationX: 0, y: 2 * toolbarHeight) : .identity
                self.view.layoutIfNeeded()
            }
        }
    }
    
    private func setToolbarHidden(_ isHidden: Bool, animated: Bool) {
        UIView.animate(withDuration: animated ? 0.3 : 0) {
            self.isToolbarHidden = isHidden
        }

    }
    
    // MARK: - SplitViewMasterViewControllerProtocol
    
    // References on the currently selected room
    private(set) var selectedRoomId: String?
    private(set) var selectedEventId: String?
    private(set) var selectedRoomSession: MXSession?
    private(set) var selectedRoomPreviewData: RoomPreviewData?
    
    // References on the currently selected contact
    private(set) var selectedContact: MXKContact?
    
    // Reference to the current onboarding flow. It is always nil unless the flow is being presented.
    private(set) var onboardingCoordinatorBridgePresenter: OnboardingCoordinatorBridgePresenter?
    
    // Tell whether the onboarding screen is preparing.
    private(set) var isOnboardingInProgress: Bool = false
    
    private var toolbarHeight: CGFloat = 0

    // MARK: - Lifecycle
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        editActionProvider.delegate = self
        spaceActionProvider.delegate = self
        
        recentsTableView.tag = RecentsDataSourceMode.allChats.rawValue
        recentsTableView.clipsToBounds = false
        recentsTableView.register(RecentEmptySectionTableViewCell.nib, forCellReuseIdentifier: RecentEmptySectionTableViewCell.reuseIdentifier)
        recentsTableView.register(RecentEmptySpaceSectionTableViewCell.nib, forCellReuseIdentifier: RecentEmptySpaceSectionTableViewCell.reuseIdentifier)
        recentsTableView.register(RecentsInvitesTableViewCell.nib, forCellReuseIdentifier: RecentsInvitesTableViewCell.reuseIdentifier)
        recentsTableView.contentInsetAdjustmentBehavior = .automatic
        
        toolbarHeight = toolbar.frame.height
        emptyViewBottomAnchor = toolbar.topAnchor

        updateUI()
        
        navigationItem.largeTitleDisplayMode = .automatic
        navigationController?.navigationBar.prefersLargeTitles = true

        searchController.obscuresBackgroundDuringPresentation = false
        searchController.searchResultsUpdater = self
        searchController.delegate = self

        NotificationCenter.default.addObserver(self, selector: #selector(self.setupEditOptions), name: AllChatsLayoutSettingsManager.didUpdateSettings, object: nil)
        NotificationCenter.default.addObserver(self, selector: #selector(self.updateBadgeButton), name: MXSpaceNotificationCounter.didUpdateNotificationCount, object: nil)
    }
    
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        
        self.toolbar.tintColor = theme.colors.accent
        if self.navigationItem.searchController == nil {
            self.navigationItem.searchController = searchController
        }

        NotificationCenter.default.addObserver(self, selector: #selector(self.spaceListDidChange), name: MXSpaceService.didInitialise, object: nil)
        NotificationCenter.default.addObserver(self, selector: #selector(self.spaceListDidChange), name: MXSpaceService.didBuildSpaceGraph, object: nil)
        
        set(tableHeadeView: self.bannerView)
    }
    
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        
        // Check whether we're not logged in
        let authIsShown: Bool
        if MXKAccountManager.shared().accounts.isEmpty {
            showOnboardingFlow()
            authIsShown = true
        } else {
            // Display a login screen if the account is soft logout
            // Note: We support only one account
            if let account = MXKAccountManager.shared().accounts.first, account.isSoftLogout {
                showSoftLogoutOnboardingFlow(with: account.mxCredentials)
                authIsShown = true
            } else {
                authIsShown = false
            }
        }
        
        guard !authIsShown else {
            return
        }

        AppDelegate.theDelegate().checkAppVersion()
    }
    
    override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
        super.viewWillTransition(to: size, with: coordinator)
        
        coordinator.animate { context in
            self.recentsTableView?.tableHeaderView?.layoutIfNeeded()
            self.recentsTableView?.tableHeaderView = self.recentsTableView?.tableHeaderView
        }
    }
    
    // MARK: - Public
    
    func switchSpace(withId spaceId: String?) {
        searchController.isActive = false

        guard let spaceId = spaceId else {
            dataSource?.currentSpace = nil
            updateUI()

            return
        }

        guard let space = self.mainSession.spaceService.getSpace(withId: spaceId) else {
            MXLog.warning("[AllChatsViewController] switchSpace: no space found with id \(spaceId)")
            return
        }
        
        dataSource?.currentSpace = space
        updateUI()
        
        self.recentsTableView.scrollToRow(at: IndexPath(row: 0, section: 0), at: .top, animated: true)
    }

    override var recentsDataSourceMode: RecentsDataSourceMode {
        .allChats
    }
    
    override func addMatrixSession(_ mxSession: MXSession!) {
        super.addMatrixSession(mxSession)
        
        if let dataSource = dataSource, !dataSource.mxSessions.contains(where: { $0 as? MXSession == mxSession }) {
            dataSource.addMatrixSession(mxSession)
            // Setting the delegate is required to send a RecentsViewControllerDataReadyNotification.
            // Without this, when clearing the cache we end up with an infinite green spinner.
            (dataSource as? RecentsDataSource)?.setDelegate(self, andRecentsDataSourceMode: recentsDataSourceMode)
        } else {
            initDataSource()
        }
    }
    
    override func removeMatrixSession(_ mxSession: MXSession!) {
        super.removeMatrixSession(mxSession)
        
        guard let dataSource = dataSource else { return }
        dataSource.removeMatrixSession(mxSession)
        
        if dataSource.mxSessions.isEmpty {
            // The user logged out -> we need to reset the data source
            displayList(nil)
        }
    }
    
    private func initDataSource() {
        guard self.dataSource == nil, let mainSession = self.mxSessions.first as? MXSession else {
            return
        }
        
        MXLog.debug("[AllChatsViewController] initDataSource")
        let recentsListService = RecentsListService(withSession: mainSession)
        let recentsDataSource = RecentsDataSource(matrixSession: mainSession, recentsListService: recentsListService)
        displayList(recentsDataSource)
        recentsDataSource?.setDelegate(self, andRecentsDataSourceMode: self.recentsDataSourceMode)
    }
    
    @objc private func spaceListDidChange() {
        guard self.editActionProvider.shouldUpdate(with: self.mainSession, parentSpace: self.dataSource?.currentSpace) else {
            return
        }
        
        updateUI()
    }

    @objc private func addFabButton() {
        // Nothing to do. We don't need FAB
    }

    @objc private func sections() -> Array<Int> {
        return [
            RecentsDataSourceSectionType.directory.rawValue,
            RecentsDataSourceSectionType.invites.rawValue,
            RecentsDataSourceSectionType.favorites.rawValue,
            RecentsDataSourceSectionType.people.rawValue,
            RecentsDataSourceSectionType.allChats.rawValue,
            RecentsDataSourceSectionType.lowPriority.rawValue,
            RecentsDataSourceSectionType.serverNotice.rawValue,
            RecentsDataSourceSectionType.suggestedRooms.rawValue,
            RecentsDataSourceSectionType.breadcrumbs.rawValue
        ]
    }
    
    override func startActivityIndicator() {
        super.startActivityIndicator()
    }
    
    // MARK: - Actions
    
    @objc private func showSpaceSelectorAction(sender: AnyObject) {
        Analytics.shared.viewRoomTrigger = .roomList
        let currentSpaceId = dataSource?.currentSpace?.spaceId ?? SpaceSelectorConstants.homeSpaceId
        let spaceSelectorBridgePresenter = SpaceSelectorBottomSheetCoordinatorBridgePresenter(session: self.mainSession, selectedSpaceId: currentSpaceId, showHomeSpace: true)
        spaceSelectorBridgePresenter.present(from: self, animated: true)
        spaceSelectorBridgePresenter.delegate = self
        self.spaceSelectorBridgePresenter = spaceSelectorBridgePresenter
    }
    
    // MARK: - UITableViewDataSource
    
    private func sectionType(forSectionAt index: Int) -> RecentsDataSourceSectionType? {
        guard let recentsDataSource = dataSource as? RecentsDataSource else {
            return nil
        }
        
        return recentsDataSource.sections.sectionType(forSectionIndex: index)
    }
    
    override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        guard let sectionType = sectionType(forSectionAt: section), sectionType == .invites else {
            return super.tableView(tableView, numberOfRowsInSection: section)
        }
        
        return dataSource?.tableView(tableView, numberOfRowsInSection: section) ?? 0
    }
    
    override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
        guard let sectionType = sectionType(forSectionAt: indexPath.section), sectionType == .invites else {
            return super.tableView(tableView, cellForRowAt: indexPath)
        }
        
        guard let dataSource = dataSource else {
            MXLog.failure("Missing data source")
            return UITableViewCell()
        }
        return dataSource.tableView(tableView, cellForRowAt: indexPath)
    }
    
    // MARK: - UITableViewDelegate
    
    override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        guard let sectionType = sectionType(forSectionAt: indexPath.section), sectionType == .invites else {
            return super.tableView(tableView, heightForRowAt: indexPath)
        }
        
        return dataSource?.cellHeight(at: indexPath) ?? 0
    }
    
    override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        guard let sectionType = sectionType(forSectionAt: indexPath.section), sectionType == .invites else {
            super.tableView(tableView, didSelectRowAt: indexPath)
            return
        }

        showRoomInviteList()
    }
    
    override func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
        super.tableView(tableView, willDisplay: cell, forRowAt: indexPath)

        guard let recentsDataSource = dataSource as? RecentsDataSource else {
            return
        }
        
        let sectionType = recentsDataSource.sections.sectionType(forSectionIndex: indexPath.section)
        // We need to trottle a bit earlier so the next section is not visible even if the tableview scrolls faster
        guard sectionType == .allChats, let numberOfRowsInSection = recentsDataSource.recentsListService.allChatsRoomListData?.counts.numberOfRooms, indexPath.row == numberOfRowsInSection - 4 else {
            return
        }
        
        tableViewPaginationThrottler.throttle {
            recentsDataSource.paginate(inSection: indexPath.section)
        }
    }

    // MARK: - Toolbar animation
    
    private var initialScrollPosition: Double = 0
    
    private func scrollPosition(of scrollView: UIScrollView) -> Double {
        return scrollView.contentOffset.y + scrollView.adjustedContentInset.top
    }

    override func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
        guard scrollView == recentsTableView else {
            return
        }
        
        initialScrollPosition = scrollPosition(of: scrollView)
    }

    override func scrollViewDidScroll(_ scrollView: UIScrollView) {
        super.scrollViewDidScroll(scrollView)

        guard scrollView == recentsTableView else {
            return
        }
        
        let scrollPosition = scrollPosition(of: scrollView)
        
        if !self.recentsTableView.isDragging && scrollPosition == 0 && self.isToolbarHidden == true {
            self.setToolbarHidden(false, animated: true)
        }

        guard self.recentsTableView.isDragging else {
            return
        }

        guard scrollPosition > 0 && scrollPosition < self.recentsTableView.contentSize.height - self.recentsTableView.bounds.height else {
            return
        }

        let isToolBarHidden: Bool = scrollPosition - initialScrollPosition > 0
        if isToolBarHidden != self.isToolbarHidden {
            self.setToolbarHidden(isToolBarHidden, animated: true)
        }
    }
    
    // MARK: - Empty view management
    
    override func updateEmptyView() {
        guard let mainSession = self.mainSession else {
            return
        }
        
        let title: String
        let informationText: String
        if let currentSpace = self.dataSource?.currentSpace {
            title = VectorL10n.allChatsEmptyViewTitle(currentSpace.summary?.displayName ?? VectorL10n.spaceTag)
            informationText = VectorL10n.allChatsEmptySpaceInformation
        } else {
            let myUser = mainSession.myUser
            let displayName = (myUser?.displayName ?? myUser?.userId) ?? ""
            let appName = AppInfo.current.displayName
            title = VectorL10n.homeEmptyViewTitle(appName, displayName)
            informationText = VectorL10n.allChatsEmptyViewInformation
        }
        
        self.emptyView?.fill(with: emptyViewArtwork,
                             title: title,
                             informationText: informationText)
    }
    
    private var emptyViewArtwork: UIImage {
        if self.dataSource?.currentSpace == nil {
            return ThemeService.shared().isCurrentThemeDark() ? Asset.Images.allChatsEmptyScreenArtworkDark.image : Asset.Images.allChatsEmptyScreenArtwork.image
        } else {
            return ThemeService.shared().isCurrentThemeDark() ? Asset.Images.allChatsEmptySpaceArtworkDark.image : Asset.Images.allChatsEmptySpaceArtwork.image
        }
    }
    
    override func shouldShowEmptyView() -> Bool {
        let shouldShowEmptyView = super.shouldShowEmptyView() && !AllChatsLayoutSettingsManager.shared.hasAnActiveFilter
        
        if shouldShowEmptyView {
            self.navigationItem.searchController = nil
            navigationItem.largeTitleDisplayMode = .never
        } else {
            self.navigationItem.searchController = searchController
            navigationItem.largeTitleDisplayMode = .automatic
        }

        return shouldShowEmptyView
    }
    

    // MARK: - Theme management
    
    override func userInterfaceThemeDidChange() {
        super.userInterfaceThemeDidChange()
        
        guard self.toolbarItems != nil else {
            return
        }
        
        self.update(with: theme)
    }
    
    private func update(with theme: Theme) {
        self.navigationController?.toolbar?.tintColor = theme.colors.accent
    }
    
    // MARK: - Private
    
    private func set(tableHeadeView: UIView?) {
        guard let tableView = recentsTableView else {
            return
        }
        
        tableView.tableHeaderView = tableHeadeView
        tableView.tableHeaderView?.widthAnchor.constraint(equalTo: tableView.widthAnchor).isActive = true
        tableView.tableHeaderView?.layoutIfNeeded()
        tableView.tableHeaderView = self.recentsTableView?.tableHeaderView
    }

    @objc private func setupEditOptions() {
        guard let currentSpace = self.dataSource?.currentSpace else {
            updateRightNavigationItem(with: AllChatsActionProvider().menu)
            return
        }
        
        updateRightNavigationItem(with: spaceActionProvider.updateMenu(with: mainSession, space: currentSpace) { [weak self] menu in
            self?.updateRightNavigationItem(with: menu)
        })
    }

    private func updateUI() {
        let currentSpace = self.dataSource?.currentSpace
        self.title = currentSpace?.summary?.displayName ?? VectorL10n.allChatsTitle
        
        setupEditOptions()
        updateToolbar(with: editActionProvider.updateMenu(with: mainSession, parentSpace: currentSpace, completion: { [weak self] menu in
            self?.updateToolbar(with: menu)
        }))
        updateEmptyView()
        updateBadgeButton()
    }
    
    private func updateRightNavigationItem(with menu: UIMenu) {
        self.navigationItem.rightBarButtonItem = UIBarButtonItem(image: UIImage(systemName: "ellipsis.circle"), menu: menu)
    }
    
    private lazy var spacesButton: BadgedBarButtonItem = {
        let innerButton = UIButton(type: .system)
        innerButton.accessibilityLabel = VectorL10n.spaceSelectorTitle
        innerButton.addTarget(self, action: #selector(self.showSpaceSelectorAction(sender:)), for: .touchUpInside)
        innerButton.setImage(Asset.Images.allChatsSpacesIcon.image, for: .normal)
        return BadgedBarButtonItem(withBaseButton: innerButton, theme: theme)
    }()
    
    @objc private func updateBadgeButton() {
        guard isViewLoaded, let session = mainSession else {
            return
        }
        
        let notificationCount = session.spaceService.missedNotificationsCount
        let hasSpaceInvite = session.spaceService.hasSpaceInvite
        let isBadgeHighlighed = session.spaceService.hasHighlightNotification || hasSpaceInvite
        let badgeValue: String
        
        switch notificationCount {
        case 0:
            badgeValue = hasSpaceInvite ? "!" : "0"
        case (1 ... Constants.spacesButtonMaxCount):
            badgeValue = "\(notificationCount)"
        default:
            badgeValue = "\(Constants.spacesButtonMaxCount)+"
        }
        
        spacesButton.badgeText = badgeValue
        spacesButton.badgeBackgroundColor = isBadgeHighlighed ? theme.noticeColor : theme.noticeSecondaryColor
    }
    
    private func updateToolbar(with menu: UIMenu) {
        guard isViewLoaded else {
            return
        }
        
        self.isToolbarHidden = false
        self.update(with: theme)
        
        self.toolbar.items = [
            spacesButton,
            UIBarButtonItem.flexibleSpace(),
            UIBarButtonItem(image: Asset.Images.allChatsEditIcon.image, menu: menu)
        ]
    }
    
    private func showCreateSpace(parentSpaceId: String?) {
        let coordinator = SpaceCreationCoordinator(parameters: SpaceCreationCoordinatorParameters(session: self.mainSession, parentSpaceId: parentSpaceId))
        let presentable = coordinator.toPresentable()
        self.present(presentable, animated: true, completion: nil)
        coordinator.callback = { [weak self] result in
            guard let self = self else {
                return
            }
            
            coordinator.toPresentable().dismiss(animated: true) {
                self.remove(childCoordinator: coordinator)
                switch result {
                case .cancel:
                    break
                case .done(let spaceId):
                    self.switchSpace(withId: spaceId)
                }
            }
        }
        add(childCoordinator: coordinator)
        coordinator.start()
    }
    
    private func add(childCoordinator: Coordinator) {
        self.childCoordinators.append(childCoordinator)
    }
    
    private func remove(childCoordinator: Coordinator) {
        self.childCoordinators.append(childCoordinator)
    }
    
    private func showSpaceInvite() {
        guard let session = mainSession, let spaceRoom = dataSource?.currentSpace?.room else {
            return
        }
        
        let coordinator = ContactsPickerCoordinator(session: session, room: spaceRoom, initialSearchText: nil, actualParticipants: nil, invitedParticipants: nil, userParticipant: nil)
        coordinator.delegate = self
        coordinator.start()
        add(childCoordinator: coordinator)
        present(coordinator.toPresentable(), animated: true)
    }
    
    private func showSpaceMembers() {
        guard let session = mainSession, let spaceId = dataSource?.currentSpace?.spaceId else {
            return
        }
        
        let coordinator = SpaceMembersCoordinator(parameters: SpaceMembersCoordinatorParameters(userSessionsService: UserSessionsService.shared, session: session, spaceId: spaceId))
        coordinator.delegate = self
        let presentable = coordinator.toPresentable()
        presentable.presentationController?.delegate = self
        coordinator.start()
        add(childCoordinator: coordinator)
        present(presentable, animated: true, completion: nil)
    }

    private func showSpaceSettings() {
        guard let session = mainSession, let spaceId = dataSource?.currentSpace?.spaceId else {
            return
        }
        
        let coordinator = SpaceSettingsModalCoordinator(parameters: SpaceSettingsModalCoordinatorParameters(session: session, spaceId: spaceId, parentSpaceId: nil))
        coordinator.callback = { [weak self] result in
            guard let self = self else { return }
            
            coordinator.toPresentable().dismiss(animated: true) {
                self.remove(childCoordinator: coordinator)
            }
        }
        
        let presentable = coordinator.toPresentable()
        presentable.presentationController?.delegate = self
        present(presentable, animated: true, completion: nil)
        coordinator.start()
        add(childCoordinator: coordinator)
    }
    
    private func showLeaveSpace() {
        guard let session = mainSession, let spaceSummary = dataSource?.currentSpace?.summary else {
            return
        }
        
        let name = spaceSummary.displayName ?? VectorL10n.spaceTag
        
        let selectionHeader = MatrixItemChooserSelectionHeader(title: VectorL10n.leaveSpaceSelectionTitle,
                                                               selectAllTitle: VectorL10n.leaveSpaceSelectionAllRooms,
                                                               selectNoneTitle: VectorL10n.leaveSpaceSelectionNoRooms)
        let paramaters = MatrixItemChooserCoordinatorParameters(session: session,
                                                                title: VectorL10n.leaveSpaceTitle(name),
                                                                detail: VectorL10n.leaveSpaceMessage(name),
                                                                selectionHeader: selectionHeader,
                                                                viewProvider: LeaveSpaceViewProvider(navTitle: nil),
                                                                itemsProcessor: LeaveSpaceItemsProcessor(spaceId: spaceSummary.roomId, session: session))
        let coordinator = MatrixItemChooserCoordinator(parameters: paramaters)
        coordinator.toPresentable().presentationController?.delegate = self
        coordinator.start()
        add(childCoordinator: coordinator)
        coordinator.completion = { [weak self] result in
            // switching to home space
            self?.switchSpace(withId: nil)
            coordinator.toPresentable().dismiss(animated: true) {
                self?.remove(childCoordinator: coordinator)
            }
        }
        present(coordinator.toPresentable(), animated: true)
    }
    
    private func showRoomInviteList() {
        let invitesViewController = RoomInvitesViewController.instantiate()
        invitesViewController.userIndicatorStore = self.userIndicatorStore
        let recentsListService = RecentsListService(withSession: mainSession)
        let recentsDataSource = RecentsDataSource(matrixSession: mainSession, recentsListService: recentsListService)
        invitesViewController.displayList(recentsDataSource)
        self.navigationController?.pushViewController(invitesViewController, animated: true)
    }
    
}

private extension AllChatsViewController {
    enum Constants {
        static let spacesButtonMaxCount: UInt = 999
    }
}

// MARK: - SpaceSelectorBottomSheetCoordinatorBridgePresenterDelegate
extension AllChatsViewController: SpaceSelectorBottomSheetCoordinatorBridgePresenterDelegate {
    
    func spaceSelectorBottomSheetCoordinatorBridgePresenterDidCancel(_ coordinatorBridgePresenter: SpaceSelectorBottomSheetCoordinatorBridgePresenter) {
        coordinatorBridgePresenter.dismiss(animated: true) {
            self.spaceSelectorBridgePresenter = nil
        }
    }
    
    func spaceSelectorBottomSheetCoordinatorBridgePresenterDidSelectHome(_ coordinatorBridgePresenter: SpaceSelectorBottomSheetCoordinatorBridgePresenter) {
        coordinatorBridgePresenter.dismiss(animated: true) {
            self.spaceSelectorBridgePresenter = nil
        }
        
        switchSpace(withId: nil)
    }
    
    func spaceSelectorBottomSheetCoordinatorBridgePresenter(_ coordinatorBridgePresenter: SpaceSelectorBottomSheetCoordinatorBridgePresenter, didSelectSpaceWithId spaceId: String) {
        coordinatorBridgePresenter.dismiss(animated: true) {
            self.spaceSelectorBridgePresenter = nil
        }
        
        switchSpace(withId: spaceId)
    }

    func spaceSelectorBottomSheetCoordinatorBridgePresenter(_ coordinatorBridgePresenter: SpaceSelectorBottomSheetCoordinatorBridgePresenter, didCreateSpaceWithinSpaceWithId parentSpaceId: String?) {
        coordinatorBridgePresenter.dismiss(animated: true) {
            self.spaceSelectorBridgePresenter = nil
        }
        self.showCreateSpace(parentSpaceId: parentSpaceId)
    }

}

// MARK: - UISearchResultsUpdating
extension AllChatsViewController: UISearchResultsUpdating {
    func updateSearchResults(for searchController: UISearchController) {
        guard let searchText = searchController.searchBar.text, !searchText.isEmpty else {
            self.dataSource?.search(withPatterns: nil)
            return
        }
        
        self.dataSource?.search(withPatterns: [searchText])
    }
}

// MARK: - UISearchControllerDelegate
extension AllChatsViewController: UISearchControllerDelegate {
    func willPresentSearchController(_ searchController: UISearchController) {
        // Fix for https://github.com/vector-im/element-ios/issues/6680
        self.recentsTableView.scrollToRow(at: IndexPath(row: 0, section: 0), at: .top, animated: true)
    }
}

// MARK: - UIAdaptivePresentationControllerDelegate
extension AllChatsViewController: UIAdaptivePresentationControllerDelegate {
    
    func presentationControllerDidDismiss(_ presentationController: UIPresentationController) {
        guard let coordinator = childCoordinators.last else {
            return
        }
        
        remove(childCoordinator: coordinator)
    }
}

// MARK: - AllChatsEditActionProviderDelegate
extension AllChatsViewController: AllChatsEditActionProviderDelegate {
    
    func allChatsEditActionProvider(_ actionProvider: AllChatsEditActionProvider, didSelect option: AllChatsEditActionProviderOption) {
        switch option {
        case .exploreRooms:
            joinARoom()
        case .createRoom:
            createNewRoom()
        case .startChat:
            startChat()
        case .createSpace:
            showCreateSpace(parentSpaceId: dataSource?.currentSpace?.spaceId)
        }
    }
    
}

// MARK: - AllChatsSpaceActionProviderDelegate
extension AllChatsViewController: AllChatsSpaceActionProviderDelegate {
    func allChatsSpaceActionProvider(_ actionProvider: AllChatsSpaceActionProvider, didSelect option: AllChatsSpaceActionProviderOption) {
        switch option {
        case .invitePeople:
            showSpaceInvite()
        case .spaceMembers:
            showSpaceMembers()
        case .spaceSettings:
            showSpaceSettings()
        case .leaveSpace:
            showLeaveSpace()
        }
    }
}

// MARK: - ContactsPickerCoordinatorDelegate
extension AllChatsViewController: ContactsPickerCoordinatorDelegate {
    
    func contactsPickerCoordinatorDidStartLoading(_ coordinator: ContactsPickerCoordinatorProtocol) {
    }
    
    func contactsPickerCoordinatorDidEndLoading(_ coordinator: ContactsPickerCoordinatorProtocol) {
    }
    
    func contactsPickerCoordinatorDidClose(_ coordinator: ContactsPickerCoordinatorProtocol) {
        remove(childCoordinator: coordinator)
    }
    
}

// MARK: - SpaceMembersCoordinatorDelegate
extension AllChatsViewController: SpaceMembersCoordinatorDelegate {
    
    func spaceMembersCoordinatorDidCancel(_ coordinator: SpaceMembersCoordinatorType) {
        coordinator.toPresentable().dismiss(animated: true) {
            self.remove(childCoordinator: coordinator)
        }
    }
    
}

// MARK: - BannerPresentationProtocol
extension AllChatsViewController: BannerPresentationProtocol {
    func presentBannerView(_ bannerView: UIView, animated: Bool) {
        self.bannerView = bannerView
    }
    
    func dismissBannerView(animated: Bool) {
        self.bannerView = nil
    }
}

// TODO: The `MasterTabBarViewController` is called from the entire app through the `LegacyAppDelegate`. this part of the code should be moved into `AppCoordinator`
// MARK: - SplitViewMasterViewControllerProtocol
extension AllChatsViewController: SplitViewMasterViewControllerProtocol {

    /// Release the current selected item (if any).
    func releaseSelectedItem() {
        selectedRoomId = nil
        selectedEventId = nil
        selectedRoomSession = nil
        selectedRoomPreviewData = nil
        selectedContact = nil
    }
    
    /// Refresh the missed conversations badges on tab bar icon
    func refreshTabBarBadges() {
        // Nothing to do here as we don't have tab bar
    }
    
    /// Verify the current device if needed.
    ///
    /// - Parameters:
    ///   - session: the matrix session.
    func presentVerifyCurrentSessionAlertIfNeeded(with session: MXSession) {
        guard !RiotSettings.shared.hideVerifyThisSessionAlert,
              !isOnboardingInProgress,
              presentedViewController == nil,
              viewIfLoaded?.window != nil else {
            return
        }
        
        // Force verification if required by the HS configuration
        guard !session.vc_homeserverConfiguration().encryption.isSecureBackupRequired else {
            MXLog.debug("[AllChatsViewController] presentVerifyCurrentSessionAlertIfNeededWithSession: Force verification of the device")
            AppDelegate.theDelegate().presentCompleteSecurity(for: session)
            return
        }

        presentVerifyCurrentSessionAlert(with: session)
    }

    /// Verify others device if needed.
    ///
    /// - Parameters:
    ///   - session: the matrix session.
    func presentReviewUnverifiedSessionsAlertIfNeeded(with session: MXSession) {
        guard BuildSettings.showUnverifiedSessionsAlert,
              !reviewSessionAlertSnoozeController.isSnoozed(),
              presentedViewController == nil,
              viewIfLoaded?.window != nil else {
            return
        }

        if let userId = mainSession.myUserId, let crypto = mainSession.crypto {
            let devices = crypto.devices(forUser: userId).values
            let userHasOneUnverifiedDevice = devices.contains(where: {!$0.trustLevel.isCrossSigningVerified})
            if userHasOneUnverifiedDevice {
                presentReviewUnverifiedSessionsAlert(with: session)
            }
        }
    }
    
    func showOnboardingFlow() {
        MXLog.debug("[AllChatsViewController] showOnboardingFlow")
        self.showOnboardingFlowAndResetSessionFlags(true)
    }

    /// Display the onboarding flow configured to log back into a soft logout session.
    ///
    /// - Parameters:
    ///   - credentials: the credentials of the soft logout session.
    func showSoftLogoutOnboardingFlow(with credentials: MXCredentials?) {
        // This method can be called after the user chooses to clear their data as the MXSession
        // is opened to call logout from. So we only set the credentials when authentication isn't
        // in progress to prevent a second soft logout screen being shown.
        guard self.onboardingCoordinatorBridgePresenter == nil && !self.isOnboardingCoordinatorPreparing else {
            return
        }

        MXLog.debug("[AllChatsViewController] showAuthenticationScreenAfterSoftLogout")
        AuthenticationService.shared.softLogoutCredentials = credentials
        self.showOnboardingFlowAndResetSessionFlags(false)
    }

    /// Open the room with the provided identifier in a specific matrix session.
    ///
    /// - Parameters:
    ///   - parameters: the presentation parameters that contains room information plus display information.
    ///   - completion: the block to execute at the end of the operation.
    func selectRoom(with parameters: RoomNavigationParameters, completion: @escaping () -> Void) {
        releaseSelectedItem()
        
        selectedRoomId = parameters.roomId
        selectedEventId = parameters.eventId
        selectedRoomSession = parameters.mxSession

        allChatsDelegate?.allChatsViewController(self, didSelectRoomWithParameters: parameters, completion: completion)

        refreshSelectedControllerSelectedCellIfNeeded()
    }
    
    /// Open the RoomViewController to display the preview of a room that is unknown for the user.
    /// This room can come from an email invitation link or a simple link to a room.
    /// - Parameters:
    ///   - parameters: the presentation parameters that contains room preview information plus display information.
    ///   - completion: the block to execute at the end of the operation.
    func selectRoomPreview(with parameters: RoomPreviewNavigationParameters, completion: (() -> Void)?) {
        releaseSelectedItem()
        
        let roomPreviewData = parameters.previewData
        
        selectedRoomPreviewData = roomPreviewData
        selectedRoomId = roomPreviewData.roomId
        selectedRoomSession = roomPreviewData.mxSession

        allChatsDelegate?.allChatsViewController(self, didSelectRoomPreviewWithParameters: parameters, completion: completion)

        refreshSelectedControllerSelectedCellIfNeeded()
    }

    /// Open a ContactDetailsViewController to display the information of the provided contact.
    func select(_ contact: MXKContact) {
        let presentationParameters = ScreenPresentationParameters(restoreInitialDisplay: true, stackAboveVisibleViews: false)
        select(contact, with: presentationParameters)
    }
    
    /// Open a ContactDetailsViewController to display the information of the provided contact.
    func select(_ contact: MXKContact, with presentationParameters: ScreenPresentationParameters) {
        releaseSelectedItem()
        
        selectedContact = contact
        
        allChatsDelegate?.allChatsViewController(self, didSelectContact: contact, with: presentationParameters)

        refreshSelectedControllerSelectedCellIfNeeded()
    }

    /// The current number of rooms with missed notifications, including the invites.
    func missedDiscussionsCount() -> UInt {
        guard let session = mxSessions as? [MXSession] else {
            return 0
        }
        
        return session.reduce(0) { $0 + $1.vc_missedDiscussionsCount() }
    }

    /// The current number of rooms with unread highlighted messages.
    func missedHighlightDiscussionsCount() -> UInt {
        guard let session = mxSessions as? [MXSession] else {
            return 0
        }
        
        return session.reduce(0) { $0 + $1.missedHighlightDiscussionsCount() }
    }
    
    /// Emulated `UItabBarViewController.selectedViewController` member
    var selectedViewController: UIViewController? {
        return self
    }
    
    var tabBar: UITabBar? {
        return nil
    }
    
    // MARK: - Private
    
    private func presentVerifyCurrentSessionAlert(with session: MXSession) {
        MXLog.debug("[AllChatsViewController] presentVerifyCurrentSessionAlertWithSession")
        
        let title: String
        let message: String
        
        if MXSDKOptions.sharedInstance().cryptoMigrationDelegate?.needsVerificationUpgrade == true {
            title = VectorL10n.keyVerificationSelfVerifySecurityUpgradeAlertTitle
            message = VectorL10n.keyVerificationSelfVerifySecurityUpgradeAlertMessage
        } else {
            title = VectorL10n.keyVerificationSelfVerifyCurrentSessionAlertTitle
            message = VectorL10n.keyVerificationSelfVerifyCurrentSessionAlertMessage
        }
        
        let alert = UIAlertController(title: title,
                                      message: message,
                                      preferredStyle: .alert)
        
        alert.addAction(UIAlertAction(title: VectorL10n.keyVerificationSelfVerifyCurrentSessionAlertValidateAction,
                                      style: .default,
                                      handler: { action in
            AppDelegate.theDelegate().presentCompleteSecurity(for: session)
        }))
        
        alert.addAction(UIAlertAction(title: VectorL10n.later, style: .cancel))
        
        alert.addAction(UIAlertAction(title: VectorL10n.doNotAskAgain,
                                      style: .destructive,
                                      handler: { action in
            RiotSettings.shared.hideVerifyThisSessionAlert = true
        }))
        
        self.present(alert, animated: true)
    }

    private func presentReviewUnverifiedSessionsAlert(with session: MXSession) {
        MXLog.debug("[AllChatsViewController] presentReviewUnverifiedSessionsAlert")
        
        let alert = UIAlertController(title: VectorL10n.keyVerificationAlertTitle,
                                      message: VectorL10n.keyVerificationAlertBody,
                                      preferredStyle: .alert)
        
        alert.addAction(UIAlertAction(title: VectorL10n.keyVerificationSelfVerifyUnverifiedSessionsAlertValidateAction,
                                      style: .default,
                                      handler: { action in
            self.showSettingsSecurityScreen(with: session)
        }))
        
        alert.addAction(UIAlertAction(title: VectorL10n.later, style: .cancel, handler: { [weak self] _ in
            self?.reviewSessionAlertSnoozeController.snooze()
        }))
        
        present(alert, animated: true)
    }

    private func showSettingsSecurityScreen(with session: MXSession) {
        guard let settingsViewController = SettingsViewController.instantiate() else {
            MXLog.warning("[AllChatsViewController] showSettingsSecurityScreen: cannot instantiate SettingsViewController")
            return
        }
        
        guard let securityViewController = SecurityViewController.instantiate(withMatrixSession: session) else {
            MXLog.warning("[AllChatsViewController] showSettingsSecurityScreen: cannot instantiate SecurityViewController")
            return
        }
        
        settingsViewController.loadViewIfNeeded()
        AppDelegate.theDelegate().restoreInitialDisplay {
            if RiotSettings.shared.enableNewSessionManager {
                self.navigationController?.viewControllers = [self, settingsViewController]
                settingsViewController.showUserSessionsFlow()
            } else {
                self.navigationController?.viewControllers = [self, settingsViewController, securityViewController]
            }
        }
    }
    
    private func showOnboardingFlowAndResetSessionFlags(_ resetSessionFlags: Bool) {
        // Check whether an authentication screen is not already shown or preparing
        guard self.onboardingCoordinatorBridgePresenter == nil && !self.isOnboardingCoordinatorPreparing else {
            return
        }
        
        self.isOnboardingCoordinatorPreparing = true
        self.isOnboardingInProgress = true
        
        if resetSessionFlags {
            resetReviewSessionsFlags()
        }
        
        AppDelegate.theDelegate().restoreInitialDisplay {
            self.presentOnboardingFlow()
        }
    }

    private func resetReviewSessionsFlags() {
        RiotSettings.shared.hideVerifyThisSessionAlert = false
    }
    
    private func presentOnboardingFlow() {
        MXLog.debug("[AllChatsViewController] presentOnboardingFlow")
        
        let onboardingCoordinatorBridgePresenter = OnboardingCoordinatorBridgePresenter()
        onboardingCoordinatorBridgePresenter.completion = { [weak self] in
            guard let self = self else { return }
            
            self.onboardingCoordinatorBridgePresenter?.dismiss(animated: true, completion: {
                self.onboardingCoordinatorBridgePresenter = nil
            })
            
            self.isOnboardingInProgress = false   // Must be set before calling didCompleteAuthentication
            self.allChatsDelegate?.allChatsViewControllerDidCompleteAuthentication(self)
        }
        
        onboardingCoordinatorBridgePresenter.present(from: self, animated: true)
        self.onboardingCoordinatorBridgePresenter = onboardingCoordinatorBridgePresenter
        self.isOnboardingCoordinatorPreparing = false
    }
    
    private func refreshSelectedControllerSelectedCellIfNeeded() {
        guard splitViewController != nil else {
            return
        }
        
        // Refresh selected cell without scrolling the selected cell (We suppose it's visible here)
        self.refreshCurrentSelectedCell(false)
    }
}

private extension MXSpaceService {
    var hasSpaceInvite: Bool {
        spaceSummaries.contains(where: { $0.isJoined == false })
    }
    
    var missedNotificationsCount: UInt {
        let notificationState = notificationCounter.homeNotificationState
        let groupNotifications = notificationState.groupMissedDiscussionsCount
        let directNotifications = notificationState.directMissedDiscussionsCount
        
        // `notificationState.allCount` returns twice the messages for favourite rooms. Fixing it here.
        return groupNotifications + directNotifications
    }
    
    var hasHighlightNotification: Bool {
        notificationCounter.homeNotificationState.allHighlightCount > 0
    }
}
