import SwiftLintCore
import SwiftSyntax

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

    static let description = RuleDescription(
        identifier: "yoda_condition",
        name: "Yoda Condition",
        description: "The constant literal should be placed on the right-hand side of the comparison operator",
        kind: .lint,
        nonTriggeringExamples: #examples([
            "if foo == 42 {}",
            "if foo <= 42.42 {}",
            "guard foo >= 42 else { return }",
            "guard foo != \"str str\" else { return }",
            "while foo < 10 { }",
            "while foo > 1 { }",
            "while foo + 1 == 2 {}",
            "if optionalValue?.property ?? 0 == 2 {}",
            "if foo == nil {}",
            "if flags & 1 == 1 {}",
            "if true {}".asExample(excludeFromDocumentation: true),
            "if true == false || b, 2 != 3 {}".asExample(excludeFromDocumentation: true),
        ]),
        triggeringExamples: #examples([
            "if ↓42 == foo {}",
            "if ↓42.42 >= foo {}",
            "guard ↓42 <= foo else { return }",
            "guard ↓\"str str\" != foo else { return }",
            "while ↓10 > foo { }",
            "while ↓1 < foo { }",
            "if ↓nil == foo {}",
            "while ↓1 > i + 5 {}",
            "if ↓200 <= i && i <= 299 || ↓600 <= i {}",
        ]))
}

private extension YodaConditionRule {
    final class Visitor: ViolationsSyntaxVisitor<ConfigurationType> {
        override func visitPost(_ node: IfExprSyntax) {
            visit(conditions: node.conditions)
        }

        override func visitPost(_ node: GuardStmtSyntax) {
            visit(conditions: node.conditions)
        }

        override func visitPost(_ node: RepeatStmtSyntax) {
            visit(condition: node.condition)
        }

        override func visitPost(_ node: WhileStmtSyntax) {
            visit(conditions: node.conditions)
        }

        private func visit(conditions: ConditionElementListSyntax) {
            for condition in conditions.compactMap({ $0.condition.as(ExprSyntax.self) }) {
                visit(condition: condition)
            }
        }

        private func visit(condition: ExprSyntax) {
            guard let elements = condition.as(SequenceExprSyntax.self)?.elements else {
                return
            }
            let children = elements.children(viewMode: .sourceAccurate)
            let comparisonOperators = children
                .compactMap { $0.as(BinaryOperatorExprSyntax.self) }
                .filter { ["==", "!=", ">", "<", ">=", "<="].contains($0.operator.text) }
            for comparisonOperator in comparisonOperators {
                guard let operatorIndex = children.index(of: comparisonOperator) else {
                    continue
                }
                let rhsIdx = children.index(after: operatorIndex)
                if children[rhsIdx].isLiteral {
                    guard children.endIndex != children.index(after: rhsIdx) else {
                        // This is already the end of the expression.
                        continue
                    }
                    let afterRhsIndex = children.index(after: rhsIdx)
                    if children[afterRhsIndex].isLogicalBinaryOperator {
                        // Next token is an operator with weaker binding. Thus, the literal is unique on the
                        // right-hand side of the comparison operator.
                        continue
                    }
                }
                let lhsIdx = children.index(before: operatorIndex)
                let lhs = children[lhsIdx]
                if lhs.isLiteral,
                   children.startIndex == lhsIdx || children[children.index(before: lhsIdx)].isLogicalBinaryOperator {
                    // Literal is at the very beginning of the expression or the previous token is an operator with
                    // weaker binding. Thus, the literal is unique on the left-hand side of the comparison operator.
                    violations.append(lhs.positionAfterSkippingLeadingTrivia)
                }
            }
        }
    }
}

private extension Syntax {
    var isLiteral: Bool {
           `is`(IntegerLiteralExprSyntax.self)
        || `is`(FloatLiteralExprSyntax.self)
        || `is`(BooleanLiteralExprSyntax.self)
        || `is`(StringLiteralExprSyntax.self)
        || `is`(NilLiteralExprSyntax.self)
    }

    var isLogicalBinaryOperator: Bool {
        guard let binaryOperator = `as`(BinaryOperatorExprSyntax.self) else {
            return false
        }
        return ["&&", "||"].contains(binaryOperator.operator.text)
    }
}
