/* InterfaceObjectFile.swift
   GraphBuilder
 
   Created by Scott Yelvington on 9/21/19.
   Copyright (c) 2019 Scott Yelvington
 
   This work is licensed under the terms of the MIT license.
   For a copy, see <https://opensource.org/licenses/MIT>. */

import Foundation

public final class InterfaceObjectFile: ReturnObjectFile {

    public var children: [BaseTypes: [GraphField]]

    public init(location: FileLocation, fields: [GraphField], type: BaseTypes, parentType: BaseTypes?, parentObject: GraphPolymorphicTypeProtocol?, children: [BaseTypes: [GraphField]]) {
        self.children = children
        super.init(location: location,
                   fields: fields,
                   type: type,
                   parentType: parentType,
                   parentObject: parentObject)
    }

    public override func generateOutputString(using encoding: String.Encoding, customImportHeader: String?) -> String? {
        return formatOutputString(filename: location.file.rawValue, customImportHeader: customImportHeader)
    }
    
    private func formatOutputString(filename: String, customImportHeader: String?) -> String {
        
        // Some formatting constants
        let indent1 = "    "
        let indent2 = indent1 + indent1
        let closeBracket0 = "}"
        let closeBracket1 = "\(indent1)}"
        let closeBracket2 = "\(indent2)}"
        let selfSwitch = "\(indent2)switch self {"
        let enumName = type.gqlDataTypeCodingKeys
        let parentName = parent?.gqlBaseDataType ?? "GQLReturnModel"
        
        // the interface declaration
        let disclaimer: String = .disclaimer(for: filename, customImportHeader: customImportHeader)
        let header = "public class \(filename)<FieldKey: GQLField>: \(parentName)<FieldKey> {"
        let headerStrings = [disclaimer, header, indent1]
        
        // build variants
        let openVariants = "\(indent1)override class func buildVariants(for fields: [\(self.type.gqlBaseDataType)<FieldKey>.Field]) -> [String: [GraphQLSelection]]? {"
        let variantsInline = """
\(indent2)guard let fields = fields as? [\((self.type.gqlBaseDataType))<\((self.type.gqlDataTypeCodingKeys))>.Field] else { return nil }
\(indent2)var variants = [String: [GraphQLSelection]]()
\(indent2)variants.reserveCapacity(fields.count)
\(indent2)return fields.reduce(into: variants) { (dict, field) in
"""
        let variantCases = buildVariantCaseStrings()
        let variantStrings = [openVariants, variantsInline] + variantCases + [closeBracket2, closeBracket1, indent1]
        
        // properties
        let propertyStrings = buildClassPropertiesStrings() + [indent1]
        
        // the initialization function
        let openInit = "\(indent1)public required init(snapshot: Snapshot) {"
        let initCases = buildInitVariables()
        let superInit = "\(indent2)super.init(snapshot: snapshot)"
        let initStrings = [openInit, indent2] + initCases + [indent2, superInit, closeBracket1]
        
        // build interface enum
        let openInterface = "public enum \(type.gqlAbstractType) {"
        let interfaceCaseStrings = [openInterface] + buildInterfaceCases() + [indent1]
        
        // build the init for the interface
        let openInterfaceInit = "\(indent1)public init(snapshot: Snapshot) {"
        let interfaceInitStrings = [openInterfaceInit] + buildInterfaceInit() + [closeBracket1]
        
        // build the enum keys
        let enumHeader = "public enum \(enumName): CaseIterable, GQLField {"
        let enumCases = buildEnumCaseStrings()
        let enumStrings = [enumHeader] + enumCases + [indent1]
        
        // build raw value property
        let openRaw = "\(indent1)public var rawValue: String {"
        let rawCases = buildRawCases()
        let rawStrings = [openRaw, selfSwitch] + rawCases + [closeBracket2, closeBracket1, indent1]
        
        // build the generate field string function
        let openField = "\(indent1)public func fieldString(inset: Int) -> String {"
        let fieldCases = buildFieldStringGeneratorSwitch()
        let fieldStrings = [openField] + fieldCases + [closeBracket1, indent1]
        
        // build the selections property
        let openSelection = "\(indent1)public var selection: GraphQLField {"
        let selectionCases = buildSelectionsSwitch()
        let selectionStrings = [openSelection] + selectionCases + [closeBracket1, indent1]
        
        // build the dataType property
        let openDataType = "\(indent1)public var dataType: JSONDecodable.Type {"
        let dataTypeCases = buildDataTypeSwitch()
        let dataTypeStrings = [openDataType] + dataTypeCases + [closeBracket1, indent1]
        
        // build the all cases property
        let openAllCases = "\(indent1)public static var allCases: [\(enumName)] {"
        let allCases = buildAllCasesProperty()
        let allCasesStrings = [openAllCases, allCases, closeBracket1, indent1]
        
        // build the variants property
        let openVariantsProperty = "\(indent1)public var isVariant: Bool {"
        let variantsPropertyStrings = [openVariantsProperty] + buildIsVariantProperty() + [closeBracket1]
        
        
        // concatinate the strings
        var allLines = [String]()
        allLines.append(contentsOf: headerStrings)
        allLines.append(contentsOf: variantStrings)
        allLines.append(contentsOf: propertyStrings)
        allLines.append(contentsOf: initStrings)
        allLines.append(closeBracket0)
        allLines.append("")
        allLines.append(contentsOf: interfaceCaseStrings)
        allLines.append(contentsOf: interfaceInitStrings)
        allLines.append(closeBracket0)
        allLines.append("")
        allLines.append(contentsOf: enumStrings)
        allLines.append(contentsOf: rawStrings)
        allLines.append(contentsOf: fieldStrings)
        allLines.append(contentsOf: selectionStrings)
        allLines.append(contentsOf: dataTypeStrings)
        allLines.append(contentsOf: allCasesStrings)
        allLines.append(contentsOf: variantsPropertyStrings)
        allLines.append(closeBracket0)
        
        return allLines.joined(separator: "\n")
    }
    
    private func buildVariantCaseStrings() -> [String] {
        var caseStrings = [String]()
        caseStrings.reserveCapacity(children.count)
        let indent1 = "    "
        let indent2 = indent1 + indent1
        let indent3 = indent2 + indent1
        let indent4 = indent3 + indent1
        
        caseStrings.append("\(indent3)switch field {")
        
        for (child, _) in children {
            let field = "\(indent3)case .on\(child.rawValue):"
            
            // build the data type for this field...
            let dataType: String = "\(indent4)dict[field.rawValue] = \(child.gqlDataTypeCodingKeys).allCases.map({ $0.selection })"
            caseStrings.append(field)
            caseStrings.append(dataType)
        }
        
        caseStrings.append("\(indent3)default: break")
        caseStrings.append("\(indent3)}")
        return caseStrings
    }
    
    private func buildClassPropertiesStrings() -> [String] {
        var caseStrings = [String]()
        caseStrings.reserveCapacity(fields.count)
        let indent1 = "    "
        
        let excludedFields: Set<String>
        if let fields = self.parentObject?.fields.map({ $0.fieldName }) {
            excludedFields = Set<String>(fields)
        } else {
            excludedFields = []
        }
        
        for case .selection(let name, let returnType, _, let isArray) in fields {
            guard !excludedFields.contains(name) else { continue }
            var field = "\(indent1)public final var \(name): "
            
            // build the data type for this field...
            var dataType: String = returnType.gqlAbstractType
            dataType = isArray ? "[\(dataType)]" : dataType
            field += dataType + "?"
            caseStrings.append(field)
        }
        
        for case .operation(let name, _, let returnType, _, let isArray) in fields {
            guard !excludedFields.contains(name) else { continue }
            var field = "\(indent1)public final var \(name): "
            
            // build the data type for this field...
            var dataType: String = returnType.gqlAbstractType
            dataType = isArray ? "[\(dataType)]" : dataType
            field += dataType + "?"
            caseStrings.append(field)
        }
        
        return caseStrings
    }
    
    private func buildInitVariables() -> [String] {
        var caseStrings = [String]()
        caseStrings.reserveCapacity(fields.count)
        let indent1 = "    "
        let indent2 = indent1 + indent1
        let indent3 = indent2 + indent1
        
        var excludedFields: Set<String>
        if let fields = self.parentObject?.fields.map({ $0.fieldName }) {
            excludedFields = Set<String>(fields)
        } else {
            excludedFields = []
        }
        
        for case .selection(let name, let returnType, _, let isArray) in fields {
            guard !excludedFields.contains(name) else { continue }
            var dataType: String
            switch returnType {
            case .scalar, .enum:
                dataType = returnType.gqlAbstractType
                dataType = isArray ? "[\(dataType)?]" : dataType
                dataType = isArray ? "(snapshot[\"\(name)\"] as? \(dataType))?.compactMap({ $0 })" : "snapshot[\"\(name)\"] as? \(dataType)"
                let field = "\(indent2)\(name) = \(dataType)"
                caseStrings.append(field)
            case .object, .interface:
                caseStrings.append("\(indent2)if let snapshot = snapshot[\"\(name)\"] as? \(isArray ? "[Snapshot?]" : "Snapshot") {")
                if isArray {
                    caseStrings.append("\(indent3)\(name) = snapshot.compactMap({ $0 }).map({ \(returnType.gqlAbstractType).init(snapshot: $0) })")
                } else {
                    caseStrings.append("\(indent3)\(name) = \(returnType.gqlAbstractType).init(snapshot: snapshot)")
                }
                caseStrings.append("\(indent2)}")
            case .input, .schema:
                break
            }
        }
        
        for case .operation(let name, _, let returnType, _, let isArray) in fields {
            guard !excludedFields.contains(name) else { continue }
            var dataType: String
            switch returnType {
            case .scalar, .enum:
                dataType = returnType.gqlAbstractType
                dataType = isArray ? "[\(dataType)]" : dataType
                let field = "\(indent2)\(name) = snapshot[\"\(name)\"] as? \(dataType)"
                caseStrings.append(field)
            case .object, .interface:
                caseStrings.append("\(indent2)if let snapshot = snapshot[\"\(name)\"] as? \(isArray ? "[Snapshot?]" : "Snapshot") {")
                if isArray {
                    caseStrings.append("\(indent3)\(name) = snapshot.compactMap({ $0 }).map({ \(returnType.gqlAbstractType).init(snapshot: $0) })")
                } else {
                    caseStrings.append("\(indent3)\(name) = \(returnType.gqlAbstractType).init(snapshot: snapshot)")
                }
                caseStrings.append("\(indent2)}")
            case .input, .schema:
                break
            }
        }
        
        return caseStrings
    }
    
    //======
    // MARK: - Coding Key Enum functions
    //======
    
    private func buildEnumCaseStrings() -> [String] {
        var caseStrings = [String]()
        caseStrings.reserveCapacity((fields.count + children.count) * 2)
        let indent1 = "    "
        
        for case .selection(let name, let returnType, _, _) in fields {
            var field = "\(indent1)case `\(name)`"
            switch returnType {
            case .scalar, .schema, .input, .enum:
                break
            case .object, .interface:
                // build the data type for this field...
                let dataType: String = "([\(returnType.gqlDataTypeCodingKeys)])"
                field += dataType
            }
            caseStrings.append(field)
        }
        
        for case .operation(let name, let arguements, let returnType, _, _) in fields {
            var field = "\(indent1)case `\(name)`"
            var argStrings = [String]()
            for case .selection(let name, _, _, _) in arguements {
                argStrings.append(name + ": GQLArguementValue")
            }
            switch returnType {
            case .scalar, .schema, .input, .enum:
                break
            case .object, .interface:
                // build the data type for this field...
                argStrings.append("GQLReturnModel: [\(returnType.gqlDataTypeCodingKeys)]")
            }
            field += "(\(argStrings.joined(separator: ", ")))"
            caseStrings.append(field)
        }
        
        for (child, _) in children {
            let field = "\(indent1)case `on\(child.rawValue)`([\(child.gqlDataTypeCodingKeys)])"
            caseStrings.append(field)
        }
        
        return caseStrings
    }
    
    private func buildRawCases() -> [String] {
        var caseStrings = [String]()
        caseStrings.reserveCapacity(fields.count)
        let indent1 = "    "
        let indent2 = indent1 + indent1
        let indent3 = indent2 + indent1
        
        for case .selection(let name, _, _, _) in fields {
            let field = "\(indent2)case .\(name):"
            
            let returnValue = "\"\(name)\""
            let returnStatement = "\(indent3)return \(returnValue)"
            
            caseStrings.append(field)
            caseStrings.append(returnStatement)
        }
        
        for case .operation(let name, _, _, _, _) in fields {
            let field = "\(indent2)case .\(name):"
            
            let returnValue = "\"\(name)\""
            let returnStatement = "\(indent3)return \(returnValue)"
            
            caseStrings.append(field)
            caseStrings.append(returnStatement)
        }
        
        for (child, _) in children {
            let field = "\(indent2)case .on\(child.rawValue):"
            let dataType = "\(indent3)return \"\(child.rawValue)\""
            caseStrings.append(field)
            caseStrings.append(dataType)
        }
        
        return caseStrings
    }
    
    private func buildFieldStringGeneratorSwitch() -> [String] {
        var caseStrings = [String]()
        caseStrings.reserveCapacity((fields.count * 2) + 2)
        
        let indent1 = "    "
        let indent2 = indent1 + indent1
        let indent3 = indent2 + indent1
        
        let selfSwitch = "\(indent2)switch self {"
        caseStrings.append(selfSwitch)
        
        var shouldIncludeDefaultCase = false
        for case .selection(let name, let returnType, _, _) in fields {
            switch returnType {
            case .scalar, .input, .enum, .schema:
                shouldIncludeDefaultCase = true
                continue
            case .object, .interface:
                break
            }
            let field = "\(indent2)case .\(name)(let fields):"
            var dataType = returnType.gqlBaseDataType
            dataType = "\(indent3)return rawValue + \" \" + \(dataType).generateReturnFieldString(fields, inset: inset + 1)"
            caseStrings.append(field)
            caseStrings.append(dataType)
        }
        
        for case .operation(let name, let arguements, let returnType, _, _) in fields {
            var field = "\(indent2)case .\(name)"
            let innerFields = arguements.map({ "let \($0.fieldName)" }) + ["let fields"]
            field = "\(field)(\(innerFields.joined(separator: ", "))):"
            
            let dataType = returnType.gqlBaseDataType
            let arguementStrings = arguements.map({ "\($0.fieldName): \\(\($0.fieldName).parameter)" })
            var returnString = "\(indent3)return rawValue + \"(\(arguementStrings.joined(separator: ", ")))"
            
            switch returnType {
            case .scalar, .input, .enum, .schema:
                returnString += "\""
            case .object, .interface:
                returnString = "\(returnString) \" + \(dataType).generateReturnFieldString(fields, inset: inset + 1)"
            }
            caseStrings.append(field)
            caseStrings.append(returnString)
        }
        
        for (child, _) in children {
            let field = "\(indent2)case .on\(child.rawValue)(let fields):"
            let dataType = "\(indent3)return \"... on \" + rawValue + \" \" + \(child.gqlBaseDataType).generateReturnFieldString(fields, inset: inset + 1)"
            caseStrings.append(field)
            caseStrings.append(dataType)
        }
        
        if shouldIncludeDefaultCase {
            caseStrings.append("\(indent2)default: return rawValue")
        }
        caseStrings.append("\(indent2)}")
        
        return caseStrings
        
    }
    
    private func buildSelectionsSwitch() -> [String] {
        var caseStrings = [String]()
        caseStrings.reserveCapacity((fields.count * 2) + 2)
        
        // test the fields in this function to see if we even
        // need to build a full switch or just return the raw value
        var shouldBuildLongVersion = false
        for case .operation in fields {
            shouldBuildLongVersion = true
        }
        fields.map({ $0.returnType }).forEach { (returnType) in
            switch returnType {
            case .scalar, .input, .enum, .schema:
                break
            case .object, .interface:
                shouldBuildLongVersion = true
            }
        }
        
        let isArrayResults = Set<Bool>(fields.map({ $0.isArray }))
        shouldBuildLongVersion = isArrayResults.count > 1 ? true : shouldBuildLongVersion
        
        let indent1 = "    "
        let indent2 = indent1 + indent1
        let indent3 = indent2 + indent1
        
        // if this field needs a full switch, move forward and build it, otherwise stop here
        guard shouldBuildLongVersion else {
            let isArray = isArrayResults.first ?? false
            let scalar = ".scalar(dataType)"
            let arrayScalar = ".list(.scalar(dataType))"
            caseStrings.append("\(indent2)return GraphQLField(rawValue, type: \(isArray ? arrayScalar : scalar))")
            return caseStrings
        }
        
        let selfSwitch = "\(indent2)switch self {"
        caseStrings.append(selfSwitch)
        
        for case .selection(let name, let returnType, _, let isArray) in fields {
            switch returnType {
            case .scalar, .input, .enum, .schema:
                continue
            case .object, .interface:
                break
            }
            let field = "\(indent2)case .\(name)(let fields):"
            var dataType = returnType.gqlBaseDataType
            dataType = isArray ? ".list(.object(\(dataType).selections(for: fields)))" : ".object(\(dataType).selections(for: fields))"
            dataType = "\(indent3)return GraphQLField(rawValue, type: \(dataType))"
            caseStrings.append(field)
            caseStrings.append(dataType)
        }
        
        for case .operation(let name, let arguements, let returnType, _, let isArray) in fields {
            switch returnType {
            case .scalar, .input, .enum, .schema:
                continue
            case .object, .interface:
                break
            }
            
            var field = "\(indent2)case .\(name)"
            
            let innerFields = arguements.map({ (_) -> String in "_" }) + ["let fields"]
            field = "\(field)(\(innerFields.joined(separator: ", "))):"
            
            var dataType = returnType.gqlBaseDataType
            switch returnType {
            case .scalar, .input, .enum, .schema:
                break
            case .object, .interface:
                dataType = isArray ? ".list(.object(\(dataType).selections(for: fields)))" : ".object(\(dataType).selections(for: fields))"
            }
            
            let returnString = """
            \(indent3)return GraphQLField(rawValue,
            arguments: self.arguements,
            type: \(dataType))
            """
            
            caseStrings.append(field)
            caseStrings.append(returnString)
        }
        
        let remainingFields = fields.filter({
            switch $0.returnType {
            case .scalar, .input, .enum, .schema:
                return true
            case .object, .interface:
                return false
            }
        })
        
        if remainingFields.count > 0 {
            let resultIsArray =  Set<Bool>(remainingFields.map({ $0.isArray }))
            
            if resultIsArray.count < 2,
                let isArray = resultIsArray.first {
                let dataType = isArray ? ".list(.scalar(dataType)))" : ".scalar(dataType))"
                caseStrings.append("\(indent2)default: return GraphQLField(rawValue, type: \(dataType)")
            } else {
                let arrayFields = remainingFields.filter({ $0.isArray }).map({ ".\($0.fieldName)" })
                let scalarFields = remainingFields.filter({ !$0.isArray }).map({ ".\($0.fieldName)" })
                caseStrings.append("\(indent2)case \(arrayFields.joined(separator: ", ")):")
                caseStrings.append("\(indent3)return GraphQLField(rawValue, type: .list(.scalar(dataType)))")
                caseStrings.append("\(indent2)case \(scalarFields.joined(separator: ", ")):")
                caseStrings.append("\(indent3)return GraphQLField(rawValue, type: .scalar(dataType))")
            }
        }
        caseStrings.append("\(indent2)}")
        
        return caseStrings
        
    }
    
    private func buildDataTypeSwitch() -> [String] {
        var caseStrings = [String]()
        caseStrings.reserveCapacity((fields.count * 2) + 2)
        
        let indent1 = "    "
        let indent2 = indent1 + indent1
        let indent3 = indent2 + indent1
        
        let selfSwitch = "\(indent2)switch self {"
        caseStrings.append(selfSwitch)
        
        var switchCases: [BaseTypes: Set<String>] = [:]
        switchCases.reserveCapacity(fields.count)
        
        for field in fields {
            var set = switchCases[field.returnType] ?? []
            set.insert(field.fieldName)
            switchCases[field.returnType] = set
        }
        
        for (returnType, cases) in switchCases {
            let field = "\(indent2)case \(cases.map({ ".\($0)" }).joined(separator: ", ")):"
            let dataType = "\(indent3)return " + returnType.gqlDataType + ".self"
            caseStrings.append(field)
            caseStrings.append(dataType)
        }
        
        if children.count > 0 {
            caseStrings.append("\(indent2)default: return String.self")
        }
        
        caseStrings.append("\(indent2)}")
        
        return caseStrings
    }
    
    private func buildAllCasesProperty() -> String {
        var caseStrings = [String]()
        caseStrings.reserveCapacity(fields.count)
        
        let indent1 = "    "
        let indent2 = indent1 + indent1
        var returnString = "\(indent2)return ["
        let indent3 = [String](repeating: " ", count: returnString.count).joined(separator: "")
        var fieldString = returnString
        
        for case .selection(let name, let returnType, _, _) in fields {
            var field = ".\(name)"
            switch returnType {
            case .scalar, .enum, .schema, .input:
                break
            case .object, .interface:
                field += "(\(returnType.gqlDataTypeCodingKeys).allCases)"
            }
            
            fieldString += field
            caseStrings.append(fieldString)
            fieldString = indent3
        }
        
        
        let excludedFields = Set<String>(fields.map({ $0.fieldName }))
        for (child, fields) in children {
            let field = "\(indent3).on\(child.rawValue)(["
            let indent4 = [String](repeating: " ", count: field.count).joined()
            fieldString = field
            
            let remainingFields = fields.filter({ !excludedFields.contains($0.fieldName) })
            var didSet = false
            for case .selection(let name, let returnType, _, _) in remainingFields {
                var field = ".\(name)"
                switch returnType {
                case .scalar, .enum, .schema, .input:
                    break
                case .object, .interface:
                    field += "(\(returnType.gqlDataTypeCodingKeys).allCases)"
                }
                fieldString += field
                caseStrings.append(fieldString)
                didSet = true
                fieldString = indent4
            }
            
            if didSet, caseStrings.count > 0 {
                var lastString = caseStrings.removeLast()
                lastString += "])"
                caseStrings.append(lastString)
            }
        }
        
        if caseStrings.count > 0 {
            var lastString = caseStrings.removeLast()
            lastString += "]"
            caseStrings.append(lastString)
        } else {
            returnString += "]"
            caseStrings.append(returnString)
        }
        
        return caseStrings.joined(separator: ",\n")
    }
    
    private func buildIsVariantProperty() -> [String] {
        var caseStrings = [String]()
        caseStrings.reserveCapacity(children.count)
        let indent1 = "    "
        let indent2 = indent1 + indent1
        
        caseStrings.append("\(indent2)switch self {")
        let variantFields = children.keys.map({ ".on\($0.rawValue)" })
        caseStrings.append("\(indent2)case \(variantFields.joined(separator: ", ")): return true")
        caseStrings.append("\(indent2)default: return false")
        caseStrings.append("\(indent2)}")
        
        return caseStrings
    }
    
    //======
    // MARK: - Interface enum functions
    //======
    
    private func buildInterfaceCases() -> [String] {
        var caseStrings = [String]()
        caseStrings.reserveCapacity(children.count + 1)
        let indent1 = "    "
        
        caseStrings.append("\(indent1)case `\(type.rawValue)`(\(type.gqlDataType))")
        
        for (child, _) in children {
            let field = "\(indent1)case `\(child.rawValue)`(\(child.gqlAbstractType))"
            caseStrings.append(field)
        }
        
        return caseStrings
    }
    
    private func buildInterfaceInit() -> [String] {
        var caseStrings = [String]()
        caseStrings.reserveCapacity(children.count)
        
        let indent1 = "    "
        let indent2 = indent1 + indent1
        let indent3 = indent2 + indent1
        
        caseStrings.append("\(indent2)if let typeString = snapshot[\"__typename\"] as? String {")
        caseStrings.append("\(indent3)switch typeString {")
        
        for (child, _) in children {
            let field = "\(indent3)case \"\(child.rawValue)\": self = .\(child.rawValue)(\(child.gqlAbstractType)(snapshot: snapshot))"
            caseStrings.append(field)
        }
        let defaultField = "\(indent3)default: self = .\(type.rawValue)(\(type.gqlDataType)(snapshot: snapshot))"
        caseStrings.append(defaultField)
        caseStrings.append("\(indent3)}")
        caseStrings.append("\(indent2)} else { self = .\(type.rawValue)(\(type.gqlDataType)(snapshot: snapshot)) }")
        return caseStrings
    }
}

/*
 public final class CarGQLModel<FieldKey: GQLField>: GQLReturnModel<FieldKey> {
 
     override class func buildVariants(for fields: [CarGQLModel<FieldKey>.Field]) -> [String: [GraphQLSelection]]? {
         guard let fields = fields as? [CarGQLModel<CarModelGQLModelKeys>.Field] else { return nil }
         var variants = [String: [GraphQLSelection]]()
         variants.reserveCapacity(fields.count)
         return fields.reduce(into: variants) { (dict, field) in
             switch field {
             case .onBus:
                 dict[field.rawValue] = BusGQLModelKeys.allCases.map({ $0.selection })
             case .onMotorcycle:
                 dict[field.rawValue] = MotorcycleGQLModel.allCases.map({ $0.selection })
             default:
                 break
             }
         }
     }
    
     public final var __typename: TypenameGQLEnum?
     public final var pkey: String?
     public final var skey: String?
     public final var carId: String?
     public final var vin: String?
     public final var driver: WorkerGQLModel?
     public final var make: String?
     public final var model: String?
     public final var isPersonal: Bool?
     
     public required init(snapshot: Snapshot) {
         
         __typename = snapshot["__typename"] as? TypenameGQLEnum
         pkey = snapshot["pkey"] as? String
         skey = snapshot["skey"] as? String
         carId = snapshot["carId"] as? String
         vin = snapshot["vin"] as? String
         if let snapshot = snapshot["driver"] as? Snapshot {
             driver = WorkerGQLModel.init(snapshot: snapshot)
         }
         make = snapshot["make"] as? String
         model = snapshot["model"] as? String
         isPersonal = snapshot["isPersonal"] as? Bool
         
         super.init(snapshot: snapshot)
     }
 }

 public enum CarGQLInterface {
     case car(CarGQLModel<CarGQLModelKeys>)
     case bus(BusGQLModel)
     case motorcycle(MotorcycleGQLModel)
     
     public init(snapshot: Snapshot) {
         if let typeString = snapshot["__typename"] as? String {
             switch typeString {
             case "Car":
                 self = .car(CarGQLModel<CarGQLModelKeys>(snapshot: snapshot))
             case "Customer":
                 self = .bus(BusGQLModel(snapshot: snapshot))
             case "Worker":
                 self = .motorcycle(MotorcycleGQLModel(snapshot: snapshot))
             default:
                 self = .car(CarGQLModel<CarGQLModelKeys>(snapshot: snapshot))
             }
         } else {
             self = .car(CarGQLModel<CarGQLModelKeys>(snapshot: snapshot))
         }
     }
 }
 
 public enum CarModelGQLModelKeys: CaseIterable, GQLField {
     case `__typename`
     case `pkey`
     case `skey`
     case `carId`
     case `vin`
     case `driver`([WorkerGQLModelKeys])
     case `make`
     case `model`
     case `isPersonal`
     case `onBus`([BusGQLModelKeys])
     case `onMotorcycle`([MotorcycleGQLModelKeys])

     public var rawValue: String {
         switch self {
         case .__typename:
             return "__typename"
         case .pkey:
             return "pkey"
         case .skey:
             return "skey"
         case .carId:
             return "carId"
         case .vin:
             return "vin"
         case .driver:
             return "driver"
         case .make:
             return "make"
         case .model:
             return "model"
         case .isPersonal:
             return "isPersonal"
         case .onBus:
             return "Bus"
         case .onMotorcycle:
             return "Motorcycle"
         }
     }
     
     public func fieldString(inset: Int) -> String {
         switch self {
         case .driver(let fields):
             return rawValue + " " + WorkerGQLModel.generateReturnFieldString(fields, inset: inset + 1)
         case .onBus(let fields):
            return "... on " + rawValue + " " + BusGQLModel.generateReturnFieldString(fields, inset: inset + 1)
         case .onMotorcycle(let fields):
            return "... on " + rawValue + " " + MotorcycleGQLModel.generateReturnFieldString(fields, inset: inset + 1)
         default: return rawValue
         }
     }
     
     public var selection: GraphQLField {
         switch self {
         case .driver(let fields):
             return GraphQLField(rawValue, type: .object(WorkerGQLModel.selections(for: fields)))
         default: return GraphQLField(rawValue, type: .scalar(dataType))
         }
     }
     
     public var dataType: JSONDecodable.Type {
         switch self {
         case .__typename:
             return TypenameGQLEnum.self
         case .isPersonal:
             return Bool.self
         case .carId, .make, .skey, .model, .vin, .pkey:
             return String.self
         case .driver:
             return WorkerGQLModel.self
         default:
             return rawValue
         }
     }
     
     public static var allCases: [CarModelGQLModelKeys] {
         return [.__typename,
                 .pkey,
                 .skey,
                 .carId,
                 .vin,
                 .driver(WorkerGQLModelKeys.allCases),
                 .make,
                 .model,
                 .isPersonal,
                 .onBus([.tirePressure,
                         .numPassengers]),
                 .onMotorcycle([.typename,
                                .wheeliesPopped,
                                .funTimesHad])]
     }
     
     public var isVariant: Bool {
         switch self {
         case .onBus, .onMotorcycle:
             return true
         default:
             return false
         }
     }
 }
 */
