//
//  DynamicSchema+Convenience.swift
//  CoreStore
//
//  Copyright © 2018 John Rommel Estropia
//
//  Permission is hereby granted, free of charge, to any person obtaining a copy
//  of this software and associated documentation files (the "Software"), to deal
//  in the Software without restriction, including without limitation the rights
//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
//  copies of the Software, and to permit persons to whom the Software is
//  furnished to do so, subject to the following conditions:
//
//  The above copyright notice and this permission notice shall be included in all
//  copies or substantial portions of the Software.
//
//  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
//  SOFTWARE.
//

import CoreData
import Foundation


// MARK: - DynamicSchema

extension DynamicSchema {
    
    /**
     Prints the `DynamicSchema` as their corresponding `CoreStoreObject` Swift declarations. This is useful for converting current `XcodeDataModelSchema`-based models into the new `CoreStoreSchema` framework. Additional adjustments may need to be done to the generated source code; for example: `Transformable` concrete types need to be provided, as well as `default` values.
     
     - Important: After transitioning to the new `CoreStoreSchema` framework, it is recommended to add the new schema as a new version that the existing versions' `XcodeDataModelSchema` can migrate to. It is discouraged to load existing SQLite files created with `XcodeDataModelSchema` directly into a `CoreStoreSchema`.
     - returns: a string that represents the source code for the `DynamicSchema` as their corresponding `CoreStoreObject` Swift declarations.
     */
    public func printCoreStoreSchema() -> String {
        
        let model = self.rawModel()
        let entitiesByName = model.entitiesByName
        
        var output = "/// Generated by CoreStore on \(DateFormatter.localizedString(from: Date(), dateStyle: .short, timeStyle: .short))\n"
        var addedInverse: Set<String> = []
        for (entityName, entity) in entitiesByName {
            
            let superName: String
            if let superEntity = entity.superentity {
                
                superName = superEntity.name!
            }
            else {
                
                superName = String(describing: CoreStoreObject.self)
            }
            output.append("class \(entityName): \(superName) {\n")
            defer {
                
                output.append("}\n")
            }
            
            let attributesByName = entity.attributesByName
            if !attributesByName.isEmpty {
                
                output.append("    \n")
                for (attributeName, attribute) in attributesByName {
                    
                    let containerType: String
                    if attribute.isTransient || attribute.attributeType == .undefinedAttributeType {

                        containerType = "Field.Virtual"
                    }
                    else if attribute.attributeType == .transformableAttributeType {

                        containerType = "Field.Coded"
                    }
                    else {

                        containerType = "Field.Stored"
                    }
                    var valueTypeString: String
                    var defaultString = ""
                    var coderString = ""
                    switch attribute.attributeType {
                        
                    case .integer16AttributeType:
                        valueTypeString = String(describing: Int16.self)
                        if let defaultValue = (attribute.defaultValue as! Int16.QueryableNativeType?).flatMap(Int16.cs_fromQueryableNativeType) {
                            
                            defaultString = " = \(defaultValue)"
                        }
                        else if attribute.isOptional {

                            valueTypeString += "?"
                            defaultString = " = nil"
                        }
                    case .integer32AttributeType:
                        valueTypeString = String(describing: Int32.self)
                        if let defaultValue = (attribute.defaultValue as! Int32.QueryableNativeType?).flatMap(Int32.cs_fromQueryableNativeType) {

                            defaultString = " = \(defaultValue)"
                        }
                        else if attribute.isOptional {

                            valueTypeString += "?"
                            defaultString = " = nil"
                        }
                    case .integer64AttributeType:
                        valueTypeString = String(describing: Int64.self)
                        if let defaultValue = (attribute.defaultValue as! Int64.QueryableNativeType?).flatMap(Int64.cs_fromQueryableNativeType) {
                            
                            defaultString = " = \(defaultValue)"
                        }
                        else if attribute.isOptional {

                            valueTypeString += "?"
                            defaultString = " = nil"
                        }
                    case .decimalAttributeType:
                        valueTypeString = String(describing: NSDecimalNumber.self)
                        if let defaultValue = (attribute.defaultValue as! NSDecimalNumber?) {
                            
                            defaultString = " = NSDecimalNumber(string: \"\(defaultValue.description(withLocale: nil))\")"
                        }
                        else if attribute.isOptional {

                            valueTypeString += "?"
                            defaultString = " = nil"
                        }
                    case .doubleAttributeType:
                        valueTypeString = String(describing: Double.self)
                        if let defaultValue = (attribute.defaultValue as! Double.QueryableNativeType?).flatMap(Double.cs_fromQueryableNativeType) {
                            
                            defaultString = " = \(defaultValue)"
                        }
                        else if attribute.isOptional {

                            valueTypeString += "?"
                            defaultString = " = nil"
                        }
                    case .floatAttributeType:
                        valueTypeString = String(describing: Float.self)
                        if let defaultValue = (attribute.defaultValue as! Float.QueryableNativeType?).flatMap(Float.cs_fromQueryableNativeType) {
                            
                            defaultString = " = \(defaultValue)"
                        }
                        else if attribute.isOptional {

                            valueTypeString += "?"
                            defaultString = " = nil"
                        }
                    case .stringAttributeType:
                        valueTypeString = String(describing: String.self)
                        if let defaultValue = (attribute.defaultValue as! String.QueryableNativeType?).flatMap(String.cs_fromQueryableNativeType) {

                            defaultString = " = \"\(defaultValue.replacingOccurrences(of: "\\", with: "\\\\"))\""
                        }
                        else if attribute.isOptional {

                            valueTypeString += "?"
                            defaultString = " = nil"
                        }
                    case .booleanAttributeType:
                        valueTypeString = String(describing: Bool.self)
                        if let defaultValue = (attribute.defaultValue as! Bool.QueryableNativeType?).flatMap(Bool.cs_fromQueryableNativeType) {
                            
                            defaultString = " = \(defaultValue ? "true" : "false")"
                        }
                        else if attribute.isOptional {

                            valueTypeString += "?"
                            defaultString = " = nil"
                        }
                    case .dateAttributeType:
                        valueTypeString = String(describing: Date.self)
                        if let defaultValue = (attribute.defaultValue as! Date.QueryableNativeType?).flatMap(Date.cs_fromQueryableNativeType) {
                            
                            defaultString = " = Date(timeIntervalSinceReferenceDate: \(defaultValue.timeIntervalSinceReferenceDate))"
                        }
                        else if attribute.isOptional {

                            valueTypeString += "?"
                            defaultString = " = nil"
                        }
                    case .binaryDataAttributeType:
                        valueTypeString = String(describing: Data.self)
                        if let defaultValue = (attribute.defaultValue as! Data.QueryableNativeType?).flatMap(Data.cs_fromQueryableNativeType) {
                            
                            let bytes = defaultValue.withUnsafeBytes { (pointer) in
                                return pointer
                                    .bindMemory(to: UInt64.self)
                                    .map({ "\("0x\(String($0, radix: 16, uppercase: false))")" })
                            }
                            defaultString = " = Data(bytes: [\(bytes.joined(separator: ", "))])"
                        }
                        else if attribute.isOptional {

                            valueTypeString += "?"
                            defaultString = " = nil"
                        }
                    case .transformableAttributeType:
                        if let valueTransformerName = attribute.valueTransformerName {

                            coderString = ", coder: /* Required compatible FieldCoderType implementation for ValueTransformer named \"\(valueTransformerName)\" */"
                        }
                        else {

                            coderString = ", coder: FieldCoders.NSCoding.self"
                        }
                        if let attributeValueClassName = attribute.attributeValueClassName {

                            valueTypeString = String(describing: NSClassFromString(attributeValueClassName)!)
                        }
                        else {

                            valueTypeString = "/* <required> */"
                        }
                        if let defaultValue = attribute.defaultValue {

                            switch defaultValue {

                            case let defaultValueBox as Internals.AnyFieldCoder.TransformableDefaultValueCodingBox:
                                if let defaultValue = defaultValueBox.value {

                                    defaultString = " = /* \"\(defaultValue)\" */"
                                }
                                else if attribute.isOptional {

                                    defaultString = " = nil"
                                }
                                else {

                                    defaultString = " = /* <required> */"
                                }

                            case let defaultValue:
                                defaultString = " = /* \"\(defaultValue)\" */"
                            }
                        }
                        else if attribute.isOptional {

                            defaultString = " = nil"
                        }
                        else {
                            
                            defaultString = " = /* <required> */"
                        }
                        if attribute.isOptional {

                            valueTypeString += "?"
                        }

                    case .undefinedAttributeType where attribute.isTransient:
                        coderString = ", customGetter: \\* <required> *\\"
                        if let attributeValueClassName = attribute.attributeValueClassName {

                            valueTypeString = String(describing: NSClassFromString(attributeValueClassName)!)
                        }
                        else {

                            valueTypeString = " = /* <required> */"
                        }
                        if attribute.isOptional {

                            valueTypeString += "?"
                            defaultString = " = nil"
                        }

                    default:
                        fatalError("Unsupported attribute type: \(attribute.attributeType.rawValue)")
                    }
                    let versionHashModifierString = attribute.versionHashModifier
                        .map({ ", versionHashModifier: \"\($0)\"" }) ?? ""

                    let renamingIdentifierString = attribute.renamingIdentifier
                        .map({ ($0 == attributeName ? "" : ", previousVersionKeyPath: \"\($0)\"") }) ?? ""
                    if attributeName.hasPrefix("_") {

                        output.append("    #warning(\"Field variable names cannot start with underscores)")
                    }
                    output.append("    @\(containerType)(\"\(attributeName)\"\(versionHashModifierString)\(renamingIdentifierString)\(coderString))\n")
                    output.append("    var \(attributeName): \(valueTypeString)\(defaultString)\n\n")
                }
            }
            
            let relationshipsByName = entity.relationshipsByName
            if !relationshipsByName.isEmpty {

                output.append("    \n")
                for (relationshipName, relationship) in relationshipsByName {
                    
                    let containerType: String
                    let destinationEntityName = relationship.destinationEntity!.name!
                    var minCountString = ""
                    var maxCountString = ""
                    if relationship.isToMany {
                        
                        let minCount = relationship.minCount
                        let maxCount = relationship.maxCount
                        if relationship.isOrdered {
                            
                            containerType = "[\(destinationEntityName)]"
                        }
                        else {
                            
                            containerType = "Set<\(destinationEntityName)>"
                        }
                        if minCount > 0 {
                            
                            minCountString = ", minCount: \(minCount)"
                        }
                        if maxCount > 0 {
                            
                            maxCountString = ", maxCount: \(maxCount)"
                        }
                    }
                    else {

                        containerType = "\(destinationEntityName)?"
                    }
                    var inverseString = ""
                    let relationshipQualifier = "\(entityName).\(relationshipName)"
                    if !addedInverse.contains(relationshipQualifier),
                        let inverseRelationship = relationship.inverseRelationship {
                        
                        inverseString = ", inverse: \\.$\(inverseRelationship.name)"
                        addedInverse.insert("\(destinationEntityName).\(inverseRelationship.name)")
                    }
                    var deleteRuleString = ""
                    if relationship.deleteRule != .nullifyDeleteRule {
                        
                        switch relationship.deleteRule {
                            
                        case .cascadeDeleteRule:
                            deleteRuleString = ", deleteRule: .cascade"
                            
                        case .denyDeleteRule:
                            deleteRuleString = ", deleteRule: .deny"
                            
                        case .nullifyDeleteRule:
                            deleteRuleString = ", deleteRule: .nullify"
                            
                        default:
                            fatalError("Unsupported delete rule \((relationship.deleteRule)) for relationship \"\(relationshipQualifier)\"")
                        }
                    }
                    let versionHashModifierString = relationship.versionHashModifier
                        .map({ ", versionHashModifier: \"\($0)\"" }) ?? ""
                    let renamingIdentifierString = relationship.renamingIdentifier
                        .map({ ($0 == relationshipName ? "" : ", previousVersionKeyPath: \"\($0)\"") }) ?? ""
                    if relationshipName.hasPrefix("_") {

                        output.append("    #error(\"Field variable names cannot start with underscores)\n")
                    }
                    output.append("    @Field.Relationship(\"\(relationshipName)\"\(minCountString)\(maxCountString)\(inverseString)\(deleteRuleString)\(versionHashModifierString)\(renamingIdentifierString))\n")
                    output.append("    var \(relationshipName): \(containerType)\n\n")
                }
            }
        }
        output.append("\n\n\n")
        output.append("CoreStoreSchema(\n")
        output.append("    modelVersion: \"\(self.modelVersion)\",\n")
        output.append("    entities: [\n")
        for (entityName, entity) in entitiesByName {
            
            var abstractString = ""
            if entity.isAbstract {
                
                abstractString = ", isAbstract: true"
            }
            var versionHashModifierString = ""
            if let versionHashModifier = entity.versionHashModifier {
                
                versionHashModifierString = ", versionHashModifier: \"\(versionHashModifier)\""
            }
            output.append("        Entity<\(entityName)>(\"\(entityName)\"\(abstractString)\(versionHashModifierString)),\n")
        }
        output.append("    ],\n")
        output.append("    versionLock: \(VersionLock(entityVersionHashesByName: model.entityVersionHashesByName).description.components(separatedBy: "\n").joined(separator: "\n    "))\n")
        output.append(")\n\n")
        return output
    }
}
