import Foundation
import SourceKittenFramework
import SwiftSyntax

struct ColonRule: SubstitutionCorrectableRule, SourceKitFreeRule {
    var configuration = ColonConfiguration()

    static let description = RuleDescription(
        identifier: "colon",
        name: "Colon Spacing",
        description: """
            Colons should be next to the identifier when specifying a type and next to the key in dictionary literals
            """,
        kind: .style,
        nonTriggeringExamples: ColonRuleExamples.nonTriggeringExamples,
        triggeringExamples: ColonRuleExamples.triggeringExamples,
        corrections: ColonRuleExamples.corrections
    )

    func validate(file: SwiftLintFile) -> [StyleViolation] {
        violationRanges(in: file).map { range in
            StyleViolation(ruleDescription: Self.description,
                           severity: configuration.severityConfiguration.severity,
                           location: Location(file: file, characterOffset: range.location))
        }
    }

    func violationRanges(in file: SwiftLintFile) -> [NSRange] {
        let syntaxTree = file.syntaxTree
        let visitor = ColonRuleVisitor(viewMode: .sourceAccurate)
        visitor.walk(syntaxTree)
        let positionsToSkip = visitor.positionsToSkip
        let dictionaryPositions = visitor.dictionaryPositions
        let caseStatementPositions = visitor.caseStatementPositions

        return syntaxTree
            .windowsOfThreeTokens()
            .compactMap { previous, current, next -> ByteRange? in
                if current.tokenKind != .colon ||
                    !configuration.applyToDictionaries && dictionaryPositions.contains(current.position) ||
                    positionsToSkip.contains(current.position) {
                    return nil
                }

                // [:]
                if previous.tokenKind == .leftSquare,
                   next.tokenKind == .rightSquare,
                   previous.trailingTrivia.isEmpty,
                   current.leadingTrivia.isEmpty,
                   current.trailingTrivia.isEmpty,
                   next.leadingTrivia.isEmpty {
                    return nil
                }

                if previous.trailingTrivia.isNotEmpty, !previous.trailingTrivia.containsBlockComments() {
                    let start = ByteCount(previous.endPositionBeforeTrailingTrivia)
                    let end = ByteCount(current.endPosition)
                    return ByteRange(location: start, length: end - start)
                }
                if current.trailingTrivia != [.spaces(1)], !next.leadingTrivia.containsNewlines() {
                    if case .spaces(1) = current.trailingTrivia.first {
                        return nil
                    }

                    let flexibleRightSpacing = configuration.flexibleRightSpacing ||
                        caseStatementPositions.contains(current.position)
                    if flexibleRightSpacing, current.trailingTrivia.isNotEmpty {
                        return nil
                    }

                    let length: ByteCount
                    if case let .spaces(spaces) = current.trailingTrivia.first {
                        length = ByteCount(spaces + 1)
                    } else {
                        length = 1
                    }

                    return ByteRange(location: ByteCount(current.position), length: length)
                }
                return nil
            }
            .compactMap { byteRange in
                file.stringView.byteRangeToNSRange(byteRange)
            }
    }

    func substitution(for violationRange: NSRange, in _: SwiftLintFile) -> (NSRange, String)? {
        (violationRange, ": ")
    }
}

private final class ColonRuleVisitor: SyntaxVisitor {
    var positionsToSkip: [AbsolutePosition] = []
    var dictionaryPositions: [AbsolutePosition] = []
    var caseStatementPositions: [AbsolutePosition] = []

    override func visitPost(_ node: TernaryExprSyntax) {
        positionsToSkip.append(node.colon.position)
    }

    override func visitPost(_ node: DeclNameArgumentsSyntax) {
        positionsToSkip.append(
            contentsOf: node.tokens(viewMode: .sourceAccurate)
                .filter { $0.tokenKind == .colon }
                .map(\.position)
        )
    }

    override func visitPost(_ node: ObjCSelectorPieceSyntax) {
        if let colon = node.colon {
            positionsToSkip.append(colon.position)
        }
    }

    override func visitPost(_ node: OperatorPrecedenceAndTypesSyntax) {
        positionsToSkip.append(node.colon.position)
    }

    override func visitPost(_ node: UnresolvedTernaryExprSyntax) {
        positionsToSkip.append(node.colon.position)
    }

    override func visitPost(_ node: DictionaryElementSyntax) {
        dictionaryPositions.append(node.colon.position)
    }

    override func visitPost(_ node: SwitchCaseLabelSyntax) {
        caseStatementPositions.append(node.colon.position)
    }

    override func visitPost(_ node: SwitchDefaultLabelSyntax) {
        caseStatementPositions.append(node.colon.position)
    }
}

private extension Trivia {
    func containsBlockComments() -> Bool {
        contains { piece in
            if case .blockComment = piece {
                return true
            }
            return false
        }
    }
}
