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

import UIKit
import Pachyderm
import AVFoundation
import TuskerComponents

protocol AttachmentViewDelegate: AnyObject {
    func attachmentViewGallery(startingAt index: Int) -> GalleryViewController?
    func attachmentViewPresent(_ vc: UIViewController, animated: Bool)
}

class AttachmentView: GIFImageView {
    
    static let queue = DispatchQueue(label: "Attachment Thumbnail", qos: .userInitiated, attributes: .concurrent)
    
    weak var delegate: AttachmentViewDelegate?

    var playImageView: UIImageView?
    var gifvView: GifvAttachmentView?
    private var badgeContainer: UIStackView?
    
    var attachment: Attachment!
    var index: Int!
    
    private var attachmentRequest: ImageCache.Request?
    private var source: Source?
    
    private var autoplayGifs: Bool {
        Preferences.shared.automaticallyPlayGifs && !ProcessInfo.processInfo.isLowPowerModeEnabled
    }
    
    private var isGrayscale = false

    init(attachment: Attachment, index: Int) {
        super.init(image: nil)
        commonInit()
        
        self.attachment = attachment
        self.index = index
        loadAttachment()
    }
    
    deinit {
        attachmentRequest?.cancel()
    }
    
    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        commonInit()
    }
    
    private func commonInit() {
        contentMode = .scaleAspectFill
        layer.masksToBounds = true
        isUserInteractionEnabled = true
        addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(imagePressed)))
        
        NotificationCenter.default.addObserver(self, selector: #selector(preferencesChanged), name: .preferencesChanged, object: nil)
        NotificationCenter.default.addObserver(self, selector: #selector(gifPlaybackModeChanged), name: .NSProcessInfoPowerStateDidChange, object: nil)
        
        addInteraction(UIContextMenuInteraction(delegate: self))
        
        isAccessibilityElement = true
        accessibilityTraits = [.image, .button]
    }
    
    @objc private func preferencesChanged() {
        gifPlaybackModeChanged()
        
        if isGrayscale != Preferences.shared.grayscaleImages {
            self.displayImage()
        }
        
        if getBadges().isEmpty != Preferences.shared.showAttachmentBadges {
            createBadgesView(getBadges())
        }
    }
    
    @objc private func gifPlaybackModeChanged() {
        // NSProcessInfoPowerStateDidChange is sometimes fired on a background thread
        DispatchQueue.main.async {
            if self.attachment.kind == .image,
               let gifController = self.gifController {
                if self.autoplayGifs && !self.isAnimatingGIF {
                    gifController.attach(to: self)
                    gifController.startAnimating()
                } else if !self.autoplayGifs && self.isAnimatingGIF {
                    // detach instead of stopping so that any other attached gif views keep animating
                    self.detachGIFController()
                }
            } else if self.attachment.kind == .gifv,
                      let gifvView = self.gifvView {
                if self.autoplayGifs {
                    gifvView.player.play()
                } else {
                    gifvView.player.pause()
                }
            }
        }
    }
    
    func loadAttachment() {
        if let hash = attachment.blurHash {
            AttachmentView.queue.async { [weak self] in
                guard let self = self else { return }
                
                guard var preview = UIImage(blurHash: hash, size: self.blurHashSize()) else {
                    return
                }
                
                if Preferences.shared.grayscaleImages,
                   let grayscale = ImageGrayscalifier.convert(url: nil, cgImage: preview.cgImage!) {
                    preview = grayscale
                }
                
                DispatchQueue.main.async { [weak self] in
                    guard let self = self, self.image == nil else { return }
                    self.image = preview
                }
            }
        }
        
        createBadgesView(getBadges())
        
        switch attachment.kind {
        case .image:
            loadImage()
        case .video:
            loadVideo()
        case .audio:
            loadAudio()
        case .gifv:
            loadGifv()
        case .unknown:
            createUnknownLabel()
        }
    }
    
    private func getBadges() -> Badges {
        guard Preferences.shared.showAttachmentBadges else {
            return []
        }
        var badges: Badges = []
        if attachment.description?.isEmpty == false {
            badges.formUnion(.alt)
        }
        if attachment.kind == .gifv || attachment.url.pathExtension == "gif" {
            badges.formUnion(.gif)
        }
        return badges
    }
    
    var attachmentAspectRatio: CGFloat? {
        if let meta = self.attachment.meta {
            if let width = meta.width, let height = meta.height {
                return CGFloat(width) / CGFloat(height)
            } else if let orig = meta.original,
                      let width = orig.width, let height = orig.height {
                return CGFloat(width) / CGFloat(height)
            }
        }
        return nil
    }
    
    private func blurHashSize() -> CGSize {
        if let aspectRatio = attachmentAspectRatio {
            if aspectRatio > 1 {
                return CGSize(width: 32, height: 32 / aspectRatio)
            } else {
                return CGSize(width: 32 * aspectRatio, height: 32)
            }
        } else {
            return CGSize(width: 32, height: 32)
        }
    }
    
    func loadImage() {
        let attachmentURL = attachment.url
        attachmentRequest = ImageCache.attachments.get(attachmentURL) { [weak self] (data, image) in
            guard let self = self,
                  self.attachment.url == attachmentURL else {
                return
            }
            
            DispatchQueue.main.async {
                self.attachmentRequest = nil
                
                if attachmentURL.pathExtension == "gif",
                   let data {
                    self.source = .gifData(attachmentURL, data, image)
                    if self.autoplayGifs {
                        let controller = GIFController(gifData: data)
                        controller.attach(to: self)
                        controller.startAnimating()
                    } else {
                        self.displayImage()
                    }
                } else if let image {
                    self.source = .image(attachmentURL, image)
                    self.displayImage()
                }
            }
        }
    }
    
    func loadVideo() {
        if let previewURL = self.attachment.previewURL {
            attachmentRequest = ImageCache.attachments.get(previewURL, completion: { [weak self] (_, image) in
                guard let self, let image else { return }
                DispatchQueue.main.async {
                    self.attachmentRequest = nil
                    self.source = .image(previewURL, image)
                    self.displayImage()
                }
            })
        } else {
            let attachmentURL = self.attachment.url
            AttachmentView.queue.async { [weak self] in
                let asset = AVURLAsset(url: attachmentURL)
                let generator = AVAssetImageGenerator(asset: asset)
                generator.appliesPreferredTrackTransform = true
                guard let image = try? generator.copyCGImage(at: .zero, actualTime: nil) else { return }
                UIImage(cgImage: image).prepareForDisplay { [weak self] image in
                    DispatchQueue.main.async { [weak self] in
                        guard let self, let image else { return }
                        self.source = .image(attachmentURL, image)
                        self.displayImage()
                    }
                }
            }
        }
        
        let playImageView = UIImageView(image: UIImage(systemName: "play.circle.fill"))
        playImageView.translatesAutoresizingMaskIntoConstraints = false
        addSubview(playImageView)
        NSLayoutConstraint.activate([
            playImageView.widthAnchor.constraint(equalToConstant: 50),
            playImageView.heightAnchor.constraint(equalToConstant: 50),
            playImageView.centerXAnchor.constraint(equalTo: centerXAnchor),
            playImageView.centerYAnchor.constraint(equalTo: centerYAnchor),
        ])
    }
    
    func loadAudio() {
        let label = UILabel()
        label.text = "Audio Only"
        let playImageView = UIImageView(image: UIImage(systemName: "play.circle.fill"))
        let stack = UIStackView(arrangedSubviews: [
            label,
            playImageView
        ])
        stack.translatesAutoresizingMaskIntoConstraints = false
        stack.axis = .vertical
        stack.spacing = 8
        stack.alignment = .center
        addSubview(stack)
        NSLayoutConstraint.activate([
            stack.centerXAnchor.constraint(equalTo: centerXAnchor),
            stack.centerYAnchor.constraint(equalTo: centerYAnchor),
            playImageView.widthAnchor.constraint(equalToConstant: 50),
            playImageView.heightAnchor.constraint(equalToConstant: 50),
        ])
    }
    
    func loadGifv() {
        let attachmentURL = self.attachment.url
        let asset = AVURLAsset(url: attachmentURL)
        AttachmentView.queue.async {
            let generator = AVAssetImageGenerator(asset: asset)
            generator.appliesPreferredTrackTransform = true
            guard let image = try? generator.copyCGImage(at: .zero, actualTime: nil) else { return }
            DispatchQueue.main.async {
                self.source = .cgImage(attachmentURL, image)
                self.displayImage()
            }
        }
        
        let gifvView = GifvAttachmentView(asset: asset, gravity: .resizeAspectFill)
        self.gifvView = gifvView
        gifvView.translatesAutoresizingMaskIntoConstraints = false
        if autoplayGifs {
            gifvView.player.play()
        }
        addSubview(gifvView)
        NSLayoutConstraint.activate([
            gifvView.leadingAnchor.constraint(equalTo: leadingAnchor),
            gifvView.trailingAnchor.constraint(equalTo: trailingAnchor),
            gifvView.topAnchor.constraint(equalTo: topAnchor),
            gifvView.bottomAnchor.constraint(equalTo: bottomAnchor),
        ])
    }
    
    func createUnknownLabel() {
        backgroundColor = .appSecondaryBackground
        let label = UILabel()
        label.text = "Unknown Attachment Type"
        label.numberOfLines = 0
        label.textAlignment = .center
        label.textColor = .secondaryLabel
        label.font = .preferredFont(forTextStyle: .body)
        label.adjustsFontForContentSizeCategory = true
        label.translatesAutoresizingMaskIntoConstraints = false
        addSubview(label)
        NSLayoutConstraint.activate([
            label.leadingAnchor.constraint(equalTo: leadingAnchor),
            label.trailingAnchor.constraint(equalTo: trailingAnchor),
            label.topAnchor.constraint(equalTo: topAnchor),
            label.bottomAnchor.constraint(equalTo: bottomAnchor),
        ])
    }
    
    @MainActor
    private func displayImage() {
        isGrayscale = Preferences.shared.grayscaleImages
        
        switch source {
        case nil:
            self.image = nil
            
        case let .image(url, sourceImage):
            if isGrayscale {
                ImageGrayscalifier.queue.async { [weak self] in
                    let grayscale = ImageGrayscalifier.convert(url: url, image: sourceImage)
                    DispatchQueue.main.async { [weak self] in
                        self?.image = grayscale
                    }
                }
            } else {
                self.image = sourceImage
            }
            
        case let .gifData(url, _, sourceImage):
            if isGrayscale,
               let sourceImage {
                ImageGrayscalifier.queue.async { [weak self] in
                    let grayscale = ImageGrayscalifier.convert(url: url, image: sourceImage)
                    DispatchQueue.main.async { [weak self] in
                        self?.image = grayscale
                    }
                }
            } else {
                self.image = sourceImage
            }
            
        case let .cgImage(url, cgImage):
            if isGrayscale {
                ImageGrayscalifier.queue.async { [weak self] in
                    let grayscale = ImageGrayscalifier.convert(url: url, cgImage: cgImage)
                    DispatchQueue.main.async { [weak self] in
                        self?.image = grayscale
                    }
                }
            } else {
                image = UIImage(cgImage: cgImage)
            }
        }
    }
    
    private func createBadgesView(_ badges: Badges) {
        guard !badges.isEmpty else {
            badgeContainer?.removeFromSuperview()
            badgeContainer = nil
            return
        }
        
        let stack = UIStackView()
        self.badgeContainer = stack
        stack.axis = .horizontal
        stack.spacing = 2
        stack.translatesAutoresizingMaskIntoConstraints = false
        
        let font = UIFontMetrics(forTextStyle: .caption1).scaledFont(for: .systemFont(ofSize: 13, weight: .bold))
        func makeBadgeView(text: String) {
            let container = UIView()
            container.backgroundColor = .secondarySystemBackground.resolvedColor(with: UITraitCollection(userInterfaceStyle: .dark))
            
            let label = UILabel()
            label.font = font
            label.adjustsFontForContentSizeCategory = true
            label.textColor = .white
            label.text = text
            label.translatesAutoresizingMaskIntoConstraints = false
            container.addSubview(label)
            NSLayoutConstraint.activate([
                label.leadingAnchor.constraint(equalTo: container.leadingAnchor, constant: 2),
                label.trailingAnchor.constraint(equalTo: container.trailingAnchor, constant: -2),
                label.topAnchor.constraint(equalTo: container.topAnchor, constant: 2),
                label.bottomAnchor.constraint(equalTo: container.bottomAnchor, constant: -2),
            ])
            stack.addArrangedSubview(container)
        }
        
        if badges.contains(.gif) {
            makeBadgeView(text: "GIF")
        }
        if badges.contains(.alt) {
            makeBadgeView(text: "ALT")
        }
        
        let first = stack.arrangedSubviews.first!
        first.layer.masksToBounds = true
        first.layer.cornerRadius = 4
        first.layer.cornerCurve = .continuous
        if stack.arrangedSubviews.count > 1 {
            first.layer.maskedCorners = [.layerMinXMinYCorner, .layerMinXMaxYCorner]
            let last = stack.arrangedSubviews.last!
            last.layer.masksToBounds = true
            last.layer.cornerRadius = 4
            last.layer.cornerCurve = .continuous
            last.layer.maskedCorners = [.layerMaxXMinYCorner, .layerMaxXMaxYCorner]
        }
        
        addSubview(stack)
        NSLayoutConstraint.activate([
            stack.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 4),
            stack.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -4),
        ])
    }
    
    // MARK: Interaction
    
    func showGallery() {
        if let delegate = delegate,
           let gallery = delegate.attachmentViewGallery(startingAt: index) {
            delegate.attachmentViewPresent(gallery, animated: true)
        }
    }
    
    @objc func imagePressed() {
        showGallery()
    }
    
    // MARK: - Accessibility
    
    override func accessibilityActivate() -> Bool {
        showGallery()
        return true
    }

}

fileprivate extension AttachmentView {
    enum Source {
        case image(URL, UIImage)
        case gifData(URL, Data, UIImage?)
        case cgImage(URL, CGImage)
    }
    
    struct Badges: OptionSet {
        static let gif = Badges(rawValue: 1 << 0)
        static let alt = Badges(rawValue: 1 << 1)
        
        let rawValue: Int
    }
}

extension AttachmentView: UIContextMenuInteractionDelegate {
    func contextMenuInteraction(_ interaction: UIContextMenuInteraction, configurationForMenuAtLocation location: CGPoint) -> UIContextMenuConfiguration? {
        return UIContextMenuConfiguration(identifier: nil, previewProvider: { () -> UIViewController? in
            if self.attachment.kind == .image {
                return AttachmentPreviewViewController(sourceView: self)
            } else if self.attachment.kind == .gifv {
                let vc = GifvAttachmentViewController(attachment: self.attachment)
                vc.preferredContentSize = self.image?.size ?? .zero
                return vc
            } else {
                return self.delegate?.attachmentViewGallery(startingAt: self.index)
            }
        }, actionProvider: nil)
    }
    func contextMenuInteraction(_ interaction: UIContextMenuInteraction, willPerformPreviewActionForMenuWith configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionCommitAnimating) {
        animator.addCompletion {
            animator.preferredCommitStyle = .pop
            if let gallery = animator.previewViewController as? GalleryViewController {
                self.delegate?.attachmentViewPresent(gallery, animated: true)
            } else {
                self.showGallery()
            }
        }
    }
}
