//
//  InputTextView.swift
//  InputBarAccessoryView
//
//  Copyright © 2017-2019 Nathan Tannar.
//
//  Permission is hereby granted, free of charge, to any person obtaining a copy
//  of this software and associated documentation files (the "Software"), to deal
//  in the Software without restriction, including without limitation the rights
//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
//  copies of the Software, and to permit persons to whom the Software is
//  furnished to do so, subject to the following conditions:
//
//  The above copyright notice and this permission notice shall be included in all
//  copies or substantial portions of the Software.
//
//  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
//  SOFTWARE.
//
//  Created by Nathan Tannar on 8/18/17.
//

import Foundation
import UIKit

/**
 A UITextView that has a UILabel embedded for placeholder text
 
 ## Important Notes ##
 1. Changing the font, textAlignment or textContainerInset automatically performs the same modifications to the placeholderLabel
 2. Intended to be used in an `InputBarAccessoryView`
 3. Default placeholder text is "Aa"
 4. Will pass a pasted image it's `InputBarAccessoryView`'s `InputPlugin`s
 */
open class InputTextView: UITextView {
    
    // MARK: - Properties
    
    open override var text: String! {
        didSet {
            postTextViewDidChangeNotification()
        }
    }
    
    open override var attributedText: NSAttributedString! {
        didSet {
            postTextViewDidChangeNotification()
        }
    }
    
    /// The images that are currently stored as NSTextAttachment's
    open var images: [UIImage] {
        return parseForAttachedImages()
    }
    
    open var components: [Any] {
        return parseForComponents()
    }
    
    open var isImagePasteEnabled: Bool = true

    /// A UILabel that holds the InputTextView's placeholder text
    public let placeholderLabel: UILabel = {
        let label = UILabel()
        label.numberOfLines = 0
        label.textColor = .lightGray
        label.text = "Aa"
        label.backgroundColor = .clear
        label.translatesAutoresizingMaskIntoConstraints = false
        return label
    }()
    
    /// The placeholder text that appears when there is no text
    open var placeholder: String? = "Aa" {
        didSet {
            placeholderLabel.text = placeholder
        }
    }
    
    /// The placeholderLabel's textColor
    open var placeholderTextColor: UIColor? = .lightGray {
        didSet {
            placeholderLabel.textColor = placeholderTextColor
        }
    }
    
    /// The UIEdgeInsets the placeholderLabel has within the InputTextView
    open var placeholderLabelInsets: UIEdgeInsets = UIEdgeInsets(top: 8, left: 4, bottom: 8, right: 4) {
        didSet {
            updateConstraintsForPlaceholderLabel()
        }
    }
    
    /// The font of the InputTextView. When set the placeholderLabel's font is also updated
    open override var font: UIFont! {
        didSet {
            placeholderLabel.font = font
        }
    }
    
    /// The textAlignment of the InputTextView. When set the placeholderLabel's textAlignment is also updated
    open override var textAlignment: NSTextAlignment {
        didSet {
            placeholderLabel.textAlignment = textAlignment
        }
    }
    
    /// The textContainerInset of the InputTextView. When set the placeholderLabelInsets is also updated
    open override var textContainerInset: UIEdgeInsets {
        didSet {
            placeholderLabelInsets = textContainerInset
        }
    }
    
    open override var scrollIndicatorInsets: UIEdgeInsets {
        didSet {
            // When .zero a rendering issue can occur
            if scrollIndicatorInsets == .zero {
                scrollIndicatorInsets = UIEdgeInsets(top: .leastNonzeroMagnitude,
                                                     left: .leastNonzeroMagnitude,
                                                     bottom: .leastNonzeroMagnitude,
                                                     right: .leastNonzeroMagnitude)
            }
        }
    }
    
    /// A weak reference to the InputBarAccessoryView that the InputTextView is contained within
    open weak var inputBarAccessoryView: InputBarAccessoryView?
    
    /// The constraints of the placeholderLabel
    private var placeholderLabelConstraintSet: NSLayoutConstraintSet?
 
    // MARK: - Initializers
    
    public convenience init() {
        self.init(frame: .zero)
    }
    
    public override init(frame: CGRect, textContainer: NSTextContainer?) {
        super.init(frame: frame, textContainer: textContainer)
        setup()
    }
    
    required public init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        setup()
    }
    
    deinit {
        NotificationCenter.default.removeObserver(self)
    }
    
    // MARK: - Setup
    
    /// Sets up the default properties
    open func setup() {
        
        backgroundColor = .clear
        font = UIFont.preferredFont(forTextStyle: .body)
        isScrollEnabled = false
        scrollIndicatorInsets = UIEdgeInsets(top: .leastNonzeroMagnitude,
                                             left: .leastNonzeroMagnitude,
                                             bottom: .leastNonzeroMagnitude,
                                             right: .leastNonzeroMagnitude)
        setupPlaceholderLabel()
        setupObservers()
    }
    
    /// Adds the placeholderLabel to the view and sets up its initial constraints
    private func setupPlaceholderLabel() {

        addSubview(placeholderLabel)
        placeholderLabelConstraintSet = NSLayoutConstraintSet(
            top:     placeholderLabel.topAnchor.constraint(equalTo: topAnchor, constant: placeholderLabelInsets.top),
            bottom:  placeholderLabel.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -placeholderLabelInsets.bottom),
            left:    placeholderLabel.leftAnchor.constraint(equalTo: leftAnchor, constant: placeholderLabelInsets.left),
            right:   placeholderLabel.rightAnchor.constraint(equalTo: rightAnchor, constant: -placeholderLabelInsets.right),
            centerX: placeholderLabel.centerXAnchor.constraint(equalTo: centerXAnchor),
            centerY: placeholderLabel.centerYAnchor.constraint(equalTo: centerYAnchor)
        )
        placeholderLabelConstraintSet?.centerX?.priority = .defaultLow
        placeholderLabelConstraintSet?.centerY?.priority = .defaultLow
        placeholderLabelConstraintSet?.activate()
    }
    
    /// Adds a notification for .UITextViewTextDidChange to detect when the placeholderLabel
    /// should be hidden or shown
    private func setupObservers() {
        
        NotificationCenter.default.addObserver(self,
                                               selector: #selector(InputTextView.redrawTextAttachments),
                                               name: UIDevice.orientationDidChangeNotification, object: nil)
        NotificationCenter.default.addObserver(self,
                                               selector: #selector(InputTextView.textViewTextDidChange),
                                               name: UITextView.textDidChangeNotification, object: nil)
    }

    /// Updates the placeholderLabels constraint constants to match the placeholderLabelInsets
    private func updateConstraintsForPlaceholderLabel() {

        placeholderLabelConstraintSet?.top?.constant = placeholderLabelInsets.top
        placeholderLabelConstraintSet?.bottom?.constant = -placeholderLabelInsets.bottom
        placeholderLabelConstraintSet?.left?.constant = placeholderLabelInsets.left
        placeholderLabelConstraintSet?.right?.constant = -placeholderLabelInsets.right
    }
    
    // MARK: - Notifications
    
    private func postTextViewDidChangeNotification() {
        NotificationCenter.default.post(name: UITextView.textDidChangeNotification, object: self)
    }
    
    @objc
    private func textViewTextDidChange() {
        let isPlaceholderHidden = !text.isEmpty
        placeholderLabel.isHidden = isPlaceholderHidden
        // Adjust constraints to prevent unambiguous content size
        if isPlaceholderHidden {
            placeholderLabelConstraintSet?.deactivate()
        } else {
            placeholderLabelConstraintSet?.activate()
        }
    }
    
    // MARK: - Image Paste Support
    
    open override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool {
        
        if action == NSSelectorFromString("paste:") && UIPasteboard.general.image != nil {
            return isImagePasteEnabled
        }
        return super.canPerformAction(action, withSender: sender)
    }
    
    open override func paste(_ sender: Any?) {
        
        guard let image = UIPasteboard.general.image else {
            return super.paste(sender)
        }
        if isImagePasteEnabled {
            pasteImageInTextContainer(with: image)
        } else {
            for plugin in inputBarAccessoryView?.inputPlugins ?? [] {
                if plugin.handleInput(of: image) {
                    return
                }
            }
        }
    }
    
    /// Addes a new UIImage to the NSTextContainer as an NSTextAttachment
    ///
    /// - Parameter image: The image to add
    private func pasteImageInTextContainer(with image: UIImage) {
        
        // Add the new image as an NSTextAttachment
        let attributedImageString = NSAttributedString(attachment: textAttachment(using: image))
        
        let isEmpty = attributedText.length == 0
        
        // Add a new line character before the image, this is what iMessage does
        let newAttributedStingComponent = isEmpty ? NSMutableAttributedString(string: "") : NSMutableAttributedString(string: "\n")
        newAttributedStingComponent.append(attributedImageString)
        
        // Add a new line character after the image, this is what iMessage does
        newAttributedStingComponent.append(NSAttributedString(string: "\n"))
        
        // The attributes that should be applied to the new NSAttributedString to match the current attributes
        let attributes: [NSAttributedString.Key: Any] = [
            NSAttributedString.Key.font: font ?? UIFont.preferredFont(forTextStyle: .body),
            NSAttributedString.Key.foregroundColor: textColor ?? .black
        ]
        newAttributedStingComponent.addAttributes(attributes, range: NSRange(location: 0, length: newAttributedStingComponent.length))
        
        textStorage.beginEditing()
        // Paste over selected text
        textStorage.replaceCharacters(in: selectedRange, with: newAttributedStingComponent)
        textStorage.endEditing()
        
        // Advance the range to the selected range plus the number of characters added
        let location = selectedRange.location + (isEmpty ? 2 : 3)
        selectedRange = NSRange(location: location, length: 0)
        
        // Broadcast a notification to recievers such as the MessageInputBar which will handle resizing
        postTextViewDidChangeNotification()
    }
    
    /// Returns an NSTextAttachment the provided image that will fit inside the NSTextContainer
    ///
    /// - Parameter image: The image to create an attachment with
    /// - Returns: The formatted NSTextAttachment
    private func textAttachment(using image: UIImage) -> NSTextAttachment {
        
        guard let cgImage = image.cgImage else { return NSTextAttachment() }
        let scale = image.size.width / (frame.width - 2 * (textContainerInset.left + textContainerInset.right))
        let textAttachment = NSTextAttachment()
        textAttachment.image = UIImage(cgImage: cgImage, scale: scale, orientation: image.imageOrientation)
        return textAttachment
    }
    
    /// Returns all images that exist as NSTextAttachment's
    ///
    /// - Returns: An array of type UIImage
    private func parseForAttachedImages() -> [UIImage] {
        
        var images = [UIImage]()
        let range = NSRange(location: 0, length: attributedText.length)
        attributedText.enumerateAttribute(.attachment, in: range, options: [], using: { value, range, _ -> Void in
            
            if let attachment = value as? NSTextAttachment {
                if let image = attachment.image {
                    images.append(image)
                } else if let image = attachment.image(forBounds: attachment.bounds,
                                                       textContainer: nil,
                                                       characterIndex: range.location) {
                    images.append(image)
                }
            }
        })
        return images
    }
    
    /// Returns an array of components (either a String or UIImage) that makes up the textContainer in
    /// the order that they were typed
    ///
    /// - Returns: An array of objects guaranteed to be of UIImage or String
    private func parseForComponents() -> [Any] {
        
        var components = [Any]()
        var attachments = [(NSRange, UIImage)]()
        let length = attributedText.length
        let range = NSRange(location: 0, length: length)
        attributedText.enumerateAttribute(.attachment, in: range) { (object, range, _) in
            if let attachment = object as? NSTextAttachment {
                if let image = attachment.image {
                    attachments.append((range, image))
                } else if let image = attachment.image(forBounds: attachment.bounds,
                                                       textContainer: nil,
                                                       characterIndex: range.location) {
                    attachments.append((range,image))
                }
            }
        }
        
        var curLocation = 0
        if attachments.count == 0 {
            let text = attributedText.string.trimmingCharacters(in: .whitespacesAndNewlines)
            if !text.isEmpty {
                components.append(text)
            }
        }
        else {
            attachments.forEach { (attachment) in
                let (range, image) = attachment
                if curLocation < range.location {
                    let textRange = NSMakeRange(curLocation, range.location - curLocation)
                    let text = attributedText.attributedSubstring(from: textRange).string.trimmingCharacters(in: .whitespacesAndNewlines)
                    if !text.isEmpty {
                        components.append(text)
                    }
                }
                
                curLocation = range.location + range.length
                components.append(image)
            }
            if curLocation < length - 1  {
                let text = attributedText.attributedSubstring(from: NSMakeRange(curLocation, length - curLocation)).string.trimmingCharacters(in: .whitespacesAndNewlines)
                if !text.isEmpty {
                    components.append(text)
                }
            }
        }
        
        return components
    }
    
    /// Redraws the NSTextAttachments in the NSTextContainer to fit the current bounds
    @objc
    private func redrawTextAttachments() {
        
        guard images.count > 0 else { return }
        let range = NSRange(location: 0, length: attributedText.length)
        attributedText.enumerateAttribute(.attachment, in: range, options: [], using: { value, _, _ -> Void in
            if let attachment = value as? NSTextAttachment, let image = attachment.image {
                
                // Calculates a new width/height ratio to fit the image in the current frame
                let newWidth = frame.width - 2 * (textContainerInset.left + textContainerInset.right)
                let ratio = image.size.height / image.size.width
                attachment.bounds.size = CGSize(width: newWidth, height: ratio * newWidth)
            }
        })
        layoutManager.invalidateLayout(forCharacterRange: range, actualCharacterRange: nil)
    }
    
}

