// Copyright 2017 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//    http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import Foundation
import Gnostic

extension String {
    func capitalizingFirstLetter() -> String {
        let first = String(characters.prefix(1)).capitalized
        let other = String(characters.dropFirst())
        return first + other
    }

    mutating func capitalizeFirstLetter() {
        self = self.capitalizingFirstLetter()
    }
}

class ServiceType {
    var name : String = ""
    var fields : [ServiceTypeField] = []
    var isInterfaceType : Bool = false
}

class ServiceTypeField {
    var name : String = ""
    var typeName : String = ""
    var isArrayType : Bool = false
    var isCastableType : Bool = false
    var isConvertibleType : Bool = false
    var elementType : String = ""
    var jsonName : String = ""
    var position: String = "" // "body", "header", "formdata", "query", or "path"
    var initialValue : String = ""


    func setTypeForName(_ name : String, _ format : String) {
        switch name {
        case "integer":
            if format == "int32" {
                self.typeName = "Int32"
            } else if format == "int64" {
                self.typeName =  "Int64"
            } else {
                self.typeName =  "Int"
            }
            self.initialValue = "0"
            self.isCastableType = true
        default:
            self.typeName = name.capitalizingFirstLetter()
            self.initialValue = self.typeName + "()"
            self.isConvertibleType = true
        }
    }


    func setTypeForSchema(_ schema : Openapi_V2_Schema, optional : Bool = false) {
        let ref = schema.ref
        if ref != "" {
            self.typeName = typeForRef(ref)
            self.isConvertibleType = true
            self.initialValue = self.typeName + "()"
        }
        if schema.hasType {
            let types = schema.type.value
            let format = schema.format
            if types.count == 1 && types[0] == "string" {
                self.typeName = "String"
                self.isCastableType = true
                self.initialValue = "\"\""
            }
            if types.count == 1 && types[0] == "integer" && format == "int32" {
                self.typeName = "Int32"
                self.isCastableType = true
                self.initialValue = "0"
            }
            if types.count == 1 && types[0] == "array" && schema.hasItems {
                // we have an array.., but of what?
                let items = schema.items.schema
                if items.count == 1 && items[0].ref != "" {
                    self.isArrayType = true
                    self.elementType = typeForRef(items[0].ref)
                    self.typeName = "[" + self.elementType + "]"
                    self.initialValue = "[]"
                }
            }
        }
        // this function is incomplete... so return a string representing anything that we don't handle
        if self.typeName == "" {
            self.typeName = "\(schema)"
        }
        if optional {
            self.typeName += "?"
            self.initialValue = "nil"
        }
    }
}

class ServiceMethod {
    var name               : String = ""
    var path               : String = ""
    var method             : String = ""
    var description        : String = ""
    var handlerName        : String = ""
    var processorName      : String = ""
    var clientName         : String = ""
    var resultTypeName     : String?
    var parametersTypeName : String?
    var responsesTypeName  : String?
    var parametersType     : ServiceType?
    var responsesType      : ServiceType?
}

func propertyNameForResponseCode(_ code:String) -> String {
    switch code {
    case "200":
        return "ok"
    case "default":
        return "error"
    default:
        return code
    }
}

func typeForRef(_ ref : String) -> String {
    let parts = ref.components(separatedBy:"/")
    return parts.last!.capitalizingFirstLetter()
}

class ServiceRenderer {
    internal var name : String = ""
    internal var package: String = ""
    internal var types : [ServiceType] = []
    internal var methods : [ServiceMethod] = []
    internal var surface : Surface_V1_Model

    public init(surface : Surface_V1_Model, document : Openapi_V2_Document) {
        self.surface = surface
        loadService(document:document)
    }

    private func loadServiceTypeFromParameters(_ name:String,
                                               _ parameters:[Openapi_V2_ParametersItem])
        -> ServiceType? {
            let t = ServiceType()
            t.name = name.capitalizingFirstLetter() + "Parameters"
            for parametersItem in parameters {
                let f = ServiceTypeField()
                f.typeName = "\(parametersItem)"

                switch parametersItem.oneof! {
                case .parameter(let parameter):
                    switch parameter.oneof! {
                    case .bodyParameter(let bodyParameter):
                        f.name = bodyParameter.name
                        if bodyParameter.hasSchema {
                            f.setTypeForSchema(bodyParameter.schema)
                            f.position = "body"
                        }
                    case .nonBodyParameter(let nonBodyParameter):
                        switch (nonBodyParameter.oneof!) {
                        case .headerParameterSubSchema(let headerParameter):
                            f.name = headerParameter.name
                            f.position = "header"
                        case .formDataParameterSubSchema(let formDataParameter):
                            f.name = formDataParameter.name
                            f.position = "formdata"
                        case .queryParameterSubSchema(let queryParameter):
                            f.name = queryParameter.name
                            f.position = "query"
                        case .pathParameterSubSchema(let pathParameter):
                            f.name = pathParameter.name
                            f.jsonName = pathParameter.name
                            f.position = "path"
                            f.setTypeForName(pathParameter.type, pathParameter.format)
                        }
                    }
                case .jsonReference: // (let reference):
                    Log("?")
                }
                t.fields.append(f)
            }
            if t.fields.count > 0 {
                self.types.append(t)
                return t
            } else {
                return nil
            }
    }

    private func loadServiceTypeFromResponses(_ m:ServiceMethod,
                                              _ name:String,
                                              _ responses:Openapi_V2_Responses)
        -> ServiceType? {
            let t = ServiceType()
            t.name = name.capitalizingFirstLetter() + "Responses"
            for responseCode in responses.responseCode {
                let f = ServiceTypeField()
                f.name = propertyNameForResponseCode(responseCode.name)
                f.jsonName = ""
                if let responseCodeValueOneOf = responseCode.value.oneof {
                    switch responseCodeValueOneOf {
                    case .response(let response):
                        let schema = response.schema
                        if let schemaOneOf = schema.oneof {
                            switch schemaOneOf {
                            case .schema(let schema):
                                f.setTypeForSchema(schema, optional:true)
                                t.fields.append(f)
                                if f.name == "ok" {
                                    m.resultTypeName = f.typeName.replacingOccurrences(of:"?", with:"")
                                }
                            default:
                                break
                            }
                        }
                    default:
                        break
                    }
                }
            }
            if t.fields.count > 0 {
                self.types.append(t)
                return t
            } else {
                return nil
            }
    }

    private func loadOperation(_ operation : Openapi_V2_Operation,
                               method : String,
                               path : String) {
        let m = ServiceMethod()
        m.name = operation.operationID
        m.path = path
        m.method = method
        m.description = operation.description_p
        m.handlerName = "handle" + m.name
        m.processorName = "" + m.name
        m.clientName = m.name
        m.parametersType = loadServiceTypeFromParameters(m.name, operation.parameters)
        if m.parametersType != nil {
            m.parametersTypeName = m.parametersType!.name
        }
        m.responsesType = loadServiceTypeFromResponses(m, m.name, operation.responses)
        if m.responsesType != nil {
            m.responsesTypeName = m.responsesType!.name
        }
        self.methods.append(m)
    }

    private func loadService(document : Openapi_V2_Document) {
        // collect service type descriptions
        for pair in document.definitions.additionalProperties {
            let t = ServiceType()
            t.isInterfaceType = true
            let schema = pair.value
            for pair2 in schema.properties.additionalProperties {
                let f = ServiceTypeField()
                f.name = pair2.name
                f.setTypeForSchema(pair2.value)
                f.jsonName = pair2.name
                t.fields.append(f)
            }
            t.name = pair.name.capitalizingFirstLetter()
            self.types.append(t)
        }
        // collect service method descriptions
        for pair in document.paths.path {
            let v = pair.value
            if v.hasGet {
                loadOperation(v.get, method:"GET", path:pair.name)
            }
            if v.hasPost {
                loadOperation(v.post, method:"POST", path:pair.name)
            }
            if v.hasPut {
                loadOperation(v.put, method:"PUT", path:pair.name)
            }
            if v.hasDelete {
                loadOperation(v.delete, method:"DELETE", path:pair.name)
            }
        }
    }

    public func generate(filenames : [String], response : inout Gnostic_Plugin_V1_Response) throws {
        for filename in filenames {
            var data : Data?
            switch filename {
            case "types.swift":
                data = renderTypes().data(using:.utf8)
            case "server.swift":
                data = renderServer().data(using:.utf8)
            case "client.swift":
                data = renderClient().data(using:.utf8)
            case "fetch.swift":
                data = renderFetch().data(using:.utf8)
            default:
                print("error: unable to render \(filename)")
            }
            if let data = data {
                var clientfile = Gnostic_Plugin_V1_File()
                clientfile.name = filename
                clientfile.data = data
                response.files.append(clientfile)
            }
        }
    }
}

let header = """
// Copyright 2017 Google Inc. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//    http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
"""
