<commit_before>// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/

import Foundation
import Shared
import Storage
import SwiftUI

private class FetchInProgressError: MaybeErrorType {
    internal var description: String {
        return "Fetch is already in-progress"
    }
}

class HistoryPanelViewModel: Loggable, FeatureFlaggable {

    enum Sections: Int, CaseIterable {
        case additionalHistoryActions
        case today
        case yesterday
        case lastWeek
        case lastMonth
        case older
        case searchResults

        var title: String? {
            switch self {
            case .today:
                return .LibraryPanel.Sections.Today
            case .yesterday:
                return .LibraryPanel.Sections.Yesterday
            case .lastWeek:
                return .LibraryPanel.Sections.LastWeek
            case .lastMonth:
                return .LibraryPanel.Sections.LastMonth
            case .older:
                return .LibraryPanel.Sections.Older
            case .additionalHistoryActions, .searchResults:
                return nil
            }
        }
    }

    // MARK: - Properties

    private let profile: Profile
    // Request limit and offset
    private let queryFetchLimit = 100
    private var currentFetchOffset = 0
    private let searchQueryFetchLimit = 50
    private var searchCurrentFetchOffset = 0

    // Search
    var isSearchInProgress = false
    var searchResultSites = [Site]()
    var searchHistoryPlaceholder: String = .LibraryPanel.History.SearchHistoryPlaceholder

    let historyActionables = HistoryActionablesModel.activeActionables
    var visibleSections: [Sections] = []
    // Groups items we should have a single datasource containing sites and groups
    var searchTermGroups: [ASGroup<Site>] = []
    // Only individual sites
    var groupedSites = DateGroupedTableData<Site>()
    var isFetchInProgress = false
    var shouldResetHistory = false
    var hiddenSections: [Sections] = []

    private var hasRecentlyClosed: Bool {
        return profile.recentlyClosedTabs.tabs.count > 0
    }

    var emptyStateText: String {
        return !isSearchInProgress ? .HistoryPanelEmptyStateTitle : .LibraryPanel.History.NoHistoryResult
    }

    let historyPanelNotifications = [Notification.Name.FirefoxAccountChanged,
                                     Notification.Name.PrivateDataClearedHistory,
                                     Notification.Name.DynamicFontChanged,
                                     Notification.Name.DatabaseWasReopened,
                                     Notification.Name.OpenClearRecentHistory]

    // MARK: - Inits

    init(profile: Profile) {
        self.profile = profile
    }

    deinit {
        browserLog.debug("HistoryPanelViewModel Deinitialized.")
    }

    /// Begin the process of fetching history data, and creating ASGroups from them. A prefetch also triggers this.
    func reloadData(completion: @escaping (Bool) -> Void) {
        // Can be called while app backgrounded and the db closed, don't try to reload the data source in this case
        guard !profile.isShutdown, !isFetchInProgress else {
            browserLog.debug("HistoryPanel tableView data could NOT be reloaded! Either the profile wasn't shut down, or there's a fetch in progress.")
            completion(false)
            return
        }

        if shouldResetHistory {
            resetHistory()
        }

        fetchData().uponQueue(.global(qos: .userInteractive)) { result in
            guard let fetchedSites = result.successValue?.asArray(), !fetchedSites.isEmpty else {
                completion(false)
                return
            }

            self.currentFetchOffset += self.queryFetchLimit
            if self.featureFlags.isFeatureEnabled(.historyGroups, checking: .buildOnly) {
                self.populateASGroups(fetchedSites: fetchedSites) {
                    self.buildVisibleSections()
                    completion(true)
                }
            } else {
                self.populateHistorySites(fetchedSites: fetchedSites)
                completion(true)
            }
        }
    }

    func performSearch(term: String, completion: @escaping (Bool) -> Void) {
        isFetchInProgress = true

        profile.history.getHistory(matching: term,
                                   limit: searchQueryFetchLimit,
                                   offset: searchCurrentFetchOffset) { results in
            self.isFetchInProgress = false
            self.searchResultSites = results
            completion(!results.isEmpty)
        }
    }

    func shouldShowEmptyState(searchText: String) -> Bool {
        guard isSearchInProgress else { return groupedSites.isEmpty && searchTermGroups.isEmpty }

        // if the search text is empty we show the regular history so the empty should not show
        return !searchText.isEmpty ? searchResultSites.isEmpty : false
    }

    func updateSearchOffset() {
        searchCurrentFetchOffset += searchQueryFetchLimit
    }

    func collapseSection(sectionIndex: Int) {
        guard let sectionToHide = visibleSections[safe: sectionIndex - 1] else { return }

        if hiddenSections.contains(where: { $0 == sectionToHide }) {
            let index = hiddenSections.firstIndex(of: sectionToHide) ?? 0
            hiddenSections.remove(at: index)
        } else {
            hiddenSections.append(sectionToHide)
        }
    }

    func removeAllData() {
        /// Since we remove all data, we reset our fetchOffset back to the start.
        currentFetchOffset = 0

        searchTermGroups.removeAll()
        groupedSites = DateGroupedTableData<Site>()
        buildVisibleSections()
    }

    func isSectionCollapsed(sectionIndex: Int) -> Bool {
        guard let sectionToHide = visibleSections[safe: sectionIndex] else { return false }

        return hiddenSections.contains(where: { $0 == sectionToHide })
    }

    // MARK: - Private helpers

    /// A helper for the reload function.
    private func fetchData() -> Deferred<Maybe<Cursor<Site>>> {
        isFetchInProgress = true

        return profile.history.getSitesByLastVisit(limit: queryFetchLimit, offset: currentFetchOffset) >>== { result in
            // Force 100ms delay between resolution of the last batch of results<commit_msg> Bugfix FXIOS-4250 [v101] Improvement for Firefox is unresponsive after scrolling the history list #10771  (#10829)<commit_after>// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/

import Foundation
import Shared
import Storage
import SwiftUI

private class FetchInProgressError: MaybeErrorType {
    internal var description: String {
        return "Fetch is already in-progress"
    }
}

class HistoryPanelViewModel: Loggable, FeatureFlaggable {

    enum Sections: Int, CaseIterable {
        case additionalHistoryActions
        case today
        case yesterday
        case lastWeek
        case lastMonth
        case older
        case searchResults

        var title: String? {
            switch self {
            case .today:
                return .LibraryPanel.Sections.Today
            case .yesterday:
                return .LibraryPanel.Sections.Yesterday
            case .lastWeek:
                return .LibraryPanel.Sections.LastWeek
            case .lastMonth:
                return .LibraryPanel.Sections.LastMonth
            case .older:
                return .LibraryPanel.Sections.Older
            case .additionalHistoryActions, .searchResults:
                return nil
            }
        }
    }

    // MARK: - Properties

    private let profile: Profile
    // Request limit and offset
    private let queryFetchLimit = 100
    private var currentFetchOffset = 0
    private let searchQueryFetchLimit = 50
    private var searchCurrentFetchOffset = 0

    // Search
    var isSearchInProgress = false
    var searchResultSites = [Site]()
    var searchHistoryPlaceholder: String = .LibraryPanel.History.SearchHistoryPlaceholder

    let historyActionables = HistoryActionablesModel.activeActionables
    var visibleSections: [Sections] = []
    // Groups items we should have a single datasource containing sites and groups
    var searchTermGroups: [ASGroup<Site>] = []
    // Only individual sites
    var groupedSites = DateGroupedTableData<Site>()
    var isFetchInProgress = false
    var shouldResetHistory = false
    var hiddenSections: [Sections] = []

    private var hasRecentlyClosed: Bool {
        return profile.recentlyClosedTabs.tabs.count > 0
    }

    var emptyStateText: String {
        return !isSearchInProgress ? .HistoryPanelEmptyStateTitle : .LibraryPanel.History.NoHistoryResult
    }

    let historyPanelNotifications = [Notification.Name.FirefoxAccountChanged,
                                     Notification.Name.PrivateDataClearedHistory,
                                     Notification.Name.DynamicFontChanged,
                                     Notification.Name.DatabaseWasReopened,
                                     Notification.Name.OpenClearRecentHistory]

    // MARK: - Inits

    init(profile: Profile) {
        self.profile = profile
    }

    deinit {
        browserLog.debug("HistoryPanelViewModel Deinitialized.")
    }

    /// Begin the process of fetching history data, and creating ASGroups from them. A prefetch also triggers this.
    func reloadData(completion: @escaping (Bool) -> Void) {
        // Can be called while app backgrounded and the db closed, don't try to reload the data source in this case
        guard !profile.isShutdown, !isFetchInProgress else {
            browserLog.debug("HistoryPanel tableView data could NOT be reloaded! Either the profile wasn't shut down, or there's a fetch in progress.")
            completion(false)
            return
        }

        if shouldResetHistory {
            resetHistory()
        }

        fetchData().uponQueue(.global(qos: .userInteractive)) { result in
            guard let fetchedSites = result.successValue?.asArray(), !fetchedSites.isEmpty else {
                completion(false)
                return
            }

            self.currentFetchOffset += self.queryFetchLimit
            if self.featureFlags.isFeatureEnabled(.historyGroups, checking: .buildOnly) {
                self.populateASGroups(fetchedSites: fetchedSites) {
                    self.buildVisibleSections()
                    completion(true)
                }
            } else {
                self.populateHistorySites(fetchedSites: fetchedSites)
                completion(true)
            }
        }
    }

    func performSearch(term: String, completion: @escaping (Bool) -> Void) {
        isFetchInProgress = true

        profile.history.getHistory(matching: term,
                                   limit: searchQueryFetchLimit,
                                   offset: searchCurrentFetchOffset) { results in
            self.isFetchInProgress = false
            self.searchResultSites = results
            completion(!results.isEmpty)
        }
    }

    func shouldShowEmptyState(searchText: String) -> Bool {
        guard isSearchInProgress else { return groupedSites.isEmpty && searchTermGroups.isEmpty }

        // if the search text is empty we show the regular history so the empty should not show
        return !searchText.isEmpty ? searchResultSites.isEmpty : false
    }

    func updateSearchOffset() {
        searchCurrentFetchOffset += searchQueryFetchLimit
    }

    func collapseSection(sectionIndex: Int) {
        guard let sectionToHide = visibleSections[safe: sectionIndex - 1] else { return }

        if hiddenSections.contains(where: { $0 == sectionToHide }) {
            let index = hiddenSections.firstIndex(of: sectionToHide) ?? 0
            hiddenSections.remove(at: index)
        } else {
            hiddenSections.append(sectionToHide)
        }
    }

    func removeAllData() {
        /// Since we remove all data, we reset our fetchOffset back to the start.
        currentFetchOffset = 0

        searchTermGroups.removeAll()
        groupedSites = DateGroupedTableData<Site>()
        buildVisibleSections()
    }

    func isSectionCollapsed(sectionIndex: Int) -> Bool {
        guard let sectionToHide = visibleSections[safe: sectionIndex] else { return false }

        return hiddenSections.contains(where: { $0 == sectionToHide })
    }

    // MARK: - Private helpers

    /// A helper for the reload function.
    private func fetchData() -> Deferred<Maybe<Cursor<Site>>> {
        guard !isFetchInProgress else {
            return deferMaybe(FetchInProgressError())
        }

        isFetchInProgress = true

        return profile.history.getSitesByLastVisit(limit: queryFetchLimit, offset: currentFetchOffset) >>== { result in
            // Force 100ms delay between resolution of the last batch of results
{'fraction_non_alphanumeric': 0.07022067187381512, 'fraction_numerical': 0.004322438765450823, 'mean_word_length': 2.1808972503617947, 'pattern_counts': {'":': 0, '<': 15, '<?xml version=': 0, '>': 31, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 12, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '6439359', 'n_tokens_mistral': 3411, 'n_tokens_neox': 3155, 'n_words': 935}