//
//  Carousel.swift
//  MVMCoreUI
//
//  Created by Scott Pfeil on 7/2/19.
//  Copyright © 2019 Verizon Wireless. All rights reserved.
//

import UIKit

/// Contracts behavior between carousel and its page control.
public protocol CarouselPageControlProtocol {
    var currentIndex: Int { get set }
    var numberOfPages: Int { get set }
    var indicatorTouchAction: ((CarouselPageControlProtocol) -> ())? { get set }
    func scrollViewDidScroll(_ collectionView: UICollectionView)
}

open class Carousel: View {
    
    public let collectionView: CollectionView = {
        let layout = CarouselCollectionLayout()
        layout.scrollDirection = .horizontal
        layout.minimumInteritemSpacing = 0
        layout.minimumLineSpacing = 0
        return CollectionView(frame: .zero, collectionViewLayout: layout)
    }()
    
    /// The current index of the collection view. Includes dummy cells when looping.
    public var currentIndex = 0
    
    /// The index of the page, does not include dummy cells.
    public var pageIndex: Int {
        get { return loop ? currentIndex - 2 : currentIndex }
        set (newIndex) {
            currentIndex = loop ? newIndex + 2 : newIndex
        }
    }
    
    /// The number of pages that there are. Used for the page control and for calculations. Should not include the looping dummy cells. Be sure to set this if subclassing and not using the molecules.
    open var numberOfPages = 0
    
    /// The models for the molecules.
    public var molecules: [MoleculeModelProtocol & CarouselItemModelProtocol]?
    
    /// The horizontal alignment of the cell in the collection view. Only noticeable if the itemWidthPercent is less than 100%.
    public var itemAlignment = UICollectionView.ScrollPosition.left
    
    /// From 0-1. The item width as a percent of the carousel width.
    public var itemWidthPercent: CGFloat = 1
    
    /// The height of the carousel.
    public var collectionViewHeight: NSLayoutConstraint?
    
    /// The view that we use for paging
    public var pagingView: (MoleculeViewProtocol & CarouselPageControlProtocol)?
    
    /// If the carousel should loop after scrolling past the first and final cells.
    public var loop = false
    
    private var dragging = false
    
    // For adding pager
    public var bottomPin: NSLayoutConstraint?
    
    public var delegateObject: MVMCoreUIDelegateObject?
    
    private var size: CGFloat?
    
    // Updates the model and index.
    public func updateModelIndex() {
        (model as? CarouselModel)?.index = pageIndex
    }
    
    open override func layoutSubviews() {
        super.layoutSubviews()
        // Accounts for any collection size changes
        carouselAccessibilityElement?.accessibilityFrameInContainerSpace = collectionView.frame
        DispatchQueue.main.async {
            self.layoutCollection()
        }
    }
    
    /// Invalidates the layout and ensures we are paged to the correct cell.
    open func layoutCollection() {
        collectionView.collectionViewLayout.invalidateLayout()
        showPeaking(false)
        
        // Go to current cell. layoutIfNeeded is needed otherwise cellForItem returns nil for peaking logic. The dispatch is a sad way to ensure the collection view is ready to be scrolled.
        guard let model = model as? CarouselModel,
              (model.paging == true || loop == true) else { return }
        DispatchQueue.main.async {
            self.collectionView.scrollToItem(at: IndexPath(row: self.currentIndex, section: 0), at: self.itemAlignment, animated: false)
            self.collectionView.layoutIfNeeded()
            self.showPeaking(true)
        }
    }
    
    //--------------------------------------------------
    // MARK: - MVMCoreViewProtocol
    //--------------------------------------------------
    
    open override func setupView() {
        super.setupView()
        collectionView.dataSource = self
        collectionView.delegate = self
        addSubview(collectionView)
        bottomPin = NSLayoutConstraint.constraintPinSubview(toSuperview: collectionView)?[ConstraintBot] as? NSLayoutConstraint
        collectionViewHeight = collectionView.heightAnchor.constraint(equalToConstant: 300)
        collectionViewHeight?.isActive = true
        
        // Fixes defects where it scrolls when it shouldn't with voiceover
        if UIAccessibility.isVoiceOverRunning {
            collectionView.isScrollEnabled = false
        }
        
        // Listen for accessibility changes
        NotificationCenter.default.addObserver(self, selector: #selector(accessibilityStatusChanged(_:)), name: UIAccessibility.voiceOverStatusDidChangeNotification, object: nil)
    }
    
    open override func updateView(_ size: CGFloat) {
        super.updateView(size)
        self.size = size
        
        // Set insets for the carousel.
        var inset = UIEdgeInsets.zero
        let carouselModel = model as? CarouselModel
        if carouselModel?.useHorizontalMargins ?? false {
            inset.left = carouselModel?.leftPadding ?? Padding.Component.horizontalPaddingForSize(size)
            inset.right = carouselModel?.rightPadding ?? Padding.Component.horizontalPaddingForSize(size)
        }
        (collectionView.collectionViewLayout as? UICollectionViewFlowLayout)?.sectionInset = inset
        
        // Update cells and re-layout.
        for cell in collectionView.visibleCells {
            (cell as? MVMCoreViewProtocol)?.updateView(size)
        }
        layoutCollection()
    }
    
    //--------------------------------------------------
    // MARK: - MVMCoreUIMoleculeViewProtocol
    //--------------------------------------------------
    
    public override func set(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) {
        self.delegateObject = delegateObject
        super.set(with: model, delegateObject, additionalData)
        
        guard let carouselModel = model as? CarouselModel else { return }
        accessibilityLabel = carouselModel.accessibilityText
        collectionView.layer.borderColor = UIColor.mvmCoolGray3.cgColor
        collectionView.layer.borderWidth = (carouselModel.border ?? false) ? 1 : 0
        (collectionView.collectionViewLayout as? CarouselCollectionLayout)?.useLines = carouselModel.border ?? false
        (collectionView.collectionViewLayout as? UICollectionViewFlowLayout)?.minimumLineSpacing = carouselModel.spacing ?? 0
        
        itemWidthPercent = carouselModel.itemWidthPercent / 100.0
        if let alignment = carouselModel.itemAlignment {
            itemAlignment = alignment
        }
        
        if let height = carouselModel.height {
            collectionViewHeight?.constant = height
            collectionViewHeight?.isActive = true
        } else {
            collectionViewHeight?.isActive = false
        }
        
        registerCells(with: carouselModel, delegateObject: delegateObject)
        prepareMolecules(with: carouselModel)
        FormValidator.setupValidation(for: carouselModel, delegate: delegateObject?.formHolderDelegate)
        
        setupPagingMolecule(carouselModel.pagingMolecule, delegateObject: delegateObject)
        
        pageIndex = carouselModel.index
        pagingView?.currentIndex = carouselModel.index
        collectionView.reloadData()
        if let selectedIndex = carouselModel.selectedIndex {
            let adjustedIndex = loop ? selectedIndex + 2 : selectedIndex
            collectionView.selectItem(at: IndexPath(row: adjustedIndex, section: 0), animated: false, scrollPosition: [])
        }
    }
    
    //--------------------------------------------------
    // MARK: - JSON Setters
    //--------------------------------------------------
    
    func prepareMolecules(with carouselModel: CarouselModel?) {
        guard let newMolecules = carouselModel?.molecules else {
            numberOfPages = 0
            molecules = nil
            return
        }
        
        numberOfPages = newMolecules.count
        molecules = newMolecules
        
        if carouselModel?.loop ?? false && newMolecules.count > 1 && !UIAccessibility.isVoiceOverRunning {
            // Sets up the row data with buffer cells on each side (for illusion of endless scroll... also has one more buffer cell on each side in case we can peek that cell).
            loop = true
            
            molecules?.insert(contentsOf: newMolecules.suffix(2), at: 0)
            molecules?.append(contentsOf: newMolecules.prefix(2))
        } else {
            loop = false
        }
        
        pageIndex = 0
    }
    
    open override func reset() {
        super.reset()
        pagingView?.reset()
    }

    var pagingMoleculeName: String?
    
    /// Sets up the paging molecule
    open func setupPagingMolecule(_ molecule: (CarouselPagingModelProtocol & MoleculeModelProtocol)?, delegateObject: MVMCoreUIDelegateObject?) {
        
        if let molecule = molecule,
           molecule.moleculeName == pagingMoleculeName {
            pagingView?.set(with: molecule, delegateObject, nil)
            pagingView?.numberOfPages = numberOfPages
            return
        }
        
        var pagingView: (MoleculeViewProtocol & CarouselPageControlProtocol)? = nil
        if let molecule = molecule,
           (!molecule.hidesForSinglePage || numberOfPages > 1) {
            pagingView = ModelRegistry.createMolecule(molecule, delegateObject: delegateObject) as? (MoleculeViewProtocol & CarouselPageControlProtocol)
            pagingMoleculeName = molecule.moleculeName
        } else {
            pagingMoleculeName = nil
        }
        
        addPaging(view: pagingView, position: molecule?.position ?? 20)
    }
    
    /// Registers the cells with the collection view
    func registerCells(with carouselModel: CarouselModel, delegateObject: MVMCoreUIDelegateObject?) {
        
        for molecule in carouselModel.molecules {
            if let info = getMoleculeInfo(with: molecule, delegateObject: delegateObject) {
                collectionView.register(info.class, forCellWithReuseIdentifier: info.identifier)
            }
        }
    }
    
    //--------------------------------------------------
    // MARK: - Convenience
    //--------------------------------------------------
    
    /// Returns the (identifier, class) of the molecule for the given map.
    func getMoleculeInfo(with molecule: MoleculeModelProtocol, delegateObject: MVMCoreUIDelegateObject?) -> (identifier: String, class: AnyClass, molecule: MoleculeModelProtocol)? {
        guard let className = ModelRegistry.getMoleculeClass(molecule) else { return nil }
        return (className.nameForReuse(with: molecule, delegateObject) ?? molecule.moleculeName, className, molecule)
    }
    
    /// Adds a paging view. Centers it horizontally with the collection view. The position is the vertical distance from the center of the page view to the bottom of the collection view.
    open func addPaging(view: (MoleculeViewProtocol & CarouselPageControlProtocol)?, position: CGFloat) {
        
        pagingView?.removeFromSuperview()
        bottomPin?.isActive = false
        
        guard var pagingView = view else {
            bottomPin = bottomAnchor.constraint(equalTo: collectionView.bottomAnchor)
            bottomPin?.isActive = true
            return
        }
        
        addSubview(pagingView)
        pagingView.centerXAnchor.constraint(equalTo: collectionView.centerXAnchor).isActive = true
        collectionView.bottomAnchor.constraint(equalTo: pagingView.centerYAnchor, constant: position).isActive = true
        bottomAnchor.constraint(greaterThanOrEqualTo: pagingView.bottomAnchor).isActive = true
        bottomPin = bottomAnchor.constraint(equalTo: collectionView.bottomAnchor)
        bottomPin?.priority = .defaultLow
        bottomPin?.isActive = true
        
        pagingView.numberOfPages = numberOfPages
        (pagingView as? MVMCoreUIViewConstrainingProtocol)?.alignHorizontal?(.fill)
        pageIndex = pagingView.currentIndex
        pagingView.indicatorTouchAction = { [weak self] pager in
            DispatchQueue.main.async {
                guard let self = self else { return }
                let currentPage = pager.currentIndex
                self.pageIndex = currentPage
                self.updateModelIndex()
                self.goTo(self.currentIndex, animated: !UIAccessibility.isVoiceOverRunning)
            }
        }
        
        self.pagingView = pagingView
    }
    
    open func showPeaking(_ peaking: Bool) {
        
        if peaking && !UIAccessibility.isVoiceOverRunning {
            // Show overlay and arrow in peaking Cell
            let visibleItemsPaths = collectionView.indexPathsForVisibleItems.sorted { $0.row < $1.row }
            
            if let firstItem = visibleItemsPaths.first, firstItem.row != currentIndex {
                (collectionView.cellForItem(at: firstItem) as? CarouselItemProtocol)?.setPeaking(true, animated: true)
            }
            if let lastItem = visibleItemsPaths.last, lastItem.row != currentIndex {
                (collectionView.cellForItem(at: lastItem) as? CarouselItemProtocol)?.setPeaking(true, animated: true)
            }
        } else {
            // Hide peaking.
            for item in collectionView.visibleCells {
                (item as? CarouselItemProtocol)?.setPeaking(false, animated: true)
            }
        }
    }
    
    func trackSwipeActionAnalyticsforIndex(_ index : Int){
        guard let itemModel = molecules?[index],
              let viewControllerObject = delegateObject?.moleculeDelegate as? MVMCoreViewControllerProtocol else { return }
        MVMCoreUILoggingHandler.shared()?.defaultLogAction(forController: viewControllerObject, actionInformation: itemModel.toJSON(), additionalData: nil)
    }
    
    //--------------------------------------------------
    // MARK: - Accessibility
    //--------------------------------------------------
    
    /// Sets accessibility for the cell. Only the current cell is accessible.
    public func setAccessiblity(_ cell: UICollectionViewCell?, index: Int) {
        guard let cell = cell else { return }
        if index == currentIndex {
            cell.accessibilityElementsHidden = false
            
            // set to nil to get fresh elements
            accessibilityElements = nil
        } else {
            cell.accessibilityElementsHidden = true
        }
    }
    
    /// Resets view
    @objc private func accessibilityStatusChanged(_ sender: Notification) {
        // Fixes defects where it scrolls when it shouldn't with voiceover
        collectionView.isScrollEnabled = !UIAccessibility.isVoiceOverRunning
        if let model = model {
            set(with: model, delegateObject, nil)
        }
    }
    
    /// Accessibility element that allows for adjustable carousel.
    private var carouselAccessibilityElement: CarouselAccessibilityElement?
    
    private var _accessibilityElements: [Any]?
    
    /// Returns the accessibilityElements. If nil, will return current cell and carouselAccessibilityElement
    override open var accessibilityElements: [Any]? {
        set {
            _accessibilityElements = newValue
        }
        
        get {
            // Only return custom accessibility if nil.
            guard _accessibilityElements == nil else {
                return _accessibilityElements
            }
            
            // Create the accessibility helper if needed.
            let carouselAccessibilityElement: CarouselAccessibilityElement
            if let theCarouselAccessibilityElement = self.carouselAccessibilityElement {
                carouselAccessibilityElement = theCarouselAccessibilityElement
            } else {
                carouselAccessibilityElement = CarouselAccessibilityElement(accessibilityContainer: self)
                carouselAccessibilityElement.accessibilityFrameInContainerSpace = collectionView.frame
                self.carouselAccessibilityElement = carouselAccessibilityElement
            }
            
            if let currentCell = collectionView.cellForItem(at: IndexPath(row: currentIndex, section: 0)) {
                _accessibilityElements = [currentCell, carouselAccessibilityElement]
            } else {
                _accessibilityElements = [carouselAccessibilityElement]
            }
            return _accessibilityElements
        }
    }
}

extension Carousel: UICollectionViewDelegateFlowLayout {
    open func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, sizeForItemAt indexPath: IndexPath) -> CGSize {
        let itemWidth = collectionView.bounds.width * itemWidthPercent
        return CGSize(width: itemWidth, height: collectionView.bounds.height)
    }
    
    open func collectionView(_ collectionView: UICollectionView, didEndDisplaying cell: UICollectionViewCell, forItemAt indexPath: IndexPath) {
        (cell as? CarouselItemProtocol)?.setPeaking(false, animated: false)
    }
}

extension Carousel: UICollectionViewDataSource {
    open func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return molecules?.count ?? 0
    }
    
    open func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        guard let molecule = molecules?[indexPath.row],
              let moleculeInfo = getMoleculeInfo(with: molecule, delegateObject: nil)
        else { return UICollectionViewCell() }
        
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: moleculeInfo.identifier, for: indexPath)
        if let protocolCell = cell as? MoleculeViewProtocol {
            protocolCell.reset()
            protocolCell.set(with: moleculeInfo.molecule, delegateObject, nil)
        }
        (cell as? MVMCoreViewProtocol)?.updateView(size ?? collectionView.bounds.width)
        setAccessiblity(cell, index: indexPath.row)
        return cell
    }
}

extension Carousel: UICollectionViewDelegate {
    public func collectionView(_ collectionView: UICollectionView, shouldSelectItemAt indexPath: IndexPath) -> Bool {
        guard let cell = collectionView.cellForItem(at: indexPath) as? CollectionTemplateItemProtocol else { return false }
        return cell.shouldSelect(at: indexPath, delegateObject: delegateObject, additionalData: nil)
    }
    
    open func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
        // Set the selection in the model
        if let model = model as? CarouselModel {
            // Adjust for looping
            var adjustedIndex = loop ? indexPath.row - 2 : indexPath.row
            if adjustedIndex < 0 {
                adjustedIndex = adjustedIndex + numberOfPages
            }
            model.selectedIndex =  adjustedIndex
        }
        if let cell = collectionView.cellForItem(at: indexPath) as? CollectionTemplateItemProtocol {
            cell.didSelectCell(at: indexPath, delegateObject: delegateObject, additionalData: nil)
        }
        _ = FormValidator.validate(delegate: delegateObject?.formHolderDelegate)
    }
}

extension Carousel: UIScrollViewDelegate {
    
    /// Go to the cell at the specified index.
    func goTo(_ index: Int, animated: Bool) {
        showPeaking(false)
        setAccessiblity(collectionView.cellForItem(at: IndexPath(row: currentIndex, section: 0)), index: index)
        currentIndex = index
        updateModelIndex()
        collectionView.scrollToItem(at: IndexPath(row: currentIndex, section: 0), at: itemAlignment, animated: animated)
        if let cell = collectionView.cellForItem(at: IndexPath(row: currentIndex, section: 0)) {
            setAccessiblity(collectionView.cellForItem(at: IndexPath(row: currentIndex, section: 0)), index: index)
            UIAccessibility.post(notification: .layoutChanged, argument: cell)
        }
        
        // Force call if no animation
        if !animated {
            scrollViewDidEndScrollingAnimation(collectionView)
        }
        _ = FormValidator.validate(delegate: delegateObject?.formHolderDelegate)
    }
    
    /// Adjusts the current contentOffset if we are going onto buffer cells while looping to help with the endless scrolling appearance.
    func adjustOffsetForLooping(_ scrollView: UIScrollView) {
        let translatedPoint = scrollView.panGestureRecognizer.translation(in: scrollView.superview).x
        if translatedPoint > 0 {
            // Moving left, see if we are moving passed the first left buffer card and adjust
            if let threshold = collectionView.layoutAttributesForItem(at: IndexPath(item: 1, section: 0))?.frame.minX,
               scrollView.contentOffset.x < threshold,
               let newOffset = collectionView.layoutAttributesForItem(at: IndexPath(item: numberOfPages + 1, section: 0))?.frame.minX {
                scrollView.contentOffset.x = newOffset
            }
        } else if translatedPoint < 0 {
            // Moving right, see if we are moving passed the first right buffer card and adjust
            if let threshold = collectionView.layoutAttributesForItem(at: IndexPath(item: numberOfPages + 2, section: 0))?.frame.maxX,
               scrollView.contentOffset.x + scrollView.bounds.width > threshold,
               let newEndOffset = collectionView.layoutAttributesForItem(at: IndexPath(item: 2, section: 0))?.frame.maxX {
                scrollView.contentOffset.x = newEndOffset - scrollView.bounds.width
            }
        }
    }
    
    open func scrollViewDidScroll(_ scrollView: UIScrollView) {
        
        // Adjust for looping
        if loop == true {
            adjustOffsetForLooping(scrollView)
        }
        
        // Let the pager know our progress if needed.
        pagingView?.scrollViewDidScroll(collectionView)
    }
    
    public func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
        
        // Disable peaking when dragging.
        dragging = true
        
        showPeaking(false)
    }
    
    public func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
        
        dragging = false
        
        // This is for setting up smooth custom paging. (Since UICollectionView only handles paging based on collection view size and not cell size). Math requires that we are using UICollectionViewFlowLayout.
        guard (model as? CarouselModel)?.paging == true,
              let layout = collectionView.collectionViewLayout as? UICollectionViewFlowLayout else { return }
        
        let separatorWidth = layout.minimumLineSpacing
        let itemWidth = collectionView.bounds.width * itemWidthPercent
        let width = itemWidth + separatorWidth
        
        // Adjusts the offset for the contentInset. Adds imaginary half separator to the left of the first card, which is necessary for determining the percent of a given card we are currently at.
        let adjustedOffset = scrollView.contentOffset.x - layout.sectionInset.left + (separatorWidth / 2)
        
        // Calculates the offset per card depending on the alignment.
        var offsetByCard: CGFloat
        switch itemAlignment {
        case .right:
            offsetByCard = ((adjustedOffset + scrollView.bounds.width) / width) - 1
        case .centeredHorizontally:
            offsetByCard = ((adjustedOffset + (scrollView.bounds.width / 2)) / width) - 0.5
        default:
            offsetByCard = adjustedOffset / width
        }
        
        // Adjust card for velocity impact.
        let velocityThreshold: CGFloat = 1.1
        var cellToSwipeTo: Int
        if velocity.x > velocityThreshold {
            cellToSwipeTo = Int(ceil(offsetByCard))
        } else if velocity.x < -velocityThreshold {
            cellToSwipeTo = Int(floor(offsetByCard))
        } else {
            cellToSwipeTo = Int(round(offsetByCard))
        }
        
        // If we are swiping to a buffer cell, change to real cell before beginning animation so we don't go out of bounds.
        if loop, cellToSwipeTo < 2 {
            let newOffset = scrollView.contentOffset.x + (width * CGFloat(numberOfPages))
            scrollView.contentOffset.x = newOffset
            targetContentOffset.pointee.x = newOffset
            cellToSwipeTo = cellToSwipeTo + numberOfPages
        } else if loop, cellToSwipeTo > numberOfPages + 1 {
            let newOffset = scrollView.contentOffset.x - (width * CGFloat(numberOfPages))
            scrollView.contentOffset.x = newOffset
            targetContentOffset.pointee.x = newOffset
            cellToSwipeTo = cellToSwipeTo - numberOfPages
        } else {
            targetContentOffset.pointee = scrollView.contentOffset
        }
        
        // Cap the index.
        let lastCellIndex = collectionView(collectionView, numberOfItemsInSection: 0) - 1
        goTo(min(max(cellToSwipeTo, 0), lastCellIndex), animated: true)
    }
    
    // To give the illusion of endless scrolling. Since we are always calling scrollToItem we can assume finished paging in here.
    public func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {
        // Cycle to other end if on buffer cell.
        pagingView?.currentIndex = pageIndex
        showPeaking(true)
        // track analyticsData
        trackSwipeActionAnalyticsforIndex(pageIndex)
    }
}

// Adapted from: https://developer.apple.com/documentation/uikit/accessibility_for_ios_and_tvos/delivering_an_exceptional_accessibility_experience
/// Ensures a good accessibility experience. Adds adjustable swiping for cards.
class CarouselAccessibilityElement: UIAccessibilityElement {
    
    /// This indicates to the user what exactly this element is supposed to be.
    override var accessibilityLabel: String? {
        get {
            guard let containerView = accessibilityContainer as? Carousel,
                  let accessibilityLabel = containerView.accessibilityLabel else { return super.accessibilityLabel }
            return accessibilityLabel
        }
        set {
            super.accessibilityLabel = newValue
        }
    }
    
    override var accessibilityValue: String? {
        get {
            // Read which card we are on.
            guard let containerView = accessibilityContainer as? Carousel,
                  let format = MVMCoreUIUtility.hardcodedString(withKey: "index_string_of_total"),
                  let indexString = MVMCoreUIUtility.getOrdinalString(forIndex: NSNumber(value: containerView.currentIndex + 1)) else {
                return super.accessibilityValue
            }
            return String(format: format, indexString, containerView.numberOfPages)
        }
        
        set {
            super.accessibilityValue = newValue
        }
    }
    
    // This tells VoiceOver that our element will support the increment and decrement callbacks.
    /// - Tag: accessibility_traits
    override var accessibilityTraits: UIAccessibilityTraits {
        get {
            return .adjustable
        }
        set {
            super.accessibilityTraits = newValue
        }
    }
    
    /**
     A convenience for forward scrolling in both `accessibilityIncrement` and `accessibilityScroll`.
     It returns a `Bool` because `accessibilityScroll` needs to know if the scroll was successful.
     */
    func accessibilityScrollForward() -> Bool {
        guard let containerView = accessibilityContainer as? Carousel else { return false }
        
        let newIndex = containerView.currentIndex + 1
        guard newIndex < containerView.numberOfPages else { return false }
        
        containerView.goTo(newIndex, animated: false)
        
        return true
    }
    
    /**
     A convenience for backward scrolling in both `accessibilityIncrement` and `accessibilityScroll`.
     It returns a `Bool` because `accessibilityScroll` needs to know if the scroll was successful.
     */
    func accessibilityScrollBackward() -> Bool {
        guard let containerView = accessibilityContainer as? Carousel else { return false }
        
        let newIndex = containerView.currentIndex - 1
        guard newIndex >= 0 else { return false }
        
        containerView.goTo(newIndex, animated: false)
        
        return true
    }
    
    /*
     Overriding the following two methods allows the user to perform increment and decrement actions
     (done by swiping up or down).
     */
    /// - Tag: accessibility_increment_decrement
    override func accessibilityIncrement() {
        // This causes the picker to move forward one if the user swipes up.
        _ = accessibilityScrollForward()
    }
    
    override func accessibilityDecrement() {
        // This causes the picker to move back one if the user swipes down.
        _ = accessibilityScrollBackward()
    }
    
    /*
     This will cause the picker to move forward or backwards on when the user does a 3-finger swipe,
     depending on the direction of the swipe. The return value indicates whether or not the scroll was successful,
     so that VoiceOver can alert the user if it was not.
     */
    override func accessibilityScroll(_ direction: UIAccessibilityScrollDirection) -> Bool {
        if direction == .left {
            return accessibilityScrollForward()
        } else if direction == .right {
            return accessibilityScrollBackward()
        }
        return false
    }
}
