import Foundation
import UIKit

public extension String {
    func removingPrefix(_ value: String) -> String {
        guard hasPrefix(value) else { return self }
        return String(trimmingPrefix(value))
    }

    /// Truncates a string to a specified number of words, separated by a given character.
    /// This also removes any whitespaces.
    ///
    /// Example:
    /// ```
    /// let text = "apple, banana, cherry, date, elderberry, fig, grape"
    /// let truncatedText = text.truncated(to: 3)
    /// print(truncatedText) // Output: "apple, banana, cherry"
    /// ```
    func truncated(to numberOfWords: Int, separator: Character = ",") -> String {
        let words = self.split(separator: separator).map { $0.trimmingCharacters(in: .whitespaces) }
        guard words.count > numberOfWords else { return self }
        return words.prefix(numberOfWords).joined(separator: "\(separator) ")
    }

    // Used to determine string width and height
    func boundingBox(with font: UIFont) -> CGSize {
        let fontAttributes = [NSAttributedString.Key.font: font]
        return self.size(withAttributes: fontAttributes)
    }

    /// Checks whether the string can be presented within the specified `lineCount`
    /// and `containerWidth`.
    func fitsIn(lineCount: Int, font: UIFont, containerWidth: CGFloat) -> Bool {
        let maxHeight = font.lineHeight * CGFloat(lineCount)
        let constraintSize = CGSize(width: containerWidth, height: .greatestFiniteMagnitude)

        let boundingBox = self.boundingRect(
            with: constraintSize,
            options: [.usesLineFragmentOrigin, .usesFontLeading],
            attributes: [.font: font],
            context: nil
        )

        return boundingBox.height <= maxHeight
    }

    /// Performs character-based truncation on the string to make it fit
    /// within the specified `lineCount` and `containerWidth` using the specified `font`.
    ///
    /// No trailing `"..."` ellipses is added to the string. The caller is responsible
    /// for adding an ellipses at the tail of this string if needed.
    func truncatedTo(
        lineCount: Int,
        font: UIFont,
        containerWidth: CGFloat
    ) -> String {
        // Check if we need truncation at all to begin with
        guard !fitsIn(lineCount: lineCount, font: font, containerWidth: containerWidth) else {
            return self
        }

        let maxHeight = font.lineHeight * CGFloat(lineCount)
        let constraintSize = CGSize(width: containerWidth, height: .greatestFiniteMagnitude)

        var truncatedString = self

        var bounds: CGRect
        repeat {
            bounds = truncatedString.boundingRect(
                with: constraintSize,
                options: .usesLineFragmentOrigin,
                attributes: [.font: font],
                context: nil
            )
            guard truncatedString.count > 1 else { break }
            truncatedString.removeLast()

        } while bounds.height > maxHeight

        return truncatedString
    }

    /// Performs word-boundary truncation on the string to make it fit within the specified
    /// `lineCount` and `containerWidth` using the specified `font`.
    ///
    /// No trailing `"..."` ellipses is added to the string. The caller is responsible
    /// for adding an ellipses at the tail of this string if needed.
    func truncatedByWord(
        lineCount: Int,
        font: UIFont,
        containerWidth: CGFloat
    ) -> String {
        // Check if we need truncation at all to begin with
        guard !fitsIn(lineCount: lineCount, font: font, containerWidth: containerWidth) else {
            return self
        }

        let maxHeight = font.lineHeight * CGFloat(lineCount)
        let constraintSize = CGSize(width: containerWidth, height: .greatestFiniteMagnitude)

        var words = self.split(separator: " ")
        var truncatedString = ""

        while !words.isEmpty {
            let testString = words.joined(separator: " ")
            let testBoundingBox = testString.boundingRect(
                with: constraintSize,
                options: [.usesLineFragmentOrigin, .usesFontLeading],
                attributes: [.font: font],
                context: nil
            )

            if testBoundingBox.height <= maxHeight {
                truncatedString = words.joined(separator: " ")
                break
            }

            // Remove the last word and retry
            words.removeLast()
        }

        return truncatedString
    }

    var ifNotEmpty: String? {
        isEmpty ? nil : self
    }
}
