import SwiftSyntax

@SwiftSyntaxRule(optIn: true)
struct AttributesRule: Rule {
    var configuration = AttributesConfiguration()

    static let description = RuleDescription(
        identifier: "attributes",
        name: "Attributes",
        description: """
            Attributes should be on their own lines in functions and types, but on the same line as variables and \
            imports
            """,
        rationale: """
        Erica Sadun says:

        > My take on things after the poll and after talking directly with a number of \
        developers is this: Placing attributes like `@objc`, `@testable`, `@available`, `@discardableResult` on \
        their own lines before a member declaration has become a conventional Swift style.

        > This approach limits declaration length. It allows a member to float below its attribute and supports \
        flush-left access modifiers, so `internal`, `public`, etc appear in the leftmost column. Many developers \
        mix-and-match styles for short Swift attributes like `@objc`

        See https://ericasadun.com/2016/10/02/quick-style-survey/ for discussion.

        SwiftLint's rule requires attributes to be on their own lines for functions and types, but on the same line \
        for variables and imports.
        """,
        kind: .style,
        nonTriggeringExamples: AttributesRuleExamples.nonTriggeringExamples,
        triggeringExamples: AttributesRuleExamples.triggeringExamples
    )
}

private extension AttributesRule {
    final class Visitor: ViolationsSyntaxVisitor<ConfigurationType> {
        override func visitPost(_ node: AttributeListSyntax) {
            guard let helper = node.makeHelper(locationConverter: locationConverter) else {
                return
            }

            let attributesAndPlacements = node.attributesAndPlacements(
                configuration: configuration,
                shouldBeOnSameLine: helper.shouldBeOnSameLine
            )

            let hasViolation = helper.hasViolation(
                locationConverter: locationConverter,
                attributesAndPlacements: attributesAndPlacements,
                attributesWithArgumentsAlwaysOnNewLine: configuration.attributesWithArgumentsAlwaysOnNewLine
            )

            switch hasViolation {
            case .argumentsAlwaysOnNewLineViolation:
                let reason = """
                    Attributes with arguments or inside always_on_line_above must be on a new line \
                    instead of the same line
                    """

                violations.append(
                    ReasonedRuleViolation(
                        position: helper.violationPosition,
                        reason: reason,
                        severity: configuration.severityConfiguration.severity
                    )
                )
                return
            case .violation:
                violations.append(helper.violationPosition)
                return
            case .noViolation:
                break
            }

            let linesForAttributes = attributesAndPlacements
                .filter { $1 == .dedicatedLine }
                .map { $0.0.endLine(locationConverter: locationConverter) }

            if linesForAttributes.isEmpty {
                return
            }
            if !linesForAttributes.contains(helper.keywordLine - 1) {
                violations.append(helper.violationPosition)
                return
            }

            let hasMultipleNewlines = node.children(viewMode: .sourceAccurate).enumerated().contains { index, element in
                if index > 0, element.leadingTrivia.hasMultipleNewlines == true {
                    return true
                }
                return element.trailingTrivia.hasMultipleNewlines == true
            }

            if hasMultipleNewlines {
                violations.append(helper.violationPosition)
                return
            }
        }
    }
}

private extension SyntaxProtocol {
    func startLine(locationConverter: SourceLocationConverter) -> Int? {
        locationConverter.location(for: positionAfterSkippingLeadingTrivia).line
    }

    func endLine(locationConverter: SourceLocationConverter) -> Int? {
        locationConverter.location(for: endPositionBeforeTrailingTrivia).line
    }
}

private extension Trivia {
    var hasMultipleNewlines: Bool {
        reduce(0, { $0 + $1.numberOfNewlines }) > 1
    }
}

private extension TriviaPiece {
    var numberOfNewlines: Int {
        if case .newlines(let numberOfNewlines) = self {
            return numberOfNewlines
        }
        return 0
    }
}

private enum AttributePlacement {
    case sameLineAsDeclaration
    case dedicatedLine
}

private enum Violation {
    case argumentsAlwaysOnNewLineViolation
    case noViolation
    case violation
}

private struct RuleHelper {
    let violationPosition: AbsolutePosition
    let keywordLine: Int
    let shouldBeOnSameLine: Bool

    func hasViolation(
        locationConverter: SourceLocationConverter,
        attributesAndPlacements: [(AttributeSyntax, AttributePlacement)],
        attributesWithArgumentsAlwaysOnNewLine: Bool
    ) -> (Violation) {
        var linesWithAttributes: Set<Int> = [keywordLine]
        for (attribute, placement) in attributesAndPlacements {
            guard let attributeStartLine = attribute.startLine(locationConverter: locationConverter) else {
                continue
            }

            switch placement {
            case .sameLineAsDeclaration:
                if attributeStartLine != keywordLine {
                    return .violation
                }
            case .dedicatedLine:
                let hasViolation = attributeStartLine == keywordLine ||
                    linesWithAttributes.contains(attributeStartLine)
                linesWithAttributes.insert(attributeStartLine)
                if hasViolation {
                    if attributesWithArgumentsAlwaysOnNewLine, shouldBeOnSameLine {
                        return .argumentsAlwaysOnNewLineViolation
                    }
                    return .violation
                }
            }
        }
        return .noViolation
    }
}

private extension AttributeListSyntax {
    func attributesAndPlacements(configuration: AttributesConfiguration, shouldBeOnSameLine: Bool)
        -> [(AttributeSyntax, AttributePlacement)] {
        children(viewMode: .sourceAccurate)
            .compactMap { $0.as(AttributeSyntax.self) }
            .map { attribute in
                let atPrefixedName = "@\(attribute.attributeNameText)"
                if configuration.alwaysOnSameLine.contains(atPrefixedName) {
                    return (attribute, .sameLineAsDeclaration)
                }
                if configuration.alwaysOnNewLine.contains(atPrefixedName) {
                    return (attribute, .dedicatedLine)
                }
                if attribute.arguments != nil, configuration.attributesWithArgumentsAlwaysOnNewLine {
                    return (attribute, .dedicatedLine)
                }

                return shouldBeOnSameLine ? (attribute, .sameLineAsDeclaration) : (attribute, .dedicatedLine)
            }
    }

    // swiftlint:disable:next cyclomatic_complexity
    func makeHelper(locationConverter: SourceLocationConverter) -> RuleHelper? {
        guard let parent else {
            return nil
        }

        let keyword: any SyntaxProtocol
        let shouldBeOnSameLine: Bool
        if let funcKeyword = parent.as(FunctionDeclSyntax.self)?.funcKeyword {
            keyword = funcKeyword
            shouldBeOnSameLine = false
        } else if let initKeyword = parent.as(InitializerDeclSyntax.self)?.initKeyword {
            keyword = initKeyword
            shouldBeOnSameLine = false
        } else if let enumKeyword = parent.as(EnumDeclSyntax.self)?.enumKeyword {
            keyword = enumKeyword
            shouldBeOnSameLine = false
        } else if let structKeyword = parent.as(StructDeclSyntax.self)?.structKeyword {
            keyword = structKeyword
            shouldBeOnSameLine = false
        } else if let classKeyword = parent.as(ClassDeclSyntax.self)?.classKeyword {
            keyword = classKeyword
            shouldBeOnSameLine = false
        } else if let extensionKeyword = parent.as(ExtensionDeclSyntax.self)?.extensionKeyword {
            keyword = extensionKeyword
            shouldBeOnSameLine = false
        } else if let protocolKeyword = parent.as(ProtocolDeclSyntax.self)?.protocolKeyword {
            keyword = protocolKeyword
            shouldBeOnSameLine = false
        } else if let importTok = parent.as(ImportDeclSyntax.self)?.importKeyword {
            keyword = importTok
            shouldBeOnSameLine = true
        } else if let letOrVarKeyword = parent.as(VariableDeclSyntax.self)?.bindingSpecifier {
            keyword = letOrVarKeyword
            shouldBeOnSameLine = true
        } else {
            return nil
        }

        guard let keywordLine = keyword.startLine(locationConverter: locationConverter) else {
            return nil
        }

        return RuleHelper(
            violationPosition: keyword.positionAfterSkippingLeadingTrivia,
            keywordLine: keywordLine,
            shouldBeOnSameLine: shouldBeOnSameLine
        )
    }
}
