import SwiftLintCore
import SwiftSyntax

// MARK: - SelfBindingRule

@SwiftSyntaxRule(explicitRewriter: true, optIn: true)
struct SelfBindingRule: Rule {
    var configuration = SelfBindingConfiguration()

    static let description = RuleDescription(
        identifier: "self_binding",
        name: "Self Binding",
        description: "Re-bind `self` to a consistent identifier name.",
        kind: .style,
        nonTriggeringExamples: #examples([
            "if let self = self { return }",
            "guard let self = self else { return }",
            "if let this = this { return }",
            "guard let this = this else { return }",
            "if let this = self { return }".asExample(configuration: ["bind_identifier": "this"]),
            "guard let this = self else { return }".asExample(configuration: ["bind_identifier": "this"]),
        ]),
        triggeringExamples: #examples([
            "if let ↓`self` = self { return }",
            "guard let ↓`self` = self else { return }",
            "if let ↓this = self { return }",
            "guard let ↓this = self else { return }",
            "if let ↓self = self { return }".asExample(configuration: ["bind_identifier": "this"]),
            "guard let ↓self = self else { return }".asExample(configuration: ["bind_identifier": "this"]),
            "if let ↓self { return }".asExample(configuration: ["bind_identifier": "this"]),
            "guard let ↓self else { return }".asExample(configuration: ["bind_identifier": "this"]),
        ]),
        corrections: #corrections([
            "if let ↓`self` = self { return }":
                "if let self = self { return }",
            "guard let ↓`self` = self else { return }":
                "guard let self = self else { return }",
            "if let ↓this = self { return }":
                "if let self = self { return }",
            "guard let ↓this = self else { return }":
                "guard let self = self else { return }",
            "if let ↓self = self { return }".asExample(configuration: ["bind_identifier": "this"]):
                "if let this = self { return }".asExample(configuration: ["bind_identifier": "this"]),
            "if let ↓self { return }".asExample(configuration: ["bind_identifier": "this"]):
                "if let this = self { return }".asExample(configuration: ["bind_identifier": "this"]),
            "guard let ↓self else { return }".asExample(configuration: ["bind_identifier": "this"]):
                "guard let this = self else { return }".asExample(configuration: ["bind_identifier": "this"]),
        ])
    )
}

private extension SelfBindingRule {
    final class Visitor: ViolationsSyntaxVisitor<ConfigurationType> {
        override func visitPost(_ node: OptionalBindingConditionSyntax) {
            if let identifierPattern = node.pattern.as(IdentifierPatternSyntax.self),
               identifierPattern.identifier.text != configuration.bindIdentifier {
                var hasViolation = false
                if let initializerIdentifier = node.initializer?.value.as(DeclReferenceExprSyntax.self) {
                    hasViolation = initializerIdentifier.baseName.text == "self"
                } else if node.initializer == nil {
                    hasViolation = identifierPattern.identifier.text == "self" && configuration.bindIdentifier != "self"
                }

                if hasViolation {
                    violations.append(
                        ReasonedRuleViolation(
                            position: identifierPattern.positionAfterSkippingLeadingTrivia,
                            reason: "`self` should always be re-bound to `\(configuration.bindIdentifier)`"
                        )
                    )
                }
            }
        }
    }

    final class Rewriter: ViolationsSyntaxRewriter<ConfigurationType> {
        override func visit(_ node: OptionalBindingConditionSyntax) -> OptionalBindingConditionSyntax {
            guard let identifierPattern = node.pattern.as(IdentifierPatternSyntax.self),
                  identifierPattern.identifier.text != configuration.bindIdentifier else {
                return super.visit(node)
            }

            if let initializerIdentifier = node.initializer?.value.as(DeclReferenceExprSyntax.self),
               initializerIdentifier.baseName.text == "self" {
                numberOfCorrections += 1
                let newPattern = PatternSyntax(
                    identifierPattern
                        .with(\.identifier, identifierPattern.identifier
                            .with(\.tokenKind, .identifier(configuration.bindIdentifier)))
                )

                return super.visit(node.with(\.pattern, newPattern))
            }
            if node.initializer == nil,
               identifierPattern.identifier.text == "self",
               configuration.bindIdentifier != "self" {
                numberOfCorrections += 1
                let newPattern = PatternSyntax(
                    identifierPattern
                        .with(\.identifier, identifierPattern.identifier
                            .with(\.tokenKind, .identifier(configuration.bindIdentifier)))
                )

                let newInitializer = InitializerClauseSyntax(
                    value: DeclReferenceExprSyntax(
                        baseName: .keyword(
                            .`self`,
                            leadingTrivia: .space,
                            trailingTrivia: identifierPattern.trailingTrivia
                        )
                    )
                )

                let newNode = node
                    .with(\.pattern, newPattern)
                    .with(\.initializer, newInitializer)
                return super.visit(newNode)
            }
            return super.visit(node)
        }
    }
}
