// Sources/protoc-gen-swift/Descriptor+Extensions.swift - Additions to Descriptors
//
// Copyright (c) 2014 - 2017 Apple Inc. and the project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See LICENSE.txt for license information:
// https://github.com/apple/swift-protobuf/blob/main/LICENSE.txt
//
// -----------------------------------------------------------------------------

import SwiftProtobufPluginLibrary

extension FileDescriptor {
    var isBundledProto: Bool {
        SwiftProtobufInfo.isBundledProto(file: self)
    }

    // Returns true if the file will need to import Foundation.
    //
    // `bytes` fields are modeled as `Data`, that is currently the only reason
    // why the generated sources need to `import Foundation`.
    var needsFoundationImport: Bool {
        if extensions.contains(where: { $0.type == .bytes }) {
            return true
        }

        if messages.contains(where: { $0.needsFoundationImport }) {
            return true
        }

        return false
    }

    // Returns a string of any import lines for the give file based on the file's
    // imports. The string may include multiple lines.
    //
    // Protocol Buffers has the concept of "public imports", these are imports
    // into a file that expose everything from within the file to the new
    // context. From the docs -
    // https://protobuf.dev/programming-guides/proto/#importing
    //   `import public` dependencies can be transitively relied upon by anyone
    //    importing the proto containing the import public statement.
    // To properly expose the types for use, it means in each file, the public
    // imports from the dependencies (recursively) have to be hoisted and
    // reexported. This way someone importing a given module still sees the type
    // even when moved.
    //
    // NOTE: There is a weakness for Swift with protobuf extensions. To make
    // the protobuf extensions easier to use, a Swift extension is declared with
    // field exposed as a property on the extended message. There is no way
    // to reexport the Swift `extension` and/or added properties. But the raw
    // types are re-exported to minimize the breaking of code if a type is moved
    // between files/modules.
    //
    // `reexportPublicImports` will cause the `import public` types to be
    // reexported to avoid breaking downstream code using a type that might have
    // moved between .proto files.
    //
    // `asImplementationOnly` will cause all of the import directives to be
    // marked as `@_implementationOnly`. It will also cause all of the `file`'s
    // `publicDependencies` to instead be recursively pulled up as direct imports
    // to ensure the generate file compiles, and no `import public` files are
    // re-exported.
    //
    // Aside: This could be moved into the plugin library, but it doesn't seem
    // like anyone else would need the logic. Swift GRPC support probably stick
    // with the support for the module mappings.
    func computeImports(
        namer: SwiftProtobufNamer,
        directive: GeneratorOptions.ImportDirective,
        reexportPublicImports: Bool
    ) -> String {
        // The namer should be configured with the module this file generated for.
        assert(namer.targetModule == (namer.mappings.moduleName(forFile: self) ?? ""))
        // Both options can't be enabled.
        assert(!reexportPublicImports || directive != .implementationOnly)

        guard namer.mappings.hasMappings else {
            // No module mappings? Everything must be the same module, so no Swift
            // imports will be needed.
            return ""
        }

        if dependencies.isEmpty {
            // No proto dependencies (imports), then no Swift imports will be needed.
            return ""
        }

        let importSnippet = directive.snippet
        var imports = Set<String>()
        for dependency in dependencies {
            if SwiftProtobufInfo.isBundledProto(file: dependency) {
                continue  // No import needed for the runtime, that's always added.
            }
            if reexportPublicImports && publicDependencies.contains(where: { $0 === dependency }) {
                // When re-exporting, the `import public` types will be imported
                // instead of importing the module.
                continue
            }
            if let depModule = namer.mappings.moduleName(forFile: dependency),
                depModule != namer.targetModule
            {
                // Different module, import it.
                imports.insert("\(importSnippet) \(depModule)")
            }
        }

        // If not re-exporting imports, then there is nothing special needed for
        // `import public` files, as any transitive `import public` directives
        // would have already re-exported the types, so everything this file needs
        // will be covered by the above imports.
        let exportingImports: [String] =
            reexportPublicImports
            ? computeSymbolReExports(
                namer: namer,
                useAccessLevelOnImports: directive.isAccessLevel
            )
            : [String]()

        var result = imports.sorted().joined(separator: "\n")
        if !exportingImports.isEmpty {
            if !result.isEmpty {
                result.append("\n")
            }
            result.append("// Use of 'import public' causes re-exports:\n")
            result.append(exportingImports.sorted().joined(separator: "\n"))
        }
        return result
    }

    // Internal helper to `computeImports(...)`.
    private func computeSymbolReExports(namer: SwiftProtobufNamer, useAccessLevelOnImports: Bool) -> [String] {
        var result = [String]()

        // To handle re-exporting, recursively walk all the `import public` files
        // and make this module do a Swift exporting import of the specific
        // symbols. That will keep any type that gets moved between .proto files
        // still exposed from the same modules so as not to break developer
        // authored code.
        var toScan = publicDependencies
        var visited = Set<String>()
        let exportedImportDirective = "@_exported\(useAccessLevelOnImports ? " public" : "") import"
        while let dependency = toScan.popLast() {
            let dependencyName = dependency.name
            if visited.contains(dependencyName) { continue }
            visited.insert(dependencyName)

            if SwiftProtobufInfo.isBundledProto(file: dependency) {
                continue  // Bundlined file, nothing to do.
            }
            guard let depModule = namer.mappings.moduleName(forFile: dependency) else {
                continue  // No mapping, assume same module, nothing to do.
            }
            if depModule == namer.targetModule {
                // Same module, nothing to do (that generated file will do any re-exports).
                continue
            }

            toScan.append(contentsOf: dependency.publicDependencies)

            // NOTE: This re-exports/imports from the module that defines the type.
            // If Xcode/SwiftPM ever were to do some sort of "layering checks" to
            // ensure there is a direct dependency on the thing being imported, this
            // could be updated do the re-export/import from the middle step in
            // chained imports.

            for m in dependency.messages {
                result.append("\(exportedImportDirective) struct \(namer.fullName(message: m))")
            }
            for e in dependency.enums {
                result.append("\(exportedImportDirective) enum \(namer.fullName(enum: e))")
            }
            // There is nothing we can do for the Swift extensions declared on the
            // extended Messages, best we can do is expose the raw extensions
            // themselves.
            for e in dependency.extensions {
                result.append("\(exportedImportDirective) let \(namer.fullName(extensionField: e))")
            }
        }
        return result
    }
}

extension Descriptor {
    /// Returns true if the message should use the message set wireformat.
    var useMessageSetWireFormat: Bool { options.messageSetWireFormat }

    /// Returns true if the file will need to import Foundation.
    ///
    /// `bytes` fields are modeled as `Data`, that is currently the only reason
    /// why the generated sources need to `import Foundation`.
    var needsFoundationImport: Bool {
        if fields.contains(where: { $0.type == .bytes }) {
            return true
        }
        if extensions.contains(where: { $0.type == .bytes }) {
            return true
        }

        // Now recurse through sub-messages.
        if messages.contains(where: { $0.needsFoundationImport }) {
            return true
        }

        return false
    }

    /// Returns True if this message recursively contains a required field.
    /// This is a helper for generating isInitialized methods.
    ///
    /// The logic for this check comes from google/protobuf; the C++ and Java
    /// generators specifically
    func containsRequiredFields() -> Bool {
        var alreadySeen = Set<String>()

        func helper(_ descriptor: Descriptor) -> Bool {
            if alreadySeen.contains(descriptor.fullName) {
                // First required thing found causes this to return true, so one can
                // assume if it is already visited and and wasn't cached, it is part
                // of a recursive cycle, so return false without caching to allow
                // the evaluation to continue on other fields of the message.
                return false
            }
            alreadySeen.insert(descriptor.fullName)

            // If it can support extensions, then return true as an extension could
            // have a required field.
            if !descriptor.messageExtensionRanges.isEmpty {
                return true
            }

            for f in descriptor.fields {
                if f.isRequired {
                    return true
                }
                if let messageType = f.messageType, helper(messageType) {
                    return true
                }
            }

            return false
        }

        return helper(self)
    }

    /// The `extensionRanges` are in the order they appear in the original .proto
    /// file; this orders them and then merges any ranges that are actually
    /// contiguous (i.e. - [(21,30),(10,20)] -> [(10,30)])
    ///
    /// This also uses Range<> since the options that could be on
    /// `extensionRanges` no longer can apply as the things have been merged.
    var _normalizedExtensionRanges: [Range<Int32>] {
        var ordered: [Range<Int32>] = self.messageExtensionRanges.sorted(by: {
            $0.start < $1.start
        }).map {
            $0.start..<$0.end
        }
        if ordered.count > 1 {
            for i in (0..<(ordered.count - 1)).reversed() {
                if ordered[i].upperBound == ordered[i + 1].lowerBound {
                    ordered[i] = ordered[i].lowerBound..<ordered[i + 1].upperBound
                    ordered.remove(at: i + 1)
                }
            }
        }
        return ordered
    }

    /// The `extensionRanges` from `normalizedExtensionRanges`, but takes a step
    /// further in that any ranges that do _not_ have any fields inbetween them
    /// are also merged together. These can then be used in context where it is
    /// ok to include field numbers that have to be extension or unknown fields.
    ///
    /// This also uses Range<> since the options that could be on
    /// `extensionRanges` no longer can apply as the things have been merged.
    var _ambitiousExtensionRanges: [Range<Int32>] {
        var merged = self._normalizedExtensionRanges
        if merged.count > 1 {
            var fieldNumbersReversedIterator =
                self.fields.map({ Int($0.number) }).sorted(by: { $0 > $1 }).makeIterator()
            var nextFieldNumber = fieldNumbersReversedIterator.next()
            while nextFieldNumber != nil && merged.last!.lowerBound < nextFieldNumber! {
                nextFieldNumber = fieldNumbersReversedIterator.next()
            }

            for i in (0..<(merged.count - 1)).reversed() {
                if nextFieldNumber == nil || merged[i].lowerBound > nextFieldNumber! {
                    // No fields left or range starts after the next field, merge it with
                    // the previous one.
                    merged[i] = merged[i].lowerBound..<merged[i + 1].upperBound
                    merged.remove(at: i + 1)
                } else {
                    // can't merge, find the next field number below this range.
                    while nextFieldNumber != nil && merged[i].lowerBound < nextFieldNumber! {
                        nextFieldNumber = fieldNumbersReversedIterator.next()
                    }
                }
            }
        }
        return merged
    }
}

extension FieldDescriptor {
    func swiftType(namer: SwiftProtobufNamer) -> String {
        if case (let keyField, let valueField)? = messageType?.mapKeyAndValue {
            let keyType = keyField.swiftType(namer: namer)
            let valueType = valueField.swiftType(namer: namer)
            return "Dictionary<" + keyType + "," + valueType + ">"
        }

        let result: String
        switch type {
        case .double: result = "Double"
        case .float: result = "Float"
        case .int64: result = "Int64"
        case .uint64: result = "UInt64"
        case .int32: result = "Int32"
        case .fixed64: result = "UInt64"
        case .fixed32: result = "UInt32"
        case .bool: result = "Bool"
        case .string: result = "String"
        case .group: result = namer.fullName(message: messageType!)
        case .message: result = namer.fullName(message: messageType!)
        case .bytes: result = "Data"
        case .uint32: result = "UInt32"
        case .enum: result = namer.fullName(enum: enumType!)
        case .sfixed32: result = "Int32"
        case .sfixed64: result = "Int64"
        case .sint32: result = "Int32"
        case .sint64: result = "Int64"
        }

        if label == .repeated {
            return "[\(result)]"
        }
        return result
    }

    func swiftStorageType(namer: SwiftProtobufNamer) -> String {
        let swiftType = self.swiftType(namer: namer)
        switch label {
        case .repeated:
            return swiftType
        case .optional, .required:
            guard realContainingOneof == nil else {
                return swiftType
            }
            if hasPresence {
                return "\(swiftType)?"
            } else {
                return swiftType
            }
        }
    }

    var protoGenericType: String {
        precondition(!isMap)

        switch type {
        case .double: return "Double"
        case .float: return "Float"
        case .int64: return "Int64"
        case .uint64: return "UInt64"
        case .int32: return "Int32"
        case .fixed64: return "Fixed64"
        case .fixed32: return "Fixed32"
        case .bool: return "Bool"
        case .string: return "String"
        case .group: return "Group"
        case .message: return "Message"
        case .bytes: return "Bytes"
        case .uint32: return "UInt32"
        case .enum: return "Enum"
        case .sfixed32: return "SFixed32"
        case .sfixed64: return "SFixed64"
        case .sint32: return "SInt32"
        case .sint64: return "SInt64"
        }
    }

    func swiftDefaultValue(namer: SwiftProtobufNamer) -> String {
        if isMap {
            return "[:]"
        }
        if label == .repeated {
            return "[]"
        }

        if let defaultValue = defaultValue {
            switch type {
            case .double:
                switch defaultValue {
                case "inf": return "Double.infinity"
                case "-inf": return "-Double.infinity"
                case "nan": return "Double.nan"
                case "-nan": return "Double.nan"
                default: return defaultValue
                }
            case .float:
                switch defaultValue {
                case "inf": return "Float.infinity"
                case "-inf": return "-Float.infinity"
                case "nan": return "Float.nan"
                case "-nan": return "Float.nan"
                default: return defaultValue
                }
            case .string:
                return stringToEscapedStringLiteral(defaultValue)
            case .bytes:
                return escapedToDataLiteral(defaultValue)
            case .enum:
                let enumValue = enumType!.value(named: defaultValue)!
                return namer.dottedRelativeName(enumValue: enumValue)
            default:
                return defaultValue
            }
        }

        switch type {
        case .bool: return "false"
        case .string: return "String()"
        case .bytes: return "Data()"
        case .group, .message:
            return namer.fullName(message: messageType!) + "()"
        case .enum:
            return namer.dottedRelativeName(enumValue: enumType!.values.first!)
        default:
            return "0"
        }
    }

    /// Calculates the traits type used for maps and extensions, they
    /// are used in decoding and visiting.
    func traitsType(namer: SwiftProtobufNamer) -> String {
        if case (let keyField, let valueField)? = messageType?.mapKeyAndValue {
            let keyTraits = keyField.traitsType(namer: namer)
            let valueTraits = valueField.traitsType(namer: namer)
            switch valueField.type {
            case .message:  // Map's can't have a group as the value
                return "\(namer.swiftProtobufModulePrefix)_ProtobufMessageMap<\(keyTraits),\(valueTraits)>"
            case .enum:
                return "\(namer.swiftProtobufModulePrefix)_ProtobufEnumMap<\(keyTraits),\(valueTraits)>"
            default:
                return "\(namer.swiftProtobufModulePrefix)_ProtobufMap<\(keyTraits),\(valueTraits)>"
            }
        }
        switch type {
        case .double: return "\(namer.swiftProtobufModulePrefix)ProtobufDouble"
        case .float: return "\(namer.swiftProtobufModulePrefix)ProtobufFloat"
        case .int64: return "\(namer.swiftProtobufModulePrefix)ProtobufInt64"
        case .uint64: return "\(namer.swiftProtobufModulePrefix)ProtobufUInt64"
        case .int32: return "\(namer.swiftProtobufModulePrefix)ProtobufInt32"
        case .fixed64: return "\(namer.swiftProtobufModulePrefix)ProtobufFixed64"
        case .fixed32: return "\(namer.swiftProtobufModulePrefix)ProtobufFixed32"
        case .bool: return "\(namer.swiftProtobufModulePrefix)ProtobufBool"
        case .string: return "\(namer.swiftProtobufModulePrefix)ProtobufString"
        case .group, .message: return namer.fullName(message: messageType!)
        case .bytes: return "\(namer.swiftProtobufModulePrefix)ProtobufBytes"
        case .uint32: return "\(namer.swiftProtobufModulePrefix)ProtobufUInt32"
        case .enum: return namer.fullName(enum: enumType!)
        case .sfixed32: return "\(namer.swiftProtobufModulePrefix)ProtobufSFixed32"
        case .sfixed64: return "\(namer.swiftProtobufModulePrefix)ProtobufSFixed64"
        case .sint32: return "\(namer.swiftProtobufModulePrefix)ProtobufSInt32"
        case .sint64: return "\(namer.swiftProtobufModulePrefix)ProtobufSInt64"
        }
    }
}

extension EnumDescriptor {

    func value(named: String) -> EnumValueDescriptor? {
        for v in values {
            if v.name == named {
                return v
            }
        }
        return nil
    }

    /// Helper object that computes the alias relationships of
    /// `EnumValueDescriptor`s for a given `EnumDescriptor`.
    final class ValueAliasInfo {
        /// The `EnumValueDescriptor`s that are not aliases of another value. In
        /// the same order as the values on the `EnumDescriptor`.
        let mainValues: [EnumValueDescriptor]

        /// Find the alias values for the given value.
        ///
        /// - Parameter value: The value descriptor to look up.
        /// - Returns The list of value descriptors that are aliases for this
        ///     value, or `nil` if there are no alias (or if this was an alias).
        func aliases(_ value: EnumValueDescriptor) -> [EnumValueDescriptor]? {
            assert(mainValues.first!.enumType === value.enumType)
            return aliasesMap[value.index]
        }

        /// Find the original for an alias.
        ///
        /// - Parameter value: The value descriptor to look up.
        /// - Returns The original/main value if this was an alias otherwise `nil`.
        func original(of: EnumValueDescriptor) -> EnumValueDescriptor? {
            assert(mainValues.first!.enumType === of.enumType)
            return aliasOfMap[of.index]
        }

        /// Mapping from index of a "main" value to the aliases for it.
        private let aliasesMap: [Int: [EnumValueDescriptor]]

        /// Mapping from value's index the main value if it was an alias.
        private let aliasOfMap: [Int: EnumValueDescriptor]

        /// Initialize the mappings for the given `EnumDescriptor`.
        init(enumDescriptor descriptor: EnumDescriptor) {
            var mainValues = [EnumValueDescriptor]()
            var aliasesMap = [Int: [EnumValueDescriptor]]()
            var aliasOfMap = [Int: EnumValueDescriptor]()

            var firstValues = [Int32: EnumValueDescriptor]()
            for v in descriptor.values {
                if let aliasing = firstValues[v.number] {
                    aliasesMap[aliasing.index, default: []].append(v)
                    aliasOfMap[v.index] = aliasing
                } else {
                    firstValues[v.number] = v
                    mainValues.append(v)
                }
            }

            self.mainValues = mainValues
            self.aliasesMap = aliasesMap
            self.aliasOfMap = aliasOfMap
        }
    }

}