import SwiftLintCore
import SwiftSyntax
import SwiftSyntaxBuilder

@SwiftSyntaxRule(explicitRewriter: true, optIn: true)
struct PreferKeyPathRule: Rule {
    var configuration = PreferKeyPathConfiguration()

    private static let extendedMode = ["restrict_to_standard_functions": false]
    private static let ignoreIdentity = ["ignore_identity_closures": true]
    private static let extendedModeAndIgnoreIdentity = [
        "restrict_to_standard_functions": false,
        "ignore_identity_closures": true,
    ]

    static let description = RuleDescription(
        identifier: "prefer_key_path",
        name: "Prefer Key Path",
        description: "Use a key path argument instead of a closure with property access",
        rationale: """
            Note: Swift 5 doesn't support identity key path conversions (`{ $0 }` -> `(\\.self)`) and so
            SwiftLint disregards `ignore_identity_closures: false` if it runs on a Swift <6 project.
            """,
        kind: .idiomatic,
        minSwiftVersion: .fiveDotTwo,
        nonTriggeringExamples: [
            Example("f {}"),
            Example("f { $0 }"),
            Example("f { $0.a }"),
            Example("let f = { $0.a }(b)"),
            Example("f {}", configuration: extendedMode),
            Example("f() { g() }", configuration: extendedMode),
            Example("f { a.b.c }", configuration: extendedMode),
            Example("f { a, b in a.b }", configuration: extendedMode),
            Example("f { (a, b) in a.b }", configuration: extendedMode),
            Example("f { $0.a } g: { $0.b }", configuration: extendedMode),
            Example("[1, 2, 3].reduce(1) { $0 + $1 }", configuration: extendedMode),
            Example("f { $0 }", configuration: extendedModeAndIgnoreIdentity),
            Example("f.map { $0 }", configuration: ignoreIdentity),
            Example("f.map(1) { $0.a }"),
            Example("f.filter({ $0.a }, x)"),
            Example("#Predicate { $0.a }"),
            Example("let transform: (Int) -> Int = nil ?? { $0.a }"),
        ],
        triggeringExamples: [
            Example("f.map ↓{ $0.a }"),
            Example("f.filter ↓{ $0.a }"),
            Example("f.first ↓{ $0.a }"),
            Example("f.contains ↓{ $0.a }"),
            Example("f.contains(where: ↓{ $0.a })"),
            Example("f(↓{ $0.a })", configuration: extendedMode),
            Example("f(a: ↓{ $0.b })", configuration: extendedMode),
            Example("f(a: ↓{ a in a.b }, x)", configuration: extendedMode),
            Example("f.map ↓{ a in a.b.c }"),
            Example("f.allSatisfy ↓{ (a: A) in a.b }"),
            Example("f.first ↓{ (a b: A) in b.c }"),
            Example("f.contains ↓{ $0.0.a }"),
            Example("f.compactMap ↓{ $0.a.b.c.d }"),
            Example("f.flatMap ↓{ $0.a.b }"),
            Example("let f: (Int) -> Int = ↓{ $0.bigEndian }", configuration: extendedMode),
            Example("transform = ↓{ $0.a }"),
        ],
        corrections: [
            Example("f.map { $0.a }"):
                Example("f.map(\\.a)"),
            Example("""
                // begin
                f.map { $0.a } // end
                """):
                Example("""
                    // begin
                    f.map(\\.a) // end
                    """),
            Example("f.map({ $0.a })"):
                Example("f.map(\\.a)"),
            Example("f(a: { $0.a })", configuration: extendedMode):
                Example("f(a: \\.a)"),
            Example("f({ $0.a })", configuration: extendedMode):
                Example("f(\\.a)"),
            Example("let f = /* begin */ { $0.a } // end", configuration: extendedMode):
                Example("let f = /* begin */ \\.a // end"),
            Example("let f = { $0.a }(b)"):
                Example("let f = { $0.a }(b)"),
            Example("let f: (Int) -> Int = ↓{ $0.bigEndian }", configuration: extendedMode):
                Example("let f: (Int) -> Int = \\.bigEndian"),
            Example("f.partition ↓{ $0.a.b }"):
                Example("f.partition(by: \\.a.b)"),
            Example("f.contains ↓{ $0.a.b }"):
                Example("f.contains(where: \\.a.b)"),
            Example("f.first ↓{ element in element.a }"):
                Example("f.first(where: \\.a)"),
            Example("f.drop ↓{ element in element.a }"):
                Example("f.drop(while: \\.a)"),
            Example("f.compactMap ↓{ $0.a.b.c.d }"):
                Example("f.compactMap(\\.a.b.c.d)"),
            Example("f { $0 }", configuration: extendedModeAndIgnoreIdentity): // no change with option enabled
                Example("f { $0 }", configuration: extendedModeAndIgnoreIdentity),
            Example("f.map { $0 }", configuration: ignoreIdentity): // no change with option enabled
                Example("f.map { $0 }", configuration: ignoreIdentity),
            Example("""
                myList
                    .map { $0.a } // swiftlint:disable:this prefer_key_path
                """):
                Example("""
                myList
                    .map { $0.a } // swiftlint:disable:this prefer_key_path
                """),
        ]
    )
}

private extension PreferKeyPathRule {
    final class Visitor: ViolationsSyntaxVisitor<ConfigurationType> {
        override func visitPost(_ node: ClosureExprSyntax) {
            if node.isInvalid(restrictToStandardFunctions: configuration.restrictToStandardFunctions) {
                return
            }
            if let onlyStmt = node.onlyExprStmt,
               onlyStmt.accesses(identifier: node.onlyParameter) {
                if onlyStmt.is(DeclReferenceExprSyntax.self),
                   configuration.ignoreIdentityClosures || SwiftVersion.current < .six {
                    return
                }

                violations.append(node.positionAfterSkippingLeadingTrivia)
            }
        }
    }

    final class Rewriter: ViolationsSyntaxRewriter<ConfigurationType> {
        override func visit(_ node: FunctionCallExprSyntax) -> ExprSyntax {
            guard node.additionalTrailingClosures.isEmpty,
                  let closure = node.trailingClosure,
                  !isDisabled(atStartPositionOf: closure),
                  !closure.isInvalid(restrictToStandardFunctions: configuration.restrictToStandardFunctions),
                  let expr = closure.onlyExprStmt,
                  expr.accesses(identifier: closure.onlyParameter) == true,
                  let replacement = expr.asKeyPath(ignoreIdentityClosures: configuration.ignoreIdentityClosures),
                  let calleeName = node.calleeName else {
                return super.visit(node)
            }
            numberOfCorrections += 1
            var node = node.with(\.calledExpression, node.calledExpression.with(\.trailingTrivia, []))
            if node.leftParen == nil {
                node = node.with(\.leftParen, .leftParenToken())
            }
            let newArg = LabeledExprSyntax(
                label: argumentLabelByStandardFunction[calleeName, default: nil],
                expression: replacement
            )
            node = node.with(\.arguments, [newArg]
            )
            if node.rightParen == nil {
                node = node.with(\.rightParen, .rightParenToken())
            }
            node = node
                .with(\.trailingClosure, nil)
                .with(\.trailingTrivia, node.trailingTrivia)
            return super.visit(node)
        }

        override func visit(_ node: ClosureExprSyntax) -> ExprSyntax {
            if node.isInvalid(restrictToStandardFunctions: configuration.restrictToStandardFunctions) {
                return super.visit(node)
            }
            if let expr = node.onlyExprStmt,
               expr.accesses(identifier: node.onlyParameter) == true,
               let replacement = expr.asKeyPath(ignoreIdentityClosures: configuration.ignoreIdentityClosures) {
                numberOfCorrections += 1
                let node = replacement
                    .with(\.leadingTrivia, node.leadingTrivia)
                    .with(\.trailingTrivia, node.trailingTrivia)
                return super.visit(node)
            }
            return super.visit(node)
        }
    }
}

private extension ExprSyntax {
    func accesses(identifier: String?) -> Bool {
        if let base = `as`(MemberAccessExprSyntax.self)?.base {
            return base.accesses(identifier: identifier)
        }
        if let declRef = `as`(DeclReferenceExprSyntax.self) {
            return declRef.baseName.text == identifier ?? "$0"
        }
        return false
    }
}

private extension ClosureExprSyntax {
    var onlyParameter: String? {
        switch signature?.parameterClause {
        case let .simpleInput(params):
            return params.onlyElement?.name.text
        case let .parameterClause(params):
            let param = params.parameters.onlyElement
            return param?.secondName?.text ?? param?.firstName.text
        case nil: return nil
        }
    }

    var onlyExprStmt: ExprSyntax? {
        if case let .expr(expr) = statements.onlyElement?.item {
            return expr
        }
        return nil
    }

    func isInvalid(restrictToStandardFunctions: Bool) -> Bool {
        guard keyPathInParent != \FunctionCallExprSyntax.calledExpression,
              let parent,
              ![.macroExpansionExpr, .multipleTrailingClosureElement].contains(parent.kind),
              previousToken(viewMode: .sourceAccurate)?.text != "??" else {
            return true
        }
        if let call = parent.as(LabeledExprSyntax.self)?.parent?.parent?.as(FunctionCallExprSyntax.self) {
            // Closure is function argument.
            return restrictToStandardFunctions && !call.isStandardFunction
        }
        if let call = parent.as(FunctionCallExprSyntax.self) {
            // Trailing closure.
            return call.additionalTrailingClosures.isNotEmpty || restrictToStandardFunctions && !call.isStandardFunction
        }
        return false
    }
}

private let argumentLabelByStandardFunction: [String: String?] = [
    "allSatisfy": nil,
    "contains": "where",
    "compactMap": nil,
    "drop": "while",
    "filter": nil,
    "first": "where",
    "flatMap": nil,
    "map": nil,
    "partition": "by",
    "prefix": "while",
]

private extension FunctionCallExprSyntax {
    var isStandardFunction: Bool {
        if let calleeName, argumentLabelByStandardFunction.keys.contains(calleeName) {
            return arguments.count + (trailingClosure == nil ? 0 : 1) == 1
        }
        return false
    }

    var calleeName: String? {
        (calledExpression.as(DeclReferenceExprSyntax.self)
            ?? calledExpression.as(MemberAccessExprSyntax.self)?.declName)?.baseName.text
    }
}

private extension ExprSyntax {
    func asKeyPath(ignoreIdentityClosures: Bool) -> ExprSyntax? {
        if let memberAccess = `as`(MemberAccessExprSyntax.self) {
            var this = memberAccess.base
            var elements = [memberAccess.declName]
            while this?.is(DeclReferenceExprSyntax.self) != true {
                if let memberAccess = this?.as(MemberAccessExprSyntax.self) {
                    elements.append(memberAccess.declName)
                    this = memberAccess.base
                }
            }
            return "\\.\(raw: elements.reversed().map(\.baseName.text).joined(separator: "."))" as Self
        }

        if !ignoreIdentityClosures, SwiftVersion.current >= .six, `is`(DeclReferenceExprSyntax.self) {
            return "\\.self"
        }
        return nil
    }
}
