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

import UIKit

class LargeImageViewController: UIViewController, UIScrollViewDelegate, LargeImageAnimatableViewController {
    
    weak var animationSourceView: UIImageView?
    var largeImageController: LargeImageViewController? { self }
    var animationImage: UIImage? { contentView.animationImage }
    var dismissInteractionController: LargeImageInteractionController?
    
    @IBOutlet weak var scrollView: UIScrollView!
    @IBOutlet weak var topControlsView: UIView!
    @IBOutlet weak var descriptionTextView: UITextView!
    
    private var shareContainer: UIView!
    private var closeContainer: UIView!
    private var shareImage: UIImageView!
    private var shareButtonTopConstraint: NSLayoutConstraint!
    private var shareButtonLeadingConstraint: NSLayoutConstraint!
    private var closeButtonTopConstraint: NSLayoutConstraint!
    private var closeButtonTrailingConstraint: NSLayoutConstraint!

    var contentView: LargeImageContentView {
        didSet {
            oldValue.removeFromSuperview()
            setupContentView()
        }
    }
    var contentViewLeadingConstraint: NSLayoutConstraint!
    var contentViewTopConstraint: NSLayoutConstraint!

    var imageDescription: String?
    
    var initialControlsVisible = true
    private(set) var controlsVisible = true {
        didSet {
            setNeedsUpdateOfHomeIndicatorAutoHidden()
        }
    }
    var shrinkGestureEnabled = true
    
    private var isInitialAppearance = true
    private var skipUpdatingControlsWhileZooming = false
    private var prevZoomScale: CGFloat?
    private var isGrayscale = false
    private var contentViewSizeObservation: NSKeyValueObservation?
    
    var isInteractivelyAnimatingDismissal: Bool = false {
        didSet {
            setNeedsStatusBarAppearanceUpdate()
        }
    }
    
    override var prefersStatusBarHidden: Bool {
        return !isInteractivelyAnimatingDismissal
    }
    override var preferredStatusBarUpdateAnimation: UIStatusBarAnimation {
        return .none
    }
    
    override var prefersHomeIndicatorAutoHidden: Bool {
        return !controlsVisible
    }
    
    init(contentView: LargeImageContentView, description: String?, sourceView: UIImageView?) {
        self.imageDescription = description
        self.animationSourceView = sourceView
        
        self.contentView = contentView

        super.init(nibName: "LargeImageViewController", bundle: nil)
        
        modalPresentationStyle = .fullScreen
    }
    
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    override func viewDidLoad() {
        super.viewDidLoad()

        setupContentView()
        setupControls()
        
        setControlsVisible(initialControlsVisible, animated: false)
        if contentView.activityItemsForSharing.isEmpty {
            shareContainer.isUserInteractionEnabled = false
            shareImage.tintColor = .systemGray
        }

        scrollView.delegate = self

        if let imageDescription = imageDescription,
           !imageDescription.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
            descriptionTextView.text = imageDescription.trimmingCharacters(in: .whitespacesAndNewlines)
            descriptionTextView.textContainerInset = UIEdgeInsets(top: 8, left: 8, bottom: 8, right: 8)
            // i'm not sure why .automatic doesn't work for this
            descriptionTextView.contentInsetAdjustmentBehavior = .always
            let height = min(150, descriptionTextView.contentSize.height)
            descriptionTextView.topAnchor.constraint(equalTo: descriptionTextView.safeAreaLayoutGuide.bottomAnchor, constant: -(height + 16)).isActive = true
        } else {
            descriptionTextView.isHidden = true
        }
        
        if shrinkGestureEnabled {
            dismissInteractionController = LargeImageInteractionController(viewController: self)
        }
        
        let singleTap = UITapGestureRecognizer(target: self, action: #selector(scrollViewPressed(_:)))
        let doubleTap = UITapGestureRecognizer(target: self, action: #selector(scrollViewDoubleTapped(_:)))
        doubleTap.numberOfTapsRequired = 2
        // this requirement is needed to make sure the double tap is ever recognized
        singleTap.require(toFail: doubleTap)
        view.addGestureRecognizer(singleTap)
        view.addGestureRecognizer(doubleTap)
        
        NotificationCenter.default.addObserver(self, selector: #selector(preferencesChanged), name: .preferencesChanged, object: nil)
        
        accessibilityElements = [
            topControlsView!,
            contentView,
            descriptionTextView!,
        ]
    }
    
    private func setupContentView() {
        contentView.owner = self
        contentView.translatesAutoresizingMaskIntoConstraints = false
        scrollView.addSubview(contentView)
        contentViewLeadingConstraint = contentView.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor)
        contentViewTopConstraint = contentView.topAnchor.constraint(equalTo: scrollView.topAnchor)
        NSLayoutConstraint.activate([
            contentViewLeadingConstraint,
            contentViewTopConstraint,
        ])
        contentViewSizeObservation = (contentView as UIView).observe(\.bounds, changeHandler: { [unowned self] _, _ in
            self.centerImage()
        })
    }
    
    private func setupControls() {
        shareContainer = UIView()
        shareContainer.isAccessibilityElement = true
        shareContainer.accessibilityTraits = .button
        shareContainer.accessibilityLabel = "Share"
        shareContainer.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(sharePressed)))
        shareContainer.translatesAutoresizingMaskIntoConstraints = false
        topControlsView.addSubview(shareContainer)
        shareImage = UIImageView(image: UIImage(systemName: "square.and.arrow.up"))
        shareImage.tintColor = .white
        shareImage.contentMode = .scaleAspectFit
        shareImage.translatesAutoresizingMaskIntoConstraints = false
        shareContainer.addSubview(shareImage)
        shareButtonTopConstraint = shareImage.topAnchor.constraint(greaterThanOrEqualTo: shareContainer.topAnchor)
        shareButtonLeadingConstraint = shareImage.leadingAnchor.constraint(greaterThanOrEqualTo: shareContainer.leadingAnchor)
        NSLayoutConstraint.activate([
            shareContainer.topAnchor.constraint(equalTo: topControlsView.topAnchor),
            shareContainer.leadingAnchor.constraint(equalTo: topControlsView.leadingAnchor),
            shareContainer.bottomAnchor.constraint(equalTo: topControlsView.bottomAnchor),
            shareContainer.widthAnchor.constraint(greaterThanOrEqualToConstant: 50),
            shareContainer.heightAnchor.constraint(greaterThanOrEqualToConstant: 50),
            
            shareImage.centerXAnchor.constraint(equalTo: shareContainer.centerXAnchor),
            shareImage.centerYAnchor.constraint(equalTo: shareContainer.centerYAnchor),
            shareButtonTopConstraint,
            shareButtonLeadingConstraint,
            
            shareImage.widthAnchor.constraint(equalToConstant: 24),
            shareImage.heightAnchor.constraint(equalToConstant: 24),
        ])
        
        closeContainer = UIView()
        closeContainer.isAccessibilityElement = true
        closeContainer.accessibilityTraits = .button
        closeContainer.accessibilityLabel = "Close"
        closeContainer.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(closeButtonPressed)))
        closeContainer.translatesAutoresizingMaskIntoConstraints = false
        topControlsView.addSubview(closeContainer)
        let closeImage = UIImageView(image: UIImage(systemName: "xmark"))
        closeImage.tintColor = .white
        closeImage.contentMode = .scaleAspectFit
        closeImage.translatesAutoresizingMaskIntoConstraints = false
        closeContainer.addSubview(closeImage)
        closeButtonTopConstraint = closeImage.topAnchor.constraint(greaterThanOrEqualTo: closeContainer.topAnchor)
        closeButtonTrailingConstraint = closeContainer.trailingAnchor.constraint(greaterThanOrEqualTo: closeImage.trailingAnchor)
        NSLayoutConstraint.activate([
            closeContainer.topAnchor.constraint(equalTo: topControlsView.topAnchor),
            closeContainer.trailingAnchor.constraint(equalTo: topControlsView.trailingAnchor),
            closeContainer.bottomAnchor.constraint(equalTo: closeContainer.bottomAnchor),
            closeContainer.widthAnchor.constraint(greaterThanOrEqualToConstant: 50),
            closeContainer.heightAnchor.constraint(greaterThanOrEqualToConstant: 50),
            
            closeImage.centerXAnchor.constraint(equalTo: closeContainer.centerXAnchor),
            closeImage.centerYAnchor.constraint(equalTo: closeContainer.centerYAnchor),
            closeButtonTopConstraint,
            closeButtonTrailingConstraint,
            
            closeImage.widthAnchor.constraint(equalToConstant: 24),
            closeImage.heightAnchor.constraint(equalToConstant: 24),
        ])
    }

    override func viewDidLayoutSubviews() {
        super.viewDidLayoutSubviews()
        
        // limit the image height to the safe area height, so the image doesn't overlap the top controls
        // while zoomed all the way out
        let maxHeight = view.bounds.height - view.safeAreaInsets.top - view.safeAreaInsets.bottom
        let heightScale = maxHeight / contentView.intrinsicContentSize.height
        let widthScale = view.bounds.width / contentView.intrinsicContentSize.width
        let minScale = min(widthScale, heightScale)
        skipUpdatingControlsWhileZooming = true
        scrollView.minimumZoomScale = minScale
        scrollView.zoomScale = minScale
        scrollView.maximumZoomScale = minScale >= 1 ? minScale + 2 : 2
        skipUpdatingControlsWhileZooming = false

        centerImage()
        
        let notchedDeviceTopInsets: [CGFloat] = [
            44, // iPhone X, Xs, Xs Max, 11 Pro, 11 Pro Max
            48, // iPhone XR, 11
            47, // iPhone 12, 12 Pro, 12 Pro Max, 13, 13 Pro, 13 Pro Max, 14, 14 Plus
            50, // iPhone 12 mini, 13 mini
        ]
        let pillDeviceTopInsets: [CGFloat] = [
            59, // iPhone 14 Pro, 14 Pro Max
        ]
        if notchedDeviceTopInsets.contains(view.safeAreaInsets.top) {
            // the notch width is not the same for the iPhones 13,
            // but what we actually want is the same offset from the edges
            // since the corner radius didn't change
            let notchWidth: CGFloat = 210
            let earWidth = (view.bounds.width - notchWidth) / 2
            let offset = (earWidth - shareImage.bounds.width) / 2
            shareButtonLeadingConstraint.constant = offset
            closeButtonTrailingConstraint.constant = offset
        } else if pillDeviceTopInsets.contains(view.safeAreaInsets.top) {
            shareButtonLeadingConstraint.constant = 24
            shareButtonTopConstraint.constant = 24
            closeButtonTrailingConstraint.constant = 24
            closeButtonTopConstraint.constant = 24
        }
    }
    
    override func viewSafeAreaInsetsDidChange() {
        super.viewSafeAreaInsetsDidChange()
        // the controls view transforms take the safe area insets into account, so they need to be updated
        updateControlsView()
    }
    
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        
        // on the first appearance, the text view flashes its own scroll indicators automatically
        // so we only need to do it on subsequent appearances
        if isInitialAppearance {
            isInitialAppearance = false
        } else {
            if animated && controlsVisible && !descriptionTextView.isHidden {
                descriptionTextView.flashScrollIndicators()
            }
        }
    }
    
    @objc private func preferencesChanged() {
        if isGrayscale != Preferences.shared.grayscaleImages {
            isGrayscale = Preferences.shared.grayscaleImages
            contentView.grayscaleStateChanged()
        }
    }

    func setControlsVisible(_ controlsVisible: Bool, animated: Bool) {
        self.controlsVisible = controlsVisible
        if animated {
            UIView.animate(withDuration: 0.2) {
                // note: the value of animated: is passed to ImageAnalysisInteractino.setSupplementaryInterfaceHidden which (as of iOS 17.0.2 (21A350)):
                // - does not animate with animated:false when wrapped in a UIView.animate block
                // - does not animate with animated:true unless also wrapped in a UIView.animate block
                self.contentView.setControlsVisible(controlsVisible, animated: true)
                self.updateControlsView()
            }
            if controlsVisible && !descriptionTextView.isHidden {
                descriptionTextView.flashScrollIndicators()
            }
        } else {
            contentView.setControlsVisible(controlsVisible, animated: false)
            updateControlsView()
        }
    }
    
    func updateControlsView() {
        let topOffset = self.controlsVisible ? 0 : -(self.topControlsView.bounds.height + self.view.safeAreaInsets.top)
        self.topControlsView.transform = CGAffineTransform(translationX: 0, y: topOffset)
        if self.imageDescription != nil {
            let bottomOffset = self.controlsVisible ? 0 : self.descriptionTextView.bounds.height + self.view.safeAreaInsets.bottom
            self.descriptionTextView.transform = CGAffineTransform(translationX: 0, y: bottomOffset)
        }
    }
    
    func viewForZooming(in scrollView: UIScrollView) -> UIView? {
        return contentView
    }
    
    func scrollViewDidZoom(_ scrollView: UIScrollView) {
        let prevZoomScale = self.prevZoomScale ?? scrollView.minimumZoomScale
        if !skipUpdatingControlsWhileZooming {
            if scrollView.zoomScale <= scrollView.minimumZoomScale {
                setControlsVisible(true, animated: true)
            } else if scrollView.zoomScale > prevZoomScale {
                setControlsVisible(false, animated: true)
            }
        }
        self.prevZoomScale = scrollView.zoomScale
    }
    
    func centerImage() {
        let yOffset = max(0, (view.bounds.size.height - contentView.frame.height) / 2)
        contentViewTopConstraint.constant = yOffset

        let xOffset = max(0, (view.bounds.size.width - contentView.frame.width) / 2)
        contentViewLeadingConstraint.constant = xOffset
    }
    
    func zoomRectFor(scale: CGFloat, center: CGPoint) -> CGRect {
        var zoomRect = CGRect.zero
        zoomRect.size.width = contentView.frame.width / scale
        zoomRect.size.height = contentView.frame.height / scale
        let newCenter = scrollView.convert(center, to: contentView)
        zoomRect.origin.x = newCenter.x - (zoomRect.width / 2)
        zoomRect.origin.y = newCenter.y - (zoomRect.height / 2)
        return zoomRect
    }
    
    // MARK: Interaction
    
    func animateZoomOut() {
        UIView.animate(withDuration: 0.3, animations: {
            self.scrollView.zoomScale = self.scrollView.minimumZoomScale
            self.view.layoutIfNeeded()
        })
    }
    
    @objc func scrollViewPressed(_ sender: UITapGestureRecognizer) {
        if scrollView.zoomScale > scrollView.minimumZoomScale {
            animateZoomOut()
        } else {
            setControlsVisible(!controlsVisible, animated: true)
        }
    }
    
    @objc func scrollViewDoubleTapped(_ recognizer: UITapGestureRecognizer) {
        if scrollView.zoomScale <= scrollView.minimumZoomScale {
            let point = recognizer.location(in: recognizer.view)
            let scale: CGFloat
            if scrollView.minimumZoomScale < 1 {
                if 1 - scrollView.zoomScale <= 0.5 {
                    scale = scrollView.zoomScale + 1
                } else {
                    scale = 1
                }
            } else {
                scale = scrollView.maximumZoomScale
            }
            let rect = zoomRectFor(scale: scale, center: point)
            UIView.animate(withDuration: 0.3) {
                self.scrollView.zoom(to: rect, animated: false)
                self.view.layoutIfNeeded()
            }
        } else {
            animateZoomOut()
        }
    }
    
    @IBAction func closeButtonPressed(_ sender: Any) {
        dismiss(animated: true)
    }
    
    @IBAction func sharePressed(_ sender: Any) {
        let activityVC = UIActivityViewController(activityItems: contentView.activityItemsForSharing, applicationActivities: [SaveToPhotosActivity()])
        activityVC.popoverPresentationController?.sourceView = shareImage
        present(activityVC, animated: true)
    }
    
}
