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

#if canImport(FoundationXML)
import FoundationXML
#endif

enum CommentXMLError: Error {
  case noRootElement
}

/// Converts from sourcekit's XML documentation format to Markdown.
///
/// This code should go away and sourcekitd should return the Markdown directly.
package func xmlDocumentationToMarkdown(_ xmlString: String) throws -> String {
  let xml = try XMLDocument(xmlString: xmlString)
  guard let root = xml.rootElement() else {
    throw CommentXMLError.noRootElement
  }

  var convert = XMLToMarkdown()
  convert.out.reserveCapacity(xmlString.utf16.count)
  convert.toMarkdown(root)
  return convert.out
}

private struct XMLToMarkdown {
  var out: String = ""
  var indentCount: Int = 0
  let indentWidth: Int = 4
  var lineNumber: Int = 0
  var inParam: Bool = false

  mutating func newlineIfNeeded(count: Int = 1) {
    if !out.isEmpty && out.last! != "\n" {
      newline(count: count)
    }
  }

  mutating func newline(count: Int = 1) {
    out += String(repeating: "\n", count: count)
    out += String(repeating: " ", count: indentWidth * indentCount)
  }

  mutating func toMarkdown(_ node: XMLNode) {
    switch node.kind {
    case .element:
      toMarkdown(node as! XMLElement)
    default:
      out += node.stringValue ?? ""
    }
  }

  // [XMLNode]? is the type of XMLNode.children.
  mutating func toMarkdown(_ nodes: [XMLNode]?, separator: String = "") {
    guard let nodes else {
      return
    }
    for node in nodes {
      toMarkdown(node)
      out += separator
    }
  }

  mutating func toMarkdown(_ node: XMLElement) {
    switch node.name {
    case "Declaration":
      newlineIfNeeded(count: 2)
      out += "```swift\n"
      toMarkdown(node.children)
      out += "\n```\n"

    case "Name", "USR", "Direction":
      break

    case "Abstract", "Para":
      if !inParam {
        newlineIfNeeded(count: 2)
      }
      toMarkdown(node.children)

    case "Discussion", "ResultDiscussion", "ThrowsDiscussion":
      if !inParam {
        newlineIfNeeded(count: 2)
      }
      out += "### "
      switch node.name {
      case "Discussion": out += "Discussion"
      case "ResultDiscussion": out += "Returns"
      case "ThrowsDiscussion": out += "Throws"
      default: fatalError("handled in outer switch")
      }
      newline(count: 2)
      toMarkdown(node.children)

    case "Parameters":
      newlineIfNeeded(count: 2)
      out += "- Parameters:"
      indentCount += 1
      toMarkdown(node.children)
      indentCount -= 1

    case "Parameter":
      guard let name = node.elements(forName: "Name").first else { break }
      newlineIfNeeded()
      out += "- "
      toMarkdown(name.children)
      if let discussion = node.elements(forName: "Discussion").first {
        out += ": "
        inParam = true
        toMarkdown(discussion.children)
        inParam = false
      }

    case "CodeListing":
      lineNumber = 0
      newlineIfNeeded(count: 2)
      out += "```\(node.attributes?.first(where: { $0.name == "language" })?.stringValue ?? "")\n"
      toMarkdown(node.children, separator: "\n")
      out += "```"
      newlineIfNeeded(count: 2)

    case "zCodeLineNumbered":
      lineNumber += 1
      out += "\(lineNumber).\t"
      toMarkdown(node.children)

    case "codeVoice":
      out += "`"
      toMarkdown(node.children)
      out += "`"

    case "emphasis":
      out += "*"
      toMarkdown(node.children)
      out += "*"

    case "bold":
      out += "**"
      toMarkdown(node.children)
      out += "**"

    case "h1", "h2", "h3", "h4", "h5", "h6":
      newlineIfNeeded(count: 2)
      let n = Int(node.name!.dropFirst())
      out += String(repeating: "#", count: n!)
      out += " "
      toMarkdown(node.children)
      out += "\n\n"

    case "Link":
      if let href = node.attributes?.first(where: { $0.name == "href" })?.stringValue {
        out += "["
        toMarkdown(node.children)
        out += "](\(href))"
      } else {
        // Not a valid link.
        toMarkdown(node.children)
      }

    default:
      toMarkdown(node.children)
    }
  }
}
