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

import Foundation
import SwiftParser
import SwiftSyntax

/// The main entry point for generating a JSON schema and Markdown documentation
/// for the SourceKit-LSP configuration file format
/// (`.sourcekit-lsp/config.json`) from the Swift type definitions in
/// `SKOptions` Swift module.
package struct ConfigSchemaGen {
  private struct WritePlan {
    fileprivate let category: String
    fileprivate let path: URL
    fileprivate let contents: () throws -> Data

    fileprivate func write() throws {
      try contents().write(to: path)
    }
  }

  private static let projectRoot = URL(fileURLWithPath: #filePath)
    .deletingLastPathComponent()
    .deletingLastPathComponent()
    .deletingLastPathComponent()
    .deletingLastPathComponent()
  private static let sourceDir =
    projectRoot
    .appending(components: "Sources", "SKOptions")
  private static let configSchemaJSONPath =
    projectRoot
    .appending(component: "config.schema.json")
  private static let configSchemaDocPath =
    projectRoot
    .appending(components: "Documentation", "Configuration File.md")

  /// Generates and writes the JSON schema and documentation for the SourceKit-LSP configuration file format.
  package static func generate() throws {
    let plans = try plan()
    for plan in plans {
      print("Writing \(plan.category) to \"\(plan.path.path)\"")
      try plan.write()
    }
  }

  /// Verifies that the generated JSON schema and documentation in the current source tree
  /// are up-to-date with the Swift type definitions in `SKOptions`.
  /// - Returns: `true` if the generated files are up-to-date, `false` otherwise.
  package static func verify() throws -> Bool {
    let plans = try plan()
    for plan in plans {
      print("Verifying \(plan.category) at \"\(plan.path.path)\"")
      let expectedContents = try plan.contents()
      let actualContents = try Data(contentsOf: plan.path)
      guard expectedContents == actualContents else {
        print("error: \(plan.category) is out-of-date!")
        print("Please run `./sourcekit-lsp-dev-utils generate-config-schema` to update it.")
        return false
      }
    }
    return true
  }

  private static func plan() throws -> [WritePlan] {
    let sourceFiles = FileManager.default.enumerator(at: sourceDir, includingPropertiesForKeys: nil)!
    let typeNameResolver = TypeDeclResolver()

    for case let fileURL as URL in sourceFiles {
      guard fileURL.pathExtension == "swift" else {
        continue
      }
      let sourceText = try String(contentsOf: fileURL)
      let sourceFile = Parser.parse(source: sourceText)
      typeNameResolver.collect(from: sourceFile)
    }
    let rootTypeDecl = try typeNameResolver.lookupType(fullyQualified: ["SourceKitLSPOptions"])
    let context = OptionSchemaContext(typeNameResolver: typeNameResolver)
    var schema = try context.buildSchema(from: rootTypeDecl)

    // Manually annotate the logging level enum since LogLevel type exists
    // outside of the SKOptions module
    schema["logging"]?["level"]?.kind = .enum(
      OptionTypeSchama.Enum(
        name: "LogLevel",
        cases: ["debug", "info", "default", "error", "fault"].map {
          OptionTypeSchama.Case(name: $0)
        }
      )
    )
    schema["logging"]?["privacyLevel"]?.kind = .enum(
      OptionTypeSchama.Enum(
        name: "PrivacyLevel",
        cases: ["public", "private", "sensitive"].map {
          OptionTypeSchama.Case(name: $0)
        }
      )
    )

    return [
      WritePlan(
        category: "JSON Schema",
        path: configSchemaJSONPath,
        contents: { try generateJSONSchema(from: schema, context: context) }
      ),
      WritePlan(
        category: "Schema Documentation",
        path: configSchemaDocPath,
        contents: { try generateDocumentation(from: schema, context: context) }
      ),
    ]
  }

  private static func generateJSONSchema(from schema: OptionTypeSchama, context: OptionSchemaContext) throws -> Data {
    let schemaBuilder = JSONSchemaBuilder(context: context)
    var jsonSchema = try schemaBuilder.build(from: schema)
    jsonSchema.title = "SourceKit-LSP Configuration"
    jsonSchema.comment = "DO NOT EDIT THIS FILE. This file is generated by \(#fileID)."
    let encoder = JSONEncoder()
    encoder.outputFormatting = [.prettyPrinted, .sortedKeys, .withoutEscapingSlashes]
    return try encoder.encode(jsonSchema)
  }

  private static func generateDocumentation(from schema: OptionTypeSchama, context: OptionSchemaContext) throws -> Data
  {
    let docBuilder = OptionDocumentBuilder(context: context)
    guard let data = try docBuilder.build(from: schema).data(using: .utf8) else {
      throw ConfigSchemaGenError("Failed to encode documentation as UTF-8")
    }
    return data
  }
}

struct ConfigSchemaGenError: Error, CustomStringConvertible {
  let description: String

  init(_ description: String) {
    self.description = description
  }
}
