import SwiftSyntax

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

    static let description = RuleDescription(
        identifier: "vertical_parameter_alignment",
        name: "Vertical Parameter Alignment",
        description: "Function parameters should be aligned vertically if they're in multiple lines in a declaration",
        kind: .style,
        nonTriggeringExamples: VerticalParameterAlignmentRuleExamples.nonTriggeringExamples,
        triggeringExamples: VerticalParameterAlignmentRuleExamples.triggeringExamples
    )
}

private extension VerticalParameterAlignmentRule {
    final class Visitor: ViolationsSyntaxVisitor<ConfigurationType> {
        override func visitPost(_ node: FunctionDeclSyntax) {
            violations.append(contentsOf: violations(for: node.signature.parameterClause.parameters))
        }

        override func visitPost(_ node: InitializerDeclSyntax) {
            violations.append(contentsOf: violations(for: node.signature.parameterClause.parameters))
        }

        private func violations(for params: FunctionParameterListSyntax) -> [AbsolutePosition] {
            guard params.count > 1 else {
                return []
            }

            let paramLocations = params.compactMap { param -> (position: AbsolutePosition, line: Int, column: Int)? in
                let position = param.positionAfterSkippingLeadingTrivia
                let location = locationConverter.location(for: position)
                return (position, location.line, graphemeColumn(line: location.line, column: location.column))
            }

            guard let firstParamLoc = paramLocations.first else { return [] }

            var violations: [AbsolutePosition] = []
            for (index, paramLoc) in paramLocations.enumerated() where index > 0 && paramLoc.line > firstParamLoc.line {
                let previousParamLoc = paramLocations[index - 1]
                if previousParamLoc.line < paramLoc.line, firstParamLoc.column != paramLoc.column {
                    violations.append(paramLoc.position)
                }
            }

            return violations
        }

        /// Converts a UTF-8 byte column into a column counted in grapheme clusters, so that parameters preceded
        /// by multi-byte characters (e.g. a non-ASCII function name) are compared by their visible alignment
        /// rather than by their byte offset.
        private func graphemeColumn(line: Int, column: Int) -> Int {
            guard let graphemeClusters = String(locationConverter.sourceLines[line - 1].utf8.prefix(column - 1)) else {
                return column
            }
            return graphemeClusters.count + 1
        }
    }
}
