//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2020-2023 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//

#if FOUNDATION_FRAMEWORK
@_spi(Unstable) internal import CollectionsInternal
#elseif canImport(_RopeModule)
internal import _RopeModule
#elseif canImport(_FoundationCollections)
internal import _FoundationCollections
#endif

extension AttributedString {
    internal final class Guts : @unchecked Sendable {
        typealias Index = AttributedString.Index
        typealias Runs = AttributedString.Runs
        typealias AttributeMergePolicy = AttributedString.AttributeMergePolicy
        typealias AttributeRunBoundaries = AttributedString.AttributeRunBoundaries
        typealias _InternalRun = AttributedString._InternalRun
        typealias _InternalRuns = AttributedString._InternalRuns
        typealias _AttributeValue = AttributedString._AttributeValue
        typealias _AttributeStorage = AttributedString._AttributeStorage

        var string: BigString
        var runs: _InternalRuns

        // Note: the caller is responsible for performing attribute fix-ups if needed based on the source of the runs
        init(string: BigString, runs: _InternalRuns) {
            precondition(string.isEmpty == runs.isEmpty, "An empty attributed string should not contain any runs")
            self.string = string
            self.runs = runs
        }

        // Note: the caller is responsible for performing attribute fix-ups if needed based on the source of the runs
        convenience init(string: String, runs: _InternalRuns) {
            self.init(string: BigString(string), runs: runs)
        }

        convenience init() {
            self.init(string: BigString(), runs: _InternalRuns())
        }
    }
}

extension AttributedString.Guts {
    __consuming func copy() -> AttributedString.Guts {
        AttributedString.Guts(string: self.string, runs: self.runs)
    }

    __consuming func copy(in range: Range<BigString.Index>) -> AttributedString.Guts {
        let string = BigString(self.string.unicodeScalars[range])
        let runs = self.runs.extract(utf8Offsets: range._utf8OffsetRange)
        let copy = AttributedString.Guts(string: string, runs: runs)
        // FIXME: Extracting a slice should not invalidate anything but .textChanged attribute runs on the edges
        if range.lowerBound != string.startIndex || range.upperBound != string.endIndex {
            var utf8Range = copy.stringBounds._utf8OffsetRange
            utf8Range = copy.enforceAttributeConstraintsBeforeMutation(to: utf8Range)
            copy.enforceAttributeConstraintsAfterMutation(in: utf8Range, type: .attributesAndCharacters)
        }
        return copy
    }
}

extension AttributedString.Guts {
    internal static func characterwiseIsEqual(
        _ left: AttributedString.Guts,
        to right: AttributedString.Guts
    ) -> Bool {
        characterwiseIsEqual(left, in: left.stringBounds, to: right, in: right.stringBounds)
    }

    internal static func characterwiseIsEqual(
        _ left: AttributedString.Guts, in leftRange: Range<BigString.Index>,
        to right: AttributedString.Guts, in rightRange: Range<BigString.Index>
    ) -> Bool {
        let leftRuns = AttributedString.Runs(left, in: leftRange)
        let rightRuns = AttributedString.Runs(right, in: rightRange)
        return _characterwiseIsEqual(leftRuns, to: rightRuns)
    }

    internal static func _characterwiseIsEqual(
        _ left: AttributedString.Runs,
        to right: AttributedString.Runs
    ) -> Bool {
        // To decide if two attributed strings are equal, we need to logically split them up on
        // run boundaries, then check that each pair of pieces contains the same attribute values
        // and NFC-normalized string contents.
        //
        // Run lengths cannot be compared directly, as NFC normalization can change string length.
        //
        // We need to separately normalize each individual string piece. We cannot simply
        // normalize the entire string up front, as that would blur attribute run boundaries
        // (especially ones that fall inside Characters).
        //
        // Note: This implementation must be precisely in sync with the `characterwiseHash(in:into:)`
        // implementation below.
        if left._guts === right._guts, left._strBounds == right._strBounds { return true }

        guard left.count == right.count else { return false }

        var leftIndex = left._strBounds.lowerBound
        var rightIndex = right._strBounds.lowerBound

        var it1 = left.makeIterator()
        var it2 = right.makeIterator()
    loop:
        while true {
            switch (it1.next(), it2.next()) {
            case let (leftRun?, rightRun?):
                guard leftRun.attributes == rightRun.attributes else { return false }

                let leftNext = left._guts.string.utf8.index(leftIndex, offsetBy: leftRun._utf8Count)
                let rightNext = right._guts.string.utf8.index(rightIndex, offsetBy: rightRun._utf8Count)

                // FIXME: This doesn't handle sub-character runs correctly.
                guard
                    left._guts.string[leftIndex ..< leftNext] == right._guts.string[rightIndex ..< rightNext]
                else {
                    return false
                }
                leftIndex = leftNext
                rightIndex = rightNext
            case (nil, nil):
                break loop
            default:
                assertionFailure() // We compared counts above
                return false
            }
        }
        assert(leftIndex == left._strBounds.upperBound)
        assert(rightIndex == right._strBounds.upperBound)
        return true
    }

    internal func characterwiseHash(
        in range: Range<BigString.Index>,
        into hasher: inout Hasher
    ) {
        // Note: This implementation must be precisely in sync with the `_characterwiseIsEqual`
        // implementation above.
        let runs = AttributedString.Runs(self, in: range)
        hasher.combine(runs.count) // Hash discriminator

        for run in runs {
            hasher.combine(run._attributes)
            // FIXME: This doesn't handle sub-character runs correctly.
            hasher.combine(string[run._range])
        }
    }
}

extension AttributedString.Guts {
    internal func description(in range: Range<BigString.Index>) -> String {
        var result = ""
        let runs = Runs(self, in: range)
        for run in runs {
            let text = String(self.string.unicodeScalars[run.range._bstringRange])
            if !result.isEmpty { result += "\n" }
            result += "\(text) \(run._attributes)"
        }
        return result
    }
}

extension AttributedString.Guts {
    var stringBounds: Range<BigString.Index> {
        Range(uncheckedBounds: (string.startIndex, string.endIndex))
    }

    var utf8OffsetRange: Range<Int> {
        0 ..< string.utf8.count
    }

    func utf8Index(at offset: Int) -> BigString.Index {
        string.utf8.index(string.startIndex, offsetBy: offset)
    }

    func utf8IndexRange(from offsets: Range<Int>) -> Range<BigString.Index> {
        let lower = utf8Index(at: offsets.lowerBound)
        let upper = string.utf8.index(lower, offsetBy: offsets.count)
        return Range(uncheckedBounds: (lower, upper))
    }

    func unicodeScalarRange(roundingDown range: Range<BigString.Index>) -> Range<BigString.Index> {
        let lower = string.unicodeScalars.index(roundingDown: range.lowerBound)
        let upper = string.unicodeScalars.index(roundingDown: range.upperBound)
        return Range(uncheckedBounds: (lower, upper))
    }

    func characterRange(roundingDown range: Range<BigString.Index>) -> Range<BigString.Index> {
        let lower = string.index(roundingDown: range.lowerBound)
        let upper = string.index(roundingDown: range.upperBound)
        return Range(uncheckedBounds: (lower, upper))
    }

    func index(afterRun i: BigString.Index) -> BigString.Index {
        // Expected semantics: Result is the end of the run that contains `i`.
        let index = self.runs.index(atUTF8Offset: i.utf8Offset).index
        let length = self.runs[index].length
        let next = self.string.utf8.index(i, offsetBy: index.utf8Offset + length - i.utf8Offset)
        assert(next > i)
        return next
    }

    func index(beforeRun i: BigString.Index) -> BigString.Index {
        // Expected semantics: result is the start of the run preceding the one that contains `i`.
        // (I.e., `i` needs to get implicitly rounded down to the nearest run boundary before we
        // step back.)
        let prev = self.string.utf8.index(before: i)
        let index = self.runs.index(atUTF8Offset: prev.utf8Offset).index
        let length = self.runs[index].length
        if index.utf8Offset + length <= i.utf8Offset {
            // Fast path: `i` already addresses a run boundary.
            return self.string.utf8.index(prev, offsetBy: index.utf8Offset - prev.utf8Offset)
        }
        precondition(index > self.runs.startIndex, "Can't advance below start index")
        let index2 = self.runs.index(before: index)
        return self.string.utf8.index(prev, offsetBy: index2.utf8Offset - prev.utf8Offset)
    }

    internal func findRun(
        at i: BigString.Index
    ) -> (runIndex: _InternalRuns.Index, start: BigString.Index) {
        let run = self.runs.index(atUTF8Offset: i.utf8Offset)
        let start = self.string.utf8.index(i, offsetBy: -run.remainingUTF8)
        return (run.index, start)
    }

    /// Returns all the runs in the receiver, in the given range.
    func runs(in utf8Bounds: Range<Int>) -> AttributedString._InternalRunsSlice {
        AttributedString._InternalRunsSlice(self, utf8Bounds: utf8Bounds)
    }

    func runs(in range: Range<BigString.Index>) -> AttributedString._InternalRunsSlice {
        return runs(in: range._utf8OffsetRange)
    }

    func run(at index: _InternalRuns.Index) -> (attributes: _AttributeStorage, utf8Range: Range<Int>) {
        let run = runs[index]
        let length = self.runs[index].length
        let utf8Range = Range(uncheckedBounds: (index.utf8Offset, index.utf8Offset + length))
        return (run.attributes, utf8Range)
    }

    /// Update the attribute dictionary at the specified index by invoking a closure on it.
    /// If the index addresses a partial run at the beginning or end of the given slice, then the
    /// underlying run is automatically split to accommodate the change.
    ///
    /// Once `body` returns, the resulting item is coalesced with its neighbors if needed;
    /// such coalescing may change the number of items in the slice.
    ///
    /// On return, this function updates `index` to address the run that includes the UTF-8 range
    /// of the original element. If the update needed to coalesce runs, the new `index` may address
    /// a wider run than the original did.
    ///
    /// If `body` does not end up actually mutating the attributes passed to it, then it may signal
    /// this fact by setting its `mutated` argument to `false`. This avoid coalescing, so it will
    /// slightly speed up execution.
    func updateRun(
        at index: inout _InternalRuns.Index,
        within utf8Bounds: Range<Int>,
        with body: (
            _ attributes: inout _AttributeStorage,
            _ utf8Range: Range<Int>,
            _ mutated: inout Bool
        ) -> Void
    ) {
        var (attributes, fullUTF8Range) = run(at: index)
        let clampedUTF8Range = fullUTF8Range.clamped(to: utf8Bounds)
        precondition(!clampedUTF8Range.isEmpty, "Index out of bounds")
        var mutated = true
        if clampedUTF8Range == fullUTF8Range {
            // Allow in-place mutations of the attribute dictionary.
            self.runs._update(at: &index) { $0.attributes = _AttributeStorage() }
            body(&attributes, clampedUTF8Range, &mutated)
            if mutated {
                self.runs.updateAndCoalesce(at: &index) { $0 = attributes }
            } else {
                self.runs._update(at: &index) { $0.attributes = attributes }
            }
        } else {
            body(&attributes, clampedUTF8Range, &mutated)
            if mutated {
                let run = _InternalRun(length: clampedUTF8Range.count, attributes: attributes)
                self.runs.replaceUTF8Subrange(clampedUTF8Range, with: CollectionOfOne(run))
                index = self.runs.index(atUTF8Offset: index.utf8Offset).index
            } else {
                return // nothing mutated
            }
        }
    }

    /// Get the uniform value for the specified key across the given range of indices.
    /// Returns nil if the given range includes multiple different values for the same key.
    func getUniformValue<K: AttributedStringKey>(
        in range: Range<BigString.Index>, key: K.Type
    ) -> _AttributeValue? {
        var result: _AttributeValue? = nil
        for run in self.runs(in: range._utf8OffsetRange) {
            guard let value = run.attributes[K.name] else {
                return nil
            }
            if let previous = result, value != previous {
                return nil
            }
            result = value
        }
        return result
    }

    /// Get all attributes that have consistent values across the given range of indices.
    /// Attributes that have multiple different values are not included in the returned storage.
    func getUniformValues(in range: Range<BigString.Index>) -> _AttributeStorage {
        var attributes = _AttributeStorage()
        var first = true
        for run in self.runs(in: range._utf8OffsetRange) {
            guard !first else {
                attributes = run.attributes
                first = false
                continue
            }
            attributes = attributes.filterWithoutInvalidatingDependents {
                guard let value = run.attributes[$0.key] else { return false }
                return value == $0.value
            }
            if attributes.isEmpty {
                break
            }
        }
        return attributes
    }

    func setAttributeValue(
        _ value: _AttributeValue,
        forKey key: String,
        in range: Range<BigString.Index>
    ) {
        let utf8Range = unicodeScalarRange(roundingDown: range)._utf8OffsetRange
        self.runs(in: utf8Range).updateEach { attributes, range, mutated in
            attributes[key] = value
        }
        if value.hasConstrainedAttributes {
            self.enforceAttributeConstraintsAfterMutation(
                in: utf8Range, type: .attributes, constraintsInvolved: value.constraintsInvolved)
        }
    }

    func setAttributeValue<K: AttributedStringKey>(
        _ value: K.Value, forKey key: K.Type, in range: Range<BigString.Index>
    ) where K.Value : Sendable {
        let value = _AttributeValue(value, for: K.self)
        self.setAttributeValue(value, forKey: K.name, in: range)
    }

    func mergeAttributes(
        _ attributes: AttributeContainer,
        in range: Range<BigString.Index>,
        mergePolicy: AttributeMergePolicy = .keepNew
    ) {
        let new = attributes.storage
        let utf8Range = unicodeScalarRange(roundingDown: range)._utf8OffsetRange
        self.runs(in: utf8Range).updateEach { attributes, range, mutated in
            attributes.mergeIn(new, mergePolicy: mergePolicy)
        }
        if new.hasConstrainedAttributes {
            self.enforceAttributeConstraintsAfterMutation(
                in: utf8Range, type: .attributes, constraintsInvolved: new.constraintsInvolved)
        }
    }

    func setAttributes(_ attributes: _AttributeStorage, in range: Range<BigString.Index>) {
        let utf8Range = unicodeScalarRange(roundingDown: range)._utf8OffsetRange
        let run = _InternalRun(length: utf8Range.count, attributes: attributes)
        self.runs.replaceUTF8Subrange(utf8Range, with: CollectionOfOne(run))
        self.enforceAttributeConstraintsAfterMutation(
            in: utf8Range,
            type: .attributes,
            constraintsInvolved: attributes.constraintsInvolved)
    }

    func removeAttributeValue<K: AttributedStringKey>(
        forKey key: K.Type, in range: Range<BigString.Index>
    ) where K.Value: Sendable {
        let utf8Range = unicodeScalarRange(roundingDown: range)._utf8OffsetRange
        self.runs(in: utf8Range).updateEach { attributes, range, mutated in
            attributes[K.self] = nil
        }
        if K.runBoundaries != nil {
            self.enforceAttributeConstraintsAfterMutation(
                in: utf8Range, type: .attributes, constraintsInvolved: K.constraintsInvolved)
        }
    }

    func removeAttributeValue(forKey key: String, in range: Range<BigString.Index>) {
        let utf8Range = unicodeScalarRange(roundingDown: range)._utf8OffsetRange
        removeAttributeValue(forKey: key, in: utf8Range)
    }

    func removeAttributeValue(
        forKey key: String, in utf8Range: Range<Int>, adjustConstrainedAttributes: Bool = true
    ) {
        self.runs(in: utf8Range).updateEach { attributes, range, mutated in
            attributes[key] = nil
        }
        if adjustConstrainedAttributes {
            // FIXME: Collect boundary constraints.
            self.enforceAttributeConstraintsAfterMutation(in: utf8Range, type: .attributes)
        }
    }

    func _prepareStringMutation(
        in range: Range<BigString.Index>
    ) -> (oldUTF8Count: Int, invalidationRange: Range<Int>) {
        let utf8TargetRange = range._utf8OffsetRange
        let invalidationRange = self.enforceAttributeConstraintsBeforeMutation(to: utf8TargetRange)
        assert(invalidationRange.lowerBound <= utf8TargetRange.lowerBound)
        assert(invalidationRange.upperBound >= utf8TargetRange.upperBound)
        return (self.string.utf8.count, invalidationRange)
    }

    func _finalizeStringMutation(
        _ state: (oldUTF8Count: Int, invalidationRange: Range<Int>)
    ) {
        let utf8Delta = self.string.utf8.count - state.oldUTF8Count
        let lower = state.invalidationRange.lowerBound
        let upper = state.invalidationRange.upperBound + utf8Delta
        self.enforceAttributeConstraintsAfterMutation(
            in: lower ..< upper,
            type: .attributesAndCharacters)
    }

    func replaceSubrange(
        _ range: Range<BigString.Index>,
        with replacement: some AttributedStringProtocol
    ) {
        let replacementScalars = replacement.unicodeScalars._unicodeScalars

        // Determine if this replacement is going to actively change character data, or if this is
        // purely an attributes update, by seeing if the replacement string slice is identical to
        // our own storage. (If it is identical, then we need to update attributes surrounding the
        // affected bounds in a different way.)
        //
        // Note: this is intentionally not comparing actual string data.
        let hasStringChanges = !replacementScalars.isIdentical(to: string.unicodeScalars[range])

        let utf8SourceRange = Range(uncheckedBounds: (
            replacementScalars.startIndex.utf8Offset,
            replacementScalars.endIndex.utf8Offset
        ))
        let replacementRuns = replacement.__guts.runs(in: utf8SourceRange)

        let utf8TargetRange = range._utf8OffsetRange
        if hasStringChanges {
            let state = _prepareStringMutation(in: range)
            self.string.unicodeScalars.replaceSubrange(range, with: replacementScalars)
            self.runs.replaceUTF8Subrange(utf8TargetRange, with: replacementRuns)
            _finalizeStringMutation(state)
        } else {
            self.runs.replaceUTF8Subrange(utf8TargetRange, with: replacementRuns)
            self.enforceAttributeConstraintsAfterMutation(in: range._utf8OffsetRange, type: .attributes)
        }
    }

    func attributesToUseForTextReplacement(in range: Range<BigString.Index>) -> _AttributeStorage {
        guard !self.string.isEmpty else { return _AttributeStorage() }

        var position = range.lowerBound
        if range.isEmpty, position.utf8Offset > 0 {
            position = self.string.utf8.index(before: position)
        }

        let runIndex = self.runs.index(atUTF8Offset: position.utf8Offset).index
        let attributes = self.runs[runIndex].attributes
        return attributes.attributesForAddedText()
    }
}
