//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014 - 2020 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//

package import Csourcekitd
package import Foundation
@_spi(SourceKitLSP) import SKLogging
import SwiftExtensions
import Synchronization
@_spi(SourceKitLSP) import ToolsProtocolsSwiftExtensions

extension sourcekitd_api_keys: @unchecked Sendable {}
extension sourcekitd_api_requests: @unchecked Sendable {}
extension sourcekitd_api_values: @unchecked Sendable {}

fileprivate extension Mutex {
  /// If the wrapped value is `nil`, run `compute` and store the computed value. If it is not `nil`, return the stored
  /// value.
  borrowing func computeIfNil<WrappedValue: Sendable>(
    compute: () -> WrappedValue
  ) -> WrappedValue where Value == WrappedValue? {
    return withLock { value in
      if let value {
        return value
      }
      let computed = compute()
      value = computed
      return computed
    }
  }
}

#if canImport(Darwin)
private func setenv(name: String, value: String, override: Bool) throws {
  struct FailedToSetEnvError: Error {
    let errorCode: Int32
  }
  try name.withCString { name in
    try value.withCString { value in
      let result = setenv(name, value, override ? 0 : 1)
      if result != 0 {
        throw FailedToSetEnvError(errorCode: result)
      }
    }
  }
}
#endif

private struct SourceKitDRequestHandle: Sendable {
  /// `nonisolated(unsafe)` is fine because we just use the handle as an opaque value.
  nonisolated(unsafe) let handle: sourcekitd_api_request_handle_t
}

package struct PluginPaths: Equatable, CustomLogStringConvertible {
  package let clientPlugin: URL
  package let servicePlugin: URL

  package init(clientPlugin: URL, servicePlugin: URL) {
    self.clientPlugin = clientPlugin
    self.servicePlugin = servicePlugin
  }

  package var description: String {
    "(client: \(clientPlugin), service: \(servicePlugin))"
  }

  var redactedDescription: String {
    "(client: \(clientPlugin.description.hashForLogging), service: \(servicePlugin.description.hashForLogging))"
  }
}

package enum SKDError: Error, Equatable {
  /// The service has crashed.
  case connectionInterrupted

  /// The request was unknown or had an invalid or missing parameter.
  case requestInvalid(String)

  /// The request failed.
  case requestFailed(String)

  /// The request was cancelled.
  case requestCancelled

  /// The request exceeded the maximum allowed duration.
  case timedOut

  /// Loading a required symbol from the sourcekitd library failed.
  case missingRequiredSymbol(String)
}

/// Wrapper for sourcekitd that provides the high-level `send` API, notification handler
/// multiplexing, and convenience UID accessors.
///
/// Dylib lifecycle (`initialize`, `shutdown`, `set_notification_handler`) is managed by the
/// underlying `SourceKitDCore` implementation, not by this type directly.
package actor SourceKitD {
  /// The underlying dylib connection (owns the handle, initialize/shutdown, and raw notification
  /// callback registration).
  nonisolated package let core: any SourceKitDCore

  /// Path to the sourcekitd dylib.
  nonisolated package var path: URL { core.path }

  /// The sourcekitd API functions loaded from `core.dlHandle`.
  nonisolated package let api: sourcekitd_api_functions_t

  /// General API for the SourceKit service and client framework, eg. for plugin initialization and to set up custom
  /// variant functions.
  ///
  /// This must not be referenced outside of `SwiftSourceKitPlugin`, `SwiftSourceKitPluginCommon`, or
  /// `SwiftSourceKitClientPlugin`.
  package nonisolated var pluginApi: sourcekitd_plugin_api_functions_t { try! pluginApiResult.get() }
  private let pluginApiResult: Result<sourcekitd_plugin_api_functions_t, any Error>

  /// The API with which the SourceKit plugin handles requests.
  ///
  /// This must not be referenced outside of `SwiftSourceKitPlugin`.
  package nonisolated var servicePluginApi: sourcekitd_service_plugin_api_functions_t {
    do {
      return try servicePluginApiResult.get()
    } catch {
      fatalError("failed to get service plugin api, error: \(error)")
    }
  }
  private let servicePluginApiResult: Result<sourcekitd_service_plugin_api_functions_t, any Error>

  /// The API with which the SourceKit plugin communicates with the type-checker in-process.
  ///
  /// This must not be referenced outside of `SwiftSourceKitPlugin`.
  package nonisolated var ideApi: sourcekitd_ide_api_functions_t { try! ideApiResult.get() }
  private let ideApiResult: Result<sourcekitd_ide_api_functions_t, any Error>

  /// Convenience for accessing known keys.
  ///
  /// These need to be computed dynamically so that a client has the chance to register a UID handler between the
  /// initialization of the SourceKit plugin and the first request being handled by it.
  private let _keys: Mutex<sourcekitd_api_keys?> = Mutex(nil)
  package nonisolated var keys: sourcekitd_api_keys {
    _keys.computeIfNil { sourcekitd_api_keys(api: self.api) }
  }

  /// Convenience for accessing known request names.
  ///
  /// These need to be computed dynamically so that a client has the chance to register a UID handler between the
  /// initialization of the SourceKit plugin and the first request being handled by it.
  private let _requests: Mutex<sourcekitd_api_requests?> = Mutex(nil)
  package nonisolated var requests: sourcekitd_api_requests {
    _requests.computeIfNil { sourcekitd_api_requests(api: self.api) }
  }

  /// Convenience for accessing known request/response values.
  ///
  /// These need to be computed dynamically so that a client has the chance to register a UID handler between the
  /// initialization of the SourceKit plugin and the first request being handled by it.
  private let _values: Mutex<sourcekitd_api_values?> = Mutex(nil)
  package nonisolated var values: sourcekitd_api_values {
    _values.computeIfNil { sourcekitd_api_values(api: self.api) }
  }

  private nonisolated let notificationHandlingQueue = AsyncQueue<Serial>()

  /// List of notification handlers that will be called for each notification.
  private var notificationHandlers: [WeakSKDNotificationHandler] = []

  /// List of hooks that should be executed and that need to finish executing before a request is sent to sourcekitd.
  private var preRequestHandlingHooks: [UUID: @Sendable (SKDRequestDictionary) async -> Void] = [:]

  /// List of hooks that should be executed after a request sent to sourcekitd.
  private var requestHandlingHooks: [UUID: (SKDRequestDictionary) -> Void] = [:]

  package static func getOrCreate(
    dylibPath: URL,
    pluginPaths: PluginPaths?
  ) async throws -> SourceKitD {
    try await SourceKitDRegistry.shared.getOrAdd(dylibPath, pluginPaths: pluginPaths) {
      return try SourceKitD(dylib: dylibPath, pluginPaths: pluginPaths)
    }
  }

  package static func getOrCreate(core: any SourceKitDCore) async throws -> SourceKitD {
    try await SourceKitDRegistry.shared.getOrAdd(core.path, pluginPaths: nil) {
      return try SourceKitD(core: core)
    }
  }

  /// Create a `SourceKitD` wrapping an existing `SourceKitDCore` connection.
  package init(core: any SourceKitDCore) throws {
    self.core = core
    let api = try sourcekitd_api_functions_t(core.dlHandle)
    self.api = api
    // We load the plugin-related functions eagerly so the members are initialized and we don't have data races on first
    // access to eg. `pluginApi`. But if one of the functions is missing, we will only emit that error when that family
    // of functions is being used. For example, it is expected that the plugin functions are not available in
    // SourceKit-LSP.
    self.ideApiResult = Result(catching: { try sourcekitd_ide_api_functions_t(core.dlHandle) })
    self.pluginApiResult = Result(catching: { try sourcekitd_plugin_api_functions_t(core.dlHandle) })
    self.servicePluginApiResult = Result(catching: { try sourcekitd_service_plugin_api_functions_t(core.dlHandle) })

    core.initializeService(
      api: api,
      notificationCallback: { [weak self] rawResponse in
        guard let self else { return }
        let response = SKDResponse(rawResponse, sourcekitd: self)
        self.notificationHandlingQueue.async {
          let handlers = await self.notificationHandlers.compactMap(\.value)
          for handler in handlers {
            handler.notification(response)
          }
        }
      }
    )
  }

  package init(dylib path: URL, pluginPaths: PluginPaths?) throws {
    try self.init(core: SourceKitDCoreImpl(dylib: path, pluginPaths: pluginPaths))
  }

  /// Adds a new notification handler (referenced weakly).
  package func addNotificationHandler(_ handler: any SKDNotificationHandler) {
    notificationHandlers.removeAll(where: { $0.value == nil })
    notificationHandlers.append(.init(handler))
  }

  /// Removes a previously registered notification handler.
  package func removeNotificationHandler(_ handler: any SKDNotificationHandler) {
    notificationHandlers.removeAll(where: { $0.value == nil || $0.value === handler })
  }

  /// Execute `body` and invoke `hook` for every sourcekitd request that is sent during the execution time of `body`.
  ///
  /// Note that `hook` will not only be executed for requests sent *by* body but this may also include sourcekitd
  /// requests that were sent by other clients of the same `DynamicallyLoadedSourceKitD` instance that just happen to
  /// send a request during that time.
  ///
  /// This is intended for testing only.
  package func withPreRequestHandlingHook(
    body: () async throws -> Void,
    hook: @escaping @Sendable (SKDRequestDictionary) async -> Void
  ) async rethrows {
    let id = UUID()
    preRequestHandlingHooks[id] = hook
    defer { preRequestHandlingHooks[id] = nil }
    try await body()
  }

  func willSend(request: SKDRequestDictionary) async {
    let request = request
    for hook in preRequestHandlingHooks.values {
      await hook(request)
    }
  }

  /// Execute `body` and invoke `hook` for every sourcekitd request that is sent during the execution time of `body`.
  ///
  /// Note that `hook` will not only be executed for requests sent *by* body but this may also include sourcekitd
  /// requests that were sent by other clients of the same `DynamicallyLoadedSourceKitD` instance that just happen to
  /// send a request during that time.
  ///
  /// This is intended for testing only.
  package func withRequestHandlingHook(
    body: () async throws -> Void,
    hook: @escaping (SKDRequestDictionary) -> Void
  ) async rethrows {
    let id = UUID()
    requestHandlingHooks[id] = hook
    defer { requestHandlingHooks[id] = nil }
    try await body()
  }

  func didSend(request: SKDRequestDictionary) {
    for hook in requestHandlingHooks.values {
      hook(request)
    }
  }

  private struct ContextualRequest {
    enum Kind {
      case editorOpen
      case codeCompleteOpen
    }
    let kind: Kind
    let request: SKDRequestDictionary
  }

  private var contextualRequests: [URL: [ContextualRequest]] = [:]

  private func recordContextualRequest(
    requestUid: sourcekitd_api_uid_t,
    request: SKDRequestDictionary,
    documentUrl: URL?
  ) {
    guard let documentUrl else {
      return
    }
    switch requestUid {
    case requests.editorOpen:
      contextualRequests[documentUrl] = [ContextualRequest(kind: .editorOpen, request: request)]
    case requests.editorClose:
      contextualRequests[documentUrl] = nil
    case requests.codeCompleteOpen:
      contextualRequests[documentUrl, default: []].removeAll(where: { $0.kind == .codeCompleteOpen })
      contextualRequests[documentUrl, default: []].append(ContextualRequest(kind: .codeCompleteOpen, request: request))
    case requests.codeCompleteClose:
      contextualRequests[documentUrl, default: []].removeAll(where: { $0.kind == .codeCompleteOpen })
      if contextualRequests[documentUrl]?.isEmpty ?? false {
        // This should never happen because we should still have an active `.editorOpen` contextual request but just be
        // safe in case we don't.
        contextualRequests[documentUrl] = nil
      }
    default:
      break
    }
  }

  /// - Parameters:
  ///   - request: The request to send to sourcekitd.
  ///   - timeout: The maximum duration how long to wait for a response. If no response is returned within this time,
  ///     declare the request as having timed out.
  ///   - fileContents: The contents of the file that the request operates on. If sourcekitd crashes, the file contents
  ///     will be logged.
  package func send(
    _ requestUid: KeyPath<sourcekitd_api_requests, sourcekitd_api_uid_t>,
    _ request: SKDRequestDictionary,
    timeout: Duration,
    restartTimeout: Duration,
    documentUrl: URL?,
    fileContents: String?
  ) async throws -> SKDResponseDictionary {
    request.set(keys.request, to: requests[keyPath: requestUid])
    recordContextualRequest(requestUid: requests[keyPath: requestUid], request: request, documentUrl: documentUrl)

    let sourcekitdResponse = try await withTimeout(timeout) {
      let restartTimeoutHandle = TimeoutHandle()
      do {
        return try await withTimeout(restartTimeout, handle: restartTimeoutHandle) {
          await self.willSend(request: request)
          return try await withCancellableCheckedThrowingContinuation { (continuation) -> SourceKitDRequestHandle? in
            logger.info(
              """
              Sending sourcekitd request:
              \(request.forLogging)
              """
            )
            var handle: sourcekitd_api_request_handle_t? = nil
            self.api.send_request(request.dict, &handle) { response in
              continuation.resume(returning: SKDResponse(response!, sourcekitd: self))
            }
            Task {
              await self.didSend(request: request)
            }
            if let handle {
              return SourceKitDRequestHandle(handle: handle)
            }
            return nil
          } cancel: { (handle: SourceKitDRequestHandle?) in
            if let handle {
              logger.info(
                """
                Cancelling sourcekitd request:
                \(request.forLogging)
                """
              )
              self.api.cancel_request(handle.handle)
            }
          }
        }
      } catch let error as TimeoutError where error.handle == restartTimeoutHandle {
        if !self.path.lastPathComponent.contains("InProc") {
          logger.fault(
            "Did not receive reply from sourcekitd after \(restartTimeout, privacy: .public). Terminating and restarting sourcekitd."
          )
          await self.crash()
        } else {
          logger.fault(
            "Did not receive reply from sourcekitd after \(restartTimeout, privacy: .public). Not terminating sourcekitd because it is run in-process."
          )
        }
        throw error
      }
    }

    logger.log(
      level: (sourcekitdResponse.error == nil || sourcekitdResponse.error == .requestCancelled) ? .debug : .error,
      """
      Received sourcekitd response:
      \(sourcekitdResponse.forLogging)
      """
    )

    guard let dict = sourcekitdResponse.value else {
      if sourcekitdResponse.error == .connectionInterrupted {
        var log = """
          Request:
          \(request.description)

          File contents:
          \(fileContents ?? "<nil>")
          """

        if let documentUrl {
          let contextualRequests = (contextualRequests[documentUrl] ?? []).filter { $0.request !== request }
          for (index, contextualRequest) in contextualRequests.enumerated() {
            log += """

              Contextual request \(index + 1) / \(contextualRequests.count):
              \(contextualRequest.request.description)
              """
          }
        }
        let chunks = splitLongMultilineMessage(message: log)
        for (index, chunk) in chunks.enumerated() {
          logger.fault(
            """
            sourcekitd crashed (\(index + 1)/\(chunks.count))
            \(chunk)
            """
          )
        }
      }
      if sourcekitdResponse.error == .requestCancelled && !Task.isCancelled {
        throw SKDError.timedOut
      }
      throw sourcekitdResponse.error!
    }

    return dict
  }

  package func crash() async {
    _ = try? await send(
      \.crashWithExit,
      dictionary([:]),
      timeout: .seconds(60),
      restartTimeout: .seconds(24 * 60 * 60),
      documentUrl: nil,
      fileContents: nil
    )
  }
}
