// Copyright © 2022 Rangeproof Pty Ltd. All rights reserved.

@objc
class GlobalSearchViewController: BaseVC, UITableViewDelegate, UITableViewDataSource {
    
    let isRecentSearchResultsEnabled = false
    
    @objc public var searchText = "" {
        didSet {
            AssertIsOnMainThread()
            // Use a slight delay to debounce updates.
            refreshSearchResults()
        }
    }
    var recentSearchResults: [String] = Array(Storage.shared.getRecentSearchResults().reversed())
    var defaultSearchResults: HomeScreenSearchResultSet = HomeScreenSearchResultSet.noteToSelfOnly
    var searchResultSet: HomeScreenSearchResultSet = HomeScreenSearchResultSet.empty
    private var lastSearchText: String?
    var searcher: FullTextSearcher {
        return FullTextSearcher.shared
    }
    var isLoading = false

    enum SearchSection: Int {
        case noResults
        case contacts
        case messages
        case recent
    }
    
    // MARK: UI Components
    
    internal lazy var searchBar: SearchBar = {
        let result = SearchBar()
        result.tintColor = Colors.text
        result.delegate = self
        result.showsCancelButton = true
        return result
    }()
    
    internal lazy var tableView: UITableView = {
        let result = UITableView(frame: .zero, style: .grouped)
        result.rowHeight = UITableView.automaticDimension
        result.estimatedRowHeight = 60
        result.separatorStyle = .none
        result.keyboardDismissMode = .onDrag
        result.register(EmptySearchResultCell.self, forCellReuseIdentifier: EmptySearchResultCell.reuseIdentifier)
        result.register(ConversationCell.self, forCellReuseIdentifier: ConversationCell.reuseIdentifier)
        result.showsVerticalScrollIndicator = false
        return result
    }()
    
    // MARK: Dependencies

    var dbReadConnection: YapDatabaseConnection {
        return OWSPrimaryStorage.shared().dbReadConnection
    }
    
    // MARK: View Lifecycle
    public override func viewDidLoad() {
        super.viewDidLoad()
        setUpGradientBackground()
        
        tableView.dataSource = self
        tableView.delegate = self
        view.addSubview(tableView)
        tableView.pin(.leading, to: .leading, of: view)
        tableView.pin(.top, to: .top, of: view, withInset: Values.smallSpacing)
        tableView.pin(.trailing, to: .trailing, of: view)
        tableView.pin(.bottom, to: .bottom, of: view)

        navigationItem.hidesBackButton = true
        setupNavigationBar()
    }
    
    public override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        searchBar.becomeFirstResponder()
    }
    
    public override func viewWillDisappear(_ animated: Bool) {
        super.viewWillDisappear(animated)
        searchBar.resignFirstResponder()
    }
    
    private func setupNavigationBar() {
        // This is a workaround for a UI issue that the navigation bar can be a bit higher if
        // the search bar is put directly to be the titleView. And this can cause the tableView
        // in home screen doing a weird scrolling when going back to home screen.
        let searchBarContainer = UIView()
        searchBarContainer.layoutMargins = UIEdgeInsets.zero
        searchBar.sizeToFit()
        searchBar.layoutMargins = UIEdgeInsets.zero
        searchBarContainer.set(.height, to: 44)
        searchBarContainer.set(.width, to: UIScreen.main.bounds.width - 32)
        searchBarContainer.addSubview(searchBar)
        searchBar.autoPinEdgesToSuperviewMargins()
        navigationItem.titleView = searchBarContainer
    }
    
    private func reloadTableData() {
        tableView.reloadData()
    }
    
    // MARK: Update Search Results

    var refreshTimer: Timer?
    
    private func refreshSearchResults() {
        refreshTimer?.invalidate()
        refreshTimer = WeakTimer.scheduledTimer(timeInterval: 0.1, target: self, userInfo: nil, repeats: false) { [weak self] _ in
            guard let self = self else { return }
            self.updateSearchResults(searchText: self.searchText)
        }
    }
    
    private func updateSearchResults(searchText rawSearchText: String) {

        let searchText = rawSearchText.stripped
        guard searchText.count > 0 else {
            searchResultSet = defaultSearchResults
            lastSearchText = nil
            reloadTableData()
            return
        }
        guard lastSearchText != searchText else { return }

        lastSearchText = searchText

        var searchResults: HomeScreenSearchResultSet?
        self.dbReadConnection.asyncRead({[weak self] transaction in
            guard let self = self else { return }
            self.isLoading = true
            // The max search result count is set according to the keyword length. This is just a workaround for performance issue.
            // The longer and more accurate the keyword is, the less search results should there be.
            searchResults = self.searcher.searchForHomeScreen(searchText: searchText, maxSearchResults: min(searchText.count * 50, 500),  transaction: transaction)
        }, completionBlock: { [weak self] in
            AssertIsOnMainThread()
            guard let self = self, let results = searchResults, self.lastSearchText == searchText else { return }
            self.searchResultSet = results
            self.isLoading = false
            self.reloadTableData()
            self.refreshTimer = nil
        })
    }
    
    // MARK: Interaction
    @objc func clearRecentSearchResults() {
        recentSearchResults = []
        tableView.reloadSections([ SearchSection.recent.rawValue ], with: .top)
        Storage.shared.clearRecentSearchResults()
    }

}

// MARK: - UISearchBarDelegate
extension GlobalSearchViewController: UISearchBarDelegate {
    public func searchBarTextDidBeginEditing(_ searchBar: UISearchBar) {
        self.updateSearchText()
    }
    
    public func searchBarTextDidEndEditing(_ searchBar: UISearchBar) {
        self.updateSearchText()
    }
    
    public func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
        self.updateSearchText()
    }
    
    public func searchBarCancelButtonClicked(_ searchBar: UISearchBar) {
        searchBar.text = nil
        searchBar.resignFirstResponder()
        self.navigationController?.popViewController(animated: true)
    }
    
    func updateSearchText() {
        guard let searchText = searchBar.text?.ows_stripped() else { return }
        self.searchText = searchText
    }
}

// MARK: - UITableViewDelegate & UITableViewDataSource
extension GlobalSearchViewController {
    
    // MARK: UITableViewDelegate
    
    public func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        tableView.deselectRow(at: indexPath, animated: false)
        guard let searchSection = SearchSection(rawValue: indexPath.section) else { return }
        switch searchSection {
        case .noResults:
            SNLog("shouldn't be able to tap 'no results' section")
        case .contacts:
            let sectionResults = searchResultSet.conversations
            guard let searchResult = sectionResults[safe: indexPath.row], let threadId = searchResult.thread.threadRecord.uniqueId, let thread = TSThread.fetch(uniqueId: threadId) else { return }
            show(thread, highlightedMessageID: nil, animated: true)
        case .messages:
            let sectionResults = searchResultSet.messages
            guard let searchResult = sectionResults[safe: indexPath.row], let threadId = searchResult.thread.threadRecord.uniqueId, let thread = TSThread.fetch(uniqueId: threadId) else { return }
            show(thread, highlightedMessageID: searchResult.messageId, animated: true)
        case .recent:
            guard let threadId = recentSearchResults[safe: indexPath.row], let thread = TSThread.fetch(uniqueId: threadId) else { return }
            show(thread, highlightedMessageID: nil, animated: true, isFromRecent: true)
        }
    }
    
    private func show(_ thread: TSThread, highlightedMessageID: String?, animated: Bool, isFromRecent: Bool = false) {
        if let threadId = thread.uniqueId {
            recentSearchResults = Array(Storage.shared.addSearchResults(threadID: threadId).reversed())
        }
        
        DispatchMainThreadSafe {
            if let presentedVC = self.presentedViewController {
                presentedVC.dismiss(animated: false, completion: nil)
            }
            let conversationVC = ConversationVC(thread: thread, focusedMessageID: highlightedMessageID)
            var viewControllers = self.navigationController?.viewControllers
            if isFromRecent, let index = viewControllers?.firstIndex(of: self) { viewControllers?.remove(at: index) }
            viewControllers?.append(conversationVC)
            self.navigationController?.setViewControllers(viewControllers!, animated: true)
        }
    }

    // MARK: UITableViewDataSource
    
    public func numberOfSections(in tableView: UITableView) -> Int {
        return 4
    }
    
    public func tableView(_ tableView: UITableView, viewForFooterInSection section: Int) -> UIView? {
        UIView()
    }

    public func tableView(_ tableView: UITableView, heightForFooterInSection section: Int) -> CGFloat {
        .leastNonzeroMagnitude
    }

    public func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
        guard nil != self.tableView(tableView, titleForHeaderInSection: section) else {
            return .leastNonzeroMagnitude
        }
        return UITableView.automaticDimension
    }
    
    public func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
        guard let searchSection = SearchSection(rawValue: section) else { return nil }
        
        guard let title = self.tableView(tableView, titleForHeaderInSection: section) else {
            return UIView()
        }
        
        let titleLabel = UILabel()
        titleLabel.text = title
        titleLabel.textColor = Colors.text
        titleLabel.font = .boldSystemFont(ofSize: Values.mediumFontSize)
        
        let container = UIView()
        container.backgroundColor = Colors.cellBackground
        container.layoutMargins = UIEdgeInsets(top: Values.smallSpacing, left: Values.mediumSpacing, bottom: Values.smallSpacing, right: Values.mediumSpacing)
        container.addSubview(titleLabel)
        titleLabel.autoPinEdgesToSuperviewMargins()
        
        if searchSection == .recent {
            let clearButton = UIButton()
            clearButton.setTitle("Clear", for: .normal)
            clearButton.setTitleColor(Colors.text, for: UIControl.State.normal)
            clearButton.titleLabel!.font = .boldSystemFont(ofSize: Values.smallFontSize)
            clearButton.addTarget(self, action: #selector(clearRecentSearchResults), for: .touchUpInside)
            container.addSubview(clearButton)
            clearButton.autoPinTrailingToSuperviewMargin()
            clearButton.autoVCenterInSuperview()
        }

        return container
    }

    public func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
        guard let searchSection = SearchSection(rawValue: section) else { return nil }

        switch searchSection {
        case .noResults:
            return nil
        case .contacts:
            if searchResultSet.conversations.count > 0 {
                return NSLocalizedString("SEARCH_SECTION_CONTACTS", comment: "")
            } else {
                return nil
            }
        case .messages:
            if searchResultSet.messages.count > 0 {
                return NSLocalizedString("SEARCH_SECTION_MESSAGES", comment: "")
            } else {
                return nil
            }
        case .recent:
            if recentSearchResults.count > 0  && searchText.isEmpty && isRecentSearchResultsEnabled {
                return NSLocalizedString("SEARCH_SECTION_RECENT", comment: "")
            } else {
                return nil
            }
        }
    }

    public func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        guard let searchSection = SearchSection(rawValue: section) else { return 0 }
        switch searchSection {
        case .noResults:
            return (searchText.count > 0 && searchResultSet.isEmpty) ? 1 : 0
        case .contacts:
            return searchResultSet.conversations.count
        case .messages:
            return searchResultSet.messages.count
        case .recent:
            return searchText.isEmpty && isRecentSearchResultsEnabled ? recentSearchResults.count : 0
        }
    }

    public func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
        return UITableView.automaticDimension
    }

    public func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {

        guard let searchSection = SearchSection(rawValue: indexPath.section) else {
            return UITableViewCell()
        }

        switch searchSection {
        case .noResults:
            guard let cell = tableView.dequeueReusableCell(withIdentifier: EmptySearchResultCell.reuseIdentifier) as? EmptySearchResultCell, indexPath.row == 0 else { return UITableViewCell() }
            cell.configure(isLoading: isLoading)
            return cell
        case .contacts:
            let sectionResults = searchResultSet.conversations
            let cell = tableView.dequeueReusableCell(withIdentifier: ConversationCell.reuseIdentifier) as! ConversationCell
            cell.isShowingGlobalSearchResult = true
            let searchResult = sectionResults[safe: indexPath.row]
            cell.threadViewModel = searchResult?.thread
            cell.configure(messageDate: searchResult?.messageDate, snippet: searchResult?.snippet, searchText: searchResultSet.searchText)
            return cell
        case .messages:
            let sectionResults = searchResultSet.messages
            let cell = tableView.dequeueReusableCell(withIdentifier: ConversationCell.reuseIdentifier) as! ConversationCell
            cell.isShowingGlobalSearchResult = true
            let searchResult = sectionResults[safe: indexPath.row]
            cell.threadViewModel = searchResult?.thread
            var message: TSMessage? = nil
            if let messageId = searchResult?.messageId { message = TSMessage.fetch(uniqueId: messageId) }
            cell.configure(messageDate: searchResult?.messageDate, snippet: searchResult?.snippet, searchText: searchResultSet.searchText, message: message)
            return cell
        case .recent:
            let cell = tableView.dequeueReusableCell(withIdentifier: ConversationCell.reuseIdentifier) as! ConversationCell
            cell.isShowingGlobalSearchResult = true
            dbReadConnection.read { transaction in
                guard let threadId = self.recentSearchResults[safe: indexPath.row], let thread = TSThread.fetch(uniqueId: threadId, transaction: transaction) else { return }
                cell.threadViewModel = ThreadViewModel(thread: thread, transaction: transaction)
            }
            cell.configureForRecent()
            return cell
        }
    }
}
