// --------------------------------------------------------------------------
//
// Copyright (c) Microsoft Corporation. All rights reserved.
//
// The MIT License (MIT)
//
// 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 Foundation
import NIO

class CommandLineArguments: Encodable {
    private var rawArgs: [String: String]

    /// Keys which are supported by Autorest.Swift
    private let supportedKeys: [String] = [
        "input-filename",
        "output-folder",
        "clear-output-folder",
        "project-folder",
        "add-credential",
        "credential-scopes",
        "license-header",
        "namespace",
        "tag",
        "head-as-boolean",
        "title",
        "description",
        "client-side-validation",
        "package-name",
        "package-version",
        "generate-as-internal"
    ]

    /// Keys which are explicitly unsupported and should throw and error if supplied
    private let unsupportedKeys: [String] = [
        "azure-arm",
        "trace"
    ]

    // MARK: Computed properties

    /// The input swagger filename
    var inputFilename: String? {
        return rawArgs["input-filename"]
    }

    /// The desired output folder for code
    var outputFolder: String? {
        return rawArgs["output-folder"]
    }

    /// Whether the output folder should be cleared before generating code.
    var clearOutputFolder: Bool? {
        return Bool(rawArgs["clear-output-folder"] ?? "False")
    }

    /// The root of the project if the code will be generated in a subfolder
    var projectFolder: String? {
        return rawArgs["project-folder"]
    }

    /// Whether to include a credential object in the generated code
    var addCredential: Bool? {
        return Bool(rawArgs["add-credential"] ?? "False")
    }

    /// The scopes to use for generated credentials
    var credentialScopes: [String]? {
        return split(string: rawArgs["credential-scopes"])
    }

    /// Which license header to use for generated files.
    var licenseHeader: String? {
        return rawArgs["license-header"]
    }

    /// The generated package namespace
    var namespace: String? {
        return rawArgs["namespace"]
    }

    /// The tag used to generated the package
    var tag: String? {
        return rawArgs["tag"]
    }

    /// Treat HEAD requests a boolean True value
    var headAsBoolean: Bool? {
        return Bool(rawArgs["head-as-boolean"] ?? "False")
    }

    /// Used to rename the base client in lieu of the Swagger title
    var title: String? {
        return rawArgs["title"]
    }

    /// Used in lieu of the root description field from Swagger
    var description: String? {
        return rawArgs["description"]
    }

    /// Generated code with client-side validation, where specified in Swagger
    var clientSideValidation: Bool? {
        return Bool(rawArgs["client-side-validation"] ?? "False")
    }

    /// The package name to use for generated code
    var packageName: String? {
        return rawArgs["package-name"]
    }

    /// The package version to use for generated code
    var packageVersion: String? {
        return rawArgs["package-version"]
    }

    /// Transforms for model types
    var generateAsInternal: ModelMap {
        return ModelMap(with: rawArgs["generate-as-internal"])
    }

    // MARK: Initializers

    init(client: ChannelClient?, sessionId: String?, completion: @escaping () -> Void) {
        self.rawArgs = [String: String]()

        // Load arguments from direct command line (run standalone)
        for item in CommandLine.arguments.dropFirst() {
            let key = parseKey(from: item)
            guard unsupportedKeys.contains(key) == false else {
                SharedLogger.fail("Autorest.Swift does not support --\(key)")
            }
            if supportedKeys.contains(key) == false {
                SharedLogger.warn("Autorest.Swift does not recognize --\(key)")
            }
            let value = parseValue(from: item)
            rawArgs[key] = value
        }

        // Load arguments from RPC, if available
        if let session = sessionId, let channelClient = client {
            collectRpcValues(with: channelClient, sessionId: session) {
                completion()
            }
        } else {
            completion()
        }
    }

    /// Returns the argument key without the `--` prefix.
    private func parseKey(from item: String) -> String {
        guard let key = item.split(separator: "=", maxSplits: 1).map({ String($0) }).first else {
            SharedLogger.fail("Item \(item) contained no key.")
        }
        guard key.hasPrefix("--") else {
            SharedLogger.fail("Item \(key) expected to start with -- prefix.")
        }
        return String(key.dropFirst(2))
    }

    private func parseValue(from item: String) -> String {
        let values = Array(item.split(separator: "=", maxSplits: 1).map { String($0) }.dropFirst())
        switch values.count {
        case 0:
            // indicates flag
            return "True"
        case 1:
            return values[0]
        default:
            SharedLogger.fail("Error in item \(item). Expected at most 1 value. Found \(values.count)")
        }
    }

    private func collectRpcValues(with client: ChannelClient, sessionId: String, completion: @escaping () -> Void) {
        let futures = supportedKeys
            .map { client.call(method: "GetValue", params: .list([.string(sessionId), .string($0)])) }
        let future = EventLoopFuture.whenAllSucceed(futures, on: client.context!.eventLoop)
        future.whenSuccess { results in
            for (index, rpcResult) in results.enumerated() {
                if case let RPCResult.success(response) = rpcResult {
                    let key = self.supportedKeys[index]
                    self.rawArgs[key] = response.asString
                }
            }
            completion()
        }
        future.whenFailure { error in
            SharedLogger.fail("GetValue failed: \(error)")
        }
    }

    func encode(to encoder: Encoder) throws {
        try rawArgs.encode(to: encoder)
    }
}

internal struct ModelMap {
    internal var map: [String: String]
    internal var reverseMap: [String: String]

    init(with string: String?) {
        self.map = [String: String]()
        self.reverseMap = [String: String]()
        guard let splitVals = split(string: string) else { return }
        for item in splitVals {
            let comps = item.split(separator: "=", maxSplits: 1).map { String($0) }
            if comps.count != 2 {
                SharedLogger.fail("Incorrect usage: --generate-as-internal NAME=ALIAS ...")
            }
            map[comps[0]] = comps[1]
            reverseMap[comps[1]] = comps[0]
        }
    }

    /// Returns internal for models which are aliased. Otherwise, public.
    internal func visibility(for name: String) -> String {
        // if the name is in the map or reverse map, the visibility should be internal
        let test = map[name] ?? reverseMap[name]
        return test != nil ? "internal" : "public"
    }

    /// Returns the alias for a model or the original model name if no alias present
    internal func aliasOrName(for name: String) -> String {
        if let alias = map[name] {
            return alias
        } else {
            return name
        }
    }

    /// Returns the alias for a given name or nil if no alias is present
    internal func alias(for name: String) -> String? {
        return map[name]
    }

    /// Returns the name for a given alias or nil if no name is present
    internal func name(for alias: String) -> String? {
        return reverseMap[alias]
    }
}

private func split(string val: String?) -> [String]? {
    guard let value = val else { return nil }
    return value.components(separatedBy: .whitespacesAndNewlines).filter { $0.count > 0 }
}


// --------------------------------------------------------------------------
//
// Copyright (c) Microsoft Corporation. All rights reserved.
//
// The MIT License (MIT)
//
// 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 Foundation

enum FileDestination {
    case root
    case tests
    case source
    case generated
    case models
    case operations
    case options
    case util
    case jazzy

    func url(forBaseUrl baseUrl: URL) -> URL {
        switch self {
        case .root:
            return baseUrl
        case .tests:
            return baseUrl.appendingPathComponent("Tests")
        case .source:
            return baseUrl.appendingPathComponent("Source")
        case .generated:
            return baseUrl.appendingPathComponent("Source").appendingPathComponent("Generated")
        case .models:
            return baseUrl.appendingPathComponent("Source").appendingPathComponent("Generated")
                .appendingPathComponent("Models")
        case .operations:
            return baseUrl.appendingPathComponent("Source").appendingPathComponent("Generated")
                .appendingPathComponent("Operations")
        case .options:
            return baseUrl.appendingPathComponent("Source").appendingPathComponent("Generated")
                .appendingPathComponent("Options")
        case .util:
            return baseUrl.appendingPathComponent("Source").appendingPathComponent("Generated")
                .appendingPathComponent("Util")
        case .jazzy:
            return baseUrl.appendingPathComponent("jazzy")
        }
    }
}


// --------------------------------------------------------------------------
//
// Copyright (c) Microsoft Corporation. All rights reserved.
//
// The MIT License (MIT)
//
// 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 Foundation

protocol LanguageShortcut {
    var language: Languages { get set }

    var name: String { get set }

    var description: String { get set }

    var summary: String? { get set }

    var serializedName: String? { get set }

    var namespace: String? { get set }
}

extension LanguageShortcut {
    var name: String {
        get {
            let name = language.swift.name.trimmingCharacters(in: .whitespacesAndNewlines)
            // apply backticks to reserved keywords
            if name.isReservedKeyword {
                return "`\(name)`"
            }
            return name
        }
        set {
            language.swift.name = newValue
        }
    }

    var variableName: String {
        let name = language.swift.name.trimmingCharacters(in: .whitespacesAndNewlines).lowercasedFirst
        // apply backticks to reserved keywords
        if name.isReservedKeyword {
            return "`\(name)`"
        }
        return name
    }

    var description: String {
        get {
            return language.swift.description.trimmingCharacters(in: .whitespacesAndNewlines)
        }
        set {
            language.swift.description = newValue
        }
    }

    var summary: String? {
        get {
            return language.swift.summary?.trimmingCharacters(in: .whitespacesAndNewlines)
        }
        set {
            language.swift.summary = newValue
        }
    }

    var serializedName: String? {
        get {
            return language.swift.serializedName?.trimmingCharacters(in: .whitespacesAndNewlines)
        }
        set {
            language.swift.serializedName = newValue
        }
    }

    var namespace: String? {
        get {
            return language.swift.namespace?.trimmingCharacters(in: .whitespacesAndNewlines)
        }
        set {
            language.swift.namespace = newValue
        }
    }
}

protocol OptionalLanguageShortcut {
    var language: Languages? { get set }

    var name: String? { get set }

    var description: String? { get set }

    var summary: String? { get set }

    var serializedName: String? { get set }

    var namespace: String? { get set }
}

extension OptionalLanguageShortcut {
    var name: String? {
        get {
            if let name = language?.swift.name.trimmingCharacters(in: .whitespacesAndNewlines) {
                // apply backticks to reserved keywords
                if name.isReservedKeyword {
                    return "`\(name)`"
                }
                return name
            }
            return nil
        }
        set {
            if let val = newValue {
                language?.swift.name = val
            }
        }
    }

    var description: String? {
        get {
            return language?.swift.description.trimmingCharacters(in: .whitespacesAndNewlines)
        }
        set {
            if let val = newValue {
                language?.swift.description = val
            }
        }
    }

    var summary: String? {
        get {
            return language?.swift.summary?.trimmingCharacters(in: .whitespacesAndNewlines)
        }
        set {
            language?.swift.summary = newValue
        }
    }

    var serializedName: String? {
        get {
            return language?.swift.serializedName?.trimmingCharacters(in: .whitespacesAndNewlines)
        }
        set {
            language?.swift.serializedName = newValue
        }
    }

    var namespace: String? {
        get {
            return language?.swift.namespace?.trimmingCharacters(in: .whitespacesAndNewlines)
        }
        set {
            language?.swift.namespace = newValue
        }
    }
}


// --------------------------------------------------------------------------
//
// Copyright (c) Microsoft Corporation. All rights reserved.
//
// The MIT License (MIT)
//
// 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 Foundation

enum LogLevel: Int {
    case error, warning, info, debug

    var label: String {
        switch self {
        case .error:
            return "ERROR"
        case .warning:
            return "WARN"
        case .info:
            return "INFO"
        case .debug:
            return "DEBUG"
        }
    }
}

protocol Logger {
    var level: LogLevel { get set }

    func log(_ message: @autoclosure @escaping () -> String?, category: String?, level: LogLevel)
    func fail(_ message: @autoclosure @escaping () -> String?, category: String?) -> Never
}

extension Logger {
    func shouldLog(forLevel level: LogLevel) -> Bool {
        return level.rawValue <= self.level.rawValue
    }
}

// MARK: - Implementation

struct SharedLogger {
    private static var logger: Logger!

    static var level: LogLevel {
        set {
            logger.level = newValue
        }

        get {
            logger.level
        }
    }

    fileprivate static let `default` = "default"

    static func set(logger: Logger, withLevel level: LogLevel = .info) {
        SharedLogger.logger = logger
        SharedLogger.level = level
    }

    static func error(
        _ message: @autoclosure @escaping () -> String?,
        category: String? = nil
    ) {
        log(message(), category: category, level: .error)
    }

    static func warn(
        _ message: @autoclosure @escaping () -> String?,
        category: String? = nil
    ) {
        log(message(), category: category, level: .warning)
    }

    static func info(
        _ message: @autoclosure @escaping () -> String?,
        category: String? = nil
    ) {
        log(message(), category: category, level: .info)
    }

    static func debug(
        _ message: @autoclosure @escaping () -> String?,
        category: String? = nil
    ) {
        log(message(), category: category, level: .debug)
    }

    private static func log(
        _ message: @autoclosure @escaping () -> String?,
        category: String? = nil,
        level: LogLevel
    ) {
        guard logger.shouldLog(forLevel: level) else { return }
        SharedLogger.logger.log(message(), category: category ?? SharedLogger.default, level: level)
    }

    static func fail(
        _ message: @autoclosure @escaping () -> String?,
        category: String = SharedLogger.default
    ) -> Never {
        SharedLogger.logger.fail(message(), category: category)
    }
}

/// Do-nothing logger
class NullLogger: Logger {
    var level: LogLevel = .info

    func log(_: @autoclosure @escaping () -> String?, category _: String? = nil, level _: LogLevel = .info) {}

    func fail(_: @autoclosure @escaping () -> String?, category _: String? = nil) -> Never {
        fatalError()
    }
}

/// Stdiout logger
class StdoutLogger: Logger {
    // MARK: Properties

    var level: LogLevel = .info

    // MARK: Methods

    func log(_ message: @autoclosure @escaping () -> String?, category: String? = nil, level: LogLevel) {
        guard let msg = message() else {
            fatalError("Unable to create log message.")
        }
        guard shouldLog(forLevel: level) else { return }
        let cat = category ?? SharedLogger.default
        print("AutorestSwift.\(cat) (\(level.label)) \(msg)")
    }

    func fail(_ message: @autoclosure @escaping () -> String?, category: String? = nil) -> Never {
        guard let msg = message() else {
            fatalError("Unable to create log message.")
        }
        log(msg, category: category, level: .error)
        fatalError(msg)
    }
}

/// Logs to a file.
class FileLogger: Logger {
    var level: LogLevel = .info

    func log(_ message: @autoclosure @escaping () -> String?, category: String? = nil, level: LogLevel) {
        guard var msg = message() else {
            fatalError("Unable to create log message.")
        }
        guard shouldLog(forLevel: level) else { return }
        if let cat = category {
            msg = "AutorestSwift.\(cat) (\(level.label)) \(msg)"
        } else {
            msg = "AutorestSwift (\(level.label)) \(msg)"
        }
        try? url.append(line: msg)
    }

    func fail(_ message: @autoclosure @escaping () -> String?, category: String? = nil) -> Never {
        guard let msg = message() else {
            fatalError("Unable to create log message.")
        }
        log(msg, category: category, level: .error)
        fatalError(msg)
    }

    // MARK: Properties

    let url: URL

    // MARK: Initializers

    internal init(withFileName name: String, deleteIfExists: Bool = true) {
        guard let documentsUrl = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else {
            fatalError("Unable to locate Documents directory.")
        }
        self.url = documentsUrl.appendingPathComponent(name)
        if deleteIfExists {
            try? FileManager.default.removeItem(atPath: url.path)
        }
    }
}


// --------------------------------------------------------------------------
//
// Copyright (c) Microsoft Corporation. All rights reserved.
//
// The MIT License (MIT)
//
// 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 Foundation
import Yams

struct StringDecodeKeys: ExtensionStringDecodeKeys {
    var stringDecodedKeys = ["clientMessageId", "shareHistoryTime", "id", "messageId", "createdAt", "version"]
}

/// Handles the configuration and orchestrations of the code generation process
class Manager {
    // MARK: Properties

    static var shared = Manager()

    var config: ManagerConfig?
    var args: CommandLineArguments?

    // MARK: Methods

    /// Initialize with an input file URL (i.e. running from Xcode).
    func configure(input: URL, destination: URL) throws {
        guard config == nil else {
            SharedLogger.warn("Manager is already configured.")
            return
        }
        // TODO: Make this configurable?
        let destUrl = destination.appendingPathComponent("generated").appendingPathComponent("sdk")
        config?.tempRootUrl = destUrl
        do {
            try destUrl.ensureExists()
        } catch {
            SharedLogger.fail("\(error)")
        }
        config = ManagerConfig(withInput: try String(contentsOf: input), destinationRootUrl: destUrl)
        args = CommandLineArguments(client: nil, sessionId: nil) {
            SharedLogger.info(self.args.debugDescription)
        }
    }

    /// Initialize with a raw YAML string (i.e. the way it is received from Autorest).
    func configure(
        input: String,
        client: ChannelClient,
        sessionId: String,
        completion: @escaping () -> Void
    ) {
        guard config == nil else {
            SharedLogger.warn("Manager is already configured.")
            return
        }
        args = CommandLineArguments(client: client, sessionId: sessionId) {
            let destUrl: URL
            // generate to a temp directory
            destUrl = URL(fileURLWithPath: NSTemporaryDirectory(), isDirectory: true)
                .appendingPathComponent(UUID().uuidString)
            self.config = ManagerConfig(withInput: input, destinationRootUrl: destUrl)
            completion()
        }
    }

    func run() throws {
        guard let config = self.config else {
            fatalError("Autorest.Swift is not configured for running.")
        }
        saveCodeModel()
        let model = try loadModel()
        _ = check(model: model, against: config.inputString)

        // Create folder structure
        let packageName = model.packageName
        let packageUrl = config.tempRootUrl.appendingPathComponent(packageName)
        try packageUrl.ensureExists()

        config.tempPackageUrl = packageUrl

        // Generate Swift code files
        SharedLogger.info("Generating code at: \(packageUrl.path)")
        let generator = SwiftGenerator(withModel: model, atBaseUrl: packageUrl)
        try generator.generate()

        // Run SwiftLint and SwiftFormat on generated files
        formatCode(atBaseUrl: packageUrl)
    }

    /// Decodes the YAML code model file into Swift objects and evaluates model consistency
    func loadModel() throws -> CodeModel {
        // decode YAML to model
        let decoder = YAMLDecoder()
        let model = try decoder.decode(
            CodeModel.self,
            from: config?.inputString ?? "",
            userInfo: [AnyCodable.extensionStringDecodedKey: StringDecodeKeys()]
        )
        return model
    }

    /// Check model for consistency and save debugging files if inconsistent
    private func check(model: CodeModel, against yamlString: String) -> Bool {
        guard let config = self.config else { return false }
        let encoder = JSONEncoder()
        encoder.outputFormatting = [.sortedKeys, .prettyPrinted]

        var beforeJsonString: String?
        var afterJsonString: String?

        // get JSON representation of YAML
        if let beforeJson = try? Yams.load(yaml: yamlString) {
            do {
                let beforeJsonData = try JSONSerialization.data(
                    withJSONObject: beforeJson,
                    options: [.sortedKeys, .prettyPrinted]
                )
                beforeJsonString = String(data: beforeJsonData, encoding: .utf8)
            } catch {
                SharedLogger.fail("\(error)")
            }
        }

        // convert model to JSON
        if let afterJsonData = try? encoder.encode(model) {
            afterJsonString = String(data: afterJsonData, encoding: .utf8)
        }

        // If JSON strings don't match, there must be a problem in the modeling.
        // Dump both files so they can be diffed.
        if beforeJsonString != afterJsonString {
            let beforeJsonUrl = config.tempRootUrl.appendingPathComponent("before.json")
            let afterJsonUrl = config.tempRootUrl.appendingPathComponent("after.json")
            SharedLogger.debug("before.json url=\(beforeJsonUrl)")
            SharedLogger.debug("after.json url=\(afterJsonUrl)")
            do {
                try beforeJsonString?.write(to: beforeJsonUrl, atomically: true, encoding: .utf8)
                try afterJsonString?.write(to: afterJsonUrl, atomically: true, encoding: .utf8)
                SharedLogger
                    .warn(
                        "Discrepancies found in round-tripped code model. Run a diff on 'before.json' and 'after.json' to troubleshoot."
                    )
            } catch {
                SharedLogger.warn(
                    "Discrepancies found in round-tripped code model. Error saving files: \(error)"
                )
            }
        } else if beforeJsonString == nil || afterJsonString == nil {
            SharedLogger.fail("Errors found trying to decode models. Please check your Swagger file.")
        } else {
            SharedLogger.info("Model file check: OK")
        }
        return (beforeJsonString == afterJsonString)
    }

    private func formatCode(atBaseUrl baseUrl: URL) {
        runTool(with: "swiftformat", configFilename: ".swiftformat", arguments: ["--quiet", baseUrl.path])
        runTool(
            with: "swiftlint",
            configFilename: ".swiftlint.yml",
            arguments: ["autocorrect", "--quiet", baseUrl.path]
        )
    }

    private func runTool(with tool: String, configFilename: String, arguments: [String]) {
        var allArguments: [String] = []

        guard let toolPath = Bundle.main.path(forResource: tool, ofType: nil) else {
            SharedLogger.warn("Can't find path for tool \(tool) in path \(Bundle.main.bundlePath)")
            return
        }
        guard let configPath = Bundle.main.path(forResource: "", ofType: configFilename) else {
            SharedLogger
                .warn(
                    "Can't find config file for tool \(tool) with config \(configFilename) in path \(Bundle.main.bundlePath)."
                )
            return
        }

        allArguments.append("--config")
        allArguments.append(configPath)

        allArguments.append(contentsOf: arguments)

        do {
            let outputPipe = Pipe()
            let errorPipe = Pipe()

            let task = Process()
            task.standardOutput = outputPipe
            task.standardError = errorPipe
            task.executableURL = URL(fileURLWithPath: toolPath)
            task.arguments = allArguments
            try task.run()
            task.waitUntilExit()

            let outputData = outputPipe.fileHandleForReading.readDataToEndOfFile()
            let errorData = errorPipe.fileHandleForReading.readDataToEndOfFile()
            SharedLogger.debug("Stdout from \(tool) \(String(decoding: outputData, as: UTF8.self))")
            SharedLogger.debug("Stderr from \(tool) \(String(decoding: errorData, as: UTF8.self))")
        } catch {
            SharedLogger.fail("Fail to run tool \(tool).")
        }
    }

    func saveCodeModel() {
        guard let documentsUrl = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first else {
            fatalError("Unable to locate Documents directory.")
        }
        let dest = documentsUrl.appendingPathComponent("code-model-v4.yaml")
        let finalString = config?.inputString.replacingOccurrences(of: "' '", with: "''")
        try? finalString?.write(to: dest, atomically: true, encoding: .utf8)
    }
}


// --------------------------------------------------------------------------
//
// Copyright (c) Microsoft Corporation. All rights reserved.
//
// The MIT License (MIT)
//
// 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 Dispatch
import Foundation
import NIO

class ManagerConfig {
    /// The YAML input string from Modeler4
    var inputString: String

    /// The local root URL for where files will be written before being sent to autorest
    /// to be written in their final destination
    var tempRootUrl: URL!

    /// The local URL to the package where files will be written before being sent to
    /// autorest to be written in their final destination
    var tempPackageUrl: URL?

    /// Initialize Manager configuration
    /// - Parameters:
    ///   - input: Input YAML string of the code model.
    ///   - destinationRootUrl: `URL` for the destination root.
    init(withInput input: String, destinationRootUrl: URL) {
        self.inputString = input
        self.tempRootUrl = destinationRootUrl
    }
}


// --------------------------------------------------------------------------
//
// Copyright (c) Microsoft Corporation. All rights reserved.
//
// The MIT License (MIT)
//
// 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 Foundation

/// Class used to generate Swift code
class SwiftGenerator: CodeGenerator {
    // MARK: Properties

    let model: CodeModel

    let baseUrl: URL

    // MARK: Initializers

    init(withModel model: CodeModel, atBaseUrl baseUrl: URL) {
        self.model = model
        self.baseUrl = baseUrl
    }

    // MARK: Methods

    fileprivate func generateModels() throws {
        // Simple dictionary (for fast lookup) to determine if a model has already been written.
        // Skips regenerating a file with a less specific schema (Schema vs ObjectSchema, for example)
        // but keeps track of the number of times the model name appears for debugging purposes.
        var modelsWritten = [String: Int]()

        // Create model files
        for object in model.schemas.objects ?? [] {
            let name = object.modelName

            guard modelsWritten[name] == nil else {
                SharedLogger.warn("\(name) has already been generated once this run. Skipping...")
                modelsWritten[name]! += 1
                continue
            }
            let viewModel = ObjectViewModel(from: object)
            try render(
                template: "Model_File",
                toSubfolder: .models,
                withFilename: name,
                andParams: ["model": viewModel]
            )
            modelsWritten[name] = 1

            if let immediates = object.parents?.immediate {
                // render any immediate parents of the object schema
                for parent in immediates {
                    let name = Manager.shared.args!.generateAsInternal.aliasOrName(for: object.name)
                    guard modelsWritten[name] == nil else {
                        SharedLogger.warn("\(name) has already been generated once this run. Skipping...")
                        modelsWritten[name]! += 1
                        continue
                    }
                    guard let parentSchema = parent as? ObjectSchema else {
                        fatalError("Expected ObjectSchema but found \(type(of: parent))")
                    }
                    let viewModel = ObjectViewModel(from: parentSchema)
                    try render(
                        template: "Model_File",
                        toSubfolder: .models,
                        withFilename: name,
                        andParams: ["model": viewModel]
                    )
                    modelsWritten[name] = 1
                }
            }
        }

        // Create group model files
        for group in model.schemas.groups ?? [] {
            let name = Manager.shared.args!.generateAsInternal.aliasOrName(for: group.name)
            guard modelsWritten[name] == nil else {
                SharedLogger.warn("\(name) has already been generated once this run. Skipping...")
                modelsWritten[name]! += 1
                continue
            }
            let viewModel = ObjectViewModel(from: group)
            try render(
                template: "Model_File",
                toSubfolder: .models,
                withFilename: name,
                andParams: ["model": viewModel]
            )
            modelsWritten[name] = 1
        }
    }

    /// Begin code generation process
    func generate() throws {
        let modelUrl = baseUrl.with(subfolder: .models)
        let optionsUrl = baseUrl.with(subfolder: .options)
        let utilUrl = baseUrl.with(subfolder: .util)
        let jazzyUrl = baseUrl.with(subfolder: .jazzy)

        // clear the generated folder to ensure renames are caught
        if let outputFolder = Manager.shared.args?.outputFolder {
            let generatedUrl = URL(fileURLWithPath: outputFolder).with(subfolder: .generated)
            try? FileManager.default.removeItem(at: generatedUrl)
        }

        try modelUrl.ensureExists()
        try optionsUrl.ensureExists()
        try utilUrl.ensureExists()
        try jazzyUrl.ensureExists()
        SharedLogger.info("Base URL: \(baseUrl.path)")

        // Create PatchUtil.swift file
        try render(
            template: "PatchUtil_File",
            toSubfolder: .util,
            withFilename: "PatchUtil",
            andParams: [:]
        )

        // Create Enumerations.swift file
        let enumViewModel = EnumerationFileViewModel(from: model.schemas)
        if enumViewModel.enums.count > 0 {
            try render(
                template: "Enumeration_File",
                toSubfolder: .models,
                withFilename: "Enumerations",
                andParams: ["models": enumViewModel]
            )
        }

        try generateModels()

        // Create Client.swift file
        let clientViewModel = ServiceClientFileViewModel(from: model)
        try render(
            template: "Client_File",
            toSubfolder: .generated,
            withFilename: clientViewModel.name,
            andParams: [
                "model": clientViewModel
            ]
        )

        // Create ClientMethodOptions.swift file
        for operationGroup in clientViewModel.operationGroups {
            try renderClientMethodOptionsFile(for: operationGroup, using: "Method_Options_File")
        }

        // Create ClientOperationGroup.swift file for each operation group with name
        for (groupName, operationGroup) in clientViewModel.namedOperationGroups {
            try render(
                template: "Named_OperationGroup_File",
                toSubfolder: .generated,
                withFilename: "\(groupName)",
                andParams: ["model": clientViewModel, "group": operationGroup]
            )
            // TODO: Find alternative to resolve naming conflicts
            try renderClientMethodOptionsFile(for: operationGroup, with: groupName, using: "Method_Options_File")
        }

        // Create ClientOptions.swift file
        let optionsFileViewModel = ClientOptionsFileViewModel(from: model)
        try render(
            template: "Client_Options_File",
            toSubfolder: .options,
            withFilename: optionsFileViewModel.name,
            andParams: ["model": optionsFileViewModel]
        )

        if !exists(filename: "README.md", inSubfolder: .root) {
            // Create README.md file
            let readmeViewModel = ReadmeFileViewModel(from: model)
            try render(
                template: "README_File",
                toSubfolder: .root,
                withFilename: "README.md",
                andParams: ["model": readmeViewModel]
            )
        }

        if !exists(filename: "Custom.swift", inSubfolder: .source) {
            // Create Custom.swift file, which allows us to wipe and
            // re-generate the Generated folder.
            try render(
                template: "Custom_File",
                toSubfolder: .source,
                withFilename: "Custom.swift",
                andParams: ["": ""]
            )
        }

        // Create Package.swift file
        if !exists(filename: "Package.swift", inSubfolder: .root) {
            let packageViewModel = PackageFileViewModel(from: model)
            try render(
                template: "Package_File",
                toSubfolder: .root,
                withFilename: "Package.swift",
                andParams: ["model": packageViewModel]
            )

            // Create Jazzy config file
            try render(
                template: "Jazzy_File",
                toSubfolder: .jazzy,
                withFilename: "\(model.packageName).yml",
                andParams: ["model": packageViewModel]
            )
        }
    }

    private func exists(filename: String, inSubfolder subfolder: FileDestination) -> Bool {
        let destRoot: URL
        // When using autorest, we must check the output folder for existence of the file, not
        // the temp directory (which will obviously never contain the file!).
        if let outputFolder = Manager.shared.args?.outputFolder {
            destRoot = URL(fileURLWithPath: outputFolder)
        } else {
            destRoot = baseUrl
        }
        let fname = filename.lowercased().contains(".") ? filename : "\(filename).swift"
        let fileUrl = destRoot.with(subfolder: subfolder).appendingPathComponent(fname)
        let result = FileManager.default.fileExists(atPath: fileUrl.path)
        return result
    }

    private func render(
        template: String,
        toSubfolder subfolder: FileDestination,
        withFilename filename: String,
        andParams params: [String: Any]
    ) throws {
        let tname = template.lowercased().hasSuffix(".stencil") ? template : "\(template).stencil"
        let fileContent = try renderTemplate(filename: tname, dictionary: params)
        let fname = filename.lowercased().contains(".") ? filename : "\(filename).swift"
        let fileUrl = baseUrl.with(subfolder: subfolder).appendingPathComponent(fname)
        try fileContent.write(to: fileUrl, atomically: true, encoding: .utf8)
    }

    private func renderClientMethodOptionsFile(
        for operationGroup: OperationGroupViewModel,
        with _: String? = nil,
        using template: String
    ) throws {
        for operation in operationGroup.operations {
            let clientMethodOptions = operation.clientMethodOptions
            var fileName = ""
            fileName += "\(clientMethodOptions.name).swift"

            try render(
                template: template,
                toSubfolder: .options,
                withFilename: fileName,
                andParams: ["model": clientMethodOptions]
            )
        }
    }
}


// --------------------------------------------------------------------------
//
// Copyright (c) Microsoft Corporation. All rights reserved.
//
// The MIT License (MIT)
//
// 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 Foundation

/// Protocol that delinates models with a usage property
protocol UsageSchema {
    var usage: [SchemaContext]? { get }
}


// --------------------------------------------------------------------------
//
// Copyright (c) Microsoft Corporation. All rights reserved.
//
// The MIT License (MIT)
//
// 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 Dispatch
import Foundation
import NIO

// swiftlint:disable force_try
class AutorestPlugin {
    var client: ChannelClient!
    var server: ChannelServer!
    var sessionId: String!
    var processRequestId: Int!

    var eventLoop = MultiThreadedEventLoopGroup(numberOfThreads: System.coreCount)

    private enum Signal: Int32 {
        case HUP = 1
        case INT = 2
        case QUIT = 3
        case ABRT = 6
        case KILL = 9
        case ALRM = 14
        case TERM = 15
    }

    func start() {
        // Start up the server. It will switch to client mode later on.
        server = ChannelServer(group: eventLoop, handler: serverHandler)
        _ = try! server.start().wait()

        // Create the client but do not start it
        client = ChannelClient(group: eventLoop, handler: clientHandler)

        // Trap to catch an abort signal via CTRL+C
        let group = DispatchGroup()
        group.enter()
        let signalSource = trap(signal: Signal.INT) { _ in
            // shut down the client and server on a termination signal
            self.client.stop()
            self.server.stop().whenComplete { _ in
                group.leave()
            }
        }
        group.wait()
        signalSource.cancel()
    }

    private func trap(signal sig: Signal, handler: @escaping (Signal) -> Void) -> DispatchSourceSignal {
        let signalSource = DispatchSource.makeSignalSource(signal: sig.rawValue)
        signal(sig.rawValue, SIG_IGN)
        signalSource.setEventHandler(handler: {
            signalSource.cancel()
            handler(sig)
        })
        signalSource.resume()
        return signalSource
    }

    /// The handler for incoming messages in server mode
    func serverHandler(
        context: ChannelHandlerContext,
        id: Int?,
        method: String,
        params: RPCObject,
        callback: (RPCResult) -> Void
    ) {
        SharedLogger.info("AutorestPlugin serverHandler method: \(method) id: \(id ?? -1)")
        switch method.lowercased() {
        case "getpluginnames":
            callback(
                .success(.list([.string("swift")]))
            )
        case "process":
            // Once a "process" message is received, switch to client mode
            sessionId = params.asList?.last?.asString
            processRequestId = id
            server.stop().whenComplete { _ in
                self.client.start(context: context, id: self.processRequestId)
            }
        default:
            callback(.failure(RPCError(kind: .invalidMethod, description: "Incoming Handler get invalid method")))
            SharedLogger.fail("invalid method: \(method)")
        }
    }

    /// The handler for client mode
    func clientHandler() {
        SharedLogger.info("AutorestPlugin clientHandler")
        let listInputsRequest: RPCObject = .list([.string(sessionId), .string("code-model-v4")])

        let future = client.call(method: "ListInputs", params: listInputsRequest)
        future.whenSuccess { result in
            switch result {
            case let .success(response):
                self.handleListInputs(response: response)
            case let .failure(error):
                SharedLogger.fail("Call ListInputs failure \(error)")
            }
        }
        future.whenFailure { error in
            SharedLogger.fail("Call ListInputs failure \(error)")
        }
    }

    func handleListInputs(response: RPCObject) {
        guard let filename = response.asList?.first?.asString else {
            SharedLogger.fail("handleListInputs filename is nil")
        }
        let readFileRequest: RPCObject = .list([.string(sessionId), .string(filename)])
        let future = client.call(method: "ReadFile", params: readFileRequest)
        future.whenSuccess { result in
            switch result {
            case let .success(response):
                self.handleReadFile(response: response)
            case let .failure(error):
                SharedLogger.fail("Call ReadFile failure \(error)")
            }
        }
        future.whenFailure { error in
            SharedLogger.fail("Call ReadFile failure \(error)")
        }
    }

    private func iterateDirectory(directory: URL) -> [String] {
        var generatedFileList: [String] = []

        guard let enumerator =
            FileManager.default.enumerator(atPath: directory.path) else {
            SharedLogger.fail("Iterate Directory fail")
        }

        while let file = enumerator.nextObject() as? String {
            if file.hasSuffix(".swift") || file.hasSuffix(".md") || file.hasSuffix(".yml") {
                generatedFileList.append(file)
            }
        }

        return generatedFileList
    }

    func handleReadFile(response: RPCObject) {
        guard let codeModel = response.asString else {
            SharedLogger.fail("Unable to retrieve code model from Autorest.")
        }
        Manager.shared.configure(input: codeModel, client: client, sessionId: sessionId) {
            let encoder = JSONEncoder()
            encoder.outputFormatting = .prettyPrinted
            let encoded = try! encoder.encode(Manager.shared.args)
            let arguments = String(data: encoded, encoding: .utf8)!
            SharedLogger.info("Autorest Configuration: \(arguments)")
            do {
                try Manager.shared.run()

                guard let packageUrl = Manager.shared.config?.tempPackageUrl else {
                    SharedLogger.fail("Unable to get packageUrl")
                }
                let generatedFileList = self.iterateDirectory(directory: packageUrl)
                generatedFileList.forEach {
                    self.sendWriteFile(fileName: $0, packageUrl: packageUrl)
                }
                self.sendProcessResponse()
            } catch {
                SharedLogger.fail("Code generation failure: \(error)")
            }
        }
    }

    func sendWriteFile(fileName: String, packageUrl: URL) {
        do {
            let fileUrl = packageUrl.appendingPathComponent(fileName)
            let fileContent = try String(contentsOf: fileUrl)

            let writeFileRequest: RPCObject = .list([.string(sessionId), .string(fileName), .string(fileContent)])
            let future = client.call(method: "WriteFile", params: writeFileRequest)
            future.whenSuccess { result in
                switch result {
                case let .success(response):
                    SharedLogger.debug("Call WriteFile succeed: \(response)")
                case let .failure(error):
                    SharedLogger.fail("Call WriteFile failure: \(error)")
                }
            }
            future.whenFailure { error in
                SharedLogger.fail("Call WriteFile failure: \(error)")
            }
        } catch {
            SharedLogger.fail("Call WriteFile failure: \(error)")
        }
    }

    func sendProcessResponse() {
        // Setup the client to write JSONResponse in output handler.
        // When Handler is setup, write JSONResponse to the handler's channel
        SharedLogger.info("SendProcessResponse")
        client.setupHandler { context in
            let response: JSONResponse
            response = JSONResponse(id: self.processRequestId, result: JSONObject(.bool(true)))
            let future = context.channel.writeAndFlush(response)
            future.whenFailure { error in
                SharedLogger.fail("Send Process Response failure: \(error)")
            }
        }
    }
}


// --------------------------------------------------------------------------
//
// Copyright (c) Microsoft Corporation. All rights reserved.
//
// The MIT License (MIT)
//
// 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.
//
// --------------------------------------------------------------------------

// TODO: Update license. Reproduced from: https://github.com/apple/swift-nio-examples/tree/master/json-rpc/Sources/JsonRpc

import Foundation
import NIO

public final class ChannelClient {
    public let group: MultiThreadedEventLoopGroup
    public var context: ChannelHandlerContext?
    private let handler: ProcessCallback
    var id: Int!

    public init(group: MultiThreadedEventLoopGroup, handler: @escaping ProcessCallback) {
        self.group = group
        self.handler = handler
        self.state = .initializing
    }

    deinit {
        assert(self.state == .stopped)
    }

    public func start(context: ChannelHandlerContext, id: Int) {
        SharedLogger.info("Client start")
        assert(state == .initializing)
        self.id = id

        let future = context.pipeline.removeHandler(name: "AutorestServer").flatMap {
            context.pipeline.removeHandler(name: "ServerHandler").flatMap {
                context.pipeline.addHandler(
                    CodableCodec<JSONResponse, JSONRequest>(),
                    name: "AutorestClient"
                ).flatMap {
                    context.pipeline.addHandler(
                        Handler(self.initalizationComplete),
                        name: "ClientHandler"
                    )
                }
            }
        }

        future.whenFailure { error in
            SharedLogger.fail("Switch to client mode failed: \(error)")
        }
    }

    public func setupHandler(initComplete: InitCompleteCallback?) {
        SharedLogger.info("Client setupHandler")
        guard let context = self.context else {
            SharedLogger.fail("No context in setupHandler")
        }

        let future = context.pipeline.removeHandler(name: "AutorestClient").flatMap {
            context.pipeline.removeHandler(name: "ClientHandler").flatMap {
                context.pipeline.addHandler(
                    CodableCodec<JSONRequest, JSONResponse>(initComplete),
                    name: "AutorestServer"
                )
            }
        }

        future.whenFailure { error in
            SharedLogger.fail("Switch to server mode failed: \(error)")
        }
    }

    private func initalizationComplete(context: ChannelHandlerContext) {
        SharedLogger.info("Client initializationComplete")
        state = .started
        self.context = context
        handler()
    }

    public func stop() {
        SharedLogger.info("Client stop")
        state = .stopped
    }

    public func call(method: String, params: RPCObject) -> EventLoopFuture<RPCResult> {
        if state != .started {
            SharedLogger.error("Client call failed. State is not started")
            return group.next().makeFailedFuture(ClientError.notReady)
        }
        guard let context = self.context else {
            SharedLogger.error("Client call failed. Context is nil")
            return group.next().makeFailedFuture(ClientError.notReady)
        }
        SharedLogger.info("Client call: \(method)")
        let promise: EventLoopPromise<JSONResponse> = context.channel.eventLoop.makePromise()
        var request: JSONRequest

        // WriteFile is a Notification (i.e. a JSONRequest with no id)
        if method == "WriteFile" {
            request = JSONRequest(method: method, params: JSONObject(params))
        } else {
            id += 1
            request = JSONRequest(id: id, method: method, params: JSONObject(params))
        }

        let requestWrapper = JSONRequestWrapper(request: request, promise: promise)
        let future = context.channel.writeAndFlush(requestWrapper)
        future.cascadeFailure(to: promise) // if write fails

        return future.flatMap {
            promise.futureResult.map { RPCResult($0) }
        }
    }

    private var _state = ChannelState.initializing
    private let lock = NSLock()
    private var state: ChannelState {
        get {
            return lock.withLock {
                _state
            }
        }
        set {
            lock.withLock {
                _state = newValue
            }
        }
    }
}

private class Handler: ChannelInboundHandler, ChannelOutboundHandler, RemovableChannelHandler {
    public typealias InboundIn = JSONResponse
    public typealias OutboundIn = JSONRequestWrapper
    public typealias OutboundOut = JSONRequest

    private var queue = CircularBuffer<(Int, EventLoopPromise<JSONResponse>)>()

    private let initComplete: InitCompleteCallback

    init(_ initComplete: @escaping InitCompleteCallback) {
        self.initComplete = initComplete
    }

    // outbound
    public func write(context: ChannelHandlerContext, data: NIOAny, promise: EventLoopPromise<Void>?) {
        SharedLogger.info("Client Handler write")
        let requestWrapper = unwrapOutboundIn(data)
        queue.append((requestWrapper.request.id ?? 0, requestWrapper.promise))
        context.write(wrapOutboundOut(requestWrapper.request), promise: promise)
    }

    // inbound
    public func channelRead(context: ChannelHandlerContext, data: NIOAny) {
        SharedLogger.info("Client channelRead")
        if queue.isEmpty {
            return context.fireChannelRead(data) // already complete
        }
        let promise = queue.removeFirst().1
        let response = unwrapInboundIn(data)
        promise.succeed(response)
    }

    public func handlerAdded(context: ChannelHandlerContext) {
        SharedLogger.info("Client handlerAdded")
        initComplete(context)
    }

    public func errorCaught(context: ChannelHandlerContext, error: Error) {
        SharedLogger.error("Client Handler errorCaught. error=\(error)")

        if queue.isEmpty {
            return context.fireErrorCaught(error) // already complete
        }
        let item = queue.removeFirst()
        let requestId = item.0
        let promise = item.1
        switch error {
        case CodecError.requestTooLarge, CodecError.badFraming, CodecError.badJSON:
            promise.succeed(JSONResponse(id: requestId, errorCode: .parseError, error: error))
        default:
            promise.fail(error)
            // close the connection
            context.close(promise: nil)
        }
    }
}

internal struct JSONRequestWrapper {
    let request: JSONRequest
    let promise: EventLoopPromise<JSONResponse>
}

internal extension ResultType where Value == RPCObject, Error == RPCError {
    init(_ response: JSONResponse) {
        if let result = response.result {
            self = .success(RPCObject(result))
        } else if let error = response.error {
            self = .failure(RPCError(error))
        } else {
            self = .failure(RPCError(kind: .invalidServerResponse, description: "invalid server response"))
        }
    }
}


// --------------------------------------------------------------------------
//
// Copyright (c) Microsoft Corporation. All rights reserved.
//
// The MIT License (MIT)
//
// 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.
//
// --------------------------------------------------------------------------

// TODO: Update license. Reproduced from: https://github.com/apple/swift-nio-examples/tree/master/json-rpc/Sources/JsonRpc

import Foundation
import NIO
import NIOFoundationCompat

private let maxPayload = 1_000_000 // 1MB
internal extension ChannelPipeline {
    func addTimeoutHandlers(_ timeout: TimeAmount) -> EventLoopFuture<Void> {
        return addHandlers([IdleStateHandler(readTimeout: timeout), HalfCloseOnTimeout()])
    }
}

internal extension ChannelPipeline {
    func addFramingHandlers(framing: Framing) -> EventLoopFuture<Void> {
        switch framing {
        case .jsonpos:
            return addHandlers([
                ByteToMessageHandler(ContentLengthHeaderFrameDecoder()),
                MessageToByteHandler(ContentLengthHeaderFrameEncoder())
            ])
        default:
            fatalError("\(self) not supported.")
        }
    }
}

// aggregate bytes till delimiter and add delimiter at end
internal final class NewlineEncoder: ByteToMessageDecoder, MessageToByteEncoder {
    public typealias InboundIn = ByteBuffer
    public typealias InboundOut = ByteBuffer
    public typealias OutboundIn = ByteBuffer
    public typealias OutboundOut = ByteBuffer

    private let delimiter1 = UInt8(ascii: "\r")
    private let delimiter2 = UInt8(ascii: "\n")

    private var lastIndex = 0

    // inbound
    public func decode(context: ChannelHandlerContext, buffer: inout ByteBuffer) throws -> DecodingState {
        guard buffer.readableBytes < maxPayload else {
            throw CodecError.requestTooLarge
        }
        guard buffer.readableBytes >= 3 else {
            return .needMoreData
        }

        // try to find a payload by looking for a \r\n delimiter
        let readableBytesView = buffer.readableBytesView.dropFirst(lastIndex)
        guard let index = readableBytesView.firstIndex(of: delimiter2) else {
            lastIndex = buffer.readableBytes
            return .needMoreData
        }
        guard readableBytesView[index - 1] == delimiter1 else {
            return .needMoreData
        }

        // slice the buffer
        let length = index - buffer.readerIndex - 1
        let slice = buffer.readSlice(length: length)!
        buffer.moveReaderIndex(forwardBy: 2)
        lastIndex = 0
        // call next handler
        context.fireChannelRead(wrapInboundOut(slice))
        return .continue
    }

    public func decodeLast(
        context: ChannelHandlerContext,
        buffer: inout ByteBuffer,
        seenEOF _: Bool
    ) throws -> DecodingState {
        while try decode(context: context, buffer: &buffer) == .continue {}
        if buffer.readableBytes > buffer.readerIndex {
            throw CodecError.badFraming
        }
        return .needMoreData
    }

    // outbound
    public func encode(data: OutboundIn, out: inout ByteBuffer) throws {
        var payload = data
        // original data
        out.writeBuffer(&payload)
        // add delimiter
        out.writeBytes([delimiter1, delimiter2])
    }
}

// bytes to codable and back
internal final class CodableCodec<In, Out>: ChannelInboundHandler, ChannelOutboundHandler,
    RemovableChannelHandler where In: Decodable,
    Out: Encodable {
    public typealias InboundIn = ByteBuffer
    public typealias InboundOut = In
    public typealias OutboundIn = Out
    public typealias OutboundOut = ByteBuffer

    private let decoder = JSONDecoder()
    private let encoder = JSONEncoder()

    private let initComplete: InitCompleteCallback?

    public init(_ initComplete: InitCompleteCallback? = nil) {
        self.initComplete = initComplete
    }

    // inbound
    public func channelRead(context: ChannelHandlerContext, data: NIOAny) {
        var buffer = unwrapInboundIn(data)
        let data = buffer.readData(length: buffer.readableBytes)!
        do {
            SharedLogger.debug(
                "--> decoding \(String(decoding: data[data.startIndex ..< min(data.startIndex + 100, data.endIndex)], as: UTF8.self))"
            )
            let decodable = try decoder.decode(In.self, from: data)
            // call next handler
            context.fireChannelRead(wrapInboundOut(decodable))
        } catch let error as DecodingError {
            SharedLogger.error("DecodingError: \(error)")
            context.fireErrorCaught(CodecError.badJSON(error))
        } catch {
            SharedLogger.error("Read Error: \(error)")
            context.fireErrorCaught(error)
        }
    }

    // outbound
    public func write(context: ChannelHandlerContext, data: NIOAny, promise: EventLoopPromise<Void>?) {
        do {
            let encodable = unwrapOutboundIn(data)
            let data = try encoder.encode(encodable)
            SharedLogger.debug("<-- encoding \(String(decoding: data, as: UTF8.self))")
            var buffer = context.channel.allocator.buffer(capacity: data.count)
            buffer.writeBytes(data)
            context.write(wrapOutboundOut(buffer), promise: promise)
        } catch let error as EncodingError {
            SharedLogger.error("EncodingError: \(error)")
            promise?.fail(CodecError.badJSON(error))
        } catch {
            SharedLogger.error("Write Error: \(error)")
            promise?.fail(error)
        }
    }

    public func handlerAdded(context: ChannelHandlerContext) {
        initComplete?(context)
    }
}

internal final class HalfCloseOnTimeout: ChannelInboundHandler {
    typealias InboundIn = Any

    func userInboundEventTriggered(context: ChannelHandlerContext, event: Any) {
        if event is IdleStateHandler.IdleStateEvent {
            // this will trigger ByteToMessageDecoder::decodeLast which is required to
            // recognize partial frames
            context.fireUserInboundEventTriggered(ChannelEvent.inputClosed)
        }
        context.fireUserInboundEventTriggered(event)
    }
}

internal enum CodecError: Error {
    case badFraming
    case badJSON(Error)
    case requestTooLarge
}


// --------------------------------------------------------------------------
//
// Copyright (c) Microsoft Corporation. All rights reserved.
//
// The MIT License (MIT)
//
// 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 Foundation
import NIO

enum ChannelState: Equatable {
    case initializing
    case starting
    case started
    case stopping
    case stopped
}


// --------------------------------------------------------------------------
//
// Copyright (c) Microsoft Corporation. All rights reserved.
//
// The MIT License (MIT)
//
// 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.
//
// --------------------------------------------------------------------------

//===----------------------------------------------------------------------===//
//
// This source file is part of the SwiftNIO open source project
//
// Copyright (c) 2019 Apple Inc. and the SwiftNIO project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of SwiftNIO project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//

import NIO

/// `ContentLengthHeaderFrameEncoder` is responsible for emitting JSON-RPC wire protocol with 'Content-Length'
/// HTTP-like headers as used by for example by LSP (Language Server Protocol).
public final class ContentLengthHeaderFrameEncoder: MessageToByteEncoder {
    /// We'll get handed one message through the `Channel` and ...
    public typealias OutboundIn = ByteBuffer
    /// ... will encode it into a `ByteBuffer`.
    public typealias OutboundOut = ByteBuffer

    private var scratchBuffer: ByteBuffer!

    public init() {
        self.scratchBuffer = ByteBuffer()
    }

    public func encode(data: OutboundIn, out: inout ByteBuffer) throws {
        // Step 1, clear the target buffer (note, we are re-using it so if we get lucky we don't need to
        // allocate at all.
        scratchBuffer.clear()

        // Step 2, write the wire protocol for the header.
        scratchBuffer.writeStaticString("Content-Length: ")
        scratchBuffer.writeString(String(data.readableBytes, radix: 10))
        scratchBuffer.writeStaticString("\r\n\r\n")

        // Step 3, send header and the raw message through the `Channel`.
        if data.readableBytes > 0 {
            out.writeBuffer(&scratchBuffer)
            var payload = data
            out.writeBuffer(&payload)
        } else {
            var payload = data
            out.writeBuffer(&payload)
        }
    }
}

/// `ContentLengthHeaderFrameDecoder` is responsible for parsing JSON-RPC wire protocol with 'Content-Length'
/// HTTP-like headers as used by for example by LSP (Language Server Protocol).
public struct ContentLengthHeaderFrameDecoder: ByteToMessageDecoder {
    /// We're emitting one `ByteBuffer` corresponding exactly to one full payload, no headers etc.
    public typealias InboundOut = ByteBuffer

    /// `ContentLengthHeaderFrameDecoder` is a simple state machine.
    private enum State {
        /// either we're waiting for the end of the header block or a new header field, ...
        case waitingForHeaderNameOrHeaderBlockEnd
        /// ... or for a header value, or ...
        case waitingForHeaderValue(name: String)
        /// ... or for the payload of a given size.
        case waitingForPayload(length: Int)
    }

    /// A `DecodingError` is sent through the pipeline if anything went wrong.
    public enum DecodingError: Error, Equatable {
        /// Missing 'Content-Length' header.
        case missingContentLengthHeader

        /// The value of the 'Content-Length' header was illegal, for example a negative number.
        case illegalContentLengthHeaderValue(String)
    }

    public init() {}

    // We start waiting for a header field (or the end of a header block).
    private var state: State = .waitingForHeaderNameOrHeaderBlockEnd
    private var payloadLength: Int?

    // Finishes a header block, most of the time that's very straighforward but we need to verify a few
    // things here.
    private mutating func processHeaderBlockEnd(context: ChannelHandlerContext) throws -> DecodingState {
        if let payloadLength = self.payloadLength {
            if payloadLength == 0 {
                // special case, we're not actually waiting for anything if it's 0 bytes...
                state = .waitingForHeaderNameOrHeaderBlockEnd
                self.payloadLength = nil
                context.fireChannelRead(wrapInboundOut(context.channel.allocator.buffer(capacity: 0)))
                return .continue
            }
            // cool, let's just shift to the `.waitingForPayload` state and continue.
            state = .waitingForPayload(length: payloadLength)
            self.payloadLength = nil
            return .continue
        } else {
            // this happens if we reached the end of the header block but we haven't seen the Content-Length
            // header, that's an error. It will be sent through the `Channel` and decoder won't be called
            // again.
            throw DecodingError.missingContentLengthHeader
        }
    }

    // `decode` will be invoked whenever there is more data available (or if we return `.continue`).
    public mutating func decode(context: ChannelHandlerContext, buffer: inout ByteBuffer) throws -> DecodingState {
        switch state {
        case .waitingForHeaderNameOrHeaderBlockEnd:
            // Given that we're waiting for the end of a header block or a new header field, it's sensible to
            // check if this might be the end of the block.
            if buffer.readableBytesView.starts(with: "\r\n".utf8) {
                buffer.moveReaderIndex(forwardBy: 2) // skip \r\n\r\n
                return try processHeaderBlockEnd(context: context)
            }

            // Given that this is not the end of a header block, it must be a new header field. A new header field
            // must always have a colon (or we don't have enough data).
            if let colonIndex = buffer.readableBytesView.firstIndex(of: UInt8(ascii: ":")) {
                let headerName = buffer.readString(length: colonIndex - buffer.readableBytesView.startIndex)!
                buffer.moveReaderIndex(forwardBy: 1) // skip the colon
                state = .waitingForHeaderValue(name: headerName.trimmed().lowercased())
                return .continue
            }

            return .needMoreData
        case let .waitingForHeaderValue(name: headerName):
            // Cool, we're waiting for a header value (ie. we're after the colon).
            // Let's not bother unless we found the whole thing
            guard let newlineIndex = buffer.readableBytesView.firstIndex(of: UInt8(ascii: "\n")) else {
                return .needMoreData
            }

            // Is this a header we actually care about?
            if headerName == "content-length" {
                // Yes, let's parse the int.
                let headerValue = buffer.readString(length: newlineIndex - buffer.readableBytesView.startIndex + 1)!
                if let length = UInt32(headerValue.trimmed()) { // anything more than 4GB or negative doesn't make sense
                    payloadLength = .init(length)
                } else {
                    throw DecodingError.illegalContentLengthHeaderValue(headerValue)
                }
            } else {
                // Nope, let's just skip over it
                buffer.moveReaderIndex(forwardBy: newlineIndex - buffer.readableBytesView.startIndex + 1)
            }

            // but in any case, we're now waiting for a new header or the end of the header block again.
            state = .waitingForHeaderNameOrHeaderBlockEnd
            return .continue
        case let .waitingForPayload(length: length):
            // That's the easiest case, let's just wait until we have enough data.
            if let payload = buffer.readSlice(length: length) {
                // Cool, we got enough data, let's go back waiting for a new header block.
                state = .waitingForHeaderNameOrHeaderBlockEnd
                // And send what we found through the pipeline.
                context.fireChannelRead(wrapInboundOut(payload))
                return .continue
            } else {
                return .needMoreData
            }
        }
    }

    /// Invoked when the `Channel` is being brough down.
    public mutating func decodeLast(
        context: ChannelHandlerContext,
        buffer: inout ByteBuffer,
        seenEOF _: Bool
    ) throws -> DecodingState {
        // Last chance to decode anything.
        while try decode(context: context, buffer: &buffer) == .continue {}

        if buffer.readableBytes > 0 {
            // Oops, there are leftovers that don't form a full message, we could ignore those but it doesn't hurt to send
            // an error.
            throw ByteToMessageDecoderError.leftoverDataWhenDone(buffer)
        }
        return .needMoreData
    }
}


// --------------------------------------------------------------------------
//
// Copyright (c) Microsoft Corporation. All rights reserved.
//
// The MIT License (MIT)
//
// 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 Foundation

extension RPCObject {
    public var asString: String? {
        switch self {
        case let .string(value):
            return value
        default:
            return nil
        }
    }

    public var asList: [RPCObject]? {
        switch self {
        case let .list(values):
            return values
        default:
            return nil
        }
    }

    public var asInt: Int? {
        switch self {
        case let .integer(value):
            return value
        default:
            return nil
        }
    }

    public var asDouble: Double? {
        switch self {
        case let .double(value):
            return value
        default:
            return nil
        }
    }

    public var asBool: Bool? {
        switch self {
        case let .bool(value):
            return value
        default:
            return nil
        }
    }

    public var asDict: [String: RPCObject]? {
        switch self {
        case let .dictionary(value):
            return value
        default:
            return nil
        }
    }
}


// --------------------------------------------------------------------------
//
// Copyright (c) Microsoft Corporation. All rights reserved.
//
// The MIT License (MIT)
//
// 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.
//
// --------------------------------------------------------------------------

// TODO: Update license. Reproduced from: https://github.com/apple/swift-nio-examples/tree/master/json-rpc/Sources/JsonRpc

import Foundation
import NIO

/*
 spec from https://www.jsonrpc.org/specification
 -----------------------------------------------
 Request object
 A rpc call is represented by sending a Request object to a Server. The Request object has the following members:
 jsonrpc
 A String specifying the version of the JSON-RPC protocol. MUST be exactly "2.0".
 method
 A String containing the name of the method to be invoked. Method names that begin with the word rpc followed by a period character (U+002E or ASCII 46) are reserved for rpc-internal methods and extensions and MUST NOT be used for anything else.
 params
 A Structured value that holds the parameter values to be used during the invocation of the method. This member MAY be omitted.
 id
 An identifier established by the Client that MUST contain a String, Number, or NULL value if included. If it is not included it is assumed to be a notification. The value SHOULD normally not be Null [1] and Numbers SHOULD NOT contain fractional parts [2]
 The Server MUST reply with the same value in the Response object if included. This member is used to correlate the context between the two objects.
 [1] The use of Null as a value for the id member in a Request object is discouraged, because this specification uses a value of Null for Responses with an unknown id. Also, because JSON-RPC 1.0 uses an id value of Null for Notifications this could cause confusion in handling.
 [2] Fractional parts may be problematic, since many decimal fractions cannot be represented exactly as binary fractions.
 4.1 Notification
 A Notification is a Request object without an "id" member. A Request object that is a Notification signifies the Client's lack of interest in the corresponding Response object, and as such no Response object needs to be returned to the client. The Server MUST NOT reply to a Notification, including those that are within a batch request.
 Notifications are not confirmable by definition, since they do not have a Response object to be returned. As such, the Client would not be aware of any errors (like e.g. "Invalid params","Internal error").
 4.2 Parameter Structures
 If present, parameters for the rpc call MUST be provided as a Structured value. Either by-position through an Array or by-name through an Object.
 by-position: params MUST be an Array, containing the values in the Server expected order.
 by-name: params MUST be an Object, with member names that match the Server expected parameter names. The absence of expected names MAY result in an error being generated. The names MUST match exactly, including case, to the method's expected parameters.
 Response object
 When a rpc call is made, the Server MUST reply with a Response, except for in the case of Notifications. The Response is expressed as a single JSON Object, with the following members:
 jsonrpc
 A String specifying the version of the JSON-RPC protocol. MUST be exactly "2.0".
 result
 This member is REQUIRED on success.
 This member MUST NOT exist if there was an error invoking the method.
 The value of this member is determined by the method invoked on the Server.
 error
 This member is REQUIRED on error.
 This member MUST NOT exist if there was no error triggered during invocation.
 The value for this member MUST be an Object as defined in section 5.1.
 id
 This member is REQUIRED.
 It MUST be the same as the value of the id member in the Request Object.
 If there was an error in detecting the id in the Request object (e.g. Parse error/Invalid Request), it MUST be Null.
 Either the result member or error member MUST be included, but both members MUST NOT be included.
 5.1 Error object
 When a rpc call encounters an error, the Response Object MUST contain the error member with a value that is a Object with the following members:
 code
 A Number that indicates the error type that occurred.
 This MUST be an integer.
 message
 A String providing a short description of the error.
 The message SHOULD be limited to a concise single sentence.
 data
 A Primitive or Structured value that contains additional information about the error.
 This may be omitted.
 The value of this member is defined by the Server (e.g. detailed error information, nested errors etc.).
 The error codes from and including -32768 to -32000 are reserved for pre-defined errors. Any code within this range, but not defined explicitly below is reserved for future use. The error codes are nearly the same as those suggested for XML-RPC at the following url: http://xmlrpc-epi.sourceforge.net/specs/rfc.fault_codes.php
 code    message    meaning
 -32700    Parse error    Invalid JSON was received by the server.
 An error occurred on the server while parsing the JSON text.
 -32600    Invalid Request    The JSON sent is not a valid Request object.
 -32601    Method not found    The method does not exist / is not available.
 -32602    Invalid params    Invalid method parameter(s).
 -32603    Internal error    Internal JSON-RPC error.
 -32000 to -32099    Server error    Reserved for implementation-defined server-errors.
 */

private let jsonrpcVersion = "2.0"

internal struct JSONRequest: Codable {
    var jsonrpc: String
    var id: Int?
    var method: String
    var params: JSONObject

    init(id: Int, method: String, params: JSONObject) {
        self.jsonrpc = jsonrpcVersion
        self.id = id
        self.method = method
        self.params = params
    }

    init(method: String, params: JSONObject) {
        self.jsonrpc = jsonrpcVersion
        self.method = method
        self.params = params
    }
}

internal struct JSONResponse: Codable {
    var jsonrpc: String
    var id: Int
    var result: JSONObject?
    var error: JSONError?

    init(id: Int, result: JSONObject) {
        self.jsonrpc = jsonrpcVersion
        self.id = id
        self.result = result
        self.error = nil
    }

    init(id: Int, error: JSONError) {
        self.jsonrpc = jsonrpcVersion
        self.id = id
        self.result = nil
        self.error = error
    }

    init(id: Int, errorCode: JSONErrorCode, error: Error) {
        self.init(id: id, error: JSONError(code: errorCode, error: error))
    }

    init(id: Int, result: RPCObject) {
        self.init(id: id, result: JSONObject(result))
    }

    init(id: Int, error: RPCError) {
        self.init(id: id, error: JSONError(error))
    }
}

internal struct JSONError: Codable {
    var code: Int
    var message: String
    var data: [String: String]?

    init(code: Int, message: String) {
        self.code = code
        self.message = message
        self.data = nil
    }

    init(code: JSONErrorCode, message: String) {
        self.init(code: code.rawValue, message: message)
    }

    init(code: JSONErrorCode, error: Error) {
        self.init(code: code, message: String(describing: error))
    }

    init(_ error: RPCError) {
        switch error.kind {
        case .invalidMethod:
            self.init(code: .methodNotFound, message: error.description)
        case .invalidParams:
            self.init(code: .invalidParams, message: error.description)
        case .invalidRequest:
            self.init(code: .invalidRequest, message: error.description)
        case .invalidServerResponse, .otherServerError:
            self.init(code: .other, message: error.description)
        }
    }
}

internal enum JSONErrorCode: Int, Codable {
    case parseError = -32700
    case invalidRequest = -32600
    case methodNotFound = -32601
    case invalidParams = -32602
    case internalError = -32603
    case other = -32000
}

internal enum JSONObject: Codable {
    case none
    case string(String)
    case integer(Int)
    case double(Double)
    case bool(Bool)
    case list([JSONObject])
    case dictionary([String: JSONObject])

    init(_ object: RPCObject) {
        switch object {
        case .none:
            self = .none
        case let .string(value):
            self = .string(value)
        case let .integer(value):
            self = .integer(value)
        case let .double(value):
            self = .double(value)
        case let .bool(value):
            self = .bool(value)
        case let .list(value):
            self = .list(value.map { JSONObject($0) })
        case let .dictionary(value):
            self = .dictionary(value.mapValues { JSONObject($0) })
        }
    }
}

internal extension JSONObject {
    enum CodingKeys: CodingKey {
        case string
        case integer
        case double
        case bool
        case list
        case dictionary
    }

    // FIXME: is there a more elegant way?
    init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        do {
            let value = try container.decode(String.self)
            self = .string(value)
        } catch {
            do {
                let value = try container.decode(Int.self)
                self = .integer(value)
            } catch {
                do {
                    let value = try container.decode(Double.self)
                    self = .double(value)
                } catch {
                    do {
                        let value = try container.decode(Bool.self)
                        self = .bool(value)
                    } catch {
                        do {
                            let value = try container.decode([JSONObject].self)
                            self = .list(value)
                        } catch {
                            do {
                                let value = try container.decode([String: JSONObject].self)
                                self = .dictionary(value)
                            } catch {
                                self = .none
                            }
                        }
                    }
                }
            }
        }
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.singleValueContainer()
        switch self {
        case .none:
            break
        case let .string(value):
            try container.encode(value)
        case let .integer(value):
            try container.encode(value)
        case let .double(value):
            try container.encode(value)
        case let .bool(value):
            try container.encode(value)
        case let .list(value):
            try container.encode(value)
        case let .dictionary(value):
            try container.encode(value)
        }
    }
}

public enum RPCObject: Equatable {
    case none
    case string(String)
    case integer(Int)
    case double(Double)
    case bool(Bool)
    case list([RPCObject])
    case dictionary([String: RPCObject])

    public init(_ value: String) {
        self = .string(value)
    }

    public init(_ value: Int) {
        self = .integer(value)
    }

    public init(_ value: Double) {
        self = .double(value)
    }

    public init(_ value: Bool) {
        self = .bool(value)
    }

    public init(_ value: [String]) {
        self = .list(value.map { RPCObject($0) })
    }

    public init(_ value: [Int]) {
        self = .list(value.map { RPCObject($0) })
    }

    public init(_ value: [String: String]) {
        self = .dictionary(value.mapValues { RPCObject($0) })
    }

    public init(_ value: [String: Int]) {
        self = .dictionary(value.mapValues { RPCObject($0) })
    }

    public init(_ value: [RPCObject]) {
        self = .list(value)
    }

    internal init(_ object: JSONObject) {
        switch object {
        case .none:
            self = .none
        case let .string(value):
            self = .string(value)
        case let .integer(value):
            self = .integer(value)
        case let .double(value):
            self = .double(value)
        case let .bool(value):
            self = .bool(value)
        case let .list(value):
            self = .list(value.map { RPCObject($0) })
        case let .dictionary(value):
            self = .dictionary(value.mapValues { RPCObject($0) })
        }
    }
}

public struct RPCError {
    public let kind: Kind
    public let description: String

    init(kind: Kind, description: String) {
        self.kind = kind
        self.description = description
    }

    internal init(_ error: JSONError) {
        self.init(
            kind: JSONErrorCode(rawValue: error.code).map { Kind($0) } ?? .otherServerError,
            description: error.message
        )
    }

    public enum Kind {
        case invalidMethod
        case invalidParams
        case invalidRequest
        case invalidServerResponse
        case otherServerError

        internal init(_ code: JSONErrorCode) {
            switch code {
            case .invalidRequest:
                self = .invalidRequest
            case .methodNotFound:
                self = .invalidMethod
            case .invalidParams:
                self = .invalidParams
            case .parseError:
                self = .invalidServerResponse
            case .internalError, .other:
                self = .otherServerError
            }
        }
    }
}

public typealias RPCClosure = (ChannelHandlerContext, Int?, String, RPCObject, (RPCResult) -> Void) -> Void
public typealias InitCompleteCallback = (ChannelHandlerContext) -> Void
public typealias ProcessCallback = () -> Void
public typealias RPCResult = ResultType<RPCObject, RPCError>