
extension String {

    func htmlEscaped() -> String {
        replacingOccurrences(of: "&", with: "&amp;")
            .replacingOccurrences(of: "\"", with: "&quot;")
            .replacingOccurrences(of: "'", with: "&#39;")
            .replacingOccurrences(of: "<", with: "&lt;")
            .replacingOccurrences(of: ">", with: "&gt;")
    }

    func percentDecoded() -> String {
        guard let decoded = removingPercentEncoding else {
            return self
        }
        return decoded
    }

    var withLeadingSlashRemoved: String {
        hasPrefix("/") ? String(dropFirst("/".count)) : self
    }

    var withLeadingSlash: String {
        hasPrefix("/") ? self : "/" + self
    }

    var withTrailingSlashRemoved: String {
        hasSuffix("/") ? String(dropLast("/".count)) : self
    }

    var withTrailingSlash: String {
        hasSuffix("/") ? self : self + "/"
    }

    var withHttpPrefixRemoved: String {
        if hasPrefix("https://") {
            return String(dropFirst("https://".count))
        }
        if hasPrefix("http://") {
            return String(dropFirst("http://".count))
        }
        return self
    }

    var removingSurroundingQuotes: String {
        if hasPrefix("\"") && hasSuffix("\"") {
            return dropBeforeFirst("\"").dropAfterLast("\"")
        }
        if hasPrefix("'") && hasSuffix("'") {
            return dropBeforeFirst("'").dropAfterLast("'")
        }
        return self
    }

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

    var trimmed: String {
        trimmingCharacters(in: .whitespacesAndNewlines)
    }

    func indented(by indentation: String) -> String {
        components(separatedBy: "\n").joined(separator: "\n" + indentation)
    }

    var withoutEmptyLines: String {
        components(separatedBy: "\n")
            .filter { !$0.trimmed.isEmpty }
            .joined(separator: "\n")
    }

    /**
     Remove the part after the last occurence of the separator (including the separator itself).

     The string is left unchanged, if it does not contain the separator.
     */
    func dropAfterLast(_ separator: String) -> String {
        guard contains(separator) else {
            return self
        }
        return components(separatedBy: separator).dropLast().joined(separator: separator)
    }

    /**
     Remove everything before the last separator.

     Also removes the separator itself. If the separator is not contained in the string, then the full string is returned.
     */
    func dropBeforeLast<T>(_ separator: T) -> String where T: StringProtocol {
        components(separatedBy: separator).last!
    }

    func dropBeforeFirst(_ separator: String) -> String {
        guard contains(separator) else {
            return self
        }
        return components(separatedBy: separator).dropFirst().joined(separator: separator)
    }

    func lastComponentAfter(_ separator: String) -> String {
        components(separatedBy: separator).last!
    }

    /**
     Insert the new content before the last occurence of the specified separator.

     If the separator does not appear in the string, then the new content is simply appended.
     */
    func insert(_ content: String, beforeLast separator: String) -> String {
        let parts = components(separatedBy: separator)
        guard parts.count > 1 else {
            return self + content
        }
        return parts.dropLast().joined(separator: separator) + content + separator + parts.last!
    }

    /**
     Remove everything behind the first separator.

     Also removes the separator itself. If the separator is not contained in the string, then the full string is returned.
     */
    func dropAfterFirst<T>(_ separator: T) -> String where T: StringProtocol {
        components(separatedBy: separator).first!
    }

    func between(_ start: String, and end: String) -> String {
        dropBeforeFirst(start).dropAfterFirst(end)
    }

    /**
     Split the string at the first occurence of the separator

     If the String does not contain the separator, then `before` will contain the whole string, and `after` will be empty
     */
    func splitAtFirst(_ separator: String) -> (before: String, after: String) {
        let parts = components(separatedBy: separator)
        return (parts.first!, parts.dropFirst().joined(separator: separator))
    }
}

extension String {

    func removingPrefix(_ prefix: String) -> String? {
        guard self.hasPrefix(prefix) else { return nil }
        return String(self.dropFirst(prefix.count))
    }
}

extension String {

    var fileNameWithoutExtension: String {
        dropAfterLast(".")
    }

    var fileExtension: String? {
        let parts = components(separatedBy: ".")
        guard parts.count > 1 else { return nil }
        return parts.last
    }

    var fileNameAndExtension: (fileName: String, fileExtension: String?) {
        let parts = components(separatedBy: ".")
        guard parts.count > 1 else {
            return (self, nil)
        }
        return (fileName: parts.dropLast().joined(separator: "."), fileExtension: parts.last)
    }
}

extension Substring {

    func dropBeforeFirst(_ separator: String) -> String {
        guard contains(separator) else {
            return String(self)
        }
        return components(separatedBy: separator).dropFirst().joined(separator: separator)
    }

    func between(_ start: String, and end: String) -> String {
        components(separatedBy: start).last!
            .components(separatedBy: end).first!
    }

    func between(first: String, andLast last: String) -> String {
        dropBeforeFirst(first).dropAfterLast(last)
    }

    func last(after: String) -> String {
        components(separatedBy: after).last!
    }

    /**
     Split the string at the first occurence of the separator
     */
    func splitAtFirst(_ separator: String) -> (String, String) {
        let parts = components(separatedBy: separator)
        return (parts.first!, parts.dropFirst().joined(separator: separator))
    }
}
