import SourceKittenFramework
import SwiftLintCore
import SwiftSyntax

@SwiftSyntaxRule
struct FileLengthRule: Rule {
    var configuration = FileLengthConfiguration()

    static let description = RuleDescription(
        identifier: "file_length",
        name: "File Length",
        description: "Files should not span too many lines.",
        kind: .metrics,
        nonTriggeringExamples: #examples([
            repeatElement("print(\"swiftlint\")\n", count: 399).joined().asExample()
        ]),
        triggeringExamples: #examples([
            repeatElement("print(\"swiftlint\")\n", count: 401).joined().asExample(),
            (repeatElement("print(\"swiftlint\")\n", count: 400) + ["//\n"]).joined().asExample(),
            repeatElement("print(\"swiftlint\")\n\n", count: 201).joined().asExample(),
        ]).skipWrappingInCommentTests()
    )
}

private extension FileLengthRule {
    final class Visitor: ViolationsSyntaxVisitor<ConfigurationType> {
        override func visitPost(_ node: SourceFileSyntax) {
            let lineCount = configuration.ignoreCommentOnlyLines
                ? CommentLinesVisitor(locationConverter: locationConverter)
                    .walk(tree: node, handler: \.linesWithCode).count
                : file.lines.count

            let severity: ViolationSeverity, upperBound: Int
            if let error = configuration.severityConfiguration.error, lineCount > error {
                severity = .error
                upperBound = error
            } else if lineCount > configuration.severityConfiguration.warning {
                severity = .warning
                upperBound = configuration.severityConfiguration.warning
            } else {
                return
            }

            let reason = "File should contain \(upperBound) lines or less" +
                       (configuration.ignoreCommentOnlyLines ? " excluding comments and whitespaces" : "") +
                       ": currently contains \(lineCount)"

            // Position violation at the start of the last line to avoid boundary issues
            let lastLine = file.lines.last
            let lastLineStartOffset = lastLine?.byteRange.location ?? 0
            let violationPosition = AbsolutePosition(utf8Offset: lastLineStartOffset.value)

            let violation = ReasonedRuleViolation(
                position: violationPosition,
                reason: reason,
                severity: severity
            )
            violations.append(violation)
        }
    }
}
