import SwiftLintCore
import SwiftSyntax

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

    static let description = RuleDescription(
        identifier: "duplicate_conditions",
        name: "Duplicate Conditions",
        description: "Duplicate sets of conditions in the same branch instruction should be avoided",
        kind: .lint,
        nonTriggeringExamples: #examples([
            """
                if x < 5 {
                    foo()
                } else if y == "s" {
                    bar()
                }
                """,
            """
                if x < 5 {
                    foo()
                }
                if x < 5 {
                    bar()
                }
                """,
            """
                if x < 5, y == "s" {
                    foo()
                } else if x < 5 {
                    bar()
                }
                """,
            """
                switch x {
                case \"a\":
                    foo()
                    bar()
                }
                """,
            """
                switch x {
                case \"a\" where y == "s":
                    foo()
                case \"a\" where y == "t":
                    bar()
                }
                """,
            """
                if let x = maybeAbc {
                    foo()
                } else if let x = maybePqr {
                    bar()
                }
                """,
            """
                if let x = maybeAbc, let z = x.maybeY {
                    foo()
                } else if let x = maybePqr, let z = x.maybeY {
                    bar()
                }
                """,
            """
                if case .p = x {
                    foo()
                } else if case .q = x {
                    bar()
                }
                """,
            """
                if true {
                    if true { foo() }
                }
                """,
        ]),
        triggeringExamples: #examples([
            """
                if ↓x < 5 {
                    foo()
                } else if y == "s" {
                    bar()
                } else if ↓x < 5 {
                    baz()
                }
                """,
            """
                if z {
                    if ↓x < 5 {
                        foo()
                    } else if y == "s" {
                        bar()
                    } else if ↓x < 5 {
                        baz()
                    }
                }
                """,
            """
                if ↓x < 5, y == "s" {
                    foo()
                } else if x < 10 {
                    bar()
                } else if ↓y == "s", x < 5 {
                    baz()
                }
                """,
            """
                switch x {
                case ↓\"a\", \"b\":
                    foo()
                case \"c\", ↓\"a\":
                    bar()
                }
                """,
            """
                switch x {
                case ↓\"a\" where y == "s":
                    foo()
                case ↓\"a\" where y == "s":
                    bar()
                }
                """,
            """
                if ↓let xyz = maybeXyz {
                    foo()
                } else if ↓let xyz = maybeXyz {
                    bar()
                }
                """,
            """
                if ↓let x = maybeAbc, let z = x.maybeY {
                    foo()
                } else if ↓let x = maybeAbc, let z = x.maybeY {
                    bar()
                }
                """,
            """
                if ↓#available(macOS 10.15, *) {
                    foo()
                } else if ↓#available(macOS 10.15, *) {
                    bar()
                }
                """,
            """
                if ↓case .p = x {
                    foo()
                } else if ↓case .p = x {
                    bar()
                }
                """,
            """
                if ↓x < 5 {}
                else if ↓x < 5 {}
                else if ↓x < 5 {}
                """,
        ])
    )
}

private extension DuplicateConditionsRule {
    final class Visitor: ViolationsSyntaxVisitor<ConfigurationType> {
        override func visitPost(_ node: IfExprSyntax) {
            if  node.parent?.is(IfExprSyntax.self) == true {
                // We can skip these cases - they will be picked up when we visit the top level `if`
                return
            }

            var maybeCurr: IfExprSyntax? = node
            var statementChain: [IfExprSyntax] = []
            while let curr = maybeCurr {
                statementChain.append(curr)
                maybeCurr = curr.elseBody?.as(IfExprSyntax.self)
            }

            let positionsByConditions = statementChain
                .reduce(into: [Set<String>: [AbsolutePosition]]()) { acc, elt in
                    let conditions = elt.conditions.map(\.condition.trimmedDescription)
                    let location = elt.conditions.positionAfterSkippingLeadingTrivia
                    acc[Set(conditions), default: []].append(location)
                }

            addViolations(Array(positionsByConditions.values))
        }

        override func visitPost(_ node: SwitchCaseListSyntax) {
            let switchCases = node.compactMap { $0.as(SwitchCaseSyntax.self) }

            let positionsByCondition = switchCases
                .reduce(into: [String: [AbsolutePosition]]()) { acc, elt in
                    // Defaults don't have a condition to worry about
                    guard case let .case(caseLabel) = elt.label else { return }
                    for caseItem in caseLabel.caseItems {
                        let pattern = caseItem
                            .pattern
                            .trimmedDescription
                        let whereClause = caseItem
                            .whereClause?
                            .trimmedDescription
                            ?? ""
                        let location = caseItem.positionAfterSkippingLeadingTrivia
                        acc[pattern + whereClause, default: []].append(location)
                    }
                }

            addViolations(Array(positionsByCondition.values))
        }

        private func addViolations(_ positionsByCondition: [[AbsolutePosition]]) {
            let duplicatedPositions = positionsByCondition
                .filter { $0.count > 1 }
                .flatMap(\.self)

            violations.append(contentsOf: duplicatedPositions)
        }
    }
}
