<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 Shared
import UIKit
import Storage
import SDWebImage
import XCGLogger
import SyncTelemetry

private let log = Logger.browserLogger

// MARK: -  UX

struct FirefoxHomeUX {
    static let highlightCellHeight: CGFloat = UIDevice.current.userInterfaceIdiom == .pad ? 250 : 200
    static let jumpBackInCellHeight: CGFloat = 120
    static let recentlySavedCellHeight: CGFloat = 136
    static let sectionInsetsForSizeClass = UXSizeClasses(compact: 0, regular: 101, other: 15)
    static let numberOfItemsPerRowForSizeClassIpad = UXSizeClasses(compact: 3, regular: 4, other: 2)
    static let spacingBetweenSections: CGFloat = 24
    static let SectionInsetsForIpad: CGFloat = 101
    static let MinimumInsets: CGFloat = 15
    static let LibraryShortcutsHeight: CGFloat = 90
    static let LibraryShortcutsMaxWidth: CGFloat = 375
    static let customizeHomeHeight: CGFloat = 144
}

struct FxHomeAccessibilityIdentifiers {
    struct MoreButtons {
        static let recentlySaved = "recentlySavedSectionMoreButton"
        static let jumpBackIn = "jumpBackInSectionMoreButton"
    }

    struct SectionTitles {
        static let jumpBackIn = "jumpBackInTitle"
        static let recentlySaved = "jumpBackInTitle"
        static let pocket = "pocketTitle"
        static let library = "libraryTitle"
        static let topSites = "topSitesTitle"
    }
}

struct FxHomeDevStrings {
    struct GestureRecognizers {
        static let dismissOverlay = "dismissOverlay"
    }
}


/*
 Size classes are the way Apple requires us to specify our UI.
 Split view on iPad can make a landscape app appear with the demensions of an iPhone app
 Use UXSizeClasses to specify things like offsets/itemsizes with respect to size classes
 For a primer on size classes https://useyourloaf.com/blog/size-classes/
 */
struct UXSizeClasses {
    var compact: CGFloat
    var regular: CGFloat
    var unspecified: CGFloat

    init(compact: CGFloat, regular: CGFloat, other: CGFloat) {
        self.compact = compact
        self.regular = regular
        self.unspecified = other
    }

    subscript(sizeClass: UIUserInterfaceSizeClass) -> CGFloat {
        switch sizeClass {
            case .compact:
                return self.compact
            case .regular:
                return self.regular
            case .unspecified:
                return self.unspecified
            @unknown default:
                fatalError()
        }

    }
}

// MARK: - Home Panel

protocol HomePanelDelegate: AnyObject {
    func homePanelDidRequestToOpenInNewTab(_ url: URL, isPrivate: Bool)
    func homePanel(didSelectURL url: URL, visitType: VisitType, isGoogleTopSite: Bool)
    func homePanelDidRequestToOpenLibrary(panel: LibraryPanelType)
    func homePanelDidRequestToOpenTabTray(withFocusedTab tabToFocus: Tab?)
    func homePanelDidRequestToCustomizeHomeSettings()
    func homePanelDidPresentContextualHint(type: ContextualHintViewType)
    func homePanelDidDismissContextualHint(type: ContextualHintViewType)
}

protocol HomePanel: Themeable {
    var homePanelDelegate: HomePanelDelegate? { get set }
}

enum HomePanelType: Int {
    case topSites = 0

    var internalUrl: URL {
        let aboutUrl: URL! = URL(string: "\(InternalURL.baseUrl)/\(AboutHomeHandler.path)")
        return URL(string: "#panel=\(self.rawValue)", relativeTo: aboutUrl)!
    }
}

protocol HomePanelContextMenu {
    func getSiteDetails(for indexPath: IndexPath) -> Site?
    func getContextMenuActions(for site: Site, with indexPath: IndexPath) -> [PhotonActionSheetItem]?
    func presentContextMenu(for indexPath: IndexPath)
    func presentContextMenu(for site: Site, with indexPath: IndexPath, completionHandler: @escaping () -> PhotonActionSheet?)
}

extension HomePanelContextMenu {
    func presentContextMenu(for indexPath: IndexPath) {
        guard let site = getSiteDetails(for: indexPath) else { return }

        presentContextMenu(for: site, with: indexPath, completionHandler: {
            return self.contextMenu(for: site, with: indexPath)
        })
    }

    func contextMenu(for site: Site, with indexPath: IndexPath) -> PhotonActionSheet? {
        guard let actions = self.getContextMenuActions(for: site, with: indexPath) else { return nil }

        let contextMenu = PhotonActionSheet(site: site, actions: actions)
        contextMenu.modalPresentationStyle = .overFullScreen
        contextMenu.modalTransitionStyle = .crossDissolve

        let generator = UIImpactFeedbackGenerator(style: .heavy)
        generator.impactOccurred()

        return contextMenu
    }

    func getDefaultContextMenuActions(for site: Site, homePanelDelegate: HomePanelDelegate?) -> [PhotonActionSheetItem]? {
        guard let siteURL = URL(string: site.url) else { return nil }

        let openInNewTabAction = PhotonActionSheetItem(title: Strings.OpenInNewTabContextMenuTitle, iconString: "quick_action_new_tab") { _, _ in
            homePanelDelegate?.homePanelDidRequestToOpenInNewTab(siteURL, isPrivate: false)
        }

        let openInNewPrivateTabAction = PhotonActionSheetItem(title: Strings.OpenInNewPrivateTabContextMenuTitle, iconString: "quick_action_new_private_tab") { _, _ in
            homePanelDelegate?.homePanelDidRequestToOpenInNewTab(siteURL, isPrivate: true)
        }

        return [openInNewTabAction, openInNewPrivateTabAction]
    }
}

// MARK: - HomeVC

class FirefoxHomeViewController: UICollectionViewController, HomePanel, FeatureFlagsProtocol {
    weak var homePanelDelegate: HomePanelDelegate?
    weak var libraryPanelDelegate: LibraryPanelDelegate?
    fileprivate var hasPresentedContextualHint = false
    fileprivate var didRoate = false
    fileprivate let profile: Profile
    fileprivate let pocketAPI = Pocket()
    fileprivate let flowLayout = UICollectionViewFlowLayout()
    fileprivate let experiments: NimbusApi
    fileprivate var hasSentPocketSectionEvent = false
    fileprivate var hasSentJumpBackInSectionEvent = false
    fileprivate var timer: Timer?
    fileprivate var contextualSourceView = UIView()
    var recentlySavedViewModel = FirefoxHomeRecentlySavedViewModel()
    var jumpBackInViewModel = FirefoxHomeJumpBackInViewModel()
    fileprivate lazy var topSitesManager: ASHorizontalScrollCellManager = {
        let manager = ASHorizontalScrollCellManager()
        return manager
    }()
    
    var contextualHintViewController = ContextualHintViewController(hintType: .jumpBackIn)
    
    lazy var overlayView: UIView = .build { [weak self] overlayView in
        overlayView.backgroundColor = UIColor.Photon.Grey90A10
        overlayView.isHidden = true
    }

    fileprivate lazy var longPressRecognizer: UILongPressGestureRecognizer = {
        return UILongPressGestureRecognizer(target: self, action: #selector(longPress))
    }()
    
    private var tapGestureRecognizer: UITapGestureRecognizer {
        let dismissOverlay = UITapGestureRecognizer(target: self, action: #selector(dismissOverlayMode))
        dismissOverlay.name = FxHomeDevStrings.GestureRecognizers.dismissOverlay
        dismissOverlay.cancelsTouchesInView = false
        return dismissOverlay
    }

    // Not used for displaying. Only used for calculating layout.
    lazy var topSiteCell: ASHorizontalScrollCell = {
        let customCell = ASHorizontalScrollCell(frame: CGRect(width: self.view.frame.size.width, height: 0))
        customCell.delegate = self.topSitesManager
        return customCell
    }()
    lazy var defaultBrowserCard: DefaultBrowserCard = .build { card in
        card.backgroundColor = UIColor.theme.homePanel.topSitesBackground
    }

    var pocketStories: [PocketStory] = []
    var hasRecentBookmarks = false
    var hasReadingListitems = false
    var currentTab: Tab? {
        let tabManager = BrowserViewController.foregroundBVC().tabManager
        return tabManager.selectedTab
    }

    lazy var homescreen = experiments.withVariables(featureId: .homescreen, sendExposureEvent: false) {
        Homescreen(variables: $0)
    }

    // MARK: - Section availability variables
    var isTopSitesSectionEnabled: Bool {
        homescreen.sectionsEnabled[.topSites] == true
    }

    var isYourLibrarySectionEnabled: Bool {
        UIDevice.current.userInterfaceIdiom != .pad &&
            homescreen.sectionsEnabled[.libraryShortcuts] == true
    }

    var isJumpBackInSectionEnabled: Bool {
        guard featureFlags.isFeatureActiveForBuild(.jumpBackIn),
              homescreen.sectionsEnabled[.topSites] == true,
              featureFlags.userPreferenceFor(.jumpBackIn) == UserFeaturePreference.enabled
        else { return false }

        let tabManager = BrowserViewController.foregroundBVC().tabManager
        return !(tabManager.selectedTab?.isPrivate ?? false)
            && !tabManager.recentlyAccessedNormalTabs.isEmpty
}

    var isRecentlySavedSectionEnabled: Bool {
        guard featureFlags.isFeatureActiveForBuild(.recentlySaved),
              homescreen.sectionsEnabled[.recentlySaved] == true,
              featureFlags.userPreferenceFor(.recentlySaved) == UserFeaturePreference.enabled
        else { return false }

        return hasRecentBookmarks || hasReadingListitems
    }

    var isPocketSectionEnabled: Bool {
        // For Pocket, the user preference check returns a user preference if it exists in
        // UserDefaults, and, if it does not, it will return a default preference based on
        // a (nimbus pocket section enabled && Pocket.isLocaleSupported) check
        guard featureFlags.isFeatureActiveForBuild(.pocket),
              featureFlags.userPreferenceFor(.pocket) == UserFeaturePreference.enabled
        else { return false }

        return true
    }

    // MARK: - Initializers
    init(profile: Profile, experiments: NimbusApi = Experiments.shared) {
        self.profile = profile
        self.experiments = experiments
        super.init(collectionViewLayout: flowLayout)
        collectionView?.delegate = self
        collectionView?.dataSource = self
        collectionView.translatesAutoresizingMaskIntoConstraints = false

        collectionView?.addGestureRecognizer(longPressRecognizer)
        currentTab?.lastKnownUrl?.absoluteString.hasPrefix("internal://") ?? false ? collectionView?.addGestureRecognizer(tapGestureRecognizer) : nil

        let refreshEvents: [Notification.Name] = [.DynamicFontChanged, .HomePanelPrefsChanged, .DisplayThemeChanged]
        refreshEvents.forEach { NotificationCenter.default.addObserver(self, selector: #selector(reload), name: $0, object: nil) }
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    deinit {
        timer?.invalidate()
        timer = nil
    }

    // MARK: - View lifecycle
    override func viewDidLoad() {
        super.viewDidLoad()

        Section.allCases.forEach { collectionView.register($0.cellType, forCellWithReuseIdentifier: $0.cellIdentifier) }
        self.collectionView?.register(ASHeaderView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: "Header")
        collectionView?.keyboardDismissMode = .onDrag
        collectionView?.backgroundColor = .clear
        self.view.addSubviews(overlayView)
        self.view.addSubview(contextualSourceView)
        contextualSourceView.backgroundColor = .clear
        
        if #available(iOS 14.0, *), !UserDefaults.standard.bool(forKey: "DidDismissDefaultBrowserCard") {
            self.view.addSubview(defaultBrowserCard)
            NSLayoutConstraint.activate([
                defaultBrowserCard.topAnchor.constraint(equalTo: view.topAnchor),
                defaultBrowserCard.bottomAnchor.constraint(equalTo: collectionView.topAnchor),
                defaultBrowserCard.centerXAnchor.constraint(equalTo: view.centerXAnchor),
                defaultBrowserCard.widthAnchor.constraint(equalToConstant: 380),

                collectionView.topAnchor.constraint(equalTo: defaultBrowserCard.bottomAnchor),
                collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
                collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
                collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            ])

            defaultBrowserCard.dismissClosure = {
                self.dismissDefaultBrowserCard()
            }
        }
        
        NSLayoutConstraint.activate([
            overlayView.topAnchor.constraint(equalTo: view.topAnchor),
            overlayView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            overlayView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
            overlayView.trailingAnchor.constraint(equalTo: view.trailingAnchor)
        ])

        self.view.backgroundColor = UIColor.theme.homePanel.topSitesBackground
        self.profile.panelDataObservers.activityStream.delegate = self

        applyTheme()
    }

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        reloadAll()
    }

    override func viewDidAppear(_ animated: Bool) {
        experiments.recordExposureEvent(featureId: .homescreen)
        super.viewDidAppear(animated)
    }

    override func viewDidDisappear(_ animated: Bool) {
        super.viewDidDisappear(animated)
        timer?.invalidate()
    }

    override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
        super.viewWillTransition(to: size, with: coordinator)
        coordinator.animate(alongsideTransition: {context in
            //The AS context menu does not behave correctly. Dismiss it when rotating.
            if let _ = self.presentedViewController as? PhotonActionSheet {
                self.presentedViewController?.dismiss(animated: true, completion: nil)
            }
            self.collectionViewLayout.invalidateLayout()
            self.collectionView?.reloadData()
        }, completion: { _ in
            if !self.didRoate { self.didRoate = true }
            // Workaround: label positions are not correct without additional reload
            self.collectionView?.reloadData()
        })
    }

    // MARK: - Helpers
    override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
        super.traitCollectionDidChange(previousTraitCollection)
        self.topSitesManager.currentTraits = self.traitCollection
        applyTheme()
    }

    public func dismissDefaultBrowserCard() {
        self.defaultBrowserCard.removeFromSuperview()
        NSLayoutConstraint.activate([
            collectionView.topAnchor.constraint(equalTo: view.topAnchor),
            collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
            collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor)
        ])
    }

    @objc func reload(notification: Notification) {
        reloadAll()
    }

    func applyTheme() {
        defaultBrowserCard.applyTheme()
        self.view.backgroundColor = UIColor.theme.homePanel.topSitesBackground
        topSiteCell.collectionView.reloadData()
        if let collectionView = self.collectionView, collectionView.numberOfSections > 0, collectionView.numberOfItems(inSection: 0) > 0 {
            collectionView.reloadData()
        }
    }

    func scrollToTop(animated: Bool = false) {
        collectionView?.setContentOffset(.zero, animated: animated)
    }

    override func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
        currentTab?.lastKnownUrl?.absoluteString.hasPrefix("internal://") ?? false ? BrowserViewController.foregroundBVC().urlBar.leaveOverlayMode() : nil
    }

    @objc func dismissOverlayMode() {
        BrowserViewController.foregroundBVC().urlBar.leaveOverlayMode()
        if let gestureRecognizers = collectionView.gestureRecognizers {
            for (index, gesture) in gestureRecognizers.enumerated() {
                if gesture.name == FxHomeDevStrings.GestureRecognizers.dismissOverlay {
                    collectionView.gestureRecognizers?.remove(at: index)
                }
            }
        }
    }

    func configureItemsForRecentlySaved() {
        profile.places.getRecentBookmarks(limit: 5).uponQueue(.main) { [weak self] result in
            self?.hasRecentBookmarks = false

            if let bookmarks = result.successValue,
               !bookmarks.isEmpty,
               !RecentItemsHelper.filterStaleItems(recentItems: bookmarks, since: Date()).isEmpty {
                self?.hasRecentBookmarks = true

                TelemetryWrapper.recordEvent(category: .action,
                                             method: .view,
                                             object: .firefoxHomepage,
                                             value: .recentlySavedBookmarkItemView,
                                             extras: [TelemetryWrapper.EventObject.recentlySavedBookmarkImpressions.rawValue: bookmarks.count])
            }

            self?.collectionView.reloadData()
        }

        if let readingList = profile.readingList.getAvailableRecords().value.successValue?.prefix(RecentlySavedCollectionCellUX.readingListItemsLimit) {
            var readingListItems = Array(readingList)
            readingListItems = RecentItemsHelper.filterStaleItems(recentItems: readingListItems,
                                                                       since: Date()) as! [ReadingListItem]
            self.hasReadingListitems = !readingListItems.isEmpty

            TelemetryWrapper.recordEvent(category: .action,
                                         method: .view,
                                         object: .firefoxHomepage,
                                         value: .recentlySavedBookmarkItemView,
                                         extras: [TelemetryWrapper.EventObject.recentlySavedReadingItemImpressions.rawValue: readingListItems.count])

            self.collectionView.reloadData()
        }

    }

    func presentContextualHint() {
        overlayView.isHidden = false
        hasPresentedContextualHint = true
        
        let contentSize = CGSize(width: 325, height: contextualHintViewController.heightForDescriptionLabel)
        contextualHintViewController.preferredContentSize = contentSize
        contextualHintViewController.modalPresentationStyle = .popover
        
        if let popoverPresentationController = contextualHintViewController.popoverPresentationController {
            popoverPresentationController.sourceView = contextualSourceView
            popoverPresentationController.sourceRect = contextualSourceView.bounds
            popoverPresentationController.permittedArrowDirections = .down
            popoverPresentationController.delegate = self
        }
        
        contextualHintViewController.onViewDismissed = { [weak self] in
            self?.overlayView.isHidden = true
            self?.homePanelDelegate?.homePanelDidDismissContextualHint(type: .jumpBackIn)
        }

        contextualHintViewController.viewModel.markContextualHintPresented(profile: profile)
        homePanelDelegate?.homePanelDidPresentContextualHint(type: .jumpBackIn)
        present(contextualHintViewController, animated: true, completion: nil)
    }

    func contextualHintPresentTimer() {
        timer = Timer.scheduledTimer(timeInterval: 0.5, target: self, selector: #selector(presentContextualOverlay), userInfo: nil, repeats: false)
    }
    
    @objc func presentContextualOverlay() {
        presentContextualHint()
    }
}

// MARK: -  Section Management

extension FirefoxHomeViewController {

    enum Section: Int, CaseIterable {
        case topSites
        case libraryShortcuts
        case jumpBackIn
        case recentlySaved
        case pocket
        case customizeHome

        var title: String? {
            switch self {
            case .pocket: return Strings.ASPocketTitle2
            case .jumpBackIn: return String.FirefoxHomeJumpBackInSectionTitle
            case .recentlySaved: return Strings.RecentlySavedSectionTitle
            case .topSites: return Strings.ASShortcutsTitle
            case .libraryShortcuts: return Strings.AppMenuLibraryTitleString
            case .customizeHome: return nil
            }
        }

        var headerHeight: CGSize {
            return CGSize(width: 50, height: 40)
        }

        var headerImage: UIImage? {
            switch self {
            case .pocket: return UIImage.templateImageNamed("menu-pocket")
            case .topSites: return UIImage.templateImageNamed("menu-panel-TopSites")
            case .libraryShortcuts: return UIImage.templateImageNamed("menu-library")
            default : return nil
            }
        }

        var footerHeight: CGSize {
            switch self {
            case .pocket, .jumpBackIn, .recentlySaved, .customizeHome: return .zero
            case .topSites, .libraryShortcuts: return CGSize(width: 50, height: 5)
            }
        }

        func cellHeight(_ traits: UITraitCollection, width: CGFloat) -> CGFloat {
            switch self {
            case .pocket: return FirefoxHomeUX.highlightCellHeight
            case .jumpBackIn: return FirefoxHomeUX.jumpBackInCellHeight
            case .recentlySaved: return FirefoxHomeUX.recentlySavedCellHeight
            case .topSites: return 0 //calculated dynamically
            case .libraryShortcuts: return FirefoxHomeUX.LibraryShortcutsHeight
            case .customizeHome: return FirefoxHomeUX.customizeHomeHeight
            }
        }

        /*
         There are edge cases to handle when calculating section insets
        - An iPhone 7+ is considered regular width when in landscape
        - An iPad in 66% split view is still considered regular width
         */
        func sectionInsets(_ traits: UITraitCollection, frameWidth: CGFloat) -> CGFloat {
            var currentTraits = traits
            if (traits.horizontalSizeClass == .regular && UIScreen.main.bounds.size.width != frameWidth) || UIDevice.current.userInterfaceIdiom == .phone {
                currentTraits = UITraitCollection(horizontalSizeClass: .compact)
            }
            var insets = FirefoxHomeUX.sectionInsetsForSizeClass[currentTraits.horizontalSizeClass]
            let window = UIApplication.shared.keyWindow
            let safeAreaInsets = window?.safeAreaInsets.left ?? 0
            insets += FirefoxHomeUX.MinimumInsets + safeAreaInsets
            return insets
        }

        func numberOfItemsForRow(_ traits: UITraitCollection) -> CGFloat {
            switch self {
            case .pocket:
                var numItems: CGFloat = FirefoxHomeUX.numberOfItemsPerRowForSizeClassIpad[traits.horizontalSizeClass]
                if UIApplication.shared.statusBarOrientation.isPortrait {
                    numItems = numItems - 1
                }
                if traits.horizontalSizeClass == .compact && UIApplication.shared.statusBarOrientation.isLandscape {
                    numItems = numItems - 1
                }

                return numItems
            case .topSites, .libraryShortcuts, .jumpBackIn, .recentlySaved, .customizeHome:
                return 1
            }
        }

        func cellSize(for traits: UITraitCollection, frameWidth: CGFloat) -> CGSize {
            let height = cellHeight(traits, width: frameWidth)
            let inset = sectionInsets(traits, frameWidth: frameWidth) * 2

            switch self {
            case .pocket:
                let numItems = numberOfItemsForRow(traits)
                return CGSize(width: floor(((frameWidth - inset) - (FirefoxHomeUX.MinimumInsets * (numItems - 1))) / numItems), height: height)
            case .topSites, .libraryShortcuts, .jumpBackIn, .recentlySaved, .customizeHome:
                return CGSize(width: frameWidth - inset, height: height)
            }
        }

        var headerView: UIView? {
            let view = ASHeaderView()
            view.title = title
            return view
        }

        var cellIdentifier: String {
            switch self {
            case .topSites: return "TopSiteCell"
            case .pocket: return "PocketCell"
            case .jumpBackIn: return "JumpBackInCell"
            case .recentlySaved: return "RecentlySavedCell"
            case .libraryShortcuts: return  "LibraryShortcutsCell"
            case .customizeHome: return "CustomizeHomeCell"
            }
        }

        var cellType: UICollectionViewCell.Type {
            switch self {
            case .topSites: return ASHorizontalScrollCell.self
            case .pocket: return FirefoxHomeHighlightCell.self
            case .jumpBackIn: return FxHomeJumpBackInCollectionCell.self
            case .recentlySaved: return FxHomeRecentlySavedCollectionCell.self
            case .libraryShortcuts: return ASLibraryCell.self
            case .customizeHome: return FxHomeCustomizeHomeView.self
            }
        }

        init(at indexPath: IndexPath) {
            self.init(rawValue: indexPath.section)!
        }

        init(_ section: Int) {
            self.init(rawValue: section)!
        }
    }
}

// MARK: -  CollectionView Delegate

extension FirefoxHomeViewController: UICollectionViewDelegateFlowLayout {
    
    override func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
        switch kind {
        case UICollectionView.elementKindSectionHeader:
            let view = collectionView.dequeueReusableSupplementaryView(ofKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: "Header", for: indexPath) as! ASHeaderView
            let title = Section(indexPath.section).title
            view.title = title

            switch Section(indexPath.section) {
            case .pocket:
                // tracking pocket section shown
                if !hasSentPocketSectionEvent {
                    TelemetryWrapper.recordEvent(category: .action, method: .view, object: .pocketSectionImpression, value: nil, extras: nil)
                    hasSentPocketSectionEvent = true
                }
                view.moreButton.isHidden = false
                view.moreButton.setTitle(Strings.PocketMoreStoriesText, for: .normal)
                view.moreButton.addTarget(self, action: #selector(showMorePocketStories), for: .touchUpInside)
                view.titleLabel.accessibilityIdentifier = FxHomeAccessibilityIdentifiers.SectionTitles.pocket
                return view
            case .jumpBackIn:
                if !hasSentJumpBackInSectionEvent
                    && isJumpBackInSectionEnabled
                    && !(jumpBackInViewModel.jumpList.itemsToDisplay == 0) {
                    TelemetryWrapper.recordEvent(category: .action, method: .view, object: .jumpBackInImpressions, value: nil, extras: nil)
                    hasSentJumpBackInSectionEvent = true
                }
                view.moreButton.isHidden = false
                view.moreButton.setTitle(Strings.RecentlySavedShowAllText, for: .normal)
                view.moreButton.addTarget(self, action: #selector(openTabTray), for: .touchUpInside)
                view.moreButton.accessibilityIdentifier = FxHomeAccessibilityIdentifiers.MoreButtons.jumpBackIn
                view.titleLabel.accessibilityIdentifier = FxHomeAccessibilityIdentifiers.SectionTitles.jumpBackIn
                let attributes = collectionView.layoutAttributesForItem(at: indexPath)
                    if let frame = attributes?.frame, view.convert(frame, from: collectionView).height > 1 {
                        // Using a timer for the first presentation of contextual hint due to many reloads that happen on the collection view. Invalidating the timer prevents from showing contextual hint at the wrong position.
                        timer?.invalidate()
                        if didRoate && hasPresentedContextualHint {
                            contextualSourceView = view.titleLabel
                            didRoate = false
                        } else if !hasPresentedContextualHint && contextualHintViewController.viewModel.shouldPresentContextualHint(profile: profile) {
                            contextualSourceView = view.titleLabel
                            contextualHintPresentTimer()
                        }
                }
                return view
            case .recentlySaved:
                view.moreButton.isHidden = false
                view.moreButton.setTitle(Strings.RecentlySavedShowAllText, for: .normal)
                view.moreButton.addTarget(self, action: #selector(openBookmarks), for: .touchUpInside)
                view.moreButton.accessibilityIdentifier = FxHomeAccessibilityIdentifiers.MoreButtons.recentlySaved
                view.titleLabel.accessibilityIdentifier = FxHomeAccessibilityIdentifiers.SectionTitles.recentlySaved
                return view
            case .topSites:
                view.titleLabel.accessibilityIdentifier = FxHomeAccessibilityIdentifiers.SectionTitles.topSites
                view.moreButton.isHidden = true
                return view
            case .libraryShortcuts:
                view.moreButton.isHidden = true
                view.titleLabel.accessibilityIdentifier = FxHomeAccessibilityIdentifiers.SectionTitles.library
                return view
            case .customizeHome:
                view.moreButton.isHidden = true
                return view
        }
        default:
            return UICollectionReusableView()
        }
    }

    override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
        self.longPressRecognizer.isEnabled = false
        selectItemAtIndex(indexPath.item, inSection: Section(indexPath.section))
    }

    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
        var cellSize = Section(indexPath.section).cellSize(for: self.traitCollection, frameWidth: self.view.frame.width)

        switch Section(indexPath.section) {
        case .topSites:
            // Create a temporary cell so we can calculate the height.
            let layout = topSiteCell.collectionView.collectionViewLayout as! HorizontalFlowLayout
            let estimatedLayout = layout.calculateLayout(for: CGSize(width: cellSize.width, height: 0))
            return CGSize(width: cellSize.width, height: estimatedLayout.size.height)
        case .jumpBackIn:
            if jumpBackInViewModel.layoutVariables.scrollDirection == .horizontal {
                if jumpBackInViewModel.jumpList.itemsToDisplay > 2 {
                    cellSize.height *= 2
                }
            } else if jumpBackInViewModel.layoutVariables.scrollDirection == .vertical {
                cellSize.height *= CGFloat(jumpBackInViewModel.jumpList.itemsToDisplay)
            }
            return cellSize
        case .libraryShortcuts:
            let width = min(FirefoxHomeUX.LibraryShortcutsMaxWidth, cellSize.width)
            return CGSize(width: width, height: cellSize.height)
        case .customizeHome, .pocket, .recentlySaved:
            return cellSize
        }
    }

    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize {
        switch Section(section) {
        case .pocket:
            return pocketStories.isEmpty ? .zero : Section(section).headerHeight
        case .topSites:
            return isTopSitesSectionEnabled ? Section(section).headerHeight : .zero
        case .libraryShortcuts:
            return isYourLibrarySectionEnabled ? Section(section).headerHeight : .zero
        case .jumpBackIn:
            return isJumpBackInSectionEnabled ? Section(section).headerHeight : .zero
        case .recentlySaved:
            return isRecentlySavedSectionEnabled ? Section(section).headerHeight : .zero
        case .customizeHome:
            return .zero
        }
    }

    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForFooterInSection section: Int) -> CGSize {
        return .zero
    }

    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {
        return 0
    }

    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
        let insets = Section(section).sectionInsets(self.traitCollection, frameWidth: self.view.frame.width)
        return UIEdgeInsets(top: 0, left: insets, bottom: FirefoxHomeUX.spacingBetweenSections, right: insets)
    }

    fileprivate func showSiteWithURLHandler(_ url: URL, isGoogleTopSite: Bool = false) {
        let visitType = VisitType.bookmark
        homePanelDelegate?.homePanel(didSelectURL: url, visitType: visitType, isGoogleTopSite: isGoogleTopSite)
    }
}

// MARK: - CollectionView Data Source

extension FirefoxHomeViewController {

    override func numberOfSections(in collectionView: UICollectionView) -> Int {
        return Section.allCases.count
    }

    override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        var numItems: CGFloat = FirefoxHomeUX.numberOfItemsPerRowForSizeClassIpad[self.traitCollection.horizontalSizeClass]
        if UIApplication.shared.statusBarOrientation.isPortrait {
            numItems = numItems - 1
        }
        if self.traitCollection.horizontalSizeClass == .compact && UIApplication.shared.statusBarOrientation.isLandscape {
            numItems = numItems - 1
        }

        switch Section(section) {
        case .topSites:
            return isTopSitesSectionEnabled && !topSitesManager.content.isEmpty ? 1 : 0
        case .pocket:
            // There should always be a full row of pocket stories (numItems) otherwise don't show them
            return pocketStories.count
        case .jumpBackIn:
            return isJumpBackInSectionEnabled ? 1 : 0
        case .recentlySaved:
            return isRecentlySavedSectionEnabled ? 1 : 0
        case .libraryShortcuts:
            return isYourLibrarySectionEnabled ? 1 : 0
        case .customizeHome:
            return 1
        }
    }

    override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let identifier = Section(indexPath.section).cellIdentifier
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: identifier, for: indexPath)

        switch Section(indexPath.section) {
        case .topSites:
            return configureTopSitesCell(cell, forIndexPath: indexPath)
        case .pocket:
            return configurePocketItemCell(cell, forIndexPath: indexPath)
        case .jumpBackIn:
            return configureJumpBackInCell(cell, forIndexPath: indexPath)
        case .recentlySaved:
            return configureRecentlySavedCell(cell, forIndexPath: indexPath)
        case .libraryShortcuts:
            return configureLibraryShortcutsCell(cell, forIndexPath: indexPath)
        case .customizeHome:
            return configureCustomizeHomeCell(cell, forIndexPath: indexPath)
        }
    }

    func configureLibraryShortcutsCell(_ cell: UICollectionViewCell, forIndexPath indexPath: IndexPath) -> UICollectionViewCell {
        let libraryCell = cell as! ASLibraryCell
        let targets = [#selector(openBookmarks), #selector(openHistory), #selector(openDownloads), #selector(openReadingList)]
        libraryCell.libraryButtons.map({ $0.button }).zip(targets).forEach { (button, selector) in
            button.removeTarget(nil, action: nil, for: .allEvents)
            button.addTarget(self, action: selector, for: .touchUpInside)
        }
        libraryCell.applyTheme()

        return cell
    }

    func configureTopSitesCell(_ cell: UICollectionViewCell, forIndexPath indexPath: IndexPath) -> UICollectionViewCell {
        let topSiteCell = cell as! ASHorizontalScrollCell
        topSiteCell.delegate = self.topSitesManager
        topSiteCell.setNeedsLayout()
        topSiteCell.collectionView.reloadData()
        return cell
    }

    func configurePocketItemCell(_ cell: UICollectionViewCell, forIndexPath indexPath: IndexPath) -> UICollectionViewCell {
        let pocketStory = pocketStories[indexPath.row]
        let pocketItemCell = cell as! FirefoxHomeHighlightCell
        pocketItemCell.configureWithPocketStory(pocketStory)

        return pocketItemCell
    }

    private func configureRecentlySavedCell(_ cell: UICollectionViewCell, forIndexPath indexPath: IndexPath) -> UICollectionViewCell {
        let recentlySavedCell = cell as! FxHomeRecentlySavedCollectionCell
        recentlySavedCell.homePanelDelegate = homePanelDelegate
        recentlySavedCell.libraryPanelDelegate = libraryPanelDelegate
        recentlySavedCell.profile = profile
        recentlySavedCell.collectionView.reloadData()
        recentlySavedCell.setNeedsLayout()
        recentlySavedCell.viewModel = recentlySavedViewModel

        return recentlySavedCell
    }

    private func configureJumpBackInCell(_ cell: UICollectionViewCell, forIndexPath indexPath: IndexPath) -> UICollectionViewCell {
        let jumpBackInCell = cell as! FxHomeJumpBackInCollectionCell
        jumpBackInCell.profile = profile

        jumpBackInViewModel.onTapGroup = { [weak self] tab in
            self?.homePanelDelegate?.homePanelDidRequestToOpenTabTray(withFocusedTab: tab)
        }

        jumpBackInCell.viewModel = jumpBackInViewModel
        jumpBackInCell.collectionView.reloadData()
        jumpBackInCell.setNeedsLayout()
        
        return jumpBackInCell
    }

    private func configureCustomizeHomeCell(_ cell: UICollectionViewCell, forIndexPath indexPath: IndexPath) -> UICollectionViewCell {
        let customizeHomeCell = cell as! FxHomeCustomizeHomeView
        customizeHomeCell.goToSettingsButton.addTarget(self, action: #selector(openCustomizeHomeSettings), for: .touchUpInside)
        customizeHomeCell.setNeedsLayout()

        return customizeHomeCell
    }
}

// MARK: - Data Management

extension FirefoxHomeViewController: DataObserverDelegate {

    // Reloads both highlights and top sites data from their respective caches. Does not invalidate the cache.
    // See ActivityStreamDataObserver for invalidation logic.
    func reloadAll() {
        // If the pocket stories are not availible for the Locale the PocketAPI will return nil
        // So it is okay if the default here is true

        self.configureItemsForRecentlySaved()

        TopSitesHandler.getTopSites(profile: profile).uponQueue(.main) { [weak self] result in
            guard let self = self else { return }<commit_msg>Fixed flicker for contextual hint in jump back in section when backgrounding app (#9344)<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 Shared
import UIKit
import Storage
import SDWebImage
import XCGLogger
import SyncTelemetry

private let log = Logger.browserLogger

// MARK: -  UX

struct FirefoxHomeUX {
    static let highlightCellHeight: CGFloat = UIDevice.current.userInterfaceIdiom == .pad ? 250 : 200
    static let jumpBackInCellHeight: CGFloat = 120
    static let recentlySavedCellHeight: CGFloat = 136
    static let sectionInsetsForSizeClass = UXSizeClasses(compact: 0, regular: 101, other: 15)
    static let numberOfItemsPerRowForSizeClassIpad = UXSizeClasses(compact: 3, regular: 4, other: 2)
    static let spacingBetweenSections: CGFloat = 24
    static let SectionInsetsForIpad: CGFloat = 101
    static let MinimumInsets: CGFloat = 15
    static let LibraryShortcutsHeight: CGFloat = 90
    static let LibraryShortcutsMaxWidth: CGFloat = 375
    static let customizeHomeHeight: CGFloat = 144
}

struct FxHomeAccessibilityIdentifiers {
    struct MoreButtons {
        static let recentlySaved = "recentlySavedSectionMoreButton"
        static let jumpBackIn = "jumpBackInSectionMoreButton"
    }

    struct SectionTitles {
        static let jumpBackIn = "jumpBackInTitle"
        static let recentlySaved = "jumpBackInTitle"
        static let pocket = "pocketTitle"
        static let library = "libraryTitle"
        static let topSites = "topSitesTitle"
    }
}

struct FxHomeDevStrings {
    struct GestureRecognizers {
        static let dismissOverlay = "dismissOverlay"
    }
}


/*
 Size classes are the way Apple requires us to specify our UI.
 Split view on iPad can make a landscape app appear with the demensions of an iPhone app
 Use UXSizeClasses to specify things like offsets/itemsizes with respect to size classes
 For a primer on size classes https://useyourloaf.com/blog/size-classes/
 */
struct UXSizeClasses {
    var compact: CGFloat
    var regular: CGFloat
    var unspecified: CGFloat

    init(compact: CGFloat, regular: CGFloat, other: CGFloat) {
        self.compact = compact
        self.regular = regular
        self.unspecified = other
    }

    subscript(sizeClass: UIUserInterfaceSizeClass) -> CGFloat {
        switch sizeClass {
            case .compact:
                return self.compact
            case .regular:
                return self.regular
            case .unspecified:
                return self.unspecified
            @unknown default:
                fatalError()
        }

    }
}

// MARK: - Home Panel

protocol HomePanelDelegate: AnyObject {
    func homePanelDidRequestToOpenInNewTab(_ url: URL, isPrivate: Bool)
    func homePanel(didSelectURL url: URL, visitType: VisitType, isGoogleTopSite: Bool)
    func homePanelDidRequestToOpenLibrary(panel: LibraryPanelType)
    func homePanelDidRequestToOpenTabTray(withFocusedTab tabToFocus: Tab?)
    func homePanelDidRequestToCustomizeHomeSettings()
    func homePanelDidPresentContextualHint(type: ContextualHintViewType)
    func homePanelDidDismissContextualHint(type: ContextualHintViewType)
}

protocol HomePanel: Themeable {
    var homePanelDelegate: HomePanelDelegate? { get set }
}

enum HomePanelType: Int {
    case topSites = 0

    var internalUrl: URL {
        let aboutUrl: URL! = URL(string: "\(InternalURL.baseUrl)/\(AboutHomeHandler.path)")
        return URL(string: "#panel=\(self.rawValue)", relativeTo: aboutUrl)!
    }
}

protocol HomePanelContextMenu {
    func getSiteDetails(for indexPath: IndexPath) -> Site?
    func getContextMenuActions(for site: Site, with indexPath: IndexPath) -> [PhotonActionSheetItem]?
    func presentContextMenu(for indexPath: IndexPath)
    func presentContextMenu(for site: Site, with indexPath: IndexPath, completionHandler: @escaping () -> PhotonActionSheet?)
}

extension HomePanelContextMenu {
    func presentContextMenu(for indexPath: IndexPath) {
        guard let site = getSiteDetails(for: indexPath) else { return }

        presentContextMenu(for: site, with: indexPath, completionHandler: {
            return self.contextMenu(for: site, with: indexPath)
        })
    }

    func contextMenu(for site: Site, with indexPath: IndexPath) -> PhotonActionSheet? {
        guard let actions = self.getContextMenuActions(for: site, with: indexPath) else { return nil }

        let contextMenu = PhotonActionSheet(site: site, actions: actions)
        contextMenu.modalPresentationStyle = .overFullScreen
        contextMenu.modalTransitionStyle = .crossDissolve

        let generator = UIImpactFeedbackGenerator(style: .heavy)
        generator.impactOccurred()

        return contextMenu
    }

    func getDefaultContextMenuActions(for site: Site, homePanelDelegate: HomePanelDelegate?) -> [PhotonActionSheetItem]? {
        guard let siteURL = URL(string: site.url) else { return nil }

        let openInNewTabAction = PhotonActionSheetItem(title: Strings.OpenInNewTabContextMenuTitle, iconString: "quick_action_new_tab") { _, _ in
            homePanelDelegate?.homePanelDidRequestToOpenInNewTab(siteURL, isPrivate: false)
        }

        let openInNewPrivateTabAction = PhotonActionSheetItem(title: Strings.OpenInNewPrivateTabContextMenuTitle, iconString: "quick_action_new_private_tab") { _, _ in
            homePanelDelegate?.homePanelDidRequestToOpenInNewTab(siteURL, isPrivate: true)
        }

        return [openInNewTabAction, openInNewPrivateTabAction]
    }
}

// MARK: - HomeVC

class FirefoxHomeViewController: UICollectionViewController, HomePanel, FeatureFlagsProtocol {
    weak var homePanelDelegate: HomePanelDelegate?
    weak var libraryPanelDelegate: LibraryPanelDelegate?
    fileprivate var hasPresentedContextualHint = false
    fileprivate var didRoate = false
    fileprivate let profile: Profile
    fileprivate let pocketAPI = Pocket()
    fileprivate let flowLayout = UICollectionViewFlowLayout()
    fileprivate let experiments: NimbusApi
    fileprivate var hasSentPocketSectionEvent = false
    fileprivate var hasSentJumpBackInSectionEvent = false
    fileprivate var timer: Timer?
    fileprivate var contextualSourceView = UIView()
    var recentlySavedViewModel = FirefoxHomeRecentlySavedViewModel()
    var jumpBackInViewModel = FirefoxHomeJumpBackInViewModel()
    fileprivate lazy var topSitesManager: ASHorizontalScrollCellManager = {
        let manager = ASHorizontalScrollCellManager()
        return manager
    }()
    
    var contextualHintViewController = ContextualHintViewController(hintType: .jumpBackIn)
    
    lazy var overlayView: UIView = .build { [weak self] overlayView in
        overlayView.backgroundColor = UIColor.Photon.Grey90A10
        overlayView.isHidden = true
    }

    fileprivate lazy var longPressRecognizer: UILongPressGestureRecognizer = {
        return UILongPressGestureRecognizer(target: self, action: #selector(longPress))
    }()
    
    private var tapGestureRecognizer: UITapGestureRecognizer {
        let dismissOverlay = UITapGestureRecognizer(target: self, action: #selector(dismissOverlayMode))
        dismissOverlay.name = FxHomeDevStrings.GestureRecognizers.dismissOverlay
        dismissOverlay.cancelsTouchesInView = false
        return dismissOverlay
    }

    // Not used for displaying. Only used for calculating layout.
    lazy var topSiteCell: ASHorizontalScrollCell = {
        let customCell = ASHorizontalScrollCell(frame: CGRect(width: self.view.frame.size.width, height: 0))
        customCell.delegate = self.topSitesManager
        return customCell
    }()
    lazy var defaultBrowserCard: DefaultBrowserCard = .build { card in
        card.backgroundColor = UIColor.theme.homePanel.topSitesBackground
    }

    var pocketStories: [PocketStory] = []
    var hasRecentBookmarks = false
    var hasReadingListitems = false
    var currentTab: Tab? {
        let tabManager = BrowserViewController.foregroundBVC().tabManager
        return tabManager.selectedTab
    }

    lazy var homescreen = experiments.withVariables(featureId: .homescreen, sendExposureEvent: false) {
        Homescreen(variables: $0)
    }

    // MARK: - Section availability variables
    var isTopSitesSectionEnabled: Bool {
        homescreen.sectionsEnabled[.topSites] == true
    }

    var isYourLibrarySectionEnabled: Bool {
        UIDevice.current.userInterfaceIdiom != .pad &&
            homescreen.sectionsEnabled[.libraryShortcuts] == true
    }

    var isJumpBackInSectionEnabled: Bool {
        guard featureFlags.isFeatureActiveForBuild(.jumpBackIn),
              homescreen.sectionsEnabled[.topSites] == true,
              featureFlags.userPreferenceFor(.jumpBackIn) == UserFeaturePreference.enabled
        else { return false }

        let tabManager = BrowserViewController.foregroundBVC().tabManager
        return !(tabManager.selectedTab?.isPrivate ?? false)
            && !tabManager.recentlyAccessedNormalTabs.isEmpty
}

    var isRecentlySavedSectionEnabled: Bool {
        guard featureFlags.isFeatureActiveForBuild(.recentlySaved),
              homescreen.sectionsEnabled[.recentlySaved] == true,
              featureFlags.userPreferenceFor(.recentlySaved) == UserFeaturePreference.enabled
        else { return false }

        return hasRecentBookmarks || hasReadingListitems
    }

    var isPocketSectionEnabled: Bool {
        // For Pocket, the user preference check returns a user preference if it exists in
        // UserDefaults, and, if it does not, it will return a default preference based on
        // a (nimbus pocket section enabled && Pocket.isLocaleSupported) check
        guard featureFlags.isFeatureActiveForBuild(.pocket),
              featureFlags.userPreferenceFor(.pocket) == UserFeaturePreference.enabled
        else { return false }

        return true
    }

    // MARK: - Initializers
    init(profile: Profile, experiments: NimbusApi = Experiments.shared) {
        self.profile = profile
        self.experiments = experiments
        super.init(collectionViewLayout: flowLayout)
        collectionView?.delegate = self
        collectionView?.dataSource = self
        collectionView.translatesAutoresizingMaskIntoConstraints = false

        collectionView?.addGestureRecognizer(longPressRecognizer)
        currentTab?.lastKnownUrl?.absoluteString.hasPrefix("internal://") ?? false ? collectionView?.addGestureRecognizer(tapGestureRecognizer) : nil

        let refreshEvents: [Notification.Name] = [.DynamicFontChanged, .HomePanelPrefsChanged, .DisplayThemeChanged]
        refreshEvents.forEach { NotificationCenter.default.addObserver(self, selector: #selector(reload), name: $0, object: nil) }
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    deinit {
        timer?.invalidate()
        timer = nil
    }

    // MARK: - View lifecycle
    override func viewDidLoad() {
        super.viewDidLoad()

        Section.allCases.forEach { collectionView.register($0.cellType, forCellWithReuseIdentifier: $0.cellIdentifier) }
        self.collectionView?.register(ASHeaderView.self, forSupplementaryViewOfKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: "Header")
        collectionView?.keyboardDismissMode = .onDrag
        collectionView?.backgroundColor = .clear
        self.view.addSubviews(overlayView)
        self.view.addSubview(contextualSourceView)
        contextualSourceView.backgroundColor = .clear
        
        if #available(iOS 14.0, *), !UserDefaults.standard.bool(forKey: "DidDismissDefaultBrowserCard") {
            self.view.addSubview(defaultBrowserCard)
            NSLayoutConstraint.activate([
                defaultBrowserCard.topAnchor.constraint(equalTo: view.topAnchor),
                defaultBrowserCard.bottomAnchor.constraint(equalTo: collectionView.topAnchor),
                defaultBrowserCard.centerXAnchor.constraint(equalTo: view.centerXAnchor),
                defaultBrowserCard.widthAnchor.constraint(equalToConstant: 380),

                collectionView.topAnchor.constraint(equalTo: defaultBrowserCard.bottomAnchor),
                collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
                collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
                collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            ])

            defaultBrowserCard.dismissClosure = {
                self.dismissDefaultBrowserCard()
            }
        }
        
        NSLayoutConstraint.activate([
            overlayView.topAnchor.constraint(equalTo: view.topAnchor),
            overlayView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            overlayView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
            overlayView.trailingAnchor.constraint(equalTo: view.trailingAnchor)
        ])

        self.view.backgroundColor = UIColor.theme.homePanel.topSitesBackground
        self.profile.panelDataObservers.activityStream.delegate = self

        applyTheme()
    }

    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        reloadAll()
    }

    override func viewDidAppear(_ animated: Bool) {
        experiments.recordExposureEvent(featureId: .homescreen)
        super.viewDidAppear(animated)
    }

    override func viewDidDisappear(_ animated: Bool) {
        super.viewDidDisappear(animated)
        timer?.invalidate()
    }

    override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
        super.viewWillTransition(to: size, with: coordinator)
        coordinator.animate(alongsideTransition: {context in
            //The AS context menu does not behave correctly. Dismiss it when rotating.
            if let _ = self.presentedViewController as? PhotonActionSheet {
                self.presentedViewController?.dismiss(animated: true, completion: nil)
            }
            self.collectionViewLayout.invalidateLayout()
            self.collectionView?.reloadData()
        }, completion: { _ in
            if !self.didRoate { self.didRoate = true }
            // Workaround: label positions are not correct without additional reload
            self.collectionView?.reloadData()
        })
    }

    // MARK: - Helpers
    override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
        super.traitCollectionDidChange(previousTraitCollection)
        self.topSitesManager.currentTraits = self.traitCollection
        applyTheme()
    }

    public func dismissDefaultBrowserCard() {
        self.defaultBrowserCard.removeFromSuperview()
        NSLayoutConstraint.activate([
            collectionView.topAnchor.constraint(equalTo: view.topAnchor),
            collectionView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            collectionView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
            collectionView.trailingAnchor.constraint(equalTo: view.trailingAnchor)
        ])
    }

    @objc func reload(notification: Notification) {
        reloadAll()
    }

    func applyTheme() {
        defaultBrowserCard.applyTheme()
        self.view.backgroundColor = UIColor.theme.homePanel.topSitesBackground
        topSiteCell.collectionView.reloadData()
        if let collectionView = self.collectionView, collectionView.numberOfSections > 0, collectionView.numberOfItems(inSection: 0) > 0 {
            collectionView.reloadData()
        }
    }

    func scrollToTop(animated: Bool = false) {
        collectionView?.setContentOffset(.zero, animated: animated)
    }

    override func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
        currentTab?.lastKnownUrl?.absoluteString.hasPrefix("internal://") ?? false ? BrowserViewController.foregroundBVC().urlBar.leaveOverlayMode() : nil
    }

    @objc func dismissOverlayMode() {
        BrowserViewController.foregroundBVC().urlBar.leaveOverlayMode()
        if let gestureRecognizers = collectionView.gestureRecognizers {
            for (index, gesture) in gestureRecognizers.enumerated() {
                if gesture.name == FxHomeDevStrings.GestureRecognizers.dismissOverlay {
                    collectionView.gestureRecognizers?.remove(at: index)
                }
            }
        }
    }

    func configureItemsForRecentlySaved() {
        profile.places.getRecentBookmarks(limit: 5).uponQueue(.main) { [weak self] result in
            self?.hasRecentBookmarks = false

            if let bookmarks = result.successValue,
               !bookmarks.isEmpty,
               !RecentItemsHelper.filterStaleItems(recentItems: bookmarks, since: Date()).isEmpty {
                self?.hasRecentBookmarks = true

                TelemetryWrapper.recordEvent(category: .action,
                                             method: .view,
                                             object: .firefoxHomepage,
                                             value: .recentlySavedBookmarkItemView,
                                             extras: [TelemetryWrapper.EventObject.recentlySavedBookmarkImpressions.rawValue: bookmarks.count])
            }

            self?.collectionView.reloadData()
        }

        if let readingList = profile.readingList.getAvailableRecords().value.successValue?.prefix(RecentlySavedCollectionCellUX.readingListItemsLimit) {
            var readingListItems = Array(readingList)
            readingListItems = RecentItemsHelper.filterStaleItems(recentItems: readingListItems,
                                                                       since: Date()) as! [ReadingListItem]
            self.hasReadingListitems = !readingListItems.isEmpty

            TelemetryWrapper.recordEvent(category: .action,
                                         method: .view,
                                         object: .firefoxHomepage,
                                         value: .recentlySavedBookmarkItemView,
                                         extras: [TelemetryWrapper.EventObject.recentlySavedReadingItemImpressions.rawValue: readingListItems.count])

            self.collectionView.reloadData()
        }

    }

    func presentContextualHint() {
        overlayView.isHidden = false
        hasPresentedContextualHint = true
        
        let contentSize = CGSize(width: 325, height: contextualHintViewController.heightForDescriptionLabel)
        contextualHintViewController.preferredContentSize = contentSize
        contextualHintViewController.modalPresentationStyle = .popover
        
        if let popoverPresentationController = contextualHintViewController.popoverPresentationController {
            popoverPresentationController.sourceView = contextualSourceView
            popoverPresentationController.sourceRect = contextualSourceView.bounds
            popoverPresentationController.permittedArrowDirections = .down
            popoverPresentationController.delegate = self
        }
        
        contextualHintViewController.onViewDismissed = { [weak self] in
            self?.overlayView.isHidden = true
            self?.homePanelDelegate?.homePanelDidDismissContextualHint(type: .jumpBackIn)
        }

        contextualHintViewController.viewModel.markContextualHintPresented(profile: profile)
        homePanelDelegate?.homePanelDidPresentContextualHint(type: .jumpBackIn)
        present(contextualHintViewController, animated: true, completion: nil)
    }

    func contextualHintPresentTimer() {
        timer = Timer.scheduledTimer(timeInterval: 0.5, target: self, selector: #selector(presentContextualOverlay), userInfo: nil, repeats: false)
    }
    
    @objc func presentContextualOverlay() {
        presentContextualHint()
    }
}

// MARK: -  Section Management

extension FirefoxHomeViewController {

    enum Section: Int, CaseIterable {
        case topSites
        case libraryShortcuts
        case jumpBackIn
        case recentlySaved
        case pocket
        case customizeHome

        var title: String? {
            switch self {
            case .pocket: return Strings.ASPocketTitle2
            case .jumpBackIn: return String.FirefoxHomeJumpBackInSectionTitle
            case .recentlySaved: return Strings.RecentlySavedSectionTitle
            case .topSites: return Strings.ASShortcutsTitle
            case .libraryShortcuts: return Strings.AppMenuLibraryTitleString
            case .customizeHome: return nil
            }
        }

        var headerHeight: CGSize {
            return CGSize(width: 50, height: 40)
        }

        var headerImage: UIImage? {
            switch self {
            case .pocket: return UIImage.templateImageNamed("menu-pocket")
            case .topSites: return UIImage.templateImageNamed("menu-panel-TopSites")
            case .libraryShortcuts: return UIImage.templateImageNamed("menu-library")
            default : return nil
            }
        }

        var footerHeight: CGSize {
            switch self {
            case .pocket, .jumpBackIn, .recentlySaved, .customizeHome: return .zero
            case .topSites, .libraryShortcuts: return CGSize(width: 50, height: 5)
            }
        }

        func cellHeight(_ traits: UITraitCollection, width: CGFloat) -> CGFloat {
            switch self {
            case .pocket: return FirefoxHomeUX.highlightCellHeight
            case .jumpBackIn: return FirefoxHomeUX.jumpBackInCellHeight
            case .recentlySaved: return FirefoxHomeUX.recentlySavedCellHeight
            case .topSites: return 0 //calculated dynamically
            case .libraryShortcuts: return FirefoxHomeUX.LibraryShortcutsHeight
            case .customizeHome: return FirefoxHomeUX.customizeHomeHeight
            }
        }

        /*
         There are edge cases to handle when calculating section insets
        - An iPhone 7+ is considered regular width when in landscape
        - An iPad in 66% split view is still considered regular width
         */
        func sectionInsets(_ traits: UITraitCollection, frameWidth: CGFloat) -> CGFloat {
            var currentTraits = traits
            if (traits.horizontalSizeClass == .regular && UIScreen.main.bounds.size.width != frameWidth) || UIDevice.current.userInterfaceIdiom == .phone {
                currentTraits = UITraitCollection(horizontalSizeClass: .compact)
            }
            var insets = FirefoxHomeUX.sectionInsetsForSizeClass[currentTraits.horizontalSizeClass]
            let window = UIApplication.shared.keyWindow
            let safeAreaInsets = window?.safeAreaInsets.left ?? 0
            insets += FirefoxHomeUX.MinimumInsets + safeAreaInsets
            return insets
        }

        func numberOfItemsForRow(_ traits: UITraitCollection) -> CGFloat {
            switch self {
            case .pocket:
                var numItems: CGFloat = FirefoxHomeUX.numberOfItemsPerRowForSizeClassIpad[traits.horizontalSizeClass]
                if UIApplication.shared.statusBarOrientation.isPortrait {
                    numItems = numItems - 1
                }
                if traits.horizontalSizeClass == .compact && UIApplication.shared.statusBarOrientation.isLandscape {
                    numItems = numItems - 1
                }

                return numItems
            case .topSites, .libraryShortcuts, .jumpBackIn, .recentlySaved, .customizeHome:
                return 1
            }
        }

        func cellSize(for traits: UITraitCollection, frameWidth: CGFloat) -> CGSize {
            let height = cellHeight(traits, width: frameWidth)
            let inset = sectionInsets(traits, frameWidth: frameWidth) * 2

            switch self {
            case .pocket:
                let numItems = numberOfItemsForRow(traits)
                return CGSize(width: floor(((frameWidth - inset) - (FirefoxHomeUX.MinimumInsets * (numItems - 1))) / numItems), height: height)
            case .topSites, .libraryShortcuts, .jumpBackIn, .recentlySaved, .customizeHome:
                return CGSize(width: frameWidth - inset, height: height)
            }
        }

        var headerView: UIView? {
            let view = ASHeaderView()
            view.title = title
            return view
        }

        var cellIdentifier: String {
            switch self {
            case .topSites: return "TopSiteCell"
            case .pocket: return "PocketCell"
            case .jumpBackIn: return "JumpBackInCell"
            case .recentlySaved: return "RecentlySavedCell"
            case .libraryShortcuts: return  "LibraryShortcutsCell"
            case .customizeHome: return "CustomizeHomeCell"
            }
        }

        var cellType: UICollectionViewCell.Type {
            switch self {
            case .topSites: return ASHorizontalScrollCell.self
            case .pocket: return FirefoxHomeHighlightCell.self
            case .jumpBackIn: return FxHomeJumpBackInCollectionCell.self
            case .recentlySaved: return FxHomeRecentlySavedCollectionCell.self
            case .libraryShortcuts: return ASLibraryCell.self
            case .customizeHome: return FxHomeCustomizeHomeView.self
            }
        }

        init(at indexPath: IndexPath) {
            self.init(rawValue: indexPath.section)!
        }

        init(_ section: Int) {
            self.init(rawValue: section)!
        }
    }
}

// MARK: -  CollectionView Delegate

extension FirefoxHomeViewController: UICollectionViewDelegateFlowLayout {
    
    override func collectionView(_ collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, at indexPath: IndexPath) -> UICollectionReusableView {
        switch kind {
        case UICollectionView.elementKindSectionHeader:
            let view = collectionView.dequeueReusableSupplementaryView(ofKind: UICollectionView.elementKindSectionHeader, withReuseIdentifier: "Header", for: indexPath) as! ASHeaderView
            let title = Section(indexPath.section).title
            view.title = title

            switch Section(indexPath.section) {
            case .pocket:
                // tracking pocket section shown
                if !hasSentPocketSectionEvent {
                    TelemetryWrapper.recordEvent(category: .action, method: .view, object: .pocketSectionImpression, value: nil, extras: nil)
                    hasSentPocketSectionEvent = true
                }
                view.moreButton.isHidden = false
                view.moreButton.setTitle(Strings.PocketMoreStoriesText, for: .normal)
                view.moreButton.addTarget(self, action: #selector(showMorePocketStories), for: .touchUpInside)
                view.titleLabel.accessibilityIdentifier = FxHomeAccessibilityIdentifiers.SectionTitles.pocket
                return view
            case .jumpBackIn:
                if !hasSentJumpBackInSectionEvent
                    && isJumpBackInSectionEnabled
                    && !(jumpBackInViewModel.jumpList.itemsToDisplay == 0) {
                    TelemetryWrapper.recordEvent(category: .action, method: .view, object: .jumpBackInImpressions, value: nil, extras: nil)
                    hasSentJumpBackInSectionEvent = true
                }
                view.moreButton.isHidden = false
                view.moreButton.setTitle(Strings.RecentlySavedShowAllText, for: .normal)
                view.moreButton.addTarget(self, action: #selector(openTabTray), for: .touchUpInside)
                view.moreButton.accessibilityIdentifier = FxHomeAccessibilityIdentifiers.MoreButtons.jumpBackIn
                view.titleLabel.accessibilityIdentifier = FxHomeAccessibilityIdentifiers.SectionTitles.jumpBackIn
                let attributes = collectionView.layoutAttributesForItem(at: indexPath)
                    if let frame = attributes?.frame, view.convert(frame, from: collectionView).height > 1 {
                        // Using a timer for the first presentation of contextual hint due to many reloads that happen on the collection view. Invalidating the timer prevents from showing contextual hint at the wrong position.
                        timer?.invalidate()
                        if didRoate && hasPresentedContextualHint {
                            contextualSourceView = view.titleLabel
                            didRoate = false
                        } else if !hasPresentedContextualHint && contextualHintViewController.viewModel.shouldPresentContextualHint(profile: profile) {
                            contextualSourceView = view.titleLabel
                            contextualHintPresentTimer()
                        }
                }
                return view
            case .recentlySaved:
                view.moreButton.isHidden = false
                view.moreButton.setTitle(Strings.RecentlySavedShowAllText, for: .normal)
                view.moreButton.addTarget(self, action: #selector(openBookmarks), for: .touchUpInside)
                view.moreButton.accessibilityIdentifier = FxHomeAccessibilityIdentifiers.MoreButtons.recentlySaved
                view.titleLabel.accessibilityIdentifier = FxHomeAccessibilityIdentifiers.SectionTitles.recentlySaved
                return view
            case .topSites:
                view.titleLabel.accessibilityIdentifier = FxHomeAccessibilityIdentifiers.SectionTitles.topSites
                view.moreButton.isHidden = true
                return view
            case .libraryShortcuts:
                view.moreButton.isHidden = true
                view.titleLabel.accessibilityIdentifier = FxHomeAccessibilityIdentifiers.SectionTitles.library
                return view
            case .customizeHome:
                view.moreButton.isHidden = true
                return view
        }
        default:
            return UICollectionReusableView()
        }
    }

    override func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
        self.longPressRecognizer.isEnabled = false
        selectItemAtIndex(indexPath.item, inSection: Section(indexPath.section))
    }

    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
        var cellSize = Section(indexPath.section).cellSize(for: self.traitCollection, frameWidth: self.view.frame.width)

        switch Section(indexPath.section) {
        case .topSites:
            // Create a temporary cell so we can calculate the height.
            let layout = topSiteCell.collectionView.collectionViewLayout as! HorizontalFlowLayout
            let estimatedLayout = layout.calculateLayout(for: CGSize(width: cellSize.width, height: 0))
            return CGSize(width: cellSize.width, height: estimatedLayout.size.height)
        case .jumpBackIn:
            if jumpBackInViewModel.layoutVariables.scrollDirection == .horizontal {
                if jumpBackInViewModel.jumpList.itemsToDisplay > 2 {
                    cellSize.height *= 2
                }
            } else if jumpBackInViewModel.layoutVariables.scrollDirection == .vertical {
                cellSize.height *= CGFloat(jumpBackInViewModel.jumpList.itemsToDisplay)
            }
            return cellSize
        case .libraryShortcuts:
            let width = min(FirefoxHomeUX.LibraryShortcutsMaxWidth, cellSize.width)
            return CGSize(width: width, height: cellSize.height)
        case .customizeHome, .pocket, .recentlySaved:
            return cellSize
        }
    }

    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForHeaderInSection section: Int) -> CGSize {
        switch Section(section) {
        case .pocket:
            return pocketStories.isEmpty ? .zero : Section(section).headerHeight
        case .topSites:
            return isTopSitesSectionEnabled ? Section(section).headerHeight : .zero
        case .libraryShortcuts:
            return isYourLibrarySectionEnabled ? Section(section).headerHeight : .zero
        case .jumpBackIn:
            return isJumpBackInSectionEnabled ? Section(section).headerHeight : .zero
        case .recentlySaved:
            return isRecentlySavedSectionEnabled ? Section(section).headerHeight : .zero
        case .customizeHome:
            return .zero
        }
    }

    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, referenceSizeForFooterInSection section: Int) -> CGSize {
        return .zero
    }

    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, minimumInteritemSpacingForSectionAt section: Int) -> CGFloat {
        return 0
    }

    func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
        let insets = Section(section).sectionInsets(self.traitCollection, frameWidth: self.view.frame.width)
        return UIEdgeInsets(top: 0, left: insets, bottom: FirefoxHomeUX.spacingBetweenSections, right: insets)
    }

    fileprivate func showSiteWithURLHandler(_ url: URL, isGoogleTopSite: Bool = false) {
        let visitType = VisitType.bookmark
        homePanelDelegate?.homePanel(didSelectURL: url, visitType: visitType, isGoogleTopSite: isGoogleTopSite)
    }
}

// MARK: - CollectionView Data Source

extension FirefoxHomeViewController {

    override func numberOfSections(in collectionView: UICollectionView) -> Int {
        return Section.allCases.count
    }

    override func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        var numItems: CGFloat = FirefoxHomeUX.numberOfItemsPerRowForSizeClassIpad[self.traitCollection.horizontalSizeClass]
        if UIApplication.shared.statusBarOrientation.isPortrait {
            numItems = numItems - 1
        }
        if self.traitCollection.horizontalSizeClass == .compact && UIApplication.shared.statusBarOrientation.isLandscape {
            numItems = numItems - 1
        }

        switch Section(section) {
        case .topSites:
            return isTopSitesSectionEnabled && !topSitesManager.content.isEmpty ? 1 : 0
        case .pocket:
            // There should always be a full row of pocket stories (numItems) otherwise don't show them
            return pocketStories.count
        case .jumpBackIn:
            return isJumpBackInSectionEnabled ? 1 : 0
        case .recentlySaved:
            return isRecentlySavedSectionEnabled ? 1 : 0
        case .libraryShortcuts:
            return isYourLibrarySectionEnabled ? 1 : 0
        case .customizeHome:
            return 1
        }
    }

    override func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let identifier = Section(indexPath.section).cellIdentifier
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: identifier, for: indexPath)

        switch Section(indexPath.section) {
        case .topSites:
            return configureTopSitesCell(cell, forIndexPath: indexPath)
        case .pocket:
            return configurePocketItemCell(cell, forIndexPath: indexPath)
        case .jumpBackIn:
            return configureJumpBackInCell(cell, forIndexPath: indexPath)
        case .recentlySaved:
            return configureRecentlySavedCell(cell, forIndexPath: indexPath)
        case .libraryShortcuts:
            return configureLibraryShortcutsCell(cell, forIndexPath: indexPath)
        case .customizeHome:
            return configureCustomizeHomeCell(cell, forIndexPath: indexPath)
        }
    }

    func configureLibraryShortcutsCell(_ cell: UICollectionViewCell, forIndexPath indexPath: IndexPath) -> UICollectionViewCell {
        let libraryCell = cell as! ASLibraryCell
        let targets = [#selector(openBookmarks), #selector(openHistory), #selector(openDownloads), #selector(openReadingList)]
        libraryCell.libraryButtons.map({ $0.button }).zip(targets).forEach { (button, selector) in
            button.removeTarget(nil, action: nil, for: .allEvents)
            button.addTarget(self, action: selector, for: .touchUpInside)
        }
        libraryCell.applyTheme()

        return cell
    }

    func configureTopSitesCell(_ cell: UICollectionViewCell, forIndexPath indexPath: IndexPath) -> UICollectionViewCell {
        let topSiteCell = cell as! ASHorizontalScrollCell
        topSiteCell.delegate = self.topSitesManager
        topSiteCell.setNeedsLayout()
        topSiteCell.collectionView.reloadData()
        return cell
    }

    func configurePocketItemCell(_ cell: UICollectionViewCell, forIndexPath indexPath: IndexPath) -> UICollectionViewCell {
        let pocketStory = pocketStories[indexPath.row]
        let pocketItemCell = cell as! FirefoxHomeHighlightCell
        pocketItemCell.configureWithPocketStory(pocketStory)

        return pocketItemCell
    }

    private func configureRecentlySavedCell(_ cell: UICollectionViewCell, forIndexPath indexPath: IndexPath) -> UICollectionViewCell {
        let recentlySavedCell = cell as! FxHomeRecentlySavedCollectionCell
        recentlySavedCell.homePanelDelegate = homePanelDelegate
        recentlySavedCell.libraryPanelDelegate = libraryPanelDelegate
        recentlySavedCell.profile = profile
        recentlySavedCell.collectionView.reloadData()
        recentlySavedCell.setNeedsLayout()
        recentlySavedCell.viewModel = recentlySavedViewModel

        return recentlySavedCell
    }

    private func configureJumpBackInCell(_ cell: UICollectionViewCell, forIndexPath indexPath: IndexPath) -> UICollectionViewCell {
        let jumpBackInCell = cell as! FxHomeJumpBackInCollectionCell
        jumpBackInCell.profile = profile

        jumpBackInViewModel.onTapGroup = { [weak self] tab in
            self?.homePanelDelegate?.homePanelDidRequestToOpenTabTray(withFocusedTab: tab)
        }

        jumpBackInCell.viewModel = jumpBackInViewModel
        jumpBackInCell.collectionView.reloadData()
        jumpBackInCell.setNeedsLayout()
        
        return jumpBackInCell
    }

    private func configureCustomizeHomeCell(_ cell: UICollectionViewCell, forIndexPath indexPath: IndexPath) -> UICollectionViewCell {
        let customizeHomeCell = cell as! FxHomeCustomizeHomeView
        customizeHomeCell.goToSettingsButton.addTarget(self, action: #selector(openCustomizeHomeSettings), for: .touchUpInside)
        customizeHomeCell.setNeedsLayout()

        return customizeHomeCell
    }
}

// MARK: - Data Management

extension FirefoxHomeViewController: DataObserverDelegate {

    // Reloads both highlights and top sites data from their respective caches. Does not invalidate the cache.
    // See ActivityStreamDataObserver for invalidation logic.
    func reloadAll() {
        // Overlay view is used by contextual hint and reloading the view while the hint is shown can cause the popover to flicker
        guard overlayView.isHidden else { return }

        // If the pocket stories are not availible for the Locale the PocketAPI will return nil
        // So it is okay if the default here is true

        self.configureItemsForRecentlySaved()

        TopSitesHandler.getTopSites(profile: profile).uponQueue(.main) { [weak self] result in
            guard let self = self else { return }
{'fraction_non_alphanumeric': 0.07039546879741074, 'fraction_numerical': 0.002629715788409022, 'mean_word_length': 2.723788898827739, 'pattern_counts': {'":': 0, '<': 3, '<?xml version=': 0, '>': 61, 'https://': 2, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 40, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '7145200', 'n_tokens_mistral': 20887, 'n_tokens_neox': 19730, 'n_words': 4537}