//
//  AccountsTabViewController.swift
//  Bal
//
//  Created by Benjamin Baron on 2/9/16.
//  Copyright © 2016 Balanced Software, Inc. All rights reserved.
//

import AppKit
import Cocoa
import SnapKit

class AccountsTabViewController: NSViewController, SectionedTableViewDelegate, SectionedTableViewDataSource, NSSearchFieldDelegate {
    
    struct InternalNotifications {
        static let CellOpened   = Notification.Name("AccountsCellOpened")
        static let CellClosed   = Notification.Name("AccountsCellClosed")
        
        struct Keys {
            static let Cell     = "Cell"
        }
    }
    
    //
    // MARK: - Properties -
    //
    
    let viewModel = AccountsTabViewModel()
    var previousSelectedIndex = TableIndex.none
    
    // MARK: Body
    let scrollView = ScrollView()
    let tableView = SectionedTableView()
    let addNewAccountView = AddNewAccountView()
    
    // MARK: Fix Password Prompt
    let fixPasswordPromptView = PaintCodeView()
    
    // MARK: Footer
    let totalFooterView = VisualEffectView()
    let balanceField = LabelField()
    let totalField = LabelField()
    
    //
    // MARK: - Lifecycle -
    //
    
    init() {
        super.init(nibName: nil, bundle: nil)
    }
    
    required init?(coder: NSCoder) {
        fatalError("unsupported")
    }
    
    deinit {
        unregisterForNotifications()
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        reloadData()
        
        registerForNotifications()
    }
    
    override func viewWillAppear() {
        super.viewWillAppear()
        
        adjustWindowHeight()
        
        // Fix row heights after rearranging accounts
        tableView.reloadData()
    }
    
    fileprivate var hackDelay = 0.25
    fileprivate var hackDelayCount = 2
    func adjustWindowHeight() {
        var finalHeight = CurrentTheme.defaults.size.height
        if InstitutionRepository.si.hasInstitutions, let delegate = tableView.delegate {
            // Calculate the table rows total height
            var tableHeight: CGFloat = 100 // Account for other UI elements
            let numberOfRows = tableView.numberOfRows
            
            if numberOfRows > 0 {
                for i in 0...numberOfRows-1 {
                    tableHeight += delegate.tableView!(tableView, heightOfRow: i)
                }
            }
            
            // Calculate height
            let minHeight: CGFloat = 100
            let maxHeight: CGFloat = AppDelegate.sharedInstance.maxHeight
            var height = minHeight
            if tableHeight > minHeight && tableHeight < maxHeight {
                height = tableHeight
            } else if tableHeight >= maxHeight {
                height = maxHeight
            }
            finalHeight = height
        }
        
        // TODO: Remove delay hack. Currently there to allow for the resize to work on app launch
        async(after: hackDelay) {
            if self.hackDelay > 0.0 {
                self.hackDelayCount -= 1
                if self.hackDelayCount == 0 {
                    self.hackDelay = 0.0
                }
            }
            
            if AppDelegate.sharedInstance.visibleTab == .accounts {
                AppDelegate.sharedInstance.resizeWindowHeight(finalHeight, animated: true)
            }
        }
    }
    
    //
    // MARK: - View Creation -
    //
    
    override func loadView() {
        self.view = View()
        
        createTable()
        createFixPasswordPrompt()
        createTotalFooter()
        showAddAccountIfNeeded()
    }
    
    func createTable() {
        scrollView.drawsBackground = false
        scrollView.hasVerticalScroller = true
        scrollView.hasHorizontalScroller = false
        scrollView.verticalScrollElasticity = .none
        scrollView.documentView = tableView
        let bottomOffset = defaults.hideAddAccountPrompt ? 50 : 130
        scrollView.contentInsets = NSEdgeInsetsMake(0, 0, CGFloat(bottomOffset), 0)
        self.view.addSubview(scrollView)
        scrollView.snp.makeConstraints { make in
            make.top.equalTo(self.view)
            make.left.equalTo(self.view)
            make.right.equalTo(self.view)
            make.bottom.equalTo(self.view)
        }
        
        tableView.setAccessibilityLabel("Accounts Table")
        tableView.customDelegate = self
        tableView.customDataSource = self
        tableView.displayEmptySectionRows = true
        tableView.intercellSpacing = NSZeroSize
        tableView.selectionHighlightStyle = .none
    
        view.addSubview(addNewAccountView)
        addNewAccountView.snp.makeConstraints { make in
            make.size.equalToSuperview()
        }
        
        tableView.reloadData()
    }
    
    fileprivate func createFixPasswordPrompt() {
        let fixPasswordInstitutions = InstitutionRepository.si.institutionsWithInvalidPasswords()
        let count = fixPasswordInstitutions.count
        if count > 0 {            
            for subview in fixPasswordPromptView.subviews {
                subview.removeFromSuperview()
            }
            
            let backgroundInset = 20
            let rowInset = 12
            let headerRowHeight = 39
            let rowHeight = 44
            
            fixPasswordPromptView.isClickingEnabled = false
            fixPasswordPromptView.drawingBlock = AccountConnectionErrors.drawConnectionErrorsLight
            self.view.addSubview(fixPasswordPromptView)
            fixPasswordPromptView.snp.makeConstraints { make in
                let height = (backgroundInset * 2) + headerRowHeight + (rowHeight * count) - 1
                make.height.equalTo(height)
                make.width.equalTo(400)
                make.centerX.equalTo(self.view)
                make.bottom.equalTo(self.view).offset(-31)
            }
            
            let containerView = View()
            fixPasswordPromptView.addSubview(containerView)
            containerView.snp.makeConstraints { make in
                make.left.equalTo(fixPasswordPromptView).offset(backgroundInset)
                make.right.equalTo(fixPasswordPromptView).offset(-backgroundInset)
                make.top.equalTo(fixPasswordPromptView).offset(backgroundInset)
                make.bottom.equalTo(fixPasswordPromptView).offset(-backgroundInset)
            }
            
            let headerRow = View()
            containerView.addSubview(headerRow)
            headerRow.snp.makeConstraints { make in
                make.height.equalTo(headerRowHeight)
                make.top.equalTo(containerView)
                make.left.equalTo(containerView).offset(rowInset)
                make.right.equalTo(containerView).offset(-rowInset)
            }
            
            let headerIcon = ImageView()
            headerIcon.image = #imageLiteral(resourceName: "errorAlertLight")
            headerRow.addSubview(headerIcon)
            headerIcon.snp.makeConstraints { make in
                make.width.equalTo(headerIcon.image?.size.width ?? 0)
                make.height.equalTo(headerIcon.image?.size.width ?? 0)
                make.left.equalTo(headerRow)
                make.centerY.equalTo(headerRow)
            }
            
            let headerLabel = LabelField()
            headerLabel.stringValue = "We're having trouble connecting..."
            headerLabel.font = CurrentTheme.accounts.fixPasswordPrompt.headerFont
            headerLabel.textColor = CurrentTheme.accounts.fixPasswordPrompt.headerTextColor
            headerLabel.verticalAlignment = .center
            headerRow.addSubview(headerLabel)
            headerLabel.snp.makeConstraints { make in
                make.height.equalTo(headerRow)
                make.left.equalTo(headerIcon.snp.right).offset(7)
                make.right.equalTo(headerRow)
                make.top.equalTo(headerRow).offset(-1)
            }
            
            var row = 0
            for institution in fixPasswordInstitutions {
                let rowView = View()
                containerView.addSubview(rowView)
                rowView.snp.makeConstraints { make in
                    make.height.equalTo(rowHeight)
                    let top = headerRowHeight + (rowHeight * row)
                    make.top.equalTo(top)
                    make.left.equalTo(containerView).offset(rowInset)
                    make.right.equalTo(containerView).offset(-rowInset)
                }
                
                let reconnectButton = PaintCodeButton()
                reconnectButton.textDrawingFunction = AccountConnectionErrors.drawReconnectButtonLight
                reconnectButton.buttonText = "Reconnect"
                reconnectButton.buttonTextColor = CurrentTheme.accounts.fixPasswordPrompt.buttonTextColor
                reconnectButton.object = institution
                reconnectButton.target = self
                reconnectButton.action = #selector(reconnect(sender:))
                rowView.addSubview(reconnectButton)
                reconnectButton.snp.makeConstraints { make in
                    make.width.equalTo(82)
                    make.height.equalTo(27)
                    make.right.equalTo(rowView).offset(1)
                    make.centerY.equalTo(rowView)
                }
                
                let nameLabel = LabelField()
                nameLabel.stringValue = institution.name
                nameLabel.verticalAlignment = .center
                nameLabel.font = CurrentTheme.accounts.fixPasswordPrompt.nameFont
                nameLabel.textColor = CurrentTheme.accounts.fixPasswordPrompt.nameTextColor
                rowView.addSubview(nameLabel)
                nameLabel.snp.makeConstraints { make in
                    make.height.equalTo(rowView)
                    make.left.equalTo(rowView).offset(1)
                    make.right.equalTo(reconnectButton.snp.left).offset(-5)
                    make.centerY.equalTo(rowView).offset(-1)
                }
                
                if row != 0 {
                    let separator = View()
                    separator.layerBackgroundColor = CurrentTheme.accounts.fixPasswordPrompt.separatorColor
                    containerView.addSubview(separator)
                    separator.snp.makeConstraints { make in
                        make.height.equalTo(1)
                        make.left.equalTo(containerView)
                        make.right.equalTo(containerView)
                        make.top.equalTo(rowView)
                    }
                }
                
                row += 1
            }
        } else {
            fixPasswordPromptHide()
        }
    }
    
    fileprivate func fixPasswordPromptHide() {
        for subview in fixPasswordPromptView.subviews {
            subview.removeFromSuperview()
        }
        fixPasswordPromptView.removeFromSuperview()
    }
    
    @objc fileprivate func reconnect(sender: Button) {
        if let institution = sender.object as? Institution {
            let userInfo = Notifications.userInfoForInstitution(institution)
            NotificationCenter.postOnMainThread(name: Notifications.ShowPatchAccount, object: nil, userInfo: userInfo)
        }
    }
    
    fileprivate func createTotalFooter() {
        totalFooterView.blendingMode = .withinWindow
        totalFooterView.material = CurrentTheme.defaults.material
        totalFooterView.state = .active
        totalFooterView.layerBackgroundColor = CurrentTheme.defaults.totalFooter.totalBackgroundColor
        self.view.addSubview(totalFooterView)
        totalFooterView.snp.makeConstraints { make in
            make.height.equalTo(57)
            make.left.equalTo(self.view)
            make.right.equalTo(self.view)
            make.bottom.equalTo(self.view)
        }

        balanceField.stringValue = "Balance"
        balanceField.alignment = .left
        balanceField.verticalAlignment = .center
        balanceField.font = CurrentTheme.defaults.totalFooter.titleFont
        balanceField.textColor = CurrentTheme.defaults.totalFooter.titleColor
        balanceField.usesSingleLineMode = true
        totalFooterView.addSubview(balanceField)
        balanceField.snp.makeConstraints { make in
            make.left.equalToSuperview().offset(16)
            make.top.equalToSuperview().offset(-1)
            make.height.equalToSuperview()
        }
        
        totalField.alignment = .right
        totalField.verticalAlignment = .center
        totalField.font = CurrentTheme.defaults.totalFooter.amountFont
        totalField.textColor = CurrentTheme.defaults.totalFooter.amountColor
        totalField.usesSingleLineMode = true
        totalField.setAccessibilityLabel("Total Balance")
        totalFooterView.addSubview(totalField)
        totalField.snp.makeConstraints { make in
            make.right.equalTo(totalFooterView).offset(-16)
            make.top.equalTo(totalFooterView).offset(2)
            make.height.equalToSuperview()
        }
    }
    
    //
    // MARK: - Notifications -
    //
    
    func registerForNotifications() {
        NotificationCenter.addObserverOnMainThread(self, selector: #selector(institutionAdded(_:)), name: Notifications.InstitutionAdded)
        NotificationCenter.addObserverOnMainThread(self, selector: #selector(institutionRemoved(_:)), name: Notifications.InstitutionRemoved)
        NotificationCenter.addObserverOnMainThread(self, selector: #selector(institutionPatched(_:)), name: Notifications.InstitutionPatched)
        
        NotificationCenter.addObserverOnMainThread(self, selector: #selector(accountRemoved(_:)), name: Notifications.AccountRemoved)
        
        NotificationCenter.addObserverOnMainThread(self, selector: #selector(accountExcludedFromTotal(_:)), name: Notifications.AccountExcludedFromTotal)
        NotificationCenter.addObserverOnMainThread(self, selector: #selector(accountIncludedInTotal(_:)), name: Notifications.AccountIncludedInTotal)
        
        NotificationCenter.addObserverOnMainThread(self, selector: #selector(accountHidden(_:)), name: Notifications.AccountHidden)
        NotificationCenter.addObserverOnMainThread(self, selector: #selector(accountUnhidden(_:)), name: Notifications.AccountUnhidden)
        
        NotificationCenter.addObserverOnMainThread(self, selector: #selector(syncCompleted(_:)), name: Notifications.SyncCompleted)
        
        NotificationCenter.addObserverOnMainThread(self, selector: #selector(masterCurrencyChanged(_:)), name: Notifications.MasterCurrencyChanged)
    }
    
    func unregisterForNotifications() {
        NotificationCenter.removeObserverOnMainThread(self, name: Notifications.InstitutionAdded)
        NotificationCenter.removeObserverOnMainThread(self, name: Notifications.InstitutionRemoved)
        NotificationCenter.removeObserverOnMainThread(self, name: Notifications.InstitutionPatched)
        
        NotificationCenter.removeObserverOnMainThread(self, name: Notifications.AccountRemoved)
        
        NotificationCenter.removeObserverOnMainThread(self, name: Notifications.AccountExcludedFromTotal)
        NotificationCenter.removeObserverOnMainThread(self, name: Notifications.AccountIncludedInTotal)
        
        NotificationCenter.removeObserverOnMainThread(self, name: Notifications.AccountHidden)
        NotificationCenter.removeObserverOnMainThread(self, name: Notifications.AccountUnhidden)

        NotificationCenter.removeObserverOnMainThread(self, name: Notifications.SyncCompleted)
        
        NotificationCenter.removeObserverOnMainThread(self, name: Notifications.MasterCurrencyChanged)
    }
    
    fileprivate let institutionUpdateDelay = 0.3
    
    // Have to do all this because tableView.updateRows equality checks don't work for Swift objects, so we need to make sure
    // that the references are equal or the animation is broken
    @objc fileprivate func institutionAdded(_ notification: Notification) {
        var reload = true
        if let institutionId = notification.userInfo?[Notifications.Keys.InstitutionId] as? Int {
            if let institution = InstitutionRepository.si.institution(institutionId: institutionId) {
                let oldData = viewModel.data
                viewModel.institutionAdded(institution: institution)
                reload = false
                
                self.invalidateTouchBar()
                
                // Wait for the animation to finish before sliding in the rows
                async(after: institutionUpdateDelay) {
                    do {
                        try ObjC.catchException {
                            self.tableView.updateRows(oldObjects: oldData.flattened as NSArray, newObjects: self.viewModel.data.flattened as NSArray, animationOptions: [NSTableView.AnimationOptions.effectFade, NSTableView.AnimationOptions.slideDown])
                        }
                    } catch {
                        self.tableView.reloadData()
                    }
                    
                    self.updateTotalBalance()
                    self.adjustWindowHeight()
                    self.createFixPasswordPrompt()

                    // Scroll to the end of the table
                    async(after: self.institutionUpdateDelay) {
                        NSAnimationContext.runAnimationGroup({ context in
                            context.allowsImplicitAnimation = true
                            self.tableView.scrollRowToVisible(self.viewModel.data.count - 1)
                        }, completionHandler: nil)
                    }
                }
            }
        }
        
        if reload {
            // Note: This should never happen, just a fallback
            async(after: institutionUpdateDelay) {
                self.reloadData()
            }
        }
    }
    
    // Have to do all this because tableView.updateRows equality checks don't work for Swift objects, so we need to make sure
    // that the references are equal or the animation is broken
    @objc fileprivate func institutionRemoved(_ notification: Notification) {
        var reload = true
        if let institution = notification.userInfo?[Notifications.Keys.Institution] as? Institution {
            let oldData = viewModel.data
            viewModel.institutionRemoved(institution: institution)
            reload = false
            
            self.invalidateTouchBar()
            
            // Wait for the animation to finish before sliding in the rows
            async(after: institutionUpdateDelay) {
                do {
                    try ObjC.catchException {
                        self.tableView.updateRows(oldObjects: oldData.flattened as NSArray, newObjects: self.viewModel.data.flattened as NSArray, animationOptions: [NSTableView.AnimationOptions.effectFade, NSTableView.AnimationOptions.slideDown])
                    }
                } catch {
                    self.tableView.reloadData()
                }
                
                self.updateTotalBalance()
                self.createFixPasswordPrompt()
                
                // Only adjust if we're visible
                if self.view.window != nil {
                    self.adjustWindowHeight()
                }
            }
        }
        
        if reload {
            // Note: This should never happen, just a fallback
            async(after: institutionUpdateDelay) {
                self.reloadData()
            }
        }
    }
    
    @objc fileprivate func institutionPatched(_ notification: Notification) {
        reloadData()
    }
    
    // Have to do all this because tableView.updateRows equality checks don't work for Swift objects, so we need to make sure
    // that the references are equal or the animation is broken
    @objc fileprivate func accountRemoved(_ notification: Notification) {
        var reload = true
        if let account = notification.userInfo?[Notifications.Keys.Account] as? Account {
            let oldData = viewModel.data
            viewModel.accountRemoved(account: account)
            reload = false
            
            do {
                try ObjC.catchException {
                    self.tableView.updateRows(oldObjects: oldData.flattened as NSArray, newObjects: viewModel.data.flattened as NSArray, animationOptions: [NSTableView.AnimationOptions.effectFade, NSTableView.AnimationOptions.slideDown])
                }
            } catch {
                self.tableView.reloadData()
            }
            
            self.updateTotalBalance()
            
            self.adjustWindowHeight()
        }
        
        if reload {
            // Note: This should never happen, just a fallback
            async(after: institutionUpdateDelay) {
                self.reloadData()
            }
        }
    }
    
    @objc fileprivate func accountExcludedFromTotal(_ notification: Notification) {
        updateTotalBalance()
    }
    
    @objc fileprivate func accountIncludedInTotal(_ notification: Notification) {
        updateTotalBalance()
    }
    
    @objc fileprivate func accountHidden(_ notification: Notification) {
        reloadData()
    }

    @objc fileprivate func accountUnhidden(_ notification: Notification) {
        reloadData()
    }
    
    @objc fileprivate func syncCompleted(_ notification: Notification) {
        reloadData()
    }
    
    @objc fileprivate func masterCurrencyChanged(_ notification: Notification) {
        reloadData()
    }
    
    //
    // MARK: - Data Reloading -
    //
    
    func reloadData() {
        NSObject.cancelPreviousPerformRequests(withTarget: self, selector: #selector(reloadDataDelayed), object: nil)
        self.perform(#selector(reloadDataDelayed), with: nil, afterDelay: 0.5)
    }
    
    @objc func reloadDataDelayed() {
        // Load the sort order
        viewModel.reloadData()
        updateTotalBalance()
        tableView.reloadData()
        createFixPasswordPrompt()
        invalidateTouchBar()
        adjustWindowHeight()
    }
    
    func updateTotalBalance() {
        totalField.stringValue = amountToString(amount: viewModel.totalBalance(), currency: defaults.masterCurrency, showNegative: true, showCodeAfterValue: true)
        showAddAccountIfNeeded()
    }
    
    func showAddAccountIfNeeded() {
        addNewAccountView.hideView(viewModel.data.count > 0)
    }
    
    //
    // MARK: - Table View -
    //

    func tableView(_ tableView: SectionedTableView, clickedRow row: Int, inSection section: Int) {
        // Click to view transactions not implemented yet until we re-implement searching/filtering
        return
    }
    
    func numberOfSectionsInTableView(_ tableView: SectionedTableView) -> Int {
        return viewModel.numberOfSections()
    }
    
    func tableView(_ tableView: SectionedTableView, numberOfRowsInSection section: Int) -> Int {
        return viewModel.numberOfRows(inSection: section)
    }
    
    func tableView(_ tableView: SectionedTableView, heightOfSection section: Int) -> CGFloat {
        return CurrentTheme.accounts.headerCell.height
    }
    
    func tableView(_ tableView: SectionedTableView, heightOfRow row: Int, inSection section: Int) -> CGFloat {
        var height = CurrentTheme.accounts.cell.height
        if viewModel.isLastRow(row, inSection: section) {
            // Make last row taller
            height += 10.0
        }
        
        if TableIndex(section: section, row: row) == tableView.selectedIndex {
            return height + 50.0
        } else {
            return height
        }
    }
    
    func tableView(_ tableView: SectionedTableView, rowViewForSection section: Int) -> NSTableRowView? {
        var rowView = tableView.makeView(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: "Institution Row"), owner: self) as? HoverTableRowView
        if rowView == nil {
            rowView = HoverTableRowView()
            rowView?.identifier = NSUserInterfaceItemIdentifier(rawValue: "Institution Row")
        }
        
        if let institution = viewModel.institution(forSection: section) {
            let color = institution.source.color
            rowView?.color = color
            rowView?.hoverColor = color
        }
        
        return rowView
    }
    
    func tableView(_ tableView: SectionedTableView, rowViewForRow row: Int, inSection section: Int) -> NSTableRowView? {
        var rowView = tableView.makeView(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: "Account Row"), owner: self) as? HoverTableRowView
        if rowView == nil {
            rowView = HoverTableRowView()
            rowView?.identifier = NSUserInterfaceItemIdentifier(rawValue: "Account Row")
        }
        
        if let account = viewModel.account(forRow: row, inSection: section) {
            let color = account.source.color
            rowView?.color = color
            rowView?.hoverColor = color
        }
        
        return rowView
    }
    
    func tableView(_ tableView: SectionedTableView, viewForSection section: Int) -> NSView? {
        let cell = AccountsTabGroupCell()
        cell.identifier = NSUserInterfaceItemIdentifier(rawValue: "Group Cell")
        
        if let institution = viewModel.institution(forSection: section) {
            let previousSectionColor = viewModel.institution(forSection: section - 1)?.source.color ?? .clear
            cell.updateModel(institution, previousSectionColor: previousSectionColor)
        }
        
        return cell
    }
    
    func tableView(_ tableView: SectionedTableView, viewForRow row: Int, inSection section: Int) -> NSView? {
        let cell = tableView.makeView(withIdentifier: NSUserInterfaceItemIdentifier(rawValue: "Account Cell"), owner: self) as? AccountsTabAccountCell ?? AccountsTabAccountCell()
        cell.identifier = NSUserInterfaceItemIdentifier(rawValue: "Account Cell")
        
        let index = TableIndex(section: section, row: row)
        cell.index = index
        
        if let account = viewModel.account(forRow: row, inSection: section) {
            let isLastRowInSection =  viewModel.isLastRow(row, inSection: section)
            cell.updateModel(account, isLastRowInSection: isLastRowInSection)
        }
        
        cell.rowBackgroundColor = { TableIndex -> NSColor? in
            if let rowView = self.tableView.rowViewAtIndex(index, makeIfNecessary: false) as? HoverTableRowView {
                return rowView.currentColor
            }
            return nil
        }
        
        if index == previousSelectedIndex {
            let userInfo = [InternalNotifications.Keys.Cell: cell]
            NotificationCenter.postOnMainThread(name: InternalNotifications.CellClosed, object: nil, userInfo: userInfo)
            
            cell.hideBottomContainer()
            tableView.deselectIndex(index)
            tableView.noteHeightOfIndex(index)
            previousSelectedIndex = TableIndex.none
        }

        let selectedIndex = tableView.selectedIndex
        if selectedIndex != TableIndex.none {
            cell.alphaValue = index == selectedIndex ? 1.0 : CurrentTheme.accounts.cell.dimmedAlpha
        }
        
        return cell
    }
    
    // MARK: Rearranging
    
    func tableView(_ tableView: SectionedTableView, canDragIndex index: TableIndex) -> Bool {
        return true
    }

    func tableView(_ tableView: SectionedTableView, dragImageForProposedDragImage dragImage: NSImage, index: TableIndex) -> NSImage {
        let size = NSSize(width: dragImage.size.width, height: CurrentTheme.accounts.cell.height)
        let clippedImage = dragImage.clippedImage(size)
        let alphaImage = clippedImage.alphaImage(0.35)
        return alphaImage
    }
    
    func tableView(_ tableView: SectionedTableView, validateDropFromIndex fromIndex: TableIndex, toIndex: TableIndex) -> NSDragOperation {
        if fromIndex.row == -1 {
            // Dragging a section
            if toIndex.row == -1 && fromIndex.section != toIndex.section && fromIndex.section != toIndex.section - 1 {
                return .move
            }
        } else {
            // Dragging a row
            if fromIndex.section == toIndex.section {
                // Only allow drops within the same section and not to the same place
                if toIndex.row >= 0 && fromIndex.row != toIndex.row && fromIndex.row != toIndex.row - 1 {
                    return .move
                }
            } else if fromIndex.section == toIndex.section - 1 && toIndex.row == -1 {
                // Allow drops to the end of the section
                return .move
            }
        }
        
        return NSDragOperation()
    }
    
    func tableView(_ tableView: SectionedTableView, acceptDropFromIndex fromIndex: TableIndex, toIndex: TableIndex, dropOperation: NSTableView.DropOperation) -> Bool {
        if fromIndex.row == -1 {
            // Moving a section
            var keys = viewModel.data.keys
            let fromInstitution = keys[fromIndex.section]

            // Correct row index for moving rows down
            if fromIndex < toIndex {
                toIndex.section -= 1
            }
            
            // Modify the data model
            let oldData = viewModel.data
            keys.remove(at: fromIndex.section)
            keys.insert(fromInstitution, at: toIndex.section)
            viewModel.data.keys = keys
            viewModel.persistSortOrder()
            
            // Update the UI
            tableView.updateRows(oldObjects: oldData.flattened as NSArray, newObjects: viewModel.data.flattened as NSArray, animationOptions: NSTableView.AnimationOptions.slideDown)
            
            // Update touch bar
            invalidateTouchBar()
            
            // Analytics
            analytics.trackEvent(withName: "Accounts tab institution rearranged")
            
            return true
        } else {
            // Moving a row
            if var accounts = viewModel.data[fromIndex.section] {
                // Handle dragging to end of section
                if toIndex.section > fromIndex.section {
                    toIndex.section = fromIndex.section
                    toIndex.row = accounts.count
                }
                
                // Correct row index for moving rows down
                if fromIndex < toIndex {
                    toIndex.row -= 1
                }
                
                // Modify the data model
                let fromAccount = accounts[fromIndex.row]
                accounts.remove(at: fromIndex.row)
                accounts.insert(fromAccount, at: toIndex.row)
                viewModel.data[fromIndex.section] = accounts
                viewModel.persistSortOrder()
                
                // Update the UI
                tableView.moveRowAtTableIndex(fromIndex, toIndex: toIndex)
                
                // Analytics
                analytics.trackEvent(withName: "Accounts tab account rearranged")
                
                return true
            }
        }
        
        return false
    }
}
