import Foundation
import SourceKittenFramework
import SwiftLintCore
import SwiftSyntax

@SwiftSyntaxRule
struct TodoRule: Rule {
    var configuration = TodoConfiguration()

    static let description = RuleDescription(
        identifier: "todo",
        name: "Todo",
        description: "TODOs and FIXMEs should be resolved.",
        kind: .lint,
        nonTriggeringExamples: #examples([
            "// notaTODO:",
            "// notaFIXME:",
        ]),
        triggeringExamples: #examples([
            "// ↓TODO:",
            "// ↓FIXME:",
            "// ↓TODO(note)",
            "// ↓FIXME(note)",
            "/* ↓FIXME: */",
            "/* ↓TODO: */",
            "/** ↓FIXME: */",
            "/** ↓TODO: */",
        ]).skipWrappingInCommentTests()
    )
}

private extension TodoRule {
    final class Visitor: ViolationsSyntaxVisitor<ConfigurationType> {
        override func visitPost(_ node: TokenSyntax) {
            let leadingViolations = node.leadingTrivia.violations(offset: node.position,
                                                                  for: configuration.only)
            let trailingViolations = node.trailingTrivia.violations(offset: node.endPositionBeforeTrailingTrivia,
                                                                    for: configuration.only)
            violations.append(contentsOf: leadingViolations + trailingViolations)
        }
    }
}

private extension Trivia {
    func violations(offset: AbsolutePosition,
                    for todoKeywords: [TodoConfiguration.TodoKeyword]) -> [ReasonedRuleViolation] {
        var position = offset
        var violations = [ReasonedRuleViolation]()
        for piece in self {
            violations.append(contentsOf: piece.violations(offset: position, for: todoKeywords))
            position += piece.sourceLength
        }
        return violations
    }
}

private extension TriviaPiece {
    func violations(offset: AbsolutePosition,
                    for todoKeywords: [TodoConfiguration.TodoKeyword]) -> [ReasonedRuleViolation] {
        switch self {
        case
            .blockComment(let comment),
            .lineComment(let comment),
            .docBlockComment(let comment),
            .docLineComment(let comment):

            // Construct a regex string considering only keywords.
            let searchKeywords = todoKeywords.map(\.rawValue).joined(separator: "|")
            let matches = regex(#"\b((?:\#(searchKeywords))(?::|\b))"#)
                .matches(in: comment, range: comment.bridge().fullNSRange)
            return matches.reduce(into: []) { violations, match in
                guard let annotationRange = Range(match.range(at: 1), in: comment) else {
                    return
                }

                let maxLengthOfMessage = 30

                // customizing the reason message to be specific to fixme or todo
                let kind = comment[annotationRange].hasPrefix("FIXME") ? "FIXMEs" : "TODOs"
                let message = comment[annotationRange.upperBound...]
                    .trimmingCharacters(in: .whitespaces)
                    .truncated(maxLength: maxLengthOfMessage)
                    .prefix { $0 != "\n" }

                let reason: String
                if message.isEmpty {
                    reason = "\(kind) should be resolved"
                } else {
                    reason = "\(kind) should be resolved (\(message))"
                }

                let violation = ReasonedRuleViolation(
                    position: offset.advanced(by: comment[..<annotationRange.lowerBound].utf8.count),
                    reason: reason
                )
                violations.append(violation)
            }
        default:
            return []
        }
    }
}

private extension String {
    func truncated(maxLength: Int) -> String {
        if utf16.count > maxLength {
            let end = index(startIndex, offsetBy: maxLength, limitedBy: endIndex) ?? endIndex
            return self[..<end] + "..."
        }
        return self
    }
}
