<commit_before>//
//  URLListViewController.swift
//  Hipstapaper
//
//  Created by <NAME> on 12/16/16.
//  Copyright © 2016 <NAME>. All rights reserved.
//

import Common
import RealmSwift
import SafariServices
import UIKit

class ContentListViewController: UIViewController, RealmControllable {
    
    @IBOutlet fileprivate weak var loadingIndicatorViewController: LoadingIndicatorViewController?
    @IBOutlet fileprivate weak var tableView: UITableView? {
        didSet {
            let imageNib = UINib(nibName: ContentTableViewCell.withImageNIBName, bundle: Bundle(for: ContentTableViewCell.self))
            let noImageNib = UINib(nibName: ContentTableViewCell.withOutImageNIBName, bundle: Bundle(for: ContentTableViewCell.self))
            self.tableView?.register(imageNib, forCellReuseIdentifier: ContentTableViewCell.withImageNIBName)
            self.tableView?.register(noImageNib, forCellReuseIdentifier: ContentTableViewCell.withOutImageNIBName)
            self.tableView?.rowHeight = ContentTableViewCell.cellHeight
            self.tableView?.estimatedRowHeight = ContentTableViewCell.cellHeight
            self.tableView?.tableHeaderView = self.searchController.searchBar
        }
    }
    fileprivate lazy var searchController: UISearchController = {
        // lazy vs let fixes a bug where the searchBar icon was pixelated
        // probably caused by initializing before the screen is all configured
        let sc = UISearchController(searchResultsController: nil)
        sc.dimsBackgroundDuringPresentation = false
        sc.restorationIdentifier = StateRestorationIdentifier.searchController.rawValue
        return sc
    }()
    
    fileprivate typealias UIBBI = UIBarButtonItem
    fileprivate lazy var editBBI: UIBBI = UIBBI(barButtonSystemItem: UIBarButtonSystemItem.edit, target: self, action: #selector(self.editBBITapped(_:)))
    fileprivate lazy var doneBBI: UIBBI = UIBBI(barButtonSystemItem: UIBarButtonSystemItem.done, target: self, action: #selector(self.doneBBITapped(_:)))
    fileprivate lazy var archiveBBI: UIBBI = UIBBI(title: "📥Archive", style: .plain, target: self, action: #selector(self.archiveBBITapped(_:)))
    fileprivate lazy var unarchiveBBI: UIBBI = UIBBI(title: "📤Unarchive", style: .plain, target: self, action: #selector(self.unarchiveBBITapped(_:)))
    fileprivate lazy var tagBBI: UIBBI = UIBBI(title: "🏷Tag", style: .plain, target: self, action: #selector(self.tagBBITapped(_:)))
    fileprivate lazy var sortBBI: UIBBI = UIBBI(title: "Sort", style: .plain, target: self, action: #selector(self.sortBBITapped(_:)))
    fileprivate lazy var filterBBI: UIBBI = UIBBI(title: "Filter", style: .plain, target: self, action: #selector(self.filterBBITapped(_:)))
    fileprivate let flexibleSpaceBBI: UIBBI = UIBBI(barButtonSystemItem: .flexibleSpace, target: nil, action: nil)
    fileprivate let verticalBarSpaceBBI: UIBBI = {
        let bbi = UIBBI(title: "|", style: .plain, target: nil, action: nil)
        bbi.isEnabled = false
        return bbi
    }()
    
    // MARK: Selection
    
    var itemsToLoad = UserDefaults.standard.userSelection.itemsToLoad
    var filter = UserDefaults.standard.userSelection.filter
    var sortOrder = UserDefaults.standard.userSelection.sortOrder
    
    fileprivate weak var selectionDelegate: URLItemsToLoadChangeDelegate?
    
    // MARK: Data
    
    fileprivate var data: AnyRealmCollection<URLItem>?
    
    weak var realmController: RealmController? {
        didSet {
            // forward the message to any presented view controllers
            let addRemoveTagPopoverVC = (self.presentedViewController as? UINavigationController)?.viewControllers.first as? RealmControllable
            addRemoveTagPopoverVC?.realmController = self.realmController
            self.loadingIndicatorViewController?.realmController = self.realmController
            
            // reload the data
            self.hardReloadData()
        }
    }
    
    fileprivate var selectedURLItems: [URLItem] {
        let data = self.data
        let indexPaths = self.tableView?.indexPathsForSelectedRows
        let items = indexPaths?.flatMap({ data?[$0.row] })
        return items ?? []
    }
    
    convenience init(selectionDelegate: URLItemsToLoadChangeDelegate, controller: RealmController?) {
        self.init()
        self.selectionDelegate = selectionDelegate
        self.realmController = controller
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        
        // configure the search delegate
        self.searchController.searchResultsUpdater = self
        
        // needed for the search delegate so it presents correctly
        self.definesPresentationContext = true
        
        // set the top constraint on the LoadingIndicatorViewController
        // this can't be done in the XIB
        if let loadingIndicatorViewController = self.loadingIndicatorViewController {
            self.addChildViewController(loadingIndicatorViewController)
            loadingIndicatorViewController.realmController = self.realmController
            loadingIndicatorViewController.view.topAnchor.constraint(equalTo: self.topLayoutGuide.bottomAnchor, constant: 10).isActive = true
        }
        
        // register for 3d touch events
        if case .available = self.traitCollection.forceTouchCapability {
            self.registerForPreviewing(with: self, sourceView: view)
        }
        
        // because we are in a split view, we fully own our own Navigation Controller
        // therefore we don't need to micromanage this when switching views.
        // its always present
        self.navigationController?.setToolbarHidden(false, animated: false)
        
        // configure myself for splitview
        self.navigationItem.leftBarButtonItem = self.splitViewController?.displayModeButtonItem
        self.navigationItem.leftItemsSupplementBackButton = true
        
        // put us into non-edit mode
        self.resetToolbar()
        self.disableAllBBI()
        
        // load the data
        self.hardReloadData()
    }
    
    fileprivate func hardReloadData() {
        // get values in case things change
        let itemsToLoad = self.itemsToLoad
        let filter = self.filter
        let sortOrder = self.sortOrder
        let searchFilter = self.searchController.searchString
        
        // set title
        switch itemsToLoad {
        case .all:
            switch self.filter {
            case .all:
                self.title = "All Items"
            case .unarchived:
                self.title = "Hipstapaper"
            }
        case .tag(let tagID):
            self.title = "🏷 \(tagID.displayName)"
        }
        
        // preserve any selection
        if self.selectedUUIDsStateRestoration == nil {
            self.selectedUUIDsStateRestoration = self.selectedURLItems.map({ $0.uuid })
        }
        
        // clear things out
        self.notificationToken?.stop()
        self.notificationToken = nil
        self.data = nil
        self.tableView?.reloadData()
        
        // configure data source
        let data = self.realmController?.url_loadAll(for: itemsToLoad, sortedBy: sortOrder, filteredBy: filter, searchFilter: searchFilter)
        self.notificationToken = data?.addNotificationBlock({ [weak self] in self?.realmResultsChanged($0) })
    }
    
    private func realmResultsChanged(_ changes: RealmCollectionChange<AnyRealmCollection<URLItem>>) {
        switch changes {
        case .initial(let data):
            // set the data
            self.data = data
            // find the previously selected items
            let previousSelectionIndexes = RealmController.indexes(ofItemUUIDs: self.selectedUUIDsStateRestoration ?? [], within: data)
            self.selectedUUIDsStateRestoration = nil // reset this so we don't do this on next refresh
            // find previously visible index
            let previouslyVisibleIndex = RealmController.indexes(ofItemUUIDs: self.visibleUUIDsStateRestoration ?? [], within: data)
            self.visibleUUIDsStateRestoration = nil // reset this so we don't do this on next refresh
            // hard reload the data
            self.tableView?.reloadData()
            
            // check if state restoration left us a scroll position
            if let index = previouslyVisibleIndex.first {
                self.tableView?.scrollToRow(at: IndexPath(row: index, section: 0), at: .top, animated: false)
            // otherwise we can check for conditions where we might want to hide the search bar
            } else if let topVisibleRow = self.tableView?.indexPathsForVisibleRows?.first, // there is a visible row
                self.searchController.isActive == false, // if the search controller is active, don't hijack scrolling
                self.searchController.isVisible == false // if the search controller is visible, don't hijack scrolling (this fixes a bug that caused it to scroll when cancelling the search controller
            {
                self.tableView?.scrollToRow(at: topVisibleRow, at: .top, animated: true)
            }
        
            // select the items found after updating the table
            self.selectRowsAfterHardRefresh(atIndexes: previousSelectionIndexes)
        case .update(let data, let deletions, let insertions, let modifications):
            // UITableView does not automatically reselect updated rows like NSTableView does
            // So I need manual selection code here
            // find the previously selected items
            let previousSelectionIndexes = RealmController.indexes(ofItemUUIDs: self.selectedURLItems.map({ $0.uuid }), within: data)
            
            // update the table
            self.tableView?.beginUpdates()
            self.tableView?.insertRows(at: insertions.map({ IndexPath(row: $0, section: 0) }), with: .right)
            self.tableView?.deleteRows(at: deletions.map({ IndexPath(row: $0, section: 0)}), with: .left)
            self.tableView?.reloadRows(at: modifications.map({ IndexPath(row: $0, section: 0) }), with: .automatic)
            self.tableView?.endUpdates()
            
            // select the items found after updating the table
            self.selectRowsAfterHardRefresh(atIndexes: previousSelectionIndexes)
        case .error(let error):
            let alert = UIAlertController(title: "Error Loading Reading List", message: error.localizedDescription, preferredStyle: .alert)
            let action = UIAlertAction(title: "Dismiss", style: .cancel, handler: nil)
            alert.addAction(action)
            self.present(alert, animated: true, completion: nil)
            self.data = nil
            self.tableView?.reloadData()
        }
    }
    
    private func selectRowsAfterHardRefresh(atIndexes indexes: [Int]) {
        guard indexes.isEmpty == false else { return }
        indexes.forEach({ self.tableView?.selectRow(at: IndexPath(row: $0, section: 0), animated: false, scrollPosition: .none) })
        let selectedItems = self.selectedURLItems
        self.updateBBI(with: selectedItems)
    }
    
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        self.tableView?.flashScrollIndicators()
        let isEditing = self.tableView?.isEditing ?? false
        if isEditing == false {
            // if we're not editing when we appear its probably because of fresh launch or because the user dismissed SafariVC
            // so we need to deselect the row
            self.tableView?.deselectAllRows(animated: true)
        }
        
        // Hack to reload UISearchController
        // See DecodeStateRestoration for more info
        if let searchControllerStateRestoration = self.searchControllerStateRestoration {
            self.hideLoadingView(searchControllerStateRestoration.wasActive)
            self.searchController.searchString = searchControllerStateRestoration.searchFilter
            self.searchControllerStateRestoration = nil
        }
    }
    
    // MARK: Watch for shake gesture for deleting
    
    private func validateDelete() -> Bool {
        guard self.presentedViewController == nil, self.selectedURLItems.isEmpty == false else { return false }
        return true
    }
    
    private func shakeToDelete() {
        let items = self.selectedURLItems
        guard let realmController = self.realmController, items.isEmpty == false else { return }
        let alert = UIAlertController(title: "Delete \(items.count) Item(s)?", message: "This action cannot be undone.", preferredStyle: .actionSheet)
        let cancel = UIAlertAction(title: "Cancel", style: .cancel, handler: nil)
        let delete = UIAlertAction(title: "Delete", style: .destructive) { _ in
            realmController.delete(items)
        }
        alert.addAction(cancel)
        alert.addAction(delete)
        self.emergencyDismissPopover(thenPresentViewController: alert)
    }
    
    override func motionEnded(_ motion: UIEventSubtype, with event: UIEvent?) {
        switch motion {
        case .motionShake:
            guard self.validateDelete() else { super.motionEnded(motion, with: event); return; }
            self.shakeToDelete()
        default:
            super.motionEnded(motion, with: event)
        }
    }
    
    // MARK: State Restoration
    
    private enum StateRestoration {
        fileprivate static let kTableViewWasEditing = "kTableViewWasEditingKey"
        fileprivate static let kSearchWasActive = "kSearchWasActiveKey"
        fileprivate static let kSearchString = "kSearchStringKey"
        fileprivate static let kVisibleItems = "kVisibleItemsKey"
        fileprivate static let kSelectedItems = "kSelectedItemsKey"
    }
    
    // Horrible Hack
    // Apple's Code Sample shows repopulating the SearchController in ViewDidAppear
    // Not in DecodeStateRestoration
    // This Struct and this propery make that possible
    // https://developer.apple.com/library/content/samplecode/TableSearch_UISearchController/Listings/Swift_TableSearch_MainTableViewController_swift.html
    
    private struct SearchControllerRestoration {
        fileprivate var wasActive: Bool
        fileprivate var searchFilter: String?
    }
    
    private var searchControllerStateRestoration: SearchControllerRestoration?
    private var visibleUUIDsStateRestoration: [String]?
    private var selectedUUIDsStateRestoration: [String]?
    
    override func decodeRestorableState(with coder: NSCoder) {
        let wasEditing = coder.decodeBool(forKey: StateRestoration.kTableViewWasEditing)
        if wasEditing == true {
            self.editBBITapped(self)
        }
        let wasActive = coder.decodeBool(forKey: StateRestoration.kSearchWasActive)
        if wasActive == true {
            let searchString = coder.decodeObject(forKey: StateRestoration.kSearchString) as? String
            self.searchControllerStateRestoration = SearchControllerRestoration(wasActive: wasActive, searchFilter: searchString)
        }
        self.visibleUUIDsStateRestoration = coder.decodeObject(forKey: StateRestoration.kVisibleItems) as? [String]
        self.selectedUUIDsStateRestoration = coder.decodeObject(forKey: StateRestoration.kSelectedItems) as? [String]
        
        super.decodeRestorableState(with: coder)
    }
    
    override func encodeRestorableState(with coder: NSCoder) {
        coder.encode(self.tableView?.isEditing ?? false, forKey: StateRestoration.kTableViewWasEditing)
        coder.encode(self.searchController.isActive, forKey: StateRestoration.kSearchWasActive)
        coder.encode(self.searchController.searchBar.text, forKey: StateRestoration.kSearchString)
        let visibleUUIDs = self.tableView?.indexPathsForVisibleRows?.flatMap({ self.data?[$0.row].uuid }) ?? []
        coder.encode(visibleUUIDs, forKey: StateRestoration.kVisibleItems)
        let selectedUUIDs = self.selectedURLItems.map({ $0.uuid })
        coder.encode(selectedUUIDs, forKey: StateRestoration.kSelectedItems)
        super.encodeRestorableState(with: coder)
    }
    
    // MARK: Deinit
    
    private var notificationToken: NotificationToken?
    
    deinit {
        self.notificationToken?.stop()<commit_msg>optimized visible row encoding<commit_after>//
//  URLListViewController.swift
//  Hipstapaper
//
//  Created by <NAME> on 12/16/16.
//  Copyright © 2016 <NAME>. All rights reserved.
//

import Common
import RealmSwift
import SafariServices
import UIKit

class ContentListViewController: UIViewController, RealmControllable {
    
    @IBOutlet fileprivate weak var loadingIndicatorViewController: LoadingIndicatorViewController?
    @IBOutlet fileprivate weak var tableView: UITableView? {
        didSet {
            let imageNib = UINib(nibName: ContentTableViewCell.withImageNIBName, bundle: Bundle(for: ContentTableViewCell.self))
            let noImageNib = UINib(nibName: ContentTableViewCell.withOutImageNIBName, bundle: Bundle(for: ContentTableViewCell.self))
            self.tableView?.register(imageNib, forCellReuseIdentifier: ContentTableViewCell.withImageNIBName)
            self.tableView?.register(noImageNib, forCellReuseIdentifier: ContentTableViewCell.withOutImageNIBName)
            self.tableView?.rowHeight = ContentTableViewCell.cellHeight
            self.tableView?.estimatedRowHeight = ContentTableViewCell.cellHeight
            self.tableView?.tableHeaderView = self.searchController.searchBar
        }
    }
    fileprivate lazy var searchController: UISearchController = {
        // lazy vs let fixes a bug where the searchBar icon was pixelated
        // probably caused by initializing before the screen is all configured
        let sc = UISearchController(searchResultsController: nil)
        sc.dimsBackgroundDuringPresentation = false
        sc.restorationIdentifier = StateRestorationIdentifier.searchController.rawValue
        return sc
    }()
    
    fileprivate typealias UIBBI = UIBarButtonItem
    fileprivate lazy var editBBI: UIBBI = UIBBI(barButtonSystemItem: UIBarButtonSystemItem.edit, target: self, action: #selector(self.editBBITapped(_:)))
    fileprivate lazy var doneBBI: UIBBI = UIBBI(barButtonSystemItem: UIBarButtonSystemItem.done, target: self, action: #selector(self.doneBBITapped(_:)))
    fileprivate lazy var archiveBBI: UIBBI = UIBBI(title: "📥Archive", style: .plain, target: self, action: #selector(self.archiveBBITapped(_:)))
    fileprivate lazy var unarchiveBBI: UIBBI = UIBBI(title: "📤Unarchive", style: .plain, target: self, action: #selector(self.unarchiveBBITapped(_:)))
    fileprivate lazy var tagBBI: UIBBI = UIBBI(title: "🏷Tag", style: .plain, target: self, action: #selector(self.tagBBITapped(_:)))
    fileprivate lazy var sortBBI: UIBBI = UIBBI(title: "Sort", style: .plain, target: self, action: #selector(self.sortBBITapped(_:)))
    fileprivate lazy var filterBBI: UIBBI = UIBBI(title: "Filter", style: .plain, target: self, action: #selector(self.filterBBITapped(_:)))
    fileprivate let flexibleSpaceBBI: UIBBI = UIBBI(barButtonSystemItem: .flexibleSpace, target: nil, action: nil)
    fileprivate let verticalBarSpaceBBI: UIBBI = {
        let bbi = UIBBI(title: "|", style: .plain, target: nil, action: nil)
        bbi.isEnabled = false
        return bbi
    }()
    
    // MARK: Selection
    
    var itemsToLoad = UserDefaults.standard.userSelection.itemsToLoad
    var filter = UserDefaults.standard.userSelection.filter
    var sortOrder = UserDefaults.standard.userSelection.sortOrder
    
    fileprivate weak var selectionDelegate: URLItemsToLoadChangeDelegate?
    
    // MARK: Data
    
    fileprivate var data: AnyRealmCollection<URLItem>?
    
    weak var realmController: RealmController? {
        didSet {
            // forward the message to any presented view controllers
            let addRemoveTagPopoverVC = (self.presentedViewController as? UINavigationController)?.viewControllers.first as? RealmControllable
            addRemoveTagPopoverVC?.realmController = self.realmController
            self.loadingIndicatorViewController?.realmController = self.realmController
            
            // reload the data
            self.hardReloadData()
        }
    }
    
    fileprivate var selectedURLItems: [URLItem] {
        let data = self.data
        let indexPaths = self.tableView?.indexPathsForSelectedRows
        let items = indexPaths?.flatMap({ data?[$0.row] })
        return items ?? []
    }
    
    convenience init(selectionDelegate: URLItemsToLoadChangeDelegate, controller: RealmController?) {
        self.init()
        self.selectionDelegate = selectionDelegate
        self.realmController = controller
    }

    override func viewDidLoad() {
        super.viewDidLoad()
        
        // configure the search delegate
        self.searchController.searchResultsUpdater = self
        
        // needed for the search delegate so it presents correctly
        self.definesPresentationContext = true
        
        // set the top constraint on the LoadingIndicatorViewController
        // this can't be done in the XIB
        if let loadingIndicatorViewController = self.loadingIndicatorViewController {
            self.addChildViewController(loadingIndicatorViewController)
            loadingIndicatorViewController.realmController = self.realmController
            loadingIndicatorViewController.view.topAnchor.constraint(equalTo: self.topLayoutGuide.bottomAnchor, constant: 10).isActive = true
        }
        
        // register for 3d touch events
        if case .available = self.traitCollection.forceTouchCapability {
            self.registerForPreviewing(with: self, sourceView: view)
        }
        
        // because we are in a split view, we fully own our own Navigation Controller
        // therefore we don't need to micromanage this when switching views.
        // its always present
        self.navigationController?.setToolbarHidden(false, animated: false)
        
        // configure myself for splitview
        self.navigationItem.leftBarButtonItem = self.splitViewController?.displayModeButtonItem
        self.navigationItem.leftItemsSupplementBackButton = true
        
        // put us into non-edit mode
        self.resetToolbar()
        self.disableAllBBI()
        
        // load the data
        self.hardReloadData()
    }
    
    fileprivate func hardReloadData() {
        // get values in case things change
        let itemsToLoad = self.itemsToLoad
        let filter = self.filter
        let sortOrder = self.sortOrder
        let searchFilter = self.searchController.searchString
        
        // set title
        switch itemsToLoad {
        case .all:
            switch self.filter {
            case .all:
                self.title = "All Items"
            case .unarchived:
                self.title = "Hipstapaper"
            }
        case .tag(let tagID):
            self.title = "🏷 \(tagID.displayName)"
        }
        
        // preserve any selection
        if self.selectedUUIDsStateRestoration == nil {
            self.selectedUUIDsStateRestoration = self.selectedURLItems.map({ $0.uuid })
        }
        
        // clear things out
        self.notificationToken?.stop()
        self.notificationToken = nil
        self.data = nil
        self.tableView?.reloadData()
        
        // configure data source
        let data = self.realmController?.url_loadAll(for: itemsToLoad, sortedBy: sortOrder, filteredBy: filter, searchFilter: searchFilter)
        self.notificationToken = data?.addNotificationBlock({ [weak self] in self?.realmResultsChanged($0) })
    }
    
    private func realmResultsChanged(_ changes: RealmCollectionChange<AnyRealmCollection<URLItem>>) {
        switch changes {
        case .initial(let data):
            // set the data
            self.data = data
            // find the previously selected items
            let previousSelectionIndexes = RealmController.indexes(ofItemUUIDs: self.selectedUUIDsStateRestoration ?? [], within: data)
            self.selectedUUIDsStateRestoration = nil // reset this so we don't do this on next refresh
            // find previously visible index
            let previouslyVisibleIndex = RealmController.indexes(ofItemUUIDs: self.visibleUUIDsStateRestoration ?? [], within: data)
            self.visibleUUIDsStateRestoration = nil // reset this so we don't do this on next refresh
            // hard reload the data
            self.tableView?.reloadData()
            
            // check if state restoration left us a scroll position
            if let index = previouslyVisibleIndex.last {
                self.tableView?.scrollToRow(at: IndexPath(row: index, section: 0), at: .bottom, animated: false)
            // otherwise we can check for conditions where we might want to hide the search bar
            } else if let topVisibleRow = self.tableView?.indexPathsForVisibleRows?.first, // there is a visible row
                self.searchController.isActive == false, // if the search controller is active, don't hijack scrolling
                self.searchController.isVisible == false // if the search controller is visible, don't hijack scrolling (this fixes a bug that caused it to scroll when cancelling the search controller
            {
                self.tableView?.scrollToRow(at: topVisibleRow, at: .top, animated: true)
            }
        
            // select the items found after updating the table
            self.selectRowsAfterHardRefresh(atIndexes: previousSelectionIndexes)
        case .update(let data, let deletions, let insertions, let modifications):
            // UITableView does not automatically reselect updated rows like NSTableView does
            // So I need manual selection code here
            // find the previously selected items
            let previousSelectionIndexes = RealmController.indexes(ofItemUUIDs: self.selectedURLItems.map({ $0.uuid }), within: data)
            
            // update the table
            self.tableView?.beginUpdates()
            self.tableView?.insertRows(at: insertions.map({ IndexPath(row: $0, section: 0) }), with: .right)
            self.tableView?.deleteRows(at: deletions.map({ IndexPath(row: $0, section: 0)}), with: .left)
            self.tableView?.reloadRows(at: modifications.map({ IndexPath(row: $0, section: 0) }), with: .automatic)
            self.tableView?.endUpdates()
            
            // select the items found after updating the table
            self.selectRowsAfterHardRefresh(atIndexes: previousSelectionIndexes)
        case .error(let error):
            let alert = UIAlertController(title: "Error Loading Reading List", message: error.localizedDescription, preferredStyle: .alert)
            let action = UIAlertAction(title: "Dismiss", style: .cancel, handler: nil)
            alert.addAction(action)
            self.present(alert, animated: true, completion: nil)
            self.data = nil
            self.tableView?.reloadData()
        }
    }
    
    private func selectRowsAfterHardRefresh(atIndexes indexes: [Int]) {
        guard indexes.isEmpty == false else { return }
        indexes.forEach({ self.tableView?.selectRow(at: IndexPath(row: $0, section: 0), animated: false, scrollPosition: .none) })
        let selectedItems = self.selectedURLItems
        self.updateBBI(with: selectedItems)
    }
    
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        self.tableView?.flashScrollIndicators()
        let isEditing = self.tableView?.isEditing ?? false
        if isEditing == false {
            // if we're not editing when we appear its probably because of fresh launch or because the user dismissed SafariVC
            // so we need to deselect the row
            self.tableView?.deselectAllRows(animated: true)
        }
        
        // Hack to reload UISearchController
        // See DecodeStateRestoration for more info
        if let searchControllerStateRestoration = self.searchControllerStateRestoration {
            self.hideLoadingView(searchControllerStateRestoration.wasActive)
            self.searchController.searchString = searchControllerStateRestoration.searchFilter
            self.searchControllerStateRestoration = nil
        }
    }
    
    // MARK: Watch for shake gesture for deleting
    
    private func validateDelete() -> Bool {
        guard self.presentedViewController == nil, self.selectedURLItems.isEmpty == false else { return false }
        return true
    }
    
    private func shakeToDelete() {
        let items = self.selectedURLItems
        guard let realmController = self.realmController, items.isEmpty == false else { return }
        let alert = UIAlertController(title: "Delete \(items.count) Item(s)?", message: "This action cannot be undone.", preferredStyle: .actionSheet)
        let cancel = UIAlertAction(title: "Cancel", style: .cancel, handler: nil)
        let delete = UIAlertAction(title: "Delete", style: .destructive) { _ in
            realmController.delete(items)
        }
        alert.addAction(cancel)
        alert.addAction(delete)
        self.emergencyDismissPopover(thenPresentViewController: alert)
    }
    
    override func motionEnded(_ motion: UIEventSubtype, with event: UIEvent?) {
        switch motion {
        case .motionShake:
            guard self.validateDelete() else { super.motionEnded(motion, with: event); return; }
            self.shakeToDelete()
        default:
            super.motionEnded(motion, with: event)
        }
    }
    
    // MARK: State Restoration
    
    private enum StateRestoration {
        fileprivate static let kTableViewWasEditing = "kTableViewWasEditingKey"
        fileprivate static let kSearchWasActive = "kSearchWasActiveKey"
        fileprivate static let kSearchString = "kSearchStringKey"
        fileprivate static let kVisibleItems = "kVisibleItemsKey"
        fileprivate static let kSelectedItems = "kSelectedItemsKey"
    }
    
    // Horrible Hack
    // Apple's Code Sample shows repopulating the SearchController in ViewDidAppear
    // Not in DecodeStateRestoration
    // This Struct and this propery make that possible
    // https://developer.apple.com/library/content/samplecode/TableSearch_UISearchController/Listings/Swift_TableSearch_MainTableViewController_swift.html
    
    private struct SearchControllerRestoration {
        fileprivate var wasActive: Bool
        fileprivate var searchFilter: String?
    }
    
    private var searchControllerStateRestoration: SearchControllerRestoration?
    private var visibleUUIDsStateRestoration: [String]?
    private var selectedUUIDsStateRestoration: [String]?
    
    override func decodeRestorableState(with coder: NSCoder) {
        let wasEditing = coder.decodeBool(forKey: StateRestoration.kTableViewWasEditing)
        if wasEditing == true {
            self.editBBITapped(self)
        }
        let wasActive = coder.decodeBool(forKey: StateRestoration.kSearchWasActive)
        if wasActive == true {
            let searchString = coder.decodeObject(forKey: StateRestoration.kSearchString) as? String
            self.searchControllerStateRestoration = SearchControllerRestoration(wasActive: wasActive, searchFilter: searchString)
        }
        self.visibleUUIDsStateRestoration = coder.decodeObject(forKey: StateRestoration.kVisibleItems) as? [String]
        self.selectedUUIDsStateRestoration = coder.decodeObject(forKey: StateRestoration.kSelectedItems) as? [String]
        
        super.decodeRestorableState(with: coder)
    }
    
    override func encodeRestorableState(with coder: NSCoder) {
        coder.encode(self.tableView?.isEditing ?? false, forKey: StateRestoration.kTableViewWasEditing)
        coder.encode(self.searchController.isActive, forKey: StateRestoration.kSearchWasActive)
        coder.encode(self.searchController.searchBar.text, forKey: StateRestoration.kSearchString)
        let selectedUUIDs = self.selectedURLItems.map({ $0.uuid })
        coder.encode(selectedUUIDs, forKey: StateRestoration.kSelectedItems)
        if let visibleIndexPaths = self.tableView?.indexPathsForVisibleRows,
            visibleIndexPaths.first != IndexPath(row: 0, section: 0) // don't save anything if we're scrolled at the top
        {
            let visibleUUIDs = visibleIndexPaths.flatMap({ self.data?[$0.row].uuid })
            coder.encode(visibleUUIDs, forKey: StateRestoration.kVisibleItems)
        }
        
        super.encodeRestorableState(with: coder)
    }
    
    // MARK: Deinit
    
    private var notificationToken: NotificationToken?
    
    deinit {
        self.notificationToken?.stop()
{'fraction_non_alphanumeric': 0.08023561008362327, 'fraction_numerical': 0.0017701275712628944, 'mean_word_length': 2.7789182331910967, 'pattern_counts': {'":': 0, '<': 13, '<?xml version=': 0, '>': 15, 'https://': 2, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '6101003', 'n_tokens_mistral': 8615, 'n_tokens_neox': 8470, 'n_words': 2214}