import Foundation
import SourceKittenFramework

public func regex(_ pattern: String,
                  options: NSRegularExpression.Options? = nil) -> NSRegularExpression {
    // all patterns used for regular expressions in SwiftLint are string literals which have been
    // confirmed to work, so it's ok to force-try here.

    let options = options ?? [.anchorsMatchLines, .dotMatchesLineSeparators]
    // swiftlint:disable:next force_try
    return try! .cached(pattern: pattern, options: options)
}

extension SwiftLintFile {
    public func regions(restrictingRuleIdentifiers: Set<RuleIdentifier>? = nil) -> [Region] {
        var regions = [Region]()
        var disabledRules = Set<RuleIdentifier>()
        let commands: [Command]
        if let restrictingRuleIdentifiers {
            commands = self.commands().filter { command in
                command.ruleIdentifiers.contains(where: restrictingRuleIdentifiers.contains)
            }
        } else {
            commands = self.commands()
        }
        let commandPairs = zip(commands, Array(commands.dropFirst().map(Optional.init)) + [nil])
        for (command, nextCommand) in commandPairs {
            switch command.action {
            case .disable:
                disabledRules.formUnion(command.ruleIdentifiers)

            case .enable:
                disabledRules.subtract(command.ruleIdentifiers)

            case .invalid:
                break
            }

            let start = Location(file: path, line: command.line, character: command.range?.upperBound)
            let end = endOf(next: nextCommand)
            guard start < end else { continue }
            var didSetRegion = false
            for (index, region) in zip(regions.indices, regions) where region.start == start && region.end == end {
                regions[index] = Region(
                    start: start,
                    end: end,
                    disabledRuleIdentifiers: disabledRules.union(region.disabledRuleIdentifiers)
                )
                didSetRegion = true
            }
            if !didSetRegion {
                regions.append(
                    Region(start: start, end: end, disabledRuleIdentifiers: disabledRules)
                )
            }
        }
        return regions
    }

    public func commands(in range: NSRange? = nil) -> [Command] {
        guard let range else {
            return commands
                .flatMap { $0.expand() }
        }

        let rangeStart = Location(file: self, characterOffset: range.location)
        let rangeEnd = Location(file: self, characterOffset: NSMaxRange(range))
        return commands
            .filter { command in
                let commandLocation = Location(file: path, line: command.line, character: command.range?.upperBound)
                return rangeStart <= commandLocation && commandLocation <= rangeEnd
            }
            .flatMap { $0.expand() }
    }

    private func endOf(next command: Command?) -> Location {
        guard let nextCommand = command else {
            return Location(file: path, line: .max, character: .max)
        }
        let nextLine: Int
        let nextCharacter: Int?
        if let nextCommandCharacter = nextCommand.range?.upperBound {
            nextLine = nextCommand.line
            if nextCommandCharacter > 0 {
                nextCharacter = nextCommandCharacter - 1
            } else {
                nextCharacter = nil
            }
        } else {
            nextLine = max(nextCommand.line - 1, 0)
            nextCharacter = .max
        }
        return Location(file: path, line: nextLine, character: nextCharacter)
    }

    public func match(pattern: String, with syntaxKinds: [SyntaxKind], range: NSRange? = nil) -> [NSRange] {
        match(pattern: pattern, range: range)
            .filter { $0.1 == syntaxKinds }
            .map(\.0)
    }

    public func match(pattern: String, range: NSRange? = nil, captureGroup: Int = 0) -> [(NSRange, [SyntaxKind])] {
        let contents = stringView
        let range = range ?? contents.range
        let syntax = syntaxMap
        return regex(pattern).matches(in: contents, options: [], range: range).compactMap { match in
            let matchByteRange = contents.NSRangeToByteRange(
                start: match.range.location, length: match.range.length)
            return matchByteRange.map { (match.range(at: captureGroup), syntax.tokens(inByteRange: $0).kinds) }
        }
    }

    /**
     This function returns only matches that are not contained in a syntax kind
     specified.

     - parameter pattern: regex pattern to be matched inside file.
     - parameter excludingSyntaxKinds: syntax kinds the matches to be filtered
     when inside them.

     - returns: An array of [NSRange] objects consisting of regex matches inside
     file contents.
     */
    public func match(pattern: String,
                      excludingSyntaxKinds syntaxKinds: Set<SyntaxKind>,
                      range: NSRange? = nil,
                      captureGroup: Int = 0) -> [NSRange] {
        match(pattern: pattern, range: range, captureGroup: captureGroup)
            .filter { syntaxKinds.isDisjoint(with: $0.1) }
            .map(\.0)
    }

    public func append(_ string: String) {
        guard string.isNotEmpty else {
            return
        }
        defer { invalidateCache() }
        file.contents += string
        if isVirtual {
            return
        }
        guard let stringData = string.data(using: .utf8) else {
            queuedFatalError("can't encode '\(string)' with UTF8")
        }
        guard let path, let fileHandle = FileHandle(forWritingAtPath: path.filepath) else {
            queuedFatalError("can't write to path '\(String(describing: path))'")
        }
        _ = fileHandle.seekToEndOfFile()
        fileHandle.write(stringData)
        fileHandle.closeFile()
    }

    public func write<S: StringProtocol>(_ string: S) {
        guard string != contents else {
            return
        }
        defer { invalidateCache() }
        file.contents = String(string)
        if isVirtual {
            return
        }
        guard let path else {
            queuedFatalError("file needs a path to call write(_:)")
        }
        guard let stringData = String(string).data(using: .utf8) else {
            queuedFatalError("can't encode '\(string)' with UTF8")
        }
        do {
            try stringData.write(to: path, options: .atomic)
        } catch {
            queuedFatalError("can't write file to \(path)")
        }
    }

    public func ruleEnabled(violatingRanges: [NSRange], for rule: some Rule) -> [NSRange] {
        let fileRegions = regions()
        if fileRegions.isEmpty { return violatingRanges }
        return violatingRanges.filter { range in
            let region = fileRegions.first {
                $0.contains(Location(file: self, characterOffset: range.location))
            }
            return region?.isRuleEnabled(rule) ?? true
        }
    }

    public func ruleEnabled(violatingRange: NSRange, for rule: some Rule) -> NSRange? {
        ruleEnabled(violatingRanges: [violatingRange], for: rule).first
    }

    public func contents(for token: SwiftLintSyntaxToken) -> String? {
        stringView.substringWithByteRange(token.range)
    }
}
