import Foundation
import SourceKittenFramework

/// A value describing the version of the Swift compiler.
public struct SwiftVersion: RawRepresentable, Codable, VersionComparable, Sendable {
    public typealias RawValue = String

    public let rawValue: String

    public init(rawValue: String) {
        self.rawValue = rawValue
    }
}

/// A comparable `major.minor.patch` version number.
public protocol VersionComparable: Comparable {
    /// The version string.
    var rawValue: String { get }
}

extension VersionComparable {
    public static func == (lhs: Self, rhs: Self) -> Bool {
        if let lhsComparators = lhs.comparators, let rhsComparators = rhs.comparators {
            return lhsComparators == rhsComparators
        }
        return lhs.rawValue == rhs.rawValue
    }

    public static func < (lhs: Self, rhs: Self) -> Bool {
        if let lhsComparators = lhs.comparators, let rhsComparators = rhs.comparators {
            return lhsComparators.lexicographicallyPrecedes(rhsComparators)
        }
        return lhs.rawValue < rhs.rawValue
    }

    private var comparators: [Int]? {
        let components = rawValue.split(separator: ".").compactMap { Int($0) }
        guard let major = components.first else {
            return nil
        }
        let minor = components.dropFirst(1).first ?? 0
        let patch = components.dropFirst(2).first ?? 0
        return [major, minor, patch]
    }
}

public extension SwiftVersion {
    /// Swift 5
    static let five = SwiftVersion(rawValue: "5.0.0")
    /// Swift 5.1
    static let fiveDotOne = SwiftVersion(rawValue: "5.1.0")
    /// Swift 5.2
    static let fiveDotTwo = SwiftVersion(rawValue: "5.2.0")
    /// Swift 5.3
    static let fiveDotThree = SwiftVersion(rawValue: "5.3.0")
    /// Swift 5.4
    static let fiveDotFour = SwiftVersion(rawValue: "5.4.0")
    /// Swift 5.5
    static let fiveDotFive = SwiftVersion(rawValue: "5.5.0")
    /// Swift 5.6
    static let fiveDotSix = SwiftVersion(rawValue: "5.6.0")
    /// Swift 5.7
    static let fiveDotSeven = SwiftVersion(rawValue: "5.7.0")
    /// Swift 5.8
    static let fiveDotEight = SwiftVersion(rawValue: "5.8.0")
    /// Swift 5.9
    static let fiveDotNine = SwiftVersion(rawValue: "5.9.0")
    /// Swift 6
    static let six = SwiftVersion(rawValue: "6.0.0")
    /// Swift 6.1
    static let sixDotOne = SwiftVersion(rawValue: "6.1.0")
    /// Swift 6.1.1
    static let sixDotOneDotOne = SwiftVersion(rawValue: "6.1.1")
    /// Swift 6.1.2
    static let sixDotOneDotTwo = SwiftVersion(rawValue: "6.1.2")
    /// Swift 6.2
    static let sixDotTwo = SwiftVersion(rawValue: "6.2.0")
    /// Swift 6.2.1
    static let sixDotTwoDotOne = SwiftVersion(rawValue: "6.2.1")
    /// Swift 6.2.2
    static let sixDotTwoDotTwo = SwiftVersion(rawValue: "6.2.2")
    /// Swift 6.2.3
    static let sixDotTwoDotThree = SwiftVersion(rawValue: "6.2.3")
    /// Swift 6.2.4
    static let sixDotTwoDotFour = SwiftVersion(rawValue: "6.2.4")
    /// Swift 6.3
    static let sixDotThree = SwiftVersion(rawValue: "6.3.0")
    /// Swift 6.3.1
    static let sixDotThreeDotOne = SwiftVersion(rawValue: "6.3.1")
    /// Swift 6.3.2
    static let sixDotThreeDotTwo = SwiftVersion(rawValue: "6.3.2")
    /// Swift 6.3.3
    static let sixDotThreeDotThree = SwiftVersion(rawValue: "6.3.3")
    /// Swift 6.4
    static let sixDotFour = SwiftVersion(rawValue: "6.4.0")

    /// The current detected Swift compiler version, based on the currently accessible SourceKit version.
    ///
    /// - note: Override by setting the `SWIFTLINT_SWIFT_VERSION` environment variable.
    static let current: SwiftVersion = {
        // Allow forcing the Swift version, useful in cases where SourceKit isn't available.
        if let envVersion = ProcessInfo.processInfo.environment["SWIFTLINT_SWIFT_VERSION"] {
            return SwiftVersion(rawValue: envVersion)
        }
        if !Request.disableSourceKit {
            // This request was added in Swift 5.1
            let params: SourceKitObject = ["key.request": UID("source.request.compiler_version")]
            // Allow this specific SourceKit request outside of rule execution context
            let result = CurrentRule.$allowSourceKitRequestWithoutRule.withValue(true) {
                try? Request.customRequest(request: params).sendIfNotDisabled()
            }
            if let result,
               let major = result.versionMajor, let minor = result.versionMinor, let patch = result.versionPatch {
                return SwiftVersion(rawValue: "\(major).\(minor).\(patch)")
            }
        }
        return .five
    }()
}

private extension Dictionary where Key == String {
    var versionMajor: Int? {
        (self["key.version_major"] as? Int64).flatMap({ Int($0) })
    }

    var versionMinor: Int? {
        (self["key.version_minor"] as? Int64).flatMap({ Int($0) })
    }

    var versionPatch: Int? {
        (self["key.version_patch"] as? Int64).flatMap({ Int($0) })
    }
}
