//
//  NCViewerImageViewController.swift
//  Nextcloud
//
//  Created by Suraj Thomas K on 7/10/18 Copyright © 2018 Al Tayer Group LLC..
//  Modify for Nextcloud by Marino Faggiana on 04/03/2020.
//
//  Author Marino Faggiana <marino.faggiana@nextcloud.com>
//
//  This program is free software: you can redistribute it and/or modify
//  it under the terms of the GNU General Public License as published by
//  the Free Software Foundation, either version 3 of the License, or
//  (at your option) any later version.
//
//  This program is distributed in the hope that it will be useful,
//  but WITHOUT ANY WARRANTY; without even the implied warranty of
//  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
//  GNU General Public License for more details.
//
//  You should have received a copy of the GNU General Public License
//  along with this program.  If not, see <http://www.gnu.org/licenses/>.
//

protocol NCViewerImageViewControllerDataSource: class {
   
    func numberOfItems(in viewerImageViewController: NCViewerImageViewController) -> Int

    func viewerImageViewController(_ viewerImageViewController: NCViewerImageViewController, imageAt index: Int, completion: @escaping (_ index: Int, _ image: UIImage?, _ metadata: tableMetadata, _ zoomScale: ZoomScale?, _ error: Error?) -> Void)

    func targetFrameForDismissal(_ viewerImageViewController: NCViewerImageViewController) -> CGRect?
}

extension NCViewerImageViewControllerDataSource {

    public func targetFrameForDismissal(_ viewerImageViewController: NCViewerImageViewController) -> CGRect? { return nil }
}

// MARK: - NCViewerImageViewControllerDelegate protocol

protocol NCViewerImageViewControllerDelegate: class {

    func viewerImageViewController(_ viewerImageViewController: NCViewerImageViewController, willChangeFocusTo index: Int, view: NCViewerImageContentView, metadata: tableMetadata)
    func viewerImageViewController(_ viewerImageViewController: NCViewerImageViewController, didChangeFocusTo index: Int, view: NCViewerImageContentView, metadata: tableMetadata)

    func viewerImageViewControllerTap(_ viewerImageViewController: NCViewerImageViewController, metadata: tableMetadata)
    func viewerImageViewControllerLongPressBegan(_ viewerImageViewController: NCViewerImageViewController, metadata: tableMetadata)
    func viewerImageViewControllerLongPressEnded(_ viewerImageViewController: NCViewerImageViewController, metadata: tableMetadata)

    func viewerImageViewControllerDismiss()
}

extension NCViewerImageViewControllerDelegate {

    func viewerImageViewController(_ viewerImageViewController: NCViewerImageViewController, didChangeFocusTo index: Int, view: NCViewerImageContentView, metadata: tableMetadata) {}
    func viewerImageViewController(_ viewerImageViewController: NCViewerImageViewController, willChangeFocusTo index: Int, view: NCViewerImageContentView, metadata: tableMetadata) {}

}

public class NCViewerImageViewController: UIViewController {

    // MARK: - Exposed Enumerations

    public enum GestureDirection {

        // Horizontal (left - right) gestures.
        case horizontal
        // Vertical (up - down) gestures.
        case vertical
    }

    public enum BrowserStyle {

        // Linear browser with *0* as first index and *numItems-1* as last index.
        case linear
        // Carousel browser. The media items are repeated in a circular fashion.
        case carousel
    }

    public enum ContentDrawOrder {

        // In this mode, media items are rendered in [previous]-[current]-[next] order.
        case previousToNext
        // In this mode, media items are rendered in [next]-[current]-[previous] order.
        case nextToPrevious
    }

    // MARK: - Exposed variables

    weak var dataSource: NCViewerImageViewControllerDataSource?
    weak var delegate: NCViewerImageViewControllerDelegate?

    var gestureDirection: GestureDirection = .horizontal

    var contentTransformer: NCViewerImageContentTransformer = NCViewerImageDefaultContentTransformers.horizontalMoveInOut {
        didSet {

            NCViewerImageContentView.contentTransformer = contentTransformer
            contentViews.forEach({ $0.updateTransform() })
        }
    }
    
    var drawOrder: ContentDrawOrder = .previousToNext {
        didSet {
            if oldValue != drawOrder {
                mediaContainerView.exchangeSubview(at: 0, withSubviewAt: 2)
            }
        }
    }
    
    var browserStyle: BrowserStyle = .carousel
    // Gap between consecutive media items. Default is `50.0`.
    
    var gapBetweenMediaViews: CGFloat = Constants.gapBetweenContents {
        didSet {
            NCViewerImageContentView.interItemSpacing = gapBetweenMediaViews
            contentViews.forEach({ $0.updateTransform() })
        }
    }
    
    // Enable or disable interactive dismissal. Default is enabled.
    var enableInteractiveDismissal: Bool = true
    
    // Item index of the current item. In range `0..<numMediaItems`
    var currentItemIndex: Int {
        return sanitizeIndex(index)
    }
        
    // MARK: - Private Enumerations

    private enum Constants {

        static let gapBetweenContents: CGFloat = 50.0
        static let minimumVelocity: CGFloat = 15.0
        static let minimumTranslation: CGFloat = 0.1
        static let animationDuration = 0.3
        static let updateFrameRate: CGFloat = 60.0
        static let bounceFactor: CGFloat = 0.1

        enum PageControl {

            static let bottom: CGFloat = -10.0
            static let tintColor: UIColor = .lightGray
            static let selectedTintColor: UIColor = .white
        }
    }

    // MARK: - Private variables
    private(set) var index: Int = 0 {
        didSet {
            pageControl.currentPage = index
        }
    }

    public var contentViews: [NCViewerImageContentView] = []

    lazy private var tapGestureRecognizer: UITapGestureRecognizer = { [unowned self] in
        let gesture = UITapGestureRecognizer()
        gesture.numberOfTapsRequired = 1
        gesture.numberOfTouchesRequired = 1
        gesture.delegate = self
        gesture.addTarget(self, action: #selector(tapGestureEvent(_:)))
        return gesture
    }()
    
    lazy private var longtapGestureRecognizer: UILongPressGestureRecognizer = { [unowned self] in
        let gesture = UILongPressGestureRecognizer()
        gesture.delaysTouchesBegan = true
        gesture.minimumPressDuration = 0.3
        gesture.delegate = self
        gesture.addTarget(self, action: #selector(longpressGestureEvent(_:)))
        return gesture
    }()

    private var previousTranslation: CGPoint = .zero

    private var timer: Timer?
    private var distanceToMove: CGFloat = 0.0

    lazy private var panGestureRecognizer: UIPanGestureRecognizer = { [unowned self] in
        let gesture = UIPanGestureRecognizer()
        gesture.minimumNumberOfTouches = 1
        gesture.maximumNumberOfTouches = 1
        gesture.delegate = self
        gesture.addTarget(self, action: #selector(panGestureEvent(_:)))
        return gesture
    }()

    lazy internal private(set) var mediaContainerView: UIView = { [unowned self] in
        let container = UIView()
        container.backgroundColor = .clear
        return container
    }()
    
    lazy var statusView: UIImageView = {
        let statusView = UIImageView()
        statusView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
        statusView.contentMode = .scaleAspectFit
        statusView.clipsToBounds = true
        return statusView
    }()

    lazy private var pageControl: UIPageControl = { [unowned self] in
        let pageControl = UIPageControl()
        pageControl.hidesForSinglePage = true
        pageControl.numberOfPages = numMediaItems
        pageControl.currentPageIndicatorTintColor = Constants.PageControl.selectedTintColor
        pageControl.tintColor = Constants.PageControl.tintColor
        pageControl.currentPage = index
        return pageControl
    }()

    private var numMediaItems = 0

    private lazy var dismissController = NCViewerImageDismissAnimationController(
        gestureDirection: gestureDirection,
        viewController: self
    )

    // MARK: - Public methods

    public func reloadContentViews() {

        numMediaItems = dataSource?.numberOfItems(in: self) ?? 0
       
        for contentView in contentViews {

            updateContents(of: contentView)
        }
    }

    // MARK: - Initializers

    init(
        index: Int = 0,
        dataSource: NCViewerImageViewControllerDataSource,
        delegate: NCViewerImageViewControllerDelegate? = nil
        ) {

        self.index = index
        self.dataSource = dataSource
        self.delegate = delegate

        super.init(nibName: nil, bundle: nil)

        initialize()
    }

    public required init?(coder aDecoder: NSCoder) {

        super.init(coder: aDecoder)

        initialize()
    }

    private func initialize() {

        view.backgroundColor = .clear

        modalPresentationStyle = .custom

        modalTransitionStyle = .crossDissolve
    }
    
    public func changeInViewSize(to size: CGSize) {
        self.contentViews.forEach({ $0.handleChangeInViewSize(to: size) })
    }
}

// MARK: - View Lifecycle and Events

extension NCViewerImageViewController {

    override public var prefersStatusBarHidden: Bool {

        return true
    }

    override public func viewDidLoad() {

        super.viewDidLoad()

        numMediaItems = dataSource?.numberOfItems(in: self) ?? 0

        populateContentViews()

        view.addGestureRecognizer(panGestureRecognizer)
        view.addGestureRecognizer(tapGestureRecognizer)
        view.addGestureRecognizer(longtapGestureRecognizer)
    }

    override public func viewDidAppear(_ animated: Bool) {

        super.viewDidAppear(animated)

        contentViews.forEach({ $0.updateTransform() })
    }

    override public func viewWillDisappear(_ animated: Bool) {

        super.viewWillDisappear(animated)
    }
    
    public override func viewWillTransition(
        to size: CGSize,
        with coordinator: UIViewControllerTransitionCoordinator
        ) {

        coordinator.animate(alongsideTransition: { context in
            self.contentViews.forEach({ $0.handleChangeInViewSize(to: size) })
        }, completion: nil)

        super.viewWillTransition(to: size, with: coordinator)
    }

    private func populateContentViews() {

        view.addSubview(mediaContainerView)
        mediaContainerView.translatesAutoresizingMaskIntoConstraints = false
        NSLayoutConstraint.activate([
            mediaContainerView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            mediaContainerView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            mediaContainerView.topAnchor.constraint(equalTo: view.topAnchor),
            mediaContainerView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
        ])
        
        view.addSubview(statusView)
        statusView.translatesAutoresizingMaskIntoConstraints = false
        
        NSLayoutConstraint.activate([
            statusView.widthAnchor.constraint(equalToConstant: 30),
            statusView.heightAnchor.constraint(equalToConstant: 30),
            statusView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 2),
            statusView.leftAnchor.constraint(equalTo: view.leftAnchor, constant: 2)
        ])
       
        statusView.setNeedsLayout()
        statusView.layoutIfNeeded()

        NCViewerImageContentView.interItemSpacing = gapBetweenMediaViews
        NCViewerImageContentView.contentTransformer = contentTransformer

        contentViews.forEach({ $0.removeFromSuperview() })
        contentViews.removeAll()

        for i in -1...1 {
            let mediaView = NCViewerImageContentView(
                index: i + index,
                position: CGFloat(i),
                frame: view.bounds
            )
            mediaContainerView.addSubview(mediaView)
            mediaView.translatesAutoresizingMaskIntoConstraints = false
            NSLayoutConstraint.activate([
                mediaView.leadingAnchor.constraint(equalTo: mediaContainerView.leadingAnchor),
                mediaView.trailingAnchor.constraint(equalTo: mediaContainerView.trailingAnchor),
                mediaView.topAnchor.constraint(equalTo: mediaContainerView.topAnchor),
                mediaView.bottomAnchor.constraint(equalTo: mediaContainerView.bottomAnchor)
            ])

            contentViews.append(mediaView)

            if numMediaItems > 0 {
                updateContents(of: mediaView)
            }
        }
        if drawOrder == .nextToPrevious {
            mediaContainerView.exchangeSubview(at: 0, withSubviewAt: 2)
        }
    }
}

// MARK: - Gesture Recognizers

extension NCViewerImageViewController {

    @objc private func panGestureEvent(_ recognizer: UIPanGestureRecognizer) {

        if dismissController.interactionInProgress {
            dismissController.handleInteractiveTransition(recognizer)
            return
        }

        guard numMediaItems > 0 else {
            return
        }

        let translation = recognizer.translation(in: view)

        switch recognizer.state {
        case .began:
            previousTranslation = translation
            distanceToMove = 0.0
            timer?.invalidate()
            timer = nil
        case .changed:
            moveViews(by: CGPoint(x: translation.x - previousTranslation.x, y: translation.y - previousTranslation.y))
        case .ended, .failed, .cancelled:
            let velocity = recognizer.velocity(in: view)

            var viewsCopy = contentViews
            let previousView = viewsCopy.removeFirst()
            let middleView = viewsCopy.removeFirst()
            let nextView = viewsCopy.removeFirst()

            var toMove: CGFloat = 0.0
            let directionalVelocity = gestureDirection == .horizontal ? velocity.x : velocity.y

            if abs(directionalVelocity) < Constants.minimumVelocity &&
                abs(middleView.position) < Constants.minimumTranslation {
                toMove = -middleView.position
            } else if directionalVelocity < 0.0 {
                if middleView.position >= 0.0 {
                    toMove = -middleView.position
                } else {
                    toMove = -nextView.position
                }
            } else {
                if middleView.position <= 0.0 {
                    toMove = -middleView.position
                } else {
                    toMove = -previousView.position
                }
            }

            if browserStyle == .linear || numMediaItems <= 1 {
                if (middleView.index == 0 && ((middleView.position + toMove) > 0.0)) ||
                    (middleView.index == (numMediaItems - 1) && (middleView.position + toMove) < 0.0) {

                    toMove = -middleView.position
                }
            }

            distanceToMove = toMove

            if timer == nil {
                timer = Timer.scheduledTimer(
                    timeInterval: 1.0/Double(Constants.updateFrameRate),
                    target: self,
                    selector: #selector(update(_:)),
                    userInfo: nil,
                    repeats: true
                )
            }
        default:
            break
        }

        previousTranslation = translation
    }

    @objc private func tapGestureEvent(_ recognizer: UITapGestureRecognizer) {

        guard !dismissController.interactionInProgress else { return }
        guard let mediaView = self.mediaView(at: 1) else { return }
        
        mediaView.zoomScaleOne()
        
        self.delegate?.viewerImageViewControllerTap(self, metadata: mediaView.metadata)
    }
    
    @objc private func longpressGestureEvent(_ recognizer: UITapGestureRecognizer) {
        
        guard !dismissController.interactionInProgress else { return }
        guard let mediaView = self.mediaView(at: 1) else { return }
        
        if recognizer.state == UIGestureRecognizer.State.began {
            mediaView.zoomScaleOne()
            self.delegate?.viewerImageViewControllerLongPressBegan(self, metadata: mediaView.metadata)
        }
        
        if recognizer.state == UIGestureRecognizer.State.ended {
            self.delegate?.viewerImageViewControllerLongPressEnded(self, metadata: mediaView.metadata)
        }
    }
}

// MARK: - Updating View Positions

extension NCViewerImageViewController {

    @objc private func update(_ timeInterval: TimeInterval) {

        guard distanceToMove != 0.0 else {

            timer?.invalidate()
            timer = nil
            return
        }

        let distance = distanceToMove / (Constants.updateFrameRate * 0.1)
        distanceToMove -= distance
        moveViewsNormalized(by: CGPoint(x: distance, y: distance))

        let translation = CGPoint(
            x: distance * (view.frame.size.width + gapBetweenMediaViews),
            y: distance * (view.frame.size.height + gapBetweenMediaViews)
        )
        let directionalTranslation = (gestureDirection == .horizontal) ? translation.x : translation.y
        if abs(directionalTranslation) < 0.1 {

            moveViewsNormalized(by: CGPoint(x: distanceToMove, y: distanceToMove))
            distanceToMove = 0.0
            timer?.invalidate()
            timer = nil
        }
    }

    private func moveViews(by translation: CGPoint) {

        let viewSizeIncludingGap = CGSize(
            width: view.frame.size.width + gapBetweenMediaViews,
            height: view.frame.size.height + gapBetweenMediaViews
        )

        let normalizedTranslation = calculateNormalizedTranslation(
            translation: translation,
            viewSize: viewSizeIncludingGap
        )

        moveViewsNormalized(by: normalizedTranslation)
    }

    private func moveViewsNormalized(by normalizedTranslation: CGPoint) {

        let isGestureHorizontal = (gestureDirection == .horizontal)

        contentViews.forEach({
            $0.position += isGestureHorizontal ? normalizedTranslation.x : normalizedTranslation.y
        })

        var viewsCopy = contentViews
        let previousView = viewsCopy.removeFirst()
        let middleView = viewsCopy.removeFirst()
        let nextView = viewsCopy.removeFirst()

        let viewSizeIncludingGap = CGSize(
            width: view.frame.size.width + gapBetweenMediaViews,
            height: view.frame.size.height + gapBetweenMediaViews
        )

        let viewSize = isGestureHorizontal ? viewSizeIncludingGap.width : viewSizeIncludingGap.height
        let normalizedGap = gapBetweenMediaViews/viewSize
        let normalizedCenter = (middleView.frame.size.width / viewSize) * 0.5
        let viewCount = contentViews.count

        if middleView.position < -(normalizedGap + normalizedCenter) {

            index = sanitizeIndex(index + 1)

            // Previous item is taken and placed on right/down most side
            previousView.position += CGFloat(viewCount)
            previousView.index += viewCount
            updateContents(of: previousView)

            contentViews.removeFirst()
            contentViews.append(previousView)

            switch drawOrder {
            case .previousToNext:
                mediaContainerView.bringSubviewToFront(previousView)
            case .nextToPrevious:
                mediaContainerView.sendSubviewToBack(previousView)
            }

            delegate?.viewerImageViewController(self, willChangeFocusTo: index, view: nextView, metadata: nextView.metadata)

        } else if middleView.position > (1 + normalizedGap - normalizedCenter) {

            index = sanitizeIndex(index - 1)

            // Next item is taken and placed on left/top most side
            nextView.position -= CGFloat(viewCount)
            nextView.index -= viewCount
            updateContents(of: nextView)

            contentViews.removeLast()
            contentViews.insert(nextView, at: 0)

            switch drawOrder {
            case .previousToNext:
                mediaContainerView.sendSubviewToBack(nextView)
            case .nextToPrevious:
                mediaContainerView.bringSubviewToFront(nextView)
            }

            delegate?.viewerImageViewController(self, willChangeFocusTo: index, view: previousView, metadata: previousView.metadata)
            
        } else if middleView.position == 0 {
            
            delegate?.viewerImageViewController(self, didChangeFocusTo: index, view: middleView, metadata: middleView.metadata)
        }
    }

    private func calculateNormalizedTranslation(translation: CGPoint, viewSize: CGSize) -> CGPoint {

        guard let middleView = mediaView(at: 1) else {
            return .zero
        }

        var normalizedTranslation = CGPoint(
            x: (translation.x)/viewSize.width,
            y: (translation.y)/viewSize.height
        )

        if browserStyle != .carousel || numMediaItems <= 1 {
            let isGestureHorizontal = (gestureDirection == .horizontal)
            let directionalTranslation = isGestureHorizontal ? normalizedTranslation.x : normalizedTranslation.y
            if (middleView.index == 0 && ((middleView.position + directionalTranslation) > 0.0)) ||
                (middleView.index == (numMediaItems - 1) && (middleView.position + directionalTranslation) < 0.0) {
                if isGestureHorizontal {
                    normalizedTranslation.x *= Constants.bounceFactor
                } else {
                    normalizedTranslation.y *= Constants.bounceFactor
                }
            }
        }
        return normalizedTranslation
    }

    private func updateContents(of contentView: NCViewerImageContentView) {

        contentView.image = nil
        let convertedIndex = sanitizeIndex(contentView.index)
        contentView.isLoading = true
        dataSource?.viewerImageViewController(
            self,
            imageAt: convertedIndex,
            completion: { [weak self] (index, image, metadata, zoom, _) in

                guard let strongSelf = self else {
                    return
                }

                if index == strongSelf.sanitizeIndex(contentView.index) {
                    if image != nil {
                        contentView.image = image
                        contentView.metadata = metadata
                        contentView.zoomLevels = zoom
                    }
                    contentView.isLoading = false
                }
            }
        )
    }

    private func sanitizeIndex(_ index: Int) -> Int {

        let newIndex = index % numMediaItems
        if newIndex < 0 {
            return newIndex + numMediaItems
        }
        return newIndex
    }

    private func sourceImage() -> UIImage? {

        return mediaView(at: 1)?.image
    }

    private func mediaView(at index: Int) -> NCViewerImageContentView? {

        guard index < contentViews.count else {

            assertionFailure("Content views does not have this many views. : \(index)")
            return nil
        }
        return contentViews[index]
    }
}

// MARK: - UIGestureRecognizerDelegate

extension NCViewerImageViewController: UIGestureRecognizerDelegate {

    public func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {

        guard enableInteractiveDismissal else {
            return true
        }

        let middleView = mediaView(at: 1)
        if middleView?.zoomScale == middleView?.zoomLevels?.minimumZoomScale,
            let recognizer = gestureRecognizer as? UIPanGestureRecognizer {

            let translation = recognizer.translation(in: recognizer.view)

            if gestureDirection == .horizontal {
                dismissController.interactionInProgress = abs(translation.y) > abs(translation.x)
            } else {
                dismissController.interactionInProgress = abs(translation.x) > abs(translation.y)
            }
            if dismissController.interactionInProgress {
                dismissController.image = sourceImage()
            }
        }
        return true
    }

    public func gestureRecognizer(
        _ gestureRecognizer: UIGestureRecognizer,
        shouldBeRequiredToFailBy otherGestureRecognizer: UIGestureRecognizer
        ) -> Bool {

        if gestureRecognizer is UIPanGestureRecognizer,
            let scrollView = otherGestureRecognizer.view as? NCViewerImageContentView {
            return scrollView.zoomScale == 1.0
        }
        return false
    }

    public func gestureRecognizer(
        _ gestureRecognizer: UIGestureRecognizer,
        shouldRequireFailureOf otherGestureRecognizer: UIGestureRecognizer
        ) -> Bool {

        if gestureRecognizer is UITapGestureRecognizer {
            return otherGestureRecognizer.view is NCViewerImageContentView
        }
        return false
    }
}