import SwiftLintCore
import SwiftSyntax

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

    static let description = RuleDescription(
        identifier: "unavailable_condition",
        name: "Unavailable Condition",
        description: "Use #unavailable/#available instead of #available/#unavailable with an empty body.",
        kind: .idiomatic,
        minSwiftVersion: .fiveDotSix,
        nonTriggeringExamples: #examples([
            """
            if #unavailable(iOS 13) {
              loadMainWindow()
            }
            """,
            """
            if #available(iOS 9.0, *) {
              doSomething()
            } else {
              legacyDoSomething()
            }
            """,
            """
            if #available(macOS 11.0, *) {
               // Do nothing
            } else if #available(macOS 10.15, *) {
               print("do some stuff")
            }
            """,
            """
            if #available(macOS 11.0, *) {
               // Do nothing
            } else if i > 7 {
               print("do some stuff")
            } else if i < 2, #available(macOS 11.0, *) {
              print("something else")
            }
            """.asExample(excludeFromDocumentation: true),
        ]),
        triggeringExamples: #examples([
            """
            if ↓#available(iOS 14.0) {

            } else {
              oldIos13TrackingLogic(isEnabled: ASIdentifierManager.shared().isAdvertisingTrackingEnabled)
            }
            """,
            """
            if ↓#available(iOS 14.0) {
              // we don't need to do anything here
            } else {
              oldIos13TrackingLogic(isEnabled: ASIdentifierManager.shared().isAdvertisingTrackingEnabled)
            }
            """,
            """
            if ↓#available(iOS 13, *) {} else {
              loadMainWindow()
            }
            """,
            """
            if ↓#unavailable(iOS 13) {
              // Do nothing
            } else if i < 2 {
              loadMainWindow()
            }
            """,
        ])
    )
}

private extension UnavailableConditionRule {
    final class Visitor: ViolationsSyntaxVisitor<ConfigurationType> {
        override func visitPost(_ node: IfExprSyntax) {
            guard node.body.statements.isEmpty else {
                return
            }

            guard let condition = node.conditions.onlyElement,
                  let availability = asAvailabilityCondition(condition.condition) else {
                return
            }

            if otherAvailabilityCheckInvolved(ifStmt: node) {
                // If there are other conditional branches with availability checks it might not be possible
                // to just invert the first one.
                return
            }

            violations.append(
                ReasonedRuleViolation(
                    position: availability.positionAfterSkippingLeadingTrivia,
                    reason: reason(for: availability)
                )
            )
        }

        private func asAvailabilityCondition(_ condition: ConditionElementSyntax.Condition)
        -> AvailabilityConditionSyntax? {
            condition.as(AvailabilityConditionSyntax.self)
        }

        private func otherAvailabilityCheckInvolved(ifStmt: IfExprSyntax) -> Bool {
            if let elseBody = ifStmt.elseBody, let nestedIfStatement = elseBody.as(IfExprSyntax.self) {
                if nestedIfStatement.conditions.map(\.condition).compactMap(asAvailabilityCondition).isNotEmpty {
                    return true
                }
                return otherAvailabilityCheckInvolved(ifStmt: nestedIfStatement)
            }
            return false
        }

        private func reason(for condition: AvailabilityConditionSyntax) -> String {
            switch condition.availabilityKeyword.tokenKind {
            case .poundAvailable:
                return "Use #unavailable instead of #available with an empty body"
            case .poundUnavailable:
                return "Use #available instead of #unavailable with an empty body"
            default:
                queuedFatalError("Unknown availability check type.")
            }
        }
    }
}
