//===----------------------------------------------------------------------===//
//
// 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
//
//===----------------------------------------------------------------------===//

/// Generates a markdown document for the configuration file based on the schema.
struct OptionDocumentBuilder {
  static let preamble = """
    <!-- DO NOT EDIT THIS FILE. This file is generated by \(#fileID). -->

    # Configuration File

    `.sourcekit-lsp/config.json` configuration files can be used to modify the behavior of SourceKit-LSP in various ways. The following locations are checked. Settings in later configuration files override settings in earlier configuration files
    - `~/.sourcekit-lsp/config.json`
    - On macOS: `~/Library/Application Support/org.swift.sourcekit-lsp/config.json` from the various `Library` folders on the system
    - If the `XDG_CONFIG_HOME` environment variable is set: `$XDG_CONFIG_HOME/sourcekit-lsp/config.json`
    - Initialization options passed in the initialize request
    - A `.sourcekit-lsp/config.json` file in a workspace’s root

    Modifying the configuration file requires SourceKit-LSP to be restarted for the new options to take effect.

    The structure of the file is currently not guaranteed to be stable. Options may be removed or renamed.

    ## Structure

    `config.json` is a JSON file with the following structure. All keys are optional and unknown keys are ignored.

    """

  let context: OptionSchemaContext

  /// Builds a markdown document for the configuration file based on the schema.
  func build(from schema: OptionTypeSchama) throws -> String {
    var doc = Self.preamble

    func appendProperty(_ property: OptionTypeSchama.Property, indentLevel: Int) throws {
      let indent = String(repeating: "  ", count: indentLevel)
      let name = property.name
      doc += "\(indent)- `\(name)"
      let type = property.type
      let typeDescription: String?
      switch type.kind {
      case .struct:
        // Skip struct type as we describe its properties in the next level
        typeDescription = nil
      default:
        typeDescription = Self.typeToDisplay(type)
      }
      if let typeDescription {
        doc += ": \(typeDescription)`:"
      } else {
        doc += "`:"
      }
      if let description = property.description {
        doc += " " + description.split(separator: "\n").joined(separator: "\n\(indent)  ")
      }
      doc += "\n"
      switch type.kind {
      case .struct(let schema):
        for property in schema.properties {
          try appendProperty(property, indentLevel: indentLevel + 1)
        }
      case .enum(let schema):
        let hasAssociatedTypes = schema.cases.contains {
          $0.associatedProperties != nil && !$0.associatedProperties!.isEmpty
        }

        if hasAssociatedTypes {
          let discriminatorFieldName = schema.discriminatorFieldName ?? "type"
          doc +=
            "\(indent)  - This is a tagged union discriminated by the `\(discriminatorFieldName)` field. Each case has the following structure:\n"

          for caseInfo in schema.cases {
            doc += """
              \(indent)  - `\(discriminatorFieldName): "\(caseInfo.name)"`
              """
            if let description = caseInfo.description {
              doc += ": " + description.split(separator: "\n").joined(separator: "\n\(indent)    ")
            }
            doc += "\n"

            if let associatedProperties = caseInfo.associatedProperties {
              for assocProp in associatedProperties {
                try appendProperty(assocProp, indentLevel: indentLevel + 2)
              }
            }
          }
        } else {
          for caseInfo in schema.cases {
            guard let description = caseInfo.description else {
              continue
            }
            doc += "\(indent)  - `\(caseInfo.name)`"
            doc += ": " + description.split(separator: "\n").joined(separator: "\n\(indent)    ")
            doc += "\n"
          }
        }
      default: break
      }
    }
    guard case .struct(let schema) = schema.kind else {
      throw ConfigSchemaGenError("Root schema must be a struct")
    }
    for property in schema.properties {
      try appendProperty(property, indentLevel: 0)
    }
    return doc
  }

  static func typeToDisplay(_ type: OptionTypeSchama, shouldWrap: Bool = false) -> String {
    switch type.kind {
    case .boolean: return "boolean"
    case .integer: return "integer"
    case .number: return "number"
    case .string: return "string"
    case .array(let value):
      return "\(typeToDisplay(value, shouldWrap: true))[]"
    case .dictionary(let value):
      return "[string: \(typeToDisplay(value))]"
    case .struct(let structInfo):
      return structInfo.name
    case .enum(let enumInfo):
      let hasAssociatedTypes = enumInfo.cases.contains {
        $0.associatedProperties != nil && !$0.associatedProperties!.isEmpty
      }
      if hasAssociatedTypes {
        return "object"
      } else {
        let cases = enumInfo.cases.map { "\"\($0.name)\"" }.joined(separator: "|")
        return shouldWrap ? "(\(cases))" : cases
      }
    }
  }
}
