//
//  ProfileViewController.swift
//  Tusker
//
//  Created by Shadowfacts on 10/10/22.
//  Copyright © 2022 Shadowfacts. All rights reserved.
//

import UIKit
import Pachyderm
import Combine

class ProfileViewController: UIViewController, StateRestorableViewController {
    
    weak var mastodonController: MastodonController!
    
    // This property is optional because MyProfileViewController may not have the user's account ID
    // when first constructed. It should never be set to nil.
    var accountID: String? {
        willSet {
            precondition(newValue != nil, "Do not set ProfileViewController.accountID to nil")
        }
        didSet {
            pageControllers.forEach { $0.setAccountID(accountID!) }
            Task {
                await loadAccount()
            }
        }
    }

    private(set) var currentIndex: Int!
    private let pages = [Page.posts, .postsAndReplies, .media]
    private var pageControllers: [ProfileStatusesViewController]!
    var currentPage: Page {
        pages[currentIndex]
    }
    var currentViewController: ProfileStatusesViewController {
        pageControllers[currentIndex]
    }
    
    private var state: State = .idle
    
    private var cancellables = Set<AnyCancellable>()
    
    init(accountID: String?, mastodonController: MastodonController) {
        self.accountID = accountID
        self.mastodonController = mastodonController
        
        super.init(nibName: nil, bundle: nil)
        
        self.pageControllers = [
            .init(accountID: accountID, kind: .statuses, owner: self),
            .init(accountID: accountID, kind: .withReplies, owner: self),
            .init(accountID: accountID, kind: .onlyMedia, owner: self),
        ]
        
        // try to update the account UI immediately if possible, to avoid the navigation title popping in later
        if let accountID,
           let account = mastodonController.persistentContainer.account(for: accountID) {
            updateAccountUI(account: account)
        }
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()

        view.backgroundColor = .appBackground
        
        for pageController in pageControllers {
            pageController.profileHeaderDelegate = self
        }
        
        selectPage(at: 0, animated: false)
        
        let composeButton = UIBarButtonItem(barButtonSystemItem: .compose, target: self, action: #selector(composeMentioning))
        composeButton.menu = UIMenu(children: [
            UIAction(title: "Direct Message", image: UIImage(systemName: Visibility.direct.unfilledImageName), handler: { [unowned self] _ in
                self.composeDirectMentioning()
            })
        ])
        composeButton.isEnabled = mastodonController.loggedIn
        navigationItem.rightBarButtonItem = composeButton
        
        addKeyCommand(MenuController.prevSubTabCommand)
        addKeyCommand(MenuController.nextSubTabCommand)
        
        mastodonController.persistentContainer.accountSubject
            .receive(on: DispatchQueue.main)
            .filter { [unowned self] in $0 == self.accountID }
            .sink { [unowned self] id in
                let account = self.mastodonController.persistentContainer.account(for: id)!
                self.updateAccountUI(account: account)
            }
            .store(in: &cancellables)
        
        Task {
            await loadAccount()
        }
        
        // disable the transparent nav bar because it gets messy with multiple pages at different scroll positions
        if let nav = navigationController {
            let appearance = UINavigationBarAppearance()
            appearance.configureWithDefaultBackground()
            nav.navigationBar.scrollEdgeAppearance = appearance
        }
    }
    
    func updateUserActivity() {
        if let accountID,
           let currentAccountID = mastodonController.accountInfo?.id {
            userActivity = UserActivityManager.showProfileActivity(id: accountID, accountID: currentAccountID)
        }
    }
    
    private func loadAccount() async {
        guard let accountID else {
            return
        }
        if let account = mastodonController.persistentContainer.account(for: accountID) {
            updateAccountUI(account: account)
        } else {
            do {
                let req = Client.getAccount(id: accountID)
                let (account, _) = try await mastodonController.run(req)
                let mo = await withCheckedContinuation { continuation in
                    mastodonController.persistentContainer.addOrUpdate(account: account, in: mastodonController.persistentContainer.viewContext) { (mo) in
                        continuation.resume(returning: mo)
                    }
                }
                self.updateAccountUI(account: mo)
            } catch {
                let config = ToastConfiguration(from: error, with: "Loading Account", in: self) { [unowned self] toast in
                    toast.dismissToast(animated: true)
                    await self.loadAccount()
                }
                self.showToast(configuration: config, animated: true)
            }
        }
    }
    
    private func updateAccountUI(account: AccountMO) {
        updateUserActivity()
        navigationItem.title = account.displayNameWithoutCustomEmoji
    }
    
    private func selectPage(at index: Int, animated: Bool, completion: ((Bool) -> Void)? = nil) {
        guard case .idle = state else {
            return
        }
        
        state = .animating
        
        let new = pageControllers[index]
        
        guard let currentIndex else {
            assert(!animated)
            // if old doesn't exist, we're selecting the initial view controller, so moving the header around isn't necessary
            new.initialHeaderMode = .createView
            new.view.translatesAutoresizingMaskIntoConstraints = false
            addChild(new)
            view.addSubview(new.view)
            NSLayoutConstraint.activate([
                new.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
                new.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
                new.view.topAnchor.constraint(equalTo: view.topAnchor),
                new.view.bottomAnchor.constraint(equalTo: view.bottomAnchor),
            ])
            new.didMove(toParent: self)
            self.currentIndex = index
            state = .idle
            completion?(true)
            return
        }
        
        let direction: CGFloat
        if index - currentIndex > 0 {
            direction = 1 // forward
        } else {
            direction = -1 // reverse
        }
        
        let old = pageControllers[currentIndex]
        
        new.loadViewIfNeeded()
        
        self.currentIndex = index
        
        // TODO: old.headerCell could be nil if scrolled down and key command used
        let oldHeaderCell = old.headerCell!
        
        // old header cell must have the header view
        let headerView = oldHeaderCell.addConstraint(height: oldHeaderCell.bounds.height)!
        
        if let newHeaderCell = new.headerCell {
            _ = newHeaderCell.addConstraint(height: oldHeaderCell.bounds.height)
        } else {
            new.initialHeaderMode = .placeholder(height: oldHeaderCell.bounds.height)
        }
        
        // disable user interaction during animation, to avoid any potential weird race conditions
        headerView.isUserInteractionEnabled = false
        headerView.layer.zPosition = 100
        view.addSubview(headerView)
        let oldHeaderCellTop = oldHeaderCell.convert(CGPoint.zero, to: view).y
        let headerTopOffset = oldHeaderCellTop - view.safeAreaInsets.top
        let headerBottomOffset = oldHeaderCell.convert(CGPoint(x: 0, y: oldHeaderCell.bounds.maxY), to: view).y// - view.safeAreaInsets.top
        NSLayoutConstraint.activate([
            headerView.topAnchor.constraint(equalTo: view.topAnchor, constant: headerTopOffset),
            headerView.bottomAnchor.constraint(equalTo: view.topAnchor, constant: headerBottomOffset),
            headerView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
            headerView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
        ])
        
        // hide scroll indicators during the transition because otherwise the show through the
        // profile header, even though it has an opaque background
        old.collectionView.showsVerticalScrollIndicator = false
        new.collectionView.showsVerticalScrollIndicator = false
        
        let origOldContentOffset = old.collectionView.contentOffset
        // we can't just change the content offset during the animation, otherwise the new collection view doesn't size the cells at the top
        // and new's offset doesn't physically match old's, even though they're numerically the same
        let needsMatchContentOffsetWithTransform = new.state != .loaded
        let yTranslationToMatchOldContentOffset: CGFloat
        if needsMatchContentOffsetWithTransform {
            yTranslationToMatchOldContentOffset = -origOldContentOffset.y - view.safeAreaInsets.top
        } else {
            new.collectionView.contentOffset = origOldContentOffset
            yTranslationToMatchOldContentOffset = 0
        }
        
        if animated {
            // if the new view isn't tall enough to match content offsets
            if new.collectionView.contentSize.height - new.collectionView.bounds.height < old.collectionView.contentOffset.y {
                let additionalHeightNeededToMatchContentOffset = old.collectionView.contentOffset.y + old.collectionView.bounds.height - new.collectionView.contentSize.height
                new.collectionView.contentInset = UIEdgeInsets(top: 0, left: 0, bottom: additionalHeightNeededToMatchContentOffset, right: 0)
            }
            
            new.view.translatesAutoresizingMaskIntoConstraints = false
            embedChild(new)
            new.view.transform = CGAffineTransform(translationX: direction * view.bounds.width, y: yTranslationToMatchOldContentOffset)
            
            let animator = UIViewPropertyAnimator(duration: 0.5, timingParameters: UISpringTimingParameters(dampingRatio: 1, initialVelocity: .zero))
            animator.addAnimations {
                new.view.transform = CGAffineTransform(translationX: 0, y: yTranslationToMatchOldContentOffset)
                old.view.transform = CGAffineTransform(translationX: -direction * self.view.bounds.width, y: 0)
            }
            animator.addCompletion { _ in
                old.removeViewAndController()
                old.collectionView.transform = .identity
                
                new.collectionView.transform = .identity
                new.collectionView.contentOffset = origOldContentOffset
                
                // reenable scroll indicators after the switching animation is done
                old.collectionView.showsVerticalScrollIndicator = true
                new.collectionView.showsVerticalScrollIndicator = true
                
                headerView.isUserInteractionEnabled = true
                headerView.transform = .identity
                headerView.layer.zPosition = 0
                // move the header view into the new page controller's cell
                if let newHeaderCell = new.headerCell {
                    newHeaderCell.addHeader(headerView)
                } else {
                    new.initialHeaderMode = .useExistingView(headerView)
                }
                
                self.state = .idle
                completion?(true)
            }
            animator.startAnimation()
        } else {
            old.removeViewAndController()
            new.view.translatesAutoresizingMaskIntoConstraints = false
            embedChild(new)
            completion?(true)
        }
    }
    
    // MARK: Interaction
    
    @objc private func composeMentioning() {
        if let accountID,
           let account = mastodonController.persistentContainer.account(for: accountID) {
            compose(mentioningAcct: account.acct)
        }
    }
    
    private func composeDirectMentioning() {
        if let accountID,
           let account = mastodonController.persistentContainer.account(for: accountID) {
            let draft = mastodonController.createDraft(mentioningAcct: account.acct)
            draft.visibility = .direct
            compose(editing: draft)
        }
    }
    
    // MARK: StateRestorableViewController
    func stateRestorationActivity() -> NSUserActivity? {
        if let accountID,
           let accountInfo = mastodonController.accountInfo {
            return UserActivityManager.showProfileActivity(id: accountID, accountID: accountInfo.id)
        } else {
            return nil
        }
    }
}

extension ProfileViewController {
    enum Page: Hashable {
        case posts
        case postsAndReplies
        case media
    }
}

extension ProfileViewController {
    enum State {
        case idle
        case animating
    }
}

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

extension ProfileViewController: ToastableViewController {
}

extension ProfileViewController: ProfileHeaderViewDelegate {
    func profileHeader(_ headerView: ProfileHeaderView, selectedPageChangedTo newPage: Page) {
        guard case .idle = state else {
            headerView.pagesSegmentedControl.setSelectedOption(currentPage, animated: false)
            return
        }
        selectPage(at: pages.firstIndex(of: newPage)!, animated: true)
    }
}

extension ProfileViewController: TabbedPageViewController {
    func selectNextPage() {
        guard currentIndex < pageControllers.count - 1 else { return }
        currentViewController.headerCell?.view?.pagesSegmentedControl.setSelectedOption(pages[currentIndex + 1], animated: true)
        selectPage(at: currentIndex + 1, animated: true)
    }
    
    func selectPrevPage() {
        guard currentIndex > 0 else { return }
        currentViewController.headerCell?.view?.pagesSegmentedControl.setSelectedOption(pages[currentIndex - 1], animated: true)
        selectPage(at: currentIndex - 1, animated: true)
    }
}

extension ProfileViewController: TabBarScrollableViewController {
    func tabBarScrollToTop() {
        guard isViewLoaded else { return }
        currentViewController.tabBarScrollToTop()
    }
}

extension ProfileViewController: StatusBarTappableViewController {
    func handleStatusBarTapped(xPosition: CGFloat) -> StatusBarTapActionResult {
        guard isViewLoaded else { return .stop }
        return currentViewController.handleStatusBarTapped(xPosition: xPosition)
    }
}
