// Copyright © 2024 Rangeproof Pty Ltd. All rights reserved.

import UIKit

public class ScrollableLabel: UIView {
    public enum ScrollMode: Equatable, Hashable {
        case never
        case automatic
    }
    
    private var oldSize: CGSize = .zero
    private var layoutLoopCounter: Int = 0
    
    var scrollMode: ScrollMode = .automatic {
        didSet {
            guard scrollMode != oldValue else { return }
            
            updateContentSizeIfNeeded()
        }
    }
    
    var font: UIFont {
        get { label.font }
        set { label.font = newValue }
    }
    
    var text: String? {
        get { label.text }
        set {
            guard label.text != newValue else { return }
            
            label.text = newValue
            updateContentSizeIfNeeded()
        }
    }
    
    var attributedText: NSAttributedString? {
        get { label.attributedText }
        set {
            guard label.attributedText != newValue else { return }
            
            label.attributedText = newValue
            updateContentSizeIfNeeded()
        }
    }
    
    var themeTextColor: ThemeValue? {
        get { label.themeTextColor }
        set { label.themeTextColor = newValue }
    }
    
    var textAlignment: NSTextAlignment {
        get { label.textAlignment }
        set { label.textAlignment = newValue }
    }
    
    var lineBreakMode: NSLineBreakMode {
        get { label.lineBreakMode }
        set { label.lineBreakMode = newValue }
    }
    
    var numberOfLines: Int {
        get { label.numberOfLines }
        set { label.numberOfLines = newValue }
    }
    
    var maxNumberOfLinesWhenScrolling: Int = 5 {
        didSet {
            guard maxNumberOfLinesWhenScrolling != oldValue else { return }
            
            updateContentSizeIfNeeded()
        }
    }
    
    // MARK: - Initialization
    
    init() {
        super.init(frame: .zero)
        
        setupViews()
        setupConstraints()
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    // MARK: - UI Components
    
    private lazy var labelHeightAnchor: NSLayoutConstraint = label.set(.height, to: .height, of: scrollView).setting(isActive: false)
    private lazy var scrollViewHeightAnchor: NSLayoutConstraint = scrollView.set(.height, to: 0).setting(isActive: false)
    
    private let scrollView: UIScrollView = UIScrollView()
    private let label: UILabel = UILabel()
    
    // MARK: - Layout
    
    private func setupViews() {
        addSubview(scrollView)
        
        scrollView.addSubview(label)
    }
    
    private func setupConstraints() {
        scrollView.pin(to: self)
        
        label.setContentHugging(.vertical, to: .required)
        label.pin(to: scrollView)
        label.set(.width, to: .width, of: scrollView)
    }
    
    public override func layoutSubviews() {
        super.layoutSubviews()
        
        guard frame.size != oldSize else {
            layoutLoopCounter = 0
            return
        }
        
        updateContentSizeIfNeeded()
    }
    
    private func updateContentSizeIfNeeded() {
        // Ensure we don't get stuck in an infinite layout loop somehow
        guard layoutLoopCounter < 5 else { return }
        
        // Update the contentSize of the scrollView to match the size of the label
        scrollView.contentSize = label.sizeThatFits(
            CGSize(width: scrollView.bounds.width, height: CGFloat.greatestFiniteMagnitude)
        )
        
        // If scrolling is enabled and the maximum height we want to show is smaller than the scrollable height
        // then we need to fix the height of the scroll view to our desired maximum, other
        let maxCalculatedHeight: CGFloat = (label.font.lineHeight * CGFloat(maxNumberOfLinesWhenScrolling))
        
        switch (scrollMode != .never, maxCalculatedHeight <= scrollView.contentSize.height) {
            case (false, _), (true, false):
                scrollViewHeightAnchor.isActive = false
                labelHeightAnchor.isActive = true
                
            case (true, true):
                labelHeightAnchor.isActive = false
                scrollViewHeightAnchor.constant = maxCalculatedHeight
                scrollViewHeightAnchor.isActive = true
        }
        
        oldSize = frame.size
        
        // The view should have the same height as the scrollView, if it doesn't then we might need to relayout
        // again to ensure the frame size is correct
        guard
            scrollView.frame.size.height < CGFloat.leastNonzeroMagnitude ||
            abs(frame.size.height - scrollView.frame.size.height) > CGFloat.leastNonzeroMagnitude
        else { return }
        
        layoutLoopCounter += 1
        setNeedsLayout()
        layoutIfNeeded()
    }
}