//
//  LoadImageView.swift
//  MVMCoreUI
//
//  Created by Scott Pfeil on 3/30/20.
//  Copyright © 2020 Verizon Wireless. All rights reserved.
//


@objcMembers open class LoadImageView: View {
    //--------------------------------------------------
    // MARK: - Outlets
    //--------------------------------------------------
    
    public let loadingSpinner = MFLoadingSpinner(frame: .zero)
    public let imageView = MFTransparentGIFView(frame: .zero)
    
    //--------------------------------------------------
    // MARK: - Properties
    //--------------------------------------------------
    
    public var addSizeConstraintsForAspectRatio = true
    public var shouldNotifyDelegateOnUpdate = true
    public var shouldNotifyDelegateOnDefaultSizeChange = false

    // Allows for a view to hardcode which height to use if there is none in the json.
    var imageWidth: CGFloat?
    var imageHeight: CGFloat?
    
    var delegateObject: MVMCoreUIDelegateObject?
    
    // For keeping track of current state.
    private var edges = UIRectEdge(rawValue: 0)
    private var spinnerHeight: CGFloat = 0
    private var currentImageWidth: CGFloat?
    private var currentImageHeight: CGFloat?
    private var currentImageName: String?
    private var isFallbackImage: Bool = false
    
    private var containerHelper = ContainerHelper()
    
    open override var accessibilityLabel: String? {
        get { imageView.accessibilityLabel }
        set { imageView.accessibilityLabel = newValue }
    }
    
    //--------------------------------------------------
    // MARK: - Constraint Properties
    //--------------------------------------------------
    
    var centerX: NSLayoutConstraint?
    var centerY: NSLayoutConstraint?
    var widthConstraint: NSLayoutConstraint?
    var heightConstraint: NSLayoutConstraint?
    var loadingSpinnerHeightConstraint: NSLayoutConstraint?
    
    //--------------------------------------------------
    // MARK: - Initializers
    //--------------------------------------------------
    
    public init(pinnedEdges edge: UIRectEdge) {
        edges = edge
        super.init(frame: .zero)
    }
    
    // The default is an image that is centered with no edges pinned. So it will take the size of the content and fill as needed.
    public init() {
        super.init(frame: .zero)
    }
    
    public override init(frame: CGRect) {
        super.init(frame: frame)
    }
    
    public required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
    }
    
    required public init(model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) {
        super.init(frame: .zero)
        addSizeConstraintsForAspectRatio = true
        pinEdges(.all)
        set(with: model, delegateObject, additionalData)
    }
    
    //--------------------------------------------------
    // MARK: - MVMCoreViewProtocol
    //--------------------------------------------------
    
    override open func setupView() {
        super.setupView()
        
        guard subviews.count == 0 else { return }
        
        clipsToBounds = true
        setContentHuggingPriority(UILayoutPriority(rawValue: 950), for: .horizontal)
        setContentHuggingPriority(UILayoutPriority(rawValue: 950), for: .vertical)
        
        // Setup image.
        imageView.translatesAutoresizingMaskIntoConstraints = false;
        addSubview(imageView)
        containerHelper.constrainView(imageView)
        
        // Setup edges constraints
        pinEdges(edges)
        
        // Setup spinner.
        loadingSpinner.clipsToBounds = true
        loadingSpinner.translatesAutoresizingMaskIntoConstraints = false
        addSubview(loadingSpinner)
        NSLayoutConstraint.activate(NSLayoutConstraint.constraints(withVisualFormat: "H:|-0@500-[loadingSpinner]-0@500-|",  metrics: nil, views: ["loadingSpinner" : loadingSpinner]))
        NSLayoutConstraint.activate(NSLayoutConstraint.constraints(withVisualFormat: "V:|-0@500-[loadingSpinner]-0@500-|",  metrics: nil, views: ["loadingSpinner" : loadingSpinner]))
        loadingSpinner.centerXAnchor.constraint(equalTo: centerXAnchor).isActive = true
        loadingSpinner.centerYAnchor.constraint(equalTo: centerYAnchor).isActive = true
        if let constraint = loadingSpinner.pinWidthAndHeight()?[ConstraintHeight] as! NSLayoutConstraint? {
            loadingSpinnerHeightConstraint = constraint
            spinnerHeight = constraint.constant
            loadingSpinnerHeightConstraint?.constant = 0
            loadingSpinnerHeightConstraint?.isActive = true
            loadingSpinner.pause()
        }
    }
    
    //--------------------------------------------------
    // MARK: - Methods
    //--------------------------------------------------
    
    open func defaultCompletionBlock() -> MVMCoreGetImageBlock {
        return { image, gifData,_ in MVMCoreDispatchUtility.performBlock(onMainThread: { [weak self] in
            if let image = image {
                self?.imageView.image = image
                self?.layoutIfNeeded()
            } else if let gifData = gifData {
                self?.imageView.loadGifWithData(gifData)
                self?.layoutIfNeeded()
            }
        })}
    }
    
    open func shouldLoadImage(withName imageName: String?, width: CGFloat) -> Bool {
        // We should load a new image if there is no current image, the image names are different, the width is different, or we are using a fallback image.
        guard let currentWidth = self.currentImageWidth else { return true }
        
        return (imageView.image == nil && imageView.animatedImage == nil) || imageName != currentImageName || !MVMCoreGetterUtility.cgfequal(width, currentWidth) || isFallbackImage
    }
    
    open func shouldLoadImage(withName imageName: String?, width: CGFloat?, height: CGFloat?) -> Bool {
        // We should load a new image if there is no current image, the image names are different, or we are using a fallback image.
        if ((imageView.image == nil && imageView.animatedImage == nil) || imageName != currentImageName || isFallbackImage) {
            return true
        }
        
        // load new image if the width is different
        if let oldWidth = self.currentImageWidth, let newWidth = width, !MVMCoreGetterUtility.cgfequal(oldWidth, newWidth) {
            return true
            
        } else if (self.currentImageWidth == nil) != (width == nil) {
            return true
        }
        
        // load new image if the height is different
        if let oldHeight = self.currentImageHeight, let newHeight = height, !MVMCoreGetterUtility.cgfequal(oldHeight, newHeight) {
            return true
            
        } else if (self.currentImageHeight == nil) || (height == nil) {
            return true
        }
        
        return false
    }
    
    //--------------------------------------------------
    // MARK: - Constraint Methods
    //--------------------------------------------------
    
    public func pinEdges(_ edge: UIRectEdge) {
        edges = edge
        
        if edge == .all {
            imageView.setContentHuggingPriority(.defaultLow, for: .horizontal)
            imageView.setContentHuggingPriority(.defaultLow, for: .vertical)
        } else {
            imageView.setContentHuggingPriority(UILayoutPriority(rawValue: 900), for: .horizontal)
            imageView.setContentHuggingPriority(UILayoutPriority(rawValue: 900), for: .vertical)
        }
        
        if edge.contains(.top) && edge.contains(.bottom) {
            containerHelper.alignVertical(.fill)
        } else if edge.contains(.top) {
            containerHelper.alignVertical(.leading)
        } else if edge.contains(.bottom) {
            containerHelper.alignVertical(.trailing)
        } else {
            containerHelper.alignVertical(.center)
        }
        
        if edge.contains(.left) && edge.contains(.right) {
            containerHelper.alignHorizontal(.fill)
        } else if edge.contains(.left) {
            containerHelper.alignHorizontal(.leading)
        } else if edge.contains(.right) {
            containerHelper.alignHorizontal(.trailing)
        } else {
            containerHelper.alignHorizontal(.center)
        }
    }
    
    func setHeight(_ height: CGFloat) {
        if let heightConstraint = heightConstraint, MVMCoreGetterUtility.cgfequal(heightConstraint.multiplier, 1) {
            heightConstraint.constant = height
        } else {
            heightConstraint?.isActive = false
            heightConstraint = imageView.heightAnchor.constraint(equalToConstant: height)
        }
        heightConstraint?.isActive = true
    }
    
    func setWidth(_ width: CGFloat) {
        if let widthConstraint = widthConstraint, MVMCoreGetterUtility.cgfequal(widthConstraint.multiplier, 1) {
            widthConstraint.constant = width
        } else {
            widthConstraint = imageView.widthAnchor.constraint(equalToConstant: width)
        }
        widthConstraint?.isActive = true
    }
    
    func layoutWillChange(width: CGFloat?, height: CGFloat?, size: CGSize?) -> Bool {
        guard addSizeConstraintsForAspectRatio else { return false }
        
        let widthWillChange = !MVMCoreGetterUtility.cgfequal(widthConstraint?.constant ?? 0, width ?? 0)
        let heightWillChange = !MVMCoreGetterUtility.cgfequal(heightConstraint?.constant ?? 0, height ?? 0)
        let sizeWillChange = shouldNotifyDelegateOnDefaultSizeChange && (width == nil || height == nil) && !(size?.equalTo(imageView.image?.size ?? CGSize.zero) ?? false)
        let heightChangeFromSpinner = (heightConstraint?.isActive ?? false) ? false : ((height ?? size?.height) ?? 0) < loadingSpinnerHeightConstraint?.constant ?? CGFloat.leastNormalMagnitude
        return widthWillChange || heightWillChange || sizeWillChange || heightChangeFromSpinner
    }
    
    // Constrains the image view to be the size provided. Used to size it to the image to fix aspect fit defect.
    func addConstraints(width: CGFloat?, height: CGFloat?, size: CGSize?) {
        
        widthConstraint?.isActive = false
        heightConstraint?.isActive = false
        
        guard addSizeConstraintsForAspectRatio else { return }
        
        if let width = width, let height = height {
            setHeight(height)
            setWidth(width)
        } else if let width = width, let size = size {
            setWidth(width)
            heightConstraint = imageView.heightAnchor.constraint(equalTo: imageView.widthAnchor, multiplier: size.height/size.width)
            heightConstraint?.priority = UILayoutPriority(rawValue: 900)
            heightConstraint?.isActive = true
        } else if let height = height, let size = size {
            setHeight(height)
            widthConstraint = imageView.widthAnchor.constraint(equalTo: imageView.heightAnchor, multiplier: size.width/size.height)
            widthConstraint?.priority = UILayoutPriority(rawValue: 900)
            widthConstraint?.isActive = true
        }
    }
    
    //--------------------------------------------------
    // MARK: - MoleculeViewProtocol
    //--------------------------------------------------
    
    public override func set(with model: MoleculeModelProtocol, _ delegateObject: MVMCoreUIDelegateObject?, _ additionalData: [AnyHashable: Any]?) {
        self.delegateObject = delegateObject
        super.set(with: model, delegateObject, additionalData)
        
        guard let imageModel = model as? ImageViewModel else { return }
        
        if let accessibilityString = imageModel.accessibilityText {
            imageView.accessibilityLabel = accessibilityString
            imageView.accessibilityTraits = .staticText
            imageView.isAccessibilityElement = true
        }
        
        let width = imageModel.width ?? imageWidth
        let height = imageModel.height ?? imageHeight
        
        // For smoother transitions, set constraints that we know immediately.
        if let width = width, addSizeConstraintsForAspectRatio {
            setWidth(width)
        }
        
        if let height = height, addSizeConstraintsForAspectRatio {
            setHeight(height)
        }
        
        if shouldLoadImage(withName: imageModel.image, width: width, height: height) {
            imageView.image = nil
            imageView.animatedImage = nil
            loadImage(withName: imageModel.image, format: imageModel.imageFormat, width: width, height: height, customFallbackImage: imageModel.fallbackImage, localBundle: imageModel.localBundle)
        }
        
        if let contentMode = imageModel.contentMode {
            imageView.contentMode = contentMode
        }
        
        if let cornerRadius = imageModel.cornerRadius {
            clipsToBounds = true
            layer.cornerRadius = cornerRadius
        }
        
        if let clipsImage = imageModel.clipsImage {
            clipsToBounds = clipsImage
        }
    }
    
    //--------------------------------------------------
    // MARK: - Load Methods
    //--------------------------------------------------
    
    public func loadImage(withName imageName: String?, format: String? = nil, width: CGFloat? = nil, height: CGFloat? = nil, customFallbackImage: String? = nil, allowServerParameters: Bool = false, localBundle: Bundle? = nil, completionHandler: MVMCoreGetImageBlock? = nil) {
        
        let completionBlock = completionHandler ?? defaultCompletionBlock()
        MVMCoreDispatchUtility.performBlock(onMainThread: { [unowned self] in
            self.currentImageName = imageName
            self.currentImageWidth = width
            self.currentImageHeight = height
            if MVMCoreCache.isHostedImage(imageName) {
                self.loadingSpinner.resumeSpinnerAfterDelay()
                self.loadingSpinnerHeightConstraint?.constant = self.spinnerHeight
            }
            
            let finishedLoadingBlock: MVMCoreGetImageBlock = {[weak self] (image, data, isFallbackImage) in MVMCoreDispatchUtility.performBlock(onMainThread: { [weak self] in
                guard let self = self,
                      let loadingImageName = self.currentImageName, loadingImageName == imageName else { return }
                self.isFallbackImage = isFallbackImage
                self.loadingSpinner.pause()
                let layoutWillChange = self.shouldNotifyDelegateOnUpdate ? self.layoutWillChange(width: self.currentImageWidth, height: self.currentImageHeight, size: image?.size) : false
                self.addConstraints(width: width, height: height, size: image?.size)
                self.loadingSpinnerHeightConstraint?.constant = 0
                if layoutWillChange {
                    self.delegateObject?.moleculeDelegate?.moleculeLayoutUpdated(self)
                }
                completionBlock(image,data,isFallbackImage)
            })}
            
            let fallbackImageName = customFallbackImage ?? MVMCoreUIUtility.localizedImageName("fallback")
            if let format = format, format.lowercased().contains("gif") {
                // Gifs aren't supported by default and need special handling
                MVMCoreCache.shared()?.getGif(imageName, useWidth: width != nil, widthForS7: Int(width ?? 0), useHeight: height != nil, heightForS7: Int(height ?? 0), format: format, localFallbackImageName: fallbackImageName, allowServerQueryParameters: allowServerParameters, localBundle: localBundle, completionHandler: finishedLoadingBlock)
            } else {
                MVMCoreCache.shared()?.getImage(imageName, useWidth: width != nil, widthForS7: Int(width ?? 0), useHeight: height != nil, heightForS7: Int(height ?? 0), format: format, localFallbackImageName: fallbackImageName, allowServerQueryParameters: allowServerParameters, localBundle: localBundle, completionHandler: finishedLoadingBlock)
            }
        })
    }
    
    public func loadCroppedImage(withName imageName:
                                    String?, width: NSNumber?, height: NSNumber?, cropRect: CGRect, flipImage: Bool, customFallbackImage: String?) {
        MVMCoreDispatchUtility.performBlock(onMainThread: { [unowned self] in
            self.currentImageName = imageName
            self.loadingSpinner.resumeSpinnerAfterDelay()
            self.loadingSpinnerHeightConstraint?.constant = self.spinnerHeight
            
            MVMCoreCache.shared()?.getCroppedImage(imageName, useWidth: width != nil, widthForS7: width?.intValue ?? 0, useHeight: height != nil, heightForS7: height?.intValue ?? 0, finalRect: cropRect, flipImage: flipImage, localFallbackImageName: customFallbackImage ?? MVMCoreUIUtility.localizedImageName("fallback"), completionHandler: { [weak self] (image, data, isFallBackImage) in
                MVMCoreDispatchUtility.performBlock(onMainThread: {
                    guard let image = image, let loadingImageName = self?.currentImageName, loadingImageName == imageName else {
                        return
                    }
                    self?.loadingSpinnerHeightConstraint?.constant = 0
                    self?.loadingSpinner.pause()
                    if flipImage, let cgImage = image.cgImage {
                        self?.imageView.image = UIImage(cgImage: cgImage, scale: image.scale, orientation: UIImage.Orientation.upMirrored)
                    } else {
                        self?.imageView.image = image
                    }
                    self?.layoutIfNeeded()
                })
            })
        })
    }
    
    // Note: Exposed for objective-c interfaces.
    
    public func loadImage(withName imageName: String?) {
        loadImage(withName: imageName, format: nil, width: nil, height: nil, customFallbackImage: nil, completionHandler: defaultCompletionBlock())
    }
    
    public func loadImage(withName imageName: String?, width: NSNumber?) {
        loadImage(withName: imageName, format: nil, width: width, height: nil, customFallbackImage: nil, completionHandler: defaultCompletionBlock())
    }
    
    public func loadImage(withName imageName: String?, height: NSNumber?) {
        loadImage(withName: imageName, format: nil, width: nil, height: height, customFallbackImage: nil, completionHandler: defaultCompletionBlock())
    }
    
    public func loadImage(withName imageName: String?, width: NSNumber?, height: NSNumber?) {
        loadImage(withName: imageName, format: nil, width: width, height: height, customFallbackImage: nil, completionHandler: defaultCompletionBlock())
    }
    
    public func loadImage(withName imageName: String?, format: String?, width: NSNumber?, height: NSNumber?) {
        loadImage(withName: imageName, format: format, width: width, height: height, customFallbackImage: nil, completionHandler: defaultCompletionBlock())
    }
    
    public func loadImage(withName imageName: String?, width: NSNumber?, height: NSNumber?, customFallbackImage: String?) {
        loadImage(withName: imageName, format: nil, width: width, height: height, customFallbackImage: customFallbackImage, completionHandler: defaultCompletionBlock())
    }
    
    public func loadImage(withName imageName: String?, width: NSNumber?, height: NSNumber?, completionHandler: @escaping MVMCoreGetImageBlock) {
        loadImage(withName: imageName, format: nil, width: width, height: height, customFallbackImage: nil, completionHandler: completionHandler)
    }
    
    public func loadImage(withName imageName: String?, width: NSNumber?, height: NSNumber?, customFallbackImage: String?, completionHandler: @escaping MVMCoreGetImageBlock) {
        loadImage(withName: imageName, format: nil, width: width, height: height, customFallbackImage: customFallbackImage, completionHandler: completionHandler)
    }
    
    public func loadImage(withName imageName: String?, format: String?, width: NSNumber?, height: NSNumber?, customFallbackImage: String?) {
        loadImage(withName: imageName, format: format, width: width, height: height, customFallbackImage: customFallbackImage, completionHandler: defaultCompletionBlock())
    }
    
    public func loadImage(withName imageName: String?, format: String?, width: NSNumber?, height: NSNumber?, customFallbackImage: String?, completionHandler: @escaping MVMCoreGetImageBlock) {
        loadImage(withName: imageName, format: format, width: width?.cgfloat(), height: height?.cgfloat(), customFallbackImage: customFallbackImage, allowServerParameters: false, completionHandler: completionHandler)
    }
}
