import Foundation
import SourceKittenFramework
import SwiftDiagnostics
import SwiftIDEUtils
import SwiftOperators
import SwiftParser
import SwiftParserDiagnostics
import SwiftSyntax

// swiftlint:disable:next blanket_disable_command
// swiftlint:disable closure_end_indentation opening_brace

package typealias AssertHandler = () -> Void

// Re-enable once all parser diagnostics in tests have been addressed.
// https://github.com/realm/SwiftLint/issues/3348
@TaskLocal package var parserDiagnosticsDisabledForTests = false

private typealias SourceKitResponse = [String: any SourceKitRepresentable]

/// Wraps a cached value, distinguishing "not yet computed" from "computed (possibly nil)".
private enum Cached<T> {
    case notComputed
    case computing(DispatchGroup)
    case computed(T)
}

private enum CacheLookup<T> {
    case hit(T)
    case miss
    case wait(DispatchGroup)
    case compute(DispatchGroup)
}

/// Per-file cache storing all derived artifacts. One instance lives on each `SwiftLintFile`.
///
/// **Locking strategy**: a concurrent `DispatchQueue` guards all slots.
/// Reads use a plain `sync` (concurrent); writes use `sync(flags: .barrier)` (exclusive).
/// Each accessor computes its value *outside* the lock so that factories may safely access
/// other cached properties without risk of deadlock. Cold slots are marked as computing
/// under a barrier so concurrent first readers wait for the same result.
final class FileCache: @unchecked Sendable {
    fileprivate let queue = DispatchQueue(label: "io.realm.swiftlint.fileCache", attributes: .concurrent)

    fileprivate var syntaxTree = Cached<SourceFileSyntax>.notComputed
    fileprivate var locationConverter = Cached<SourceLocationConverter>.notComputed
    fileprivate var commands = Cached<[Command]>.notComputed
    fileprivate var syntaxClassifications = Cached<SyntaxClassifications>.notComputed
    fileprivate var linesWithTokens = Cached<Set<Int>>.notComputed
    fileprivate var commentLines = Cached<Set<Int>>.notComputed
    fileprivate var emptyLines = Cached<Set<Int>>.notComputed
    fileprivate var response = Cached<SourceKitResponse?>.notComputed
    fileprivate var structureDictionary = Cached<SourceKittenDictionary?>.notComputed
    fileprivate var foldedSyntaxTree = Cached<SourceFileSyntax?>.notComputed
    fileprivate var syntaxMap = Cached<SwiftLintSyntaxMap?>.notComputed
    fileprivate var swiftSyntaxTokens = Cached<[SwiftLintSyntaxToken]?>.notComputed
    fileprivate var assertHandlerSlot = Cached<AssertHandler?>.notComputed

    /// Returns the cached value for a slot, computing it via `factory` on a cache miss.
    /// The factory runs *outside* the lock, so it may safely access other cached properties.
    /// On a concurrent first access the winner's result is kept; the loser's is discarded.
    ///
    /// TODO: [06/05/2028] We can convert the explicit getters and setters to a keypath-based subscript once the Swift
    /// compiler bug https://github.com/swiftlang/swift/issues/69386 is resolved.
    fileprivate func getOrCompute<T>(factory: () -> T, get: () -> Cached<T>, set: (Cached<T>) -> Void) -> T {
        // swiftlint:disable:previous cyclomatic_complexity

        let initialState = queue.sync { () -> CacheLookup<T> in
            switch get() {
            case .computed(let value):
                return .hit(value)
            case .computing(let group):
                return .wait(group)
            case .notComputed:
                return .miss
            }
        }

        switch initialState {
        case .hit(let value):
            return value
        case .wait(let group):
            group.wait()
            return getOrCompute(factory: factory, get: get, set: set)
        case .miss, .compute:
            break
        }

        let lookup = queue.sync(flags: .barrier) { () -> CacheLookup<T> in
            switch get() {
            case .computed(let value):
                return .hit(value)
            case .computing(let group):
                return .wait(group)
            case .notComputed:
                let group = DispatchGroup()
                group.enter()
                set(.computing(group))
                return .compute(group)
            }
        }

        switch lookup {
        case .hit(let value):
            return value
        case .wait(let group):
            group.wait()
            return getOrCompute(factory: factory, get: get, set: set)
        case .compute(let group):
            let value = factory()
            queue.sync(flags: .barrier) {
                defer { group.leave() }
                if case .computing(let currentGroup) = get(), currentGroup === group {
                    set(.computed(value))
                }
            }
            return value
        case .miss:
            queuedFatalError("Impossible state: missed then compute")
        }
    }

    /// Resets all slots to `.notComputed`, forcing recomputation on next access.
    fileprivate func invalidateAll() {
        queue.sync(flags: .barrier) {
            syntaxTree = .notComputed
            locationConverter = .notComputed
            commands = .notComputed
            syntaxClassifications = .notComputed
            linesWithTokens = .notComputed
            commentLines = .notComputed
            emptyLines = .notComputed
            response = .notComputed
            structureDictionary = .notComputed
            foldedSyntaxTree = .notComputed
            syntaxMap = .notComputed
            swiftSyntaxTokens = .notComputed
            assertHandlerSlot = .notComputed
        }
    }
}

extension SwiftLintFile {
    public var sourcekitdFailed: Bool {
        get { cachedResponse == nil }
        set {
            fileCache.queue.sync(flags: .barrier) {
                fileCache.response = newValue ? .computed(nil) : .notComputed
            }
        }
    }

    package var assertHandler: AssertHandler? {
        get {
            fileCache.getOrCompute
                { nil }
                get: { fileCache.assertHandlerSlot }
                set: { fileCache.assertHandlerSlot = $0 }
        }
        set {
            fileCache.queue.sync(flags: .barrier) { fileCache.assertHandlerSlot = .computed(newValue) }
        }
    }

    public var parserDiagnostics: [String] {
        if parserDiagnosticsDisabledForTests {
            return []
        }
        return ParseDiagnosticsGenerator.diagnostics(for: syntaxTree)
            .filter { $0.diagMessage.severity == .error }
            .map(\.message)
    }

    public var linesWithTokens: Set<Int> {
        fileCache.getOrCompute
            { computeLinesWithTokens() }
            get: { fileCache.linesWithTokens }
            set: { fileCache.linesWithTokens = $0 }
    }

    public var structureDictionary: SourceKittenDictionary {
        let value = fileCache.getOrCompute
            { cachedResponse.map(Structure.init).map { SourceKittenDictionary($0.dictionary) } }
            get: { fileCache.structureDictionary }
            set: { fileCache.structureDictionary = $0 }
        guard let value else {
            if let handler = assertHandler {
                handler()
                return SourceKittenDictionary([:])
            }
            queuedFatalError("Never call this for file that sourcekitd fails.")
        }
        return value
    }

    public var syntaxClassifications: SyntaxClassifications {
        fileCache.getOrCompute
            { syntaxTree.classifications }
            get: { fileCache.syntaxClassifications }
            set: { fileCache.syntaxClassifications = $0 }
    }

    public var syntaxMap: SwiftLintSyntaxMap {
        let value = fileCache.getOrCompute
            { cachedResponse.map { SwiftLintSyntaxMap(value: SyntaxMap(sourceKitResponse: $0)) } }
            get: { fileCache.syntaxMap }
            set: { fileCache.syntaxMap = $0 }
        guard let value else {
            if let handler = assertHandler {
                handler()
                return SwiftLintSyntaxMap(value: SyntaxMap(data: []))
            }
            queuedFatalError("Never call this for file that sourcekitd fails.")
        }
        return value
    }

    public var syntaxTree: SourceFileSyntax {
        fileCache.getOrCompute
            { Parser.parse(source: contents) }
            get: { fileCache.syntaxTree }
            set: { fileCache.syntaxTree = $0 }
    }

    public var foldedSyntaxTree: SourceFileSyntax? {
        fileCache.getOrCompute
            {
                OperatorTable.standardOperators
                    .foldAll(syntaxTree) { _ in /* Don't handle errors. */ }
                    .as(SourceFileSyntax.self)
            }
            get: { fileCache.foldedSyntaxTree }
            set: { fileCache.foldedSyntaxTree = $0 }
    }

    public var locationConverter: SourceLocationConverter {
        fileCache.getOrCompute
            { SourceLocationConverter(fileName: path?.filepath ?? "<nopath>", tree: syntaxTree) }
            get: { fileCache.locationConverter }
            set: { fileCache.locationConverter = $0 }
    }

    public var commands: [Command] { cachedCommands.filter(\.isValid) }

    public var invalidCommands: [Command] { cachedCommands.filter { !$0.isValid } }

    public var swiftSyntaxDerivedSourceKittenTokens: [SwiftLintSyntaxToken]? {
        fileCache.getOrCompute
            { SwiftSyntaxKindBridge.sourceKittenSyntaxKinds(for: self) }
            get: { fileCache.swiftSyntaxTokens }
            set: { fileCache.swiftSyntaxTokens = $0 }
    }

    public var commentLines: Set<Int> {
        fileCache.getOrCompute
            { CommentLinesVisitor.commentLines(in: self) }
            get: { fileCache.commentLines }
            set: { fileCache.commentLines = $0 }
    }

    public var emptyLines: Set<Int> {
        fileCache.getOrCompute
            { EmptyLinesVisitor.emptyLines(in: self) }
            get: { fileCache.emptyLines }
            set: { fileCache.emptyLines = $0 }
    }

    /// Invalidates all cached data for this file.
    public func invalidateCache() {
        if !isVirtual {
            file.clearCaches()
        }
        fileCache.invalidateAll()
    }

    // MARK: - Private helpers

    /// The raw SourceKit response for this file (cached).
    private var cachedResponse: SourceKitResponse? {
        fileCache.getOrCompute
            {
                do {
                    return try Request.editorOpen(file: file).sendIfNotDisabled()
                } catch let error as Request.Error {
                    queuedPrintError(error.description)
                    return nil
                } catch {
                    return nil
                }
            }
            get: { fileCache.response }
            set: { fileCache.response = $0 }
    }

    private var cachedCommands: [Command] {
        fileCache.getOrCompute
            {
                contents.contains("swiftlint:")
                    ? CommandVisitor(locationConverter: locationConverter).walk(file: self, handler: \.commands)
                    : []
            }
            get: { fileCache.commands }
            set: { fileCache.commands = $0 }
    }
}
