//===----------------------------------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2024 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
//
//===----------------------------------------------------------------------===//

public import Csourcekitd
import Foundation
@_spi(SourceKitLSP) import SKLogging
import SourceKitD
import SwiftExtensions
import SwiftSourceKitPluginCommon
import ToolsProtocolsSwiftExtensions
@_spi(SourceKitLSP) import ToolsProtocolsSwiftExtensions

private func useNewAPI(for dict: SKDRequestDictionaryReader) -> Bool {
  guard let opts: SKDRequestDictionaryReader = dict[dict.sourcekitd.keys.codeCompleteOptions],
    opts[dict.sourcekitd.keys.useNewAPI] == 1
  else {
    return false
  }
  return true
}

final class RequestHandler: Sendable {
  enum HandleRequestResult {
    /// `handleRequest` will call `receiver`.
    case requestHandled

    /// `handleRequest` will not call `receiver` and a request response should be produced by sourcekitd (not the plugin).
    case handleInSourceKitD
  }

  let requestHandlingQueue = AsyncQueue<Serial>()
  let sourcekitd: SourceKitD
  let completionProvider: CompletionProvider

  init(params: sourcekitd_api_plugin_initialize_params_t, completionResultsBufferKind: UInt64, sourcekitd: SourceKitD) {
    let ideInspectionInstance = sourcekitd.servicePluginApi.plugin_initialize_get_swift_ide_inspection_instance(params)

    self.sourcekitd = sourcekitd
    self.completionProvider = CompletionProvider(
      completionResultsBufferKind: completionResultsBufferKind,
      opaqueIDEInspectionInstance: OpaqueIDEInspectionInstance(ideInspectionInstance),
      sourcekitd: sourcekitd
    )
  }

  func handleRequest(
    _ dict: SKDRequestDictionaryReader,
    handle: RequestHandle?,
    receiver: @Sendable @escaping (SKDResponse) -> Void
  ) -> HandleRequestResult {
    func produceResult(
      body: @escaping @Sendable () async throws -> SKDResponseDictionaryBuilder
    ) -> HandleRequestResult {
      withLoggingScope("request-\((handle?.numericValue ?? 0) % 100)") {
        let start = Date()
        logger.debug(
          """
          Plugin received sourcekitd request (handle: \(handle?.numericValue ?? -1))
          \(dict.description)
          """
        )
        requestHandlingQueue.async {
          let response: SKDResponse
          do {
            response = try await body().response
          } catch {
            response = SKDResponse.from(error: error, sourcekitd: self.sourcekitd)
          }
          logger.debug(
            """
            Finished (took \(Date().timeIntervalSince(start))s)
            \(response.description)
            """
          )
          receiver(response)
        }
        return .requestHandled
      }
    }

    func sourcekitdProducesResult(body: @escaping @Sendable () async -> Void) -> HandleRequestResult {
      requestHandlingQueue.async {
        await body()
      }
      return .handleInSourceKitD
    }

    switch dict[sourcekitd.keys.request] as sourcekitd_api_uid_t? {
    case sourcekitd.requests.editorOpen:
      return sourcekitdProducesResult {
        await self.completionProvider.handleDocumentOpen(dict)
      }
    case sourcekitd.requests.editorReplaceText:
      return sourcekitdProducesResult {
        await self.completionProvider.handleDocumentEdit(dict)
      }
    case sourcekitd.requests.editorClose:
      return sourcekitdProducesResult {
        await self.completionProvider.handleDocumentClose(dict)
      }

    case sourcekitd.requests.codeCompleteOpen:
      guard useNewAPI(for: dict) else {
        return .handleInSourceKitD
      }
      return produceResult {
        try await self.completionProvider.handleCompleteOpen(dict, handle: handle)
      }
    case sourcekitd.requests.codeCompleteUpdate:
      guard useNewAPI(for: dict) else {
        return .handleInSourceKitD
      }
      return produceResult {
        try await self.completionProvider.handleCompleteUpdate(dict)
      }
    case sourcekitd.requests.codeCompleteClose:
      guard useNewAPI(for: dict) else {
        return .handleInSourceKitD
      }
      return produceResult {
        try await self.completionProvider.handleCompleteClose(dict)
      }
    case sourcekitd.requests.codeCompleteDocumentation:
      return produceResult {
        try await self.completionProvider.handleCompletionDocumentation(dict)
      }
    case sourcekitd.requests.codeCompleteDiagnostic:
      return produceResult {
        try await self.completionProvider.handleCompletionDiagnostic(dict)
      }
    case sourcekitd.requests.codeCompleteSetPopularAPI:
      guard useNewAPI(for: dict) else {
        return .handleInSourceKitD
      }
      return produceResult {
        await self.completionProvider.handleSetPopularAPI(dict)
      }
    case sourcekitd.requests.dependencyUpdated:
      return sourcekitdProducesResult {
        await self.completionProvider.handleDependencyUpdated()
      }
    default:
      return .handleInSourceKitD
    }
  }

  func cancel(_ handle: RequestHandle) {
    logger.debug("Cancelling request with handle \(handle.numericValue)")
    self.completionProvider.cancel(handle: handle)
  }
}

/// Legacy plugin initialization logic in which sourcekitd does not inform the plugin about the sourcekitd path it was
/// loaded from.
@_cdecl("sourcekitd_plugin_initialize")
public func sourcekitd_plugin_initialize(_ params: sourcekitd_api_plugin_initialize_params_t) {
  LoggingScope.configureDefaultLoggingSubsystem("org.swift.sourcekit-lsp.service-plugin")
  logger.fault("sourcekitd_plugin_initialize has been removed in favor of sourcekitd_plugin_initialize_2")
}

#if canImport(Darwin)
private extension SourceKitD {
  /// When a plugin is initialized, it gets passed the library it was loaded from to `sourcekitd_plugin_initialize_2`.
  ///
  /// Since the plugin wants to interact with sourcekitd in-process, it needs to load `sourcekitdInProc`. This function
  /// loads `sourcekitdInProc` relative to the parent library path, if it exists, or `sourcekitd` if `sourcekitdInProc`
  /// doesn't exist (eg. on Linux where `sourcekitd` is already in-process).
  static func inProcLibrary(relativeTo parentLibraryPath: URL) throws -> SourceKitD {
    var frameworkUrl = parentLibraryPath

    // Remove path components until we reach the `sourcekitd.framework` directory. The plugin might have been loaded
    // from an XPC service, in which case `parentLibraryPath` is
    // `sourcekitd.framework/XPCServices/SourceKitService.xpc/Contents/MacOS/SourceKitService`.
    while frameworkUrl.pathExtension != "framework" {
      guard frameworkUrl.pathComponents.count > 1 else {
        struct NoFrameworkPathError: Error, CustomStringConvertible {
          var parentLibraryPath: URL
          var description: String { "Could not find .framework directory relative to '\(parentLibraryPath)'" }
        }
        throw NoFrameworkPathError(parentLibraryPath: parentLibraryPath)
      }
      frameworkUrl.deleteLastPathComponent()
    }
    frameworkUrl.deleteLastPathComponent()

    let inProcUrl =
      frameworkUrl
      .appending(components: "sourcekitdInProc.framework", "sourcekitdInProc")
    if FileManager.default.fileExists(at: inProcUrl) {
      return try SourceKitD(core: SourceKitDCoreForPlugin(dylibPath: inProcUrl))
    }

    let sourcekitdUrl =
      frameworkUrl
      .appending(components: "sourcekitd.framework", "sourcekitd")
    return try SourceKitD(core: SourceKitDCoreForPlugin(dylibPath: sourcekitdUrl))
  }
}
#endif

@_cdecl("sourcekitd_plugin_initialize_2")
public func sourcekitd_plugin_initialize_2(
  _ params: sourcekitd_api_plugin_initialize_params_t,
  _ parentLibraryPath: UnsafePointer<CChar>
) {
  LoggingScope.configureDefaultLoggingSubsystem("org.swift.sourcekit-lsp.service-plugin")

  let parentLibraryPath = String(cString: parentLibraryPath)
  #if canImport(Darwin)
  if parentLibraryPath == "SOURCEKIT_LSP_PLUGIN_PARENT_LIBRARY_RTLD_DEFAULT" {
    SourceKitD.forPlugin = try! SourceKitD(
      core: SourceKitDCoreForPlugin(dlhandle: .rtldDefault, path: URL(string: "rtld-default://")!)
    )
  } else {
    SourceKitD.forPlugin = try! SourceKitD.inProcLibrary(relativeTo: URL(fileURLWithPath: parentLibraryPath))
  }
  #else
  // On other platforms, sourcekitd is always in process, so we can load it straight away.
  SourceKitD.forPlugin = try! SourceKitD(
    core: SourceKitDCoreForPlugin(dylibPath: URL(fileURLWithPath: parentLibraryPath))
  )
  #endif
  let sourcekitd = SourceKitD.forPlugin

  let completionResultsBufferKind = sourcekitd.pluginApi.plugin_initialize_custom_buffer_start(params)
  let isClientOnly = sourcekitd.pluginApi.plugin_initialize_is_client_only(params)

  let uidFromCString = sourcekitd.pluginApi.plugin_initialize_uid_get_from_cstr(params)
  let uidGetCString = sourcekitd.pluginApi.plugin_initialize_uid_get_string_ptr(params)

  // Depending on linking and loading configuration, we may need to chain the global UID handlers back to the UID
  // handlers in the caller. The extra hop should not matter, since we cache the results.
  if unsafeBitCast(uidFromCString, to: UnsafeRawPointer.self)
    != unsafeBitCast(sourcekitd.api.uid_get_from_cstr, to: UnsafeRawPointer.self)
  {
    sourcekitd.api.set_uid_handlers(uidFromCString, uidGetCString)
  }

  sourcekitd.pluginApi.plugin_initialize_register_custom_buffer(
    params,
    completionResultsBufferKind,
    CompletionResultsArray.arrayFuncs.rawValue
  )

  if isClientOnly {
    return
  }

  let requestHandler = RequestHandler(
    params: params,
    completionResultsBufferKind: completionResultsBufferKind,
    sourcekitd: sourcekitd
  )

  sourcekitd.servicePluginApi.plugin_initialize_register_cancellation_handler(params) { handle in
    if let handle = RequestHandle(handle) {
      requestHandler.cancel(handle)
    }
  }

  sourcekitd.servicePluginApi.plugin_initialize_register_cancellable_request_handler(params) {
    (request, handle, receiver) -> Bool in
    guard let receiver, let request, let dict = SKDRequestDictionaryReader(request, sourcekitd: sourcekitd) else {
      return false
    }
    let handle = RequestHandle(handle)

    let handledRequest = requestHandler.handleRequest(dict, handle: handle) { receiver($0.underlyingValueRetained()) }

    switch handledRequest {
    case .requestHandled: return true
    case .handleInSourceKitD: return false
    }
  }
}

private final class SourceKitDCoreForPlugin: SourceKitDCore, Sendable {
  let dlHandle: DLHandle
  let path: URL
  private let ownsHandle: Bool

  init(dylibPath: URL) throws {
    #if os(Windows)
    let dlopenModes: DLOpenFlags = []
    #else
    let dlopenModes: DLOpenFlags = [.lazy, .local, .noLoad]
    #endif
    self.dlHandle = try dlopen(dylibPath.filePath, mode: dlopenModes)
    self.path = dylibPath
    self.ownsHandle = true
  }

  init(dlhandle: DLHandle, path: URL) {
    self.dlHandle = dlhandle
    self.path = path
    self.ownsHandle = false
  }

  deinit {
    if ownsHandle {
      try? dlHandle.close()
    } else {
      dlHandle.leak()
    }
  }

  func initializeService(
    api: sourcekitd_api_functions_t,
    notificationCallback: @escaping @Sendable (sourcekitd_api_response_t) -> Void
  ) {
    // Borrowed handle — sourcekitd is already initialized.
  }
}
