import Foundation
import SourceKittenFramework

public struct ForWhereRule: ASTRule, ConfigurationProviderRule {
    public var configuration = SeverityConfiguration(.warning)

    public init() {}

    public static let description = RuleDescription(
        identifier: "for_where",
        name: "For Where",
        description: "`where` clauses are preferred over a single `if` inside a `for`.",
        kind: .idiomatic,
        nonTriggeringExamples: [
            "for user in users where user.id == 1 { }\n",
            // if let
            "for user in users {\n" +
            "   if let id = user.id { }\n" +
            "}\n",
            // if var
            "for user in users {\n" +
            "   if var id = user.id { }\n" +
            "}\n",
            // if with else
            "for user in users {\n" +
            "   if user.id == 1 { } else { }\n" +
            "}\n",
            // if with else if
            "for user in users {\n" +
            "   if user.id == 1 {\n" +
            "} else if user.id == 2 { }\n" +
            "}\n",
            // if is not the only expression inside for
            "for user in users {\n" +
            "   if user.id == 1 { }\n" +
            "   print(user)\n" +
            "}\n",
            // if a variable is used
            "for user in users {\n" +
            "   let id = user.id\n" +
            "   if id == 1 { }\n" +
            "}\n",
            // if something is after if
            "for user in users {\n" +
            "   if user.id == 1 { }\n" +
            "   return true\n" +
            "}\n",
            // condition with multiple clauses
            "for user in users {\n" +
            "   if user.id == 1 && user.age > 18 { }\n" +
            "}\n",
            // if case
            "for (index, value) in array.enumerated() {\n" +
            "   if case .valueB(_) = value {\n" +
            "       return index\n" +
            "   }\n" +
            "}\n"
        ],
        triggeringExamples: [
            "for user in users {\n" +
            "   ↓if user.id == 1 { return true }\n" +
            "}\n"
        ]
    )

    private static let commentKinds = SyntaxKind.commentAndStringKinds

    public func validate(file: File, kind: StatementKind,
                         dictionary: [String: SourceKitRepresentable]) -> [StyleViolation] {

        guard kind == .forEach,
            let subDictionary = forBody(dictionary: dictionary),
            subDictionary.substructure.count == 1,
            let bodyDictionary = subDictionary.substructure.first,
            bodyDictionary.kind.flatMap(StatementKind.init) == .if,
            isOnlyOneIf(dictionary: bodyDictionary),
            isOnlyIfInsideFor(forDictionary: subDictionary, ifDictionary: bodyDictionary, file: file),
            !isComplexCondition(dictionary: bodyDictionary, file: file),
            let offset = bodyDictionary .offset else {
                return []
        }

        return [
            StyleViolation(ruleDescription: type(of: self).description,
                           severity: configuration.severity,
                           location: Location(file: file, byteOffset: offset))
        ]
    }

    private func forBody(dictionary: [String: SourceKitRepresentable]) -> [String: SourceKitRepresentable]? {
        return dictionary.substructure.first(where: { subDict -> Bool in
            subDict.kind.flatMap(StatementKind.init) == .brace
        })
    }

    private func isOnlyOneIf(dictionary: [String: SourceKitRepresentable]) -> Bool {
        let substructure = dictionary.substructure
        guard substructure.count == 1 else {
            return false
        }

        return dictionary.substructure.first?.kind.flatMap(StatementKind.init) == .brace
    }

    private func isOnlyIfInsideFor(forDictionary: [String: SourceKitRepresentable],
                                   ifDictionary: [String: SourceKitRepresentable],
                                   file: File) -> Bool {
        guard let offset = forDictionary.offset,
            let length = forDictionary.length,
            let ifOffset = ifDictionary.offset,
            let ifLength = ifDictionary.length else {
                return false
        }

        let beforeIfRange = NSRange(location: offset, length: ifOffset - offset)
        let ifFinalPosition = ifOffset + ifLength
        let afterIfRange = NSRange(location: ifFinalPosition, length: offset + length - ifFinalPosition)
        let allKinds = file.syntaxMap.kinds(inByteRange: beforeIfRange) +
            file.syntaxMap.kinds(inByteRange: afterIfRange)

        let doesntContainComments = !allKinds.contains { kind in
            !ForWhereRule.commentKinds.contains(kind)
        }

        return doesntContainComments
    }

    private func isComplexCondition(dictionary: [String: SourceKitRepresentable], file: File) -> Bool {
        let kind = "source.lang.swift.structure.elem.condition_expr"
        let contents = file.contents.bridge()
        return !dictionary.elements.filter { element in
            guard element.kind == kind,
                let offset = element.offset,
                let length = element.length,
                let range = contents.byteRangeToNSRange(start: offset, length: length) else {
                    return false
            }

            let containsKeyword = !file.match(pattern: "\\blet|var|case\\b", with: [.keyword], range: range).isEmpty
            if containsKeyword {
                return true
            }

            return !file.match(pattern: "\\|\\||&&", with: [], range: range).isEmpty
        }.isEmpty
    }

}
