import SwiftLintCore
import SwiftSyntax

@SwiftSyntaxRule
struct UnusedSetterValueRule: Rule {
    var configuration = SeverityConfiguration<Self>(.warning)

    static let description = RuleDescription(
        identifier: "unused_setter_value",
        name: "Unused Setter Value",
        description: "Setter value is not used",
        kind: .lint,
        nonTriggeringExamples: #examples([
            """
            var aValue: String {
                get {
                    return Persister.shared.aValue
                }
                set {
                    Persister.shared.aValue = newValue
                }
            }
            """,
            """
            var aValue: String {
                set {
                    Persister.shared.aValue = newValue
                }
                get {
                    return Persister.shared.aValue
                }
            }
            """,
            """
            var aValue: String {
                get {
                    return Persister.shared.aValue
                }
                set(value) {
                    Persister.shared.aValue = value
                }
            }
            """,
            """
            override var aValue: String {
             get {
                 return Persister.shared.aValue
             }
             set { }
            }
            """,
            """
            protocol Foo {
                var bar: Bool { get set }
            """.asExample(excludeFromDocumentation: true),
            """
            override var accessibilityValue: String? {
                get {
                    let index = Int(self.value)
                    guard steps.indices.contains(index) else { return "" }
                    return ""
                }
                set {}
            }
            """.asExample(excludeFromDocumentation: true),
        ]),
        triggeringExamples: #examples([
            """
            var aValue: String {
                get {
                    return Persister.shared.aValue
                }
                ↓set {
                    Persister.shared.aValue = aValue
                }
            }
            """,
            """
            var aValue: String {
                ↓set {
                    Persister.shared.aValue = aValue
                }
                get {
                    return Persister.shared.aValue
                }
            }
            """,
            """
            var aValue: String {
                get {
                    return Persister.shared.aValue
                }
                ↓set {
                    Persister.shared.aValue = aValue
                }
            }
            """,
            """
            var aValue: String {
                get {
                    let newValue = Persister.shared.aValue
                    return newValue
                }
                ↓set {
                    Persister.shared.aValue = aValue
                }
            }
            """,
            """
            var aValue: String {
                get {
                    return Persister.shared.aValue
                }
                ↓set(value) {
                    Persister.shared.aValue = aValue
                }
            }
            """,
            """
            override var aValue: String {
                get {
                    return Persister.shared.aValue
                }
                ↓set {
                    Persister.shared.aValue = aValue
                }
            }
            """,
        ])
    )
}

private extension UnusedSetterValueRule {
    final class Visitor: ViolationsSyntaxVisitor<ConfigurationType> {
        override var skippableDeclarations: [any DeclSyntaxProtocol.Type] { [ProtocolDeclSyntax.self] }

        override func visitPost(_ node: AccessorDeclSyntax) {
            guard node.accessorSpecifier.tokenKind == .keyword(.set) else {
                return
            }

            let variableName = node.parameters?.name.text ?? "newValue"
            let visitor = NewValueUsageVisitor(variableName: variableName)
            if !visitor.walk(tree: node, handler: \.isVariableUsed) {
                if Syntax(node).closestVariableOrSubscript()?.modifiers?.contains(keyword: .override) == true,
                   let body = node.body, body.statements.isEmpty {
                    return
                }

                violations.append(node.positionAfterSkippingLeadingTrivia)
            }
        }
    }
}

private final class NewValueUsageVisitor: SyntaxVisitor {
    let variableName: String
    private(set) var isVariableUsed = false

    init(variableName: String) {
        self.variableName = variableName
        super.init(viewMode: .sourceAccurate)
    }

    override func visitPost(_ node: DeclReferenceExprSyntax) {
        if node.baseName.text == variableName {
            isVariableUsed = true
        }
    }
}

private extension Syntax {
    func closestVariableOrSubscript() -> Either<SubscriptDeclSyntax, VariableDeclSyntax>? {
        if let subscriptDecl = `as`(SubscriptDeclSyntax.self) {
            return .left(subscriptDecl)
        }
        if let variableDecl = `as`(VariableDeclSyntax.self) {
            return .right(variableDecl)
        }

        return parent?.closestVariableOrSubscript()
    }
}

private enum Either<L, R> {
    case left(L)
    case right(R)
}

private extension Either<SubscriptDeclSyntax, VariableDeclSyntax> {
    var modifiers: DeclModifierListSyntax? {
        switch self {
        case .left(let left):
            return left.modifiers
        case .right(let right):
            return right.modifiers
        }
    }
}
