//
//  ConversationCollectionViewController.swift
//  Tusker
//
//  Created by Shadowfacts on 8/28/18.
//  Copyright © 2018 Shadowfacts. All rights reserved.
//

import UIKit
import Pachyderm

class ConversationCollectionViewController: UIViewController, CollectionViewController, RefreshableViewController {
    
    private unowned let conversationViewController: ConversationViewController
    private let mastodonController: MastodonController
    private let mainStatusID: String
    private let mainStatusState: CollapseState
    var statusIDToScrollToOnLoad: String
    var showStatusesAutomatically = false
    
    var collectionView: UICollectionView! {
        view as? UICollectionView
    }
    private var dataSource: UICollectionViewDiffableDataSource<Section, Item>!
    
    init(for mainStatusID: String, state: CollapseState, conversationViewController: ConversationViewController) {
        self.mainStatusID = mainStatusID
        self.mainStatusState = state
        self.statusIDToScrollToOnLoad = mainStatusID
        self.conversationViewController = conversationViewController
        self.mastodonController = conversationViewController.mastodonController
        
        super.init(nibName: nil, bundle: nil)
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override func loadView() {
        var config = UICollectionLayoutListConfiguration(appearance: .plain)
        config.backgroundColor = .appSecondaryBackground
        config.leadingSwipeActionsConfigurationProvider = { [unowned self] in
            (collectionView.cellForItem(at: $0) as? TimelineStatusCollectionViewCell)?.leadingSwipeActions()
        }
        config.trailingSwipeActionsConfigurationProvider = { [unowned self] in
            (collectionView.cellForItem(at: $0) as? TimelineStatusCollectionViewCell)?.trailingSwipeActions()
        }
        config.itemSeparatorHandler = { [unowned self] indexPath, sectionConfig in
            var config = sectionConfig
            config.topSeparatorVisibility = .hidden
            if case .ancestors = self.dataSource.sectionIdentifier(for: indexPath.section) {
                config.bottomSeparatorVisibility = .hidden
            } else if indexPath.row == self.collectionView.numberOfItems(inSection: indexPath.section) - 1 {
                config.bottomSeparatorVisibility = .visible
            } else {
                config.bottomSeparatorVisibility = .hidden
            }
            config.bottomSeparatorInsets = TimelineStatusCollectionViewCell.separatorInsets
            return config
        }
        let layout = UICollectionViewCompositionalLayout { sectionIndex, environment in
            let section = NSCollectionLayoutSection.list(using: config, layoutEnvironment: environment)
            section.readableContentInset(in: environment)
            return section
        }
        viewRespectsSystemMinimumLayoutMargins = false
        view = UICollectionView(frame: .zero, collectionViewLayout: layout)
        // something about the autoresizing mask breaks resizing the vc
        view.translatesAutoresizingMaskIntoConstraints = false
        collectionView.delegate = self
        collectionView.dragDelegate = self
        collectionView.allowsFocus = true
        
        #if !targetEnvironment(macCatalyst)
        collectionView.refreshControl = UIRefreshControl()
        collectionView.refreshControl!.addTarget(self, action: #selector(refresh), for: .valueChanged)
        #endif
        
        dataSource = createDataSource()
    }
    
    private func createDataSource() -> UICollectionViewDiffableDataSource<Section, Item> {
        let statusCell = UICollectionView.CellRegistration<TimelineStatusCollectionViewCell, (String, CollapseState, Bool, Bool)> { [unowned self] cell, indexPath, item in
            cell.delegate = self
            cell.showStatusAutomatically = self.showStatusesAutomatically
            cell.showReplyIndicator = false
            cell.updateUI(statusID: item.0, state: item.1, filterResult: .allow, precomputedContent: nil)
            cell.setShowThreadLinks(prev: item.2, next: item.3)
        }
        let mainStatusCell = UICollectionView.CellRegistration<ConversationMainStatusCollectionViewCell, (String, CollapseState, Bool)> { [unowned self] cell, indexPath, item in
            cell.delegate = self
            cell.showStatusAutomatically = self.showStatusesAutomatically
            cell.updateUI(statusID: item.0, state: item.1)
            cell.setShowThreadLinks(prev: item.2, next: false)
        }
        let expandThreadCell = UICollectionView.CellRegistration<ExpandThreadCollectionViewCell, ([ConversationNode], Bool)> { cell, indexPath, item in
            cell.updateUI(childThreads: item.0, inline: item.1)
        }
        let loadingCell = UICollectionView.CellRegistration<LoadingCollectionViewCell, Void> { cell, indexPath, itemIdentifier in
            cell.indicator.startAnimating()
        }
        return UICollectionViewDiffableDataSource(collectionView: collectionView) { [unowned self] collectionView, indexPath, itemIdentifier in
            switch itemIdentifier {
            case let .status(id: id, node: _, state: state, prevLink: prevLink, nextLink: nextLink):
                if id == self.mainStatusID {
                    return collectionView.dequeueConfiguredReusableCell(using: mainStatusCell, for: indexPath, item: (id, state, prevLink))
                } else {
                    return collectionView.dequeueConfiguredReusableCell(using: statusCell, for: indexPath, item: (id, state, prevLink, nextLink))
                }
            case .expandThread(childThreads: let childThreads, inline: let inline):
                return collectionView.dequeueConfiguredReusableCell(using: expandThreadCell, for: indexPath, item: (childThreads, inline))
            case .loadingIndicator:
                return collectionView.dequeueConfiguredReusableCell(using: loadingCell, for: indexPath, item: ())
            }
        }
    }
    
    override func viewWillAppear(_ animated: Bool) {
        super.viewWillAppear(animated)
        
        clearSelectionOnAppear(animated: animated)
    }
    
    func addMainStatus(_ status: StatusMO) {
        loadViewIfNeeded()
        
        var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
        snapshot.appendSections([.ancestors, .mainStatus])
        
        if status.inReplyToID != nil {
            snapshot.appendItems([.loadingIndicator], toSection: .ancestors)
        }
        
        // this will be replace with the actual node in the tree once it's loaded
        let tempMainNode = ConversationNode(status: status)
        let mainStatusItem = Item.status(id: mainStatusID, node: tempMainNode, state: mainStatusState, prevLink: status.inReplyToID != nil, nextLink: false)
        snapshot.appendItems([mainStatusItem], toSection: .mainStatus)
        
        dataSource.apply(snapshot, animatingDifferences: false)
    }
    
    func addTree(_ tree: ConversationTree, mainStatus: StatusMO) {
        var snapshot = NSDiffableDataSourceSnapshot<Section, Item>()
        snapshot.appendSections([.ancestors, .mainStatus])
        let mainStatusItem = Item.status(id: mainStatusID, node: tree.mainStatus, state: mainStatusState, prevLink: mainStatus.inReplyToID != nil, nextLink: false)
        snapshot.appendItems([mainStatusItem], toSection: .mainStatus)
        let parentItems = tree.ancestors.enumerated().map { index, node in
            Item.status(id: node.status.id, node: node, state: .unknown, prevLink: index > 0, nextLink: true)
        }
        snapshot.appendItems(parentItems, toSection: .ancestors)
        // don't need to reconfigure main item, since when the refreshed copy was loaded
        // it would have triggered a reconfigure via the status observer

        // convert sub-threads into items for section and add to snapshot
        self.addFlattenedChildThreadsToSnapshot(tree.descendants, mainStatus: mainStatus, snapshot: &snapshot)
        
        self.dataSource.apply(snapshot, animatingDifferences: false) {
            let item: Item
            let position: UICollectionView.ScrollPosition
            if self.statusIDToScrollToOnLoad == self.mainStatusID {
                item = mainStatusItem
                position = .centeredVertically
            } else {
                item = snapshot.itemIdentifiers.first {
                    if case .status(id: self.statusIDToScrollToOnLoad, _, _, _, _) = $0 {
                        return true
                    } else {
                        return false
                    }
                }!
                position = .top
            }
            // ensure that the status is on-screen after newly loaded statuses are added
            // todo: should this not happen if the user has already started scrolling (e.g. because the main status is very long)?
            if let indexPath = self.dataSource.indexPath(for: item) {
                self.collectionView.scrollToItem(at: indexPath, at: position, animated: false)
            }
        }
    }
    
    private func addFlattenedChildThreadsToSnapshot(_ childThreads: [ConversationNode], mainStatus: StatusMO, snapshot: inout NSDiffableDataSourceSnapshot<Section, Item>) {
        var childThreads = childThreads
        
        // child threads by the same author as the main status come first
        let pivotIndex = childThreads.partition(by: { $0.status.account.id != mainStatus.account.id })
        
        // within each group, child threads are sorted chronologically
        childThreads[0..<pivotIndex].sort(by: { $0.status.createdAt < $1.status.createdAt })
        childThreads[pivotIndex...].sort(by: { $0.status.createdAt < $1.status.createdAt })
        
        for node in childThreads {
            let section = Section.childThread(firstStatusID: node.status.id)
            snapshot.appendSections([section])
            snapshot.appendItems([.status(id: node.status.id, node: node, state: .unknown, prevLink: false, nextLink: !node.children.isEmpty)], toSection: section)
            
            var currentNode = node
            while true {
                let next: ConversationNode
                
                if currentNode.children.count == 0 {
                    break
                } else if currentNode.children.count == 1 {
                    next = currentNode.children[0]
                } else {
                    let sameAuthorStatuses = currentNode.children.filter({ $0.status.account.id == node.status.account.id })
                    if sameAuthorStatuses.count == 1 {
                        next = sameAuthorStatuses[0]
                        let nonSameAuthorChildren = currentNode.children.filter { $0.status.id != sameAuthorStatuses[0].status.id }
                        snapshot.appendItems([.expandThread(childThreads: nonSameAuthorChildren, inline: true)])
                    } else {
                        snapshot.appendItems([.expandThread(childThreads: currentNode.children, inline: false)])
                        break
                    }
                }

                currentNode = next
                snapshot.appendItems([.status(id: next.status.id, node: next, state: .unknown, prevLink: true, nextLink: !next.children.isEmpty)], toSection: section)
            }
        }
    }
    
    func updateVisibleCellCollapseState() {
        var snapshot = dataSource.snapshot()
        var cellsToMask: [StatusCollectionViewCell] = []
        for item in snapshot.itemIdentifiers {
            guard case .status(id: _, node: _, state: let state, prevLink: _, nextLink: _) = item,
                  state.collapsible == true else {
                continue
            }
            state.collapsed = !showStatusesAutomatically
            snapshot.reconfigureItems([item])
            
            if let indexPath = dataSource.indexPath(for: item),
               let cell = collectionView.cellForItem(at: indexPath) as? StatusCollectionViewCell {
                cellsToMask.append(cell)
            }
        }
        for cell in cellsToMask {
            cell.contentContainer.layer.masksToBounds = true
        }
        dataSource.apply(snapshot, animatingDifferences: true) {
            // this is an absurdly long delay, I have no idea why it's necessary
            // without it, the layer is not maksed during the animation
            // unless there's only one cell
            DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(500)) {
                for cell in cellsToMask {
                    cell.contentContainer.layer.masksToBounds = false
                }
            }
        }
    }
    
    @objc func refresh() {
        Task {
            await conversationViewController.refreshContext()
            #if !targetEnvironment(macCatalyst)
            self.collectionView.refreshControl!.endRefreshing()
            #endif
        }
    }
    
}

extension ConversationCollectionViewController {
    enum Section: Hashable {
        case ancestors
        case mainStatus
        case childThread(firstStatusID: String)
    }
    enum Item: Hashable {
        case status(id: String, node: ConversationNode, state: CollapseState, prevLink: Bool, nextLink: Bool)
        case expandThread(childThreads: [ConversationNode], inline: Bool)
        case loadingIndicator
        
        static func ==(lhs: Item, rhs: Item) -> Bool {
            switch (lhs, rhs) {
            case let (.status(id: a, node: _, state: _, prevLink: aPrev, nextLink: aNext), .status(id: b, node: _, state: _, prevLink: bPrev, nextLink: bNext)):
                return a == b && aPrev == bPrev && aNext == bNext
            case let (.expandThread(childThreads: a, inline: aInline), .expandThread(childThreads: b, inline: bInline)):
                return a.count == b.count && zip(a, b).allSatisfy { $0.status.id == $1.status.id } && aInline == bInline
            case (.loadingIndicator, .loadingIndicator):
                return true
            default:
                return false
            }
        }
        
        func hash(into hasher: inout Hasher) {
            switch self {
            case let .status(id: id, node: _, state: _, prevLink: prevLink, nextLink: nextLink):
                hasher.combine(0)
                hasher.combine(id)
                hasher.combine(prevLink)
                hasher.combine(nextLink)
            case .expandThread(childThreads: let childThreads, inline: let inline):
                hasher.combine(1)
                for thread in childThreads {
                    hasher.combine(thread.status.id)
                }
                hasher.combine(inline)
            case .loadingIndicator:
                hasher.combine(2)
            }
        }
    }
}

extension ConversationCollectionViewController: UICollectionViewDelegate {
    func collectionView(_ collectionView: UICollectionView, shouldSelectItemAt indexPath: IndexPath) -> Bool {
        switch dataSource.itemIdentifier(for: indexPath) {
        case .status(id: let id, node: _, state: _, prevLink: _, nextLink: _):
            return id != mainStatusID
        case .expandThread(childThreads: _, inline: _):
            return true
        default:
            return false
        }
    }
    
    func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
        switch dataSource.itemIdentifier(for: indexPath) {
        case nil:
            break
        case .loadingIndicator:
            break
        case .status(id: let id, node: let node, state: let state, _, _):
            // we can only take the fast path if the user tapped on a descendant status.
            // if the current main status is C, or one of its descendants, and the user taps A, then B won't be loaded:
            //    A
            //   / \
            //  B   C
            if case .childThread(_) = dataSource.sectionIdentifier(for: indexPath.section) {
                let tree = ConversationTree(ancestors: buildNewAncestors(above: indexPath), mainStatus: node)
                let conv = ConversationViewController(preloadedTree: tree, state: state.copy(), mastodonController: mastodonController)
                conv.showStatusesAutomatically = showStatusesAutomatically
                show(conv)
            } else {
                selected(status: id, state: state.copy())
            }
        case .expandThread(childThreads: let childThreads, inline: _):
            let indexPathBeforeExpandThread = IndexPath(row: indexPath.row - 1, section: indexPath.section)
            if case .status(id: _, node: let node, state: let state, _, _) = dataSource.itemIdentifier(for: indexPathBeforeExpandThread) {
                let tree = ConversationTree(ancestors: buildNewAncestors(above: indexPathBeforeExpandThread), mainStatus: node)
                let conv = ConversationViewController(preloadedTree: tree, state: state.copy(), mastodonController: mastodonController)
                conv.statusIDToScrollToOnLoad = childThreads.first!.status.id
                conv.showStatusesAutomatically = showStatusesAutomatically
                show(conv)
            }
        }
    }
    
    // ConversationNode doesn't know about its parent, so we reconstruct that info from the data source
    private func buildNewAncestors(above indexPath: IndexPath) -> [ConversationNode] {
        let snapshot = dataSource.snapshot()
        let currentAncestors = snapshot.itemIdentifiers(inSection: .ancestors).compactMap {
            if case .status(id: _, node: let node, _, _, _) = $0 {
                return node
            } else {
                return nil
            }
        }
        let currentMainStatus = snapshot.itemIdentifiers(inSection: .mainStatus).compactMap {
            if case .status(id: _, node: let node, _, _, _) = $0 {
                return node
            } else {
                return nil
            }
        }
        let parentsInCurrentSection = snapshot.itemIdentifiers(inSection: dataSource.sectionIdentifier(for: indexPath.section)!)[0..<indexPath.row].compactMap {
            if case .status(id: _, node: let node, _, _, _) = $0 {
                return node
            } else {
                return nil
            }
        }
        return currentAncestors + currentMainStatus + parentsInCurrentSection
    }
    
    
    func collectionView(_ collectionView: UICollectionView, contextMenuConfigurationForItemAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? {
        return (collectionView.cellForItem(at: indexPath) as? TimelineStatusCollectionViewCell)?.contextMenuConfiguration()
    }
    
    func collectionView(_ collectionView: UICollectionView, willPerformPreviewActionForMenuWith configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionCommitAnimating) {
        MenuPreviewHelper.willPerformPreviewAction(animator: animator, presenter: self)
    }
}

extension ConversationCollectionViewController: UICollectionViewDragDelegate {
    func collectionView(_ collectionView: UICollectionView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] {
        return (collectionView.cellForItem(at: indexPath) as? TimelineStatusCollectionViewCell)?.dragItemsForBeginning(session: session) ?? []
    }
}

extension ConversationCollectionViewController: TuskerNavigationDelegate {
    var apiController: MastodonController! { mastodonController }
}

extension ConversationCollectionViewController: MenuActionProvider {
}

extension ConversationCollectionViewController: StatusCollectionViewCellDelegate {
    func statusCellNeedsReconfigure(_ cell: StatusCollectionViewCell, animated: Bool, completion: (() -> Void)?) {
        if let indexPath = collectionView.indexPath(for: cell) {
            var snapshot = dataSource.snapshot()
            snapshot.reconfigureItems([dataSource.itemIdentifier(for: indexPath)!])
            dataSource.apply(snapshot, animatingDifferences: animated, completion: completion)
        }
    }
    
    func statusCellShowFiltered(_ cell: StatusCollectionViewCell) {
        // todo: support filtering in conversations
    }
}

extension ConversationCollectionViewController: TabBarScrollableViewController {
    func tabBarScrollToTop() {
        collectionView.scrollToTop()
    }
}

extension ConversationCollectionViewController: StatusBarTappableViewController {
    func handleStatusBarTapped(xPosition: CGFloat) -> StatusBarTapActionResult {
        collectionView.scrollToTop()
        return .stop
    }
}
